# Architecture Rules - Frontend Rules # Combined rules for JavaScript, Templates, Components, and Styling # ============================================================================ # JAVASCRIPT ARCHITECTURE RULES # ============================================================================ javascript_rules: - id: "JS-001" name: "Use centralized logger, not console" severity: "error" description: | Use window.LogConfig.createLogger() for consistent logging. Never use console.log, console.error, console.warn directly. pattern: file_pattern: "static/**/js/**/*.js" anti_patterns: - "console\\.log" - "console\\.error" - "console\\.warn" exceptions: - "// eslint-disable" - "console.log('✅" auto_exclude_files: - "init-*.js" - "vendor/" - id: "JS-002" name: "Use lowercase apiClient for API calls" severity: "error" description: | Use lowercase 'apiClient' consistently, not 'ApiClient' or 'API_CLIENT' pattern: file_pattern: "static/**/js/**/*.js" anti_patterns: - "ApiClient\\." - "API_CLIENT\\." required_pattern: "apiClient\\." - id: "JS-003" name: "Alpine components must spread ...data()" severity: "error" description: | All Alpine.js components must inherit base layout data using spread operator pattern: file_pattern: "static/**/js/**/*.js" required_in_alpine_components: - "\\.\\.\\.data\\(\\)" - id: "JS-004" name: "Alpine components must set currentPage" severity: "error" description: | All Alpine.js page components must set a currentPage identifier pattern: file_pattern: "static/**/js/**/*.js" required_in_alpine_components: - "currentPage:" - id: "JS-005" name: "Initialization methods must include guard" severity: "error" description: | Init methods should prevent duplicate initialization with guard pattern: file_pattern: "static/**/js/**/*.js" recommended_pattern: | if (window._pageInitialized) return; window._pageInitialized = true; auto_exclude_files: - "vendor/" - id: "JS-006" name: "All async operations must have try/catch with error logging" severity: "error" description: | All API calls and async operations must have error handling pattern: file_pattern: "static/**/js/**/*.js" check: "async_error_handling" - id: "JS-013" name: "Components overriding init() must call parent init" severity: "error" description: | When an Alpine.js component spreads ...data() and defines its own init() method, it MUST call the parent init() first. The parent init() sets critical properties like vendorCode (from URL), currentUser, and theme preference. Without calling parent init(), properties like vendorCode will be null, causing API calls like `/vendor/${this.vendorCode}/settings` to fail with "Endpoint not found: /api/v1/vendor/null/settings". WRONG (parent init never called): function vendorSettings() { return { ...data(), async init() { await this.loadSettings(); // this.vendorCode is null! } }; } RIGHT (call parent init first): function vendorSettings() { return { ...data(), async init() { // IMPORTANT: Call parent init first to set vendorCode from URL const parentInit = data().init; if (parentInit) { await parentInit.call(this); } await this.loadSettings(); // this.vendorCode is now set } }; } This pattern is required for ALL page-specific JavaScript files that: 1. Use ...data() to inherit base layout functionality 2. Define their own init() method pattern: file_pattern: "static/vendor/js/**/*.js" check: "parent_init_call" required_when: - "contains: '...data()'" - "contains: 'async init()'" required_pattern: - "data\\(\\)\\.init" - "parentInit" exceptions: - "init-alpine.js" - "login.js" - id: "JS-014" name: "Vendor API calls must not include vendorCode in path" severity: "error" description: | Vendor API endpoints use JWT token authentication, NOT URL path parameters. The vendor is identified from the JWT token via get_current_vendor_api dependency. Do NOT include vendorCode in API paths for authenticated vendor endpoints. WRONG (vendorCode in API path): apiClient.get(`/vendor/${this.vendorCode}/orders`) apiClient.post(`/vendor/${this.vendorCode}/products`, data) RIGHT (no vendorCode, uses JWT): apiClient.get(`/vendor/orders`) apiClient.post(`/vendor/products`, data) EXCEPTIONS (these endpoints DO use vendorCode in path): - /vendor/{vendor_code} - Public vendor info (info.router) - /vendor/{vendor_code}/content-pages/* - Content pages management - Page URLs (not API calls) like window.location.href = `/vendor/${vendorCode}/...` Why this matters: - Including vendorCode causes 404 errors ("/vendor/orion/orders" not found) - The JWT token already identifies the vendor - Consistent with the API design pattern pattern: file_pattern: "static/vendor/js/**/*.js" anti_patterns: - "apiClient\\.(get|post|put|delete|patch)\\s*\\(\\s*`/vendor/\\$\\{this\\.vendorCode\\}/(orders|products|customers|inventory|analytics|dashboard|profile|settings|team|notifications|invoices|payments|media|marketplace|letzshop|billing|features|usage)" exceptions: - "init-alpine.js" - "login.js" - "content-pages.js" - "content-page-edit.js" - id: "JS-007" name: "Set loading state before async operations" severity: "warning" description: | Loading state should be set before and cleared after async operations pattern: file_pattern: "static/**/js/**/*.js" recommended_pattern: | loading = true; try { // operation } finally { loading = false; } - id: "JS-008" name: "Use apiClient for API calls, not raw fetch()" severity: "error" description: | All API calls must use the apiClient helper instead of raw fetch(). The apiClient automatically: - Adds Authorization header with JWT token from cookies - Sets Content-Type headers - Handles error responses consistently - Provides logging integration WRONG (raw fetch): const response = await fetch('/api/v1/admin/products/123'); RIGHT (apiClient): const response = await apiClient.get('/admin/products/123'); const result = await apiClient.post('/admin/products', data); await apiClient.delete('/admin/products/123'); pattern: file_pattern: "static/**/js/**/*.js" anti_patterns: - "fetch\\('/api/" - 'fetch\\("/api/' - "fetch\\(`/api/" exceptions: - "init-api-client.js" - id: "JS-009" name: "Use Utils.showToast() for notifications, not alert() or window.showToast" severity: "error" description: | All user notifications must use Utils.showToast() from static/shared/js/utils.js. Never use browser alert() dialogs or undefined window.showToast. Utils.showToast() provides: - Consistent styling (Tailwind-based toast in bottom-right corner) - Automatic fade-out after duration - Color-coded types (success=green, error=red, warning=yellow, info=blue) WRONG (browser dialog): alert('Product saved successfully'); RIGHT (Utils helper): Utils.showToast('Product saved successfully', 'success'); Utils.showToast('Failed to save product', 'error'); pattern: file_pattern: "static/**/js/**/*.js" anti_patterns: - "alert\\(" - "window\\.showToast" exceptions: - "utils.js" - id: "JS-015" name: "Use confirm_modal macros, not native confirm()" severity: "error" description: | All confirmation dialogs must use the project's confirm_modal or confirm_modal_dynamic Jinja2 macros from shared/macros/modals.html. Never use the native browser confirm() dialog. The modal macros provide: - Consistent styled dialogs matching the admin/store theme - Dark mode support - Variant colors (danger=red, warning=yellow, info=blue) - Icon support - Double-confirm pattern for destructive operations WRONG (native browser dialog): if (!confirm('Are you sure you want to delete this?')) return; if (!confirm(I18n.t('confirmations.delete'))) return; RIGHT (state variable + modal macro): // In JS: add state variable and remove confirm() guard showDeleteModal: false, async deleteItem() { // No confirm() guard — modal already confirmed await apiClient.delete('/admin/items/' + this.item.id); } // In template: button sets state, macro shows modal {{ confirm_modal('deleteModal', 'Delete Item', 'Are you sure?', 'deleteItem()', 'showDeleteModal', 'Delete', 'Cancel', 'danger') }} For dynamic messages (containing JS expressions): {{ confirm_modal_dynamic('deleteModal', 'Delete Item', "'Delete ' + item.name + '?'", 'deleteItem()', 'showDeleteModal', 'Delete', 'Cancel', 'danger') }} pattern: file_pattern: "static/**/js/**/*.js" anti_patterns: - "confirm\\(" exceptions: - "utils.js" - "vendor/" - id: "JS-010" name: "Use PlatformSettings for pagination rows per page" severity: "error" description: | All pages with tables MUST use window.PlatformSettings.getRowsPerPage() to load the platform-configured rows per page setting. This ensures consistent pagination behavior across the entire admin and vendor interface. The setting is configured at /admin/settings under the Display tab. Settings are cached client-side for 5 minutes to minimize API calls. Required pattern in init() method: async init() { // Guard against multiple initialization if (window._pageNameInitialized) return; window._pageNameInitialized = true; // REQUIRED: Load platform settings for pagination if (window.PlatformSettings) { this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); } await this.loadData(); } WRONG (hardcoded pagination): pagination: { page: 1, per_page: 50, // Hardcoded! total: 0 } RIGHT (platform settings): pagination: { page: 1, per_page: 20, // Default, overridden by PlatformSettings total: 0 } async init() { if (window.PlatformSettings) { this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); } } Documentation: docs/frontend/shared/platform-settings.md pattern: file_pattern: "static/admin/js/**/*.js" required_in_pages_with_pagination: - "PlatformSettings\\.getRowsPerPage" - "window\\.PlatformSettings" exceptions: - "init-alpine.js" - "init-api-client.js" - "settings.js" - id: "JS-011" name: "Use standard pagination object structure" severity: "error" description: | All pages with tables MUST use the standard nested pagination object structure. This ensures compatibility with the pagination macro and consistent behavior across all pages. REQUIRED structure: pagination: { page: 1, per_page: 20, total: 0, pages: 0 } WRONG (flat structure): page: 1, limit: 20, total: 0, skip: 0 WRONG (different property names): pagination: { currentPage: 1, itemsPerPage: 20 } Required computed properties: - totalPages - startIndex - endIndex - pageNumbers Required methods: - previousPage() - nextPage() - goToPage(pageNum) Documentation: docs/frontend/shared/pagination.md pattern: file_pattern: "static/**/js/**/*.js" required_in_pages_with_pagination: - "pagination:" - "pagination\\.page" - "pagination\\.per_page" anti_patterns_in_pagination_pages: - "^\\s*page:\\s*\\d" - "^\\s*limit:\\s*\\d" - "^\\s*skip:\\s*" exceptions: - "init-alpine.js" - id: "JS-012" name: "Do not include /api/v1 prefix in API endpoints" severity: "error" description: | When using apiClient.get(), apiClient.post(), etc., do NOT include the /api/v1 prefix in the endpoint path. The apiClient automatically prepends this prefix. CORRECT: apiClient.get('/admin/vendors') apiClient.post('/admin/products') const url = '/admin/vendors' WRONG (causes double prefix /api/v1/api/v1/...): apiClient.get('/api/v1/admin/vendors') const url = '/api/v1/admin/vendors' const endpoint = '/api/v1/admin/products' Exception: Direct fetch() calls without apiClient should use full path. Documentation: docs/frontend/shared/api-client.md pattern: file_pattern: "static/**/js/**/*.js" anti_patterns: - "apiClient\\.(get|post|put|delete|patch)\\s*\\(\\s*['\"`]/api/v1" - "(const|let|var)\\s+(url|endpoint|apiEndpoint|apiUrl|path)\\s*=\\s*['\"`]/api/v1" - "\\$\\{.*\\}/api/v1" exceptions: - "init-api-client.js" - "api-client.js" # ============================================================================ # TEMPLATE RULES (Jinja2) # ============================================================================ template_rules: - id: "TPL-001" name: "Admin templates must extend admin/base.html" severity: "error" description: | All admin templates must extend the base template for consistency. Auto-excluded files: - login.html - Standalone login page (no sidebar/navigation) - errors/*.html - Error pages extend errors/base.html instead - test-*.html - Test/development templates Standalone template markers (place in first 5 lines): - {# standalone #} - Mark template as intentionally standalone - {# noqa: TPL-001 #} - Standard noqa style to suppress error - - HTML comment style pattern: file_pattern: "app/templates/admin/**/*.html" required_patterns: - "{% extends ['\"]admin/base\\.html['\"] %}" auto_exclude_files: - "login.html" - "errors/" - "test-" standalone_markers: - "{# standalone #}" - "{# noqa: tpl-001 #}" - "" exceptions: - "base.html" - "partials/" - id: "TPL-002" name: "Vendor templates must extend vendor/base.html" severity: "error" description: "All vendor templates must extend the base template" pattern: file_pattern: "app/templates/vendor/**/*.html" required_patterns: - "{% extends ['\"]vendor/base\\.html['\"] %}" exceptions: - "base.html" - "partials/" - id: "TPL-003" name: "Shop templates must extend shop/base.html" severity: "error" description: "All shop templates must extend the base template" pattern: file_pattern: "app/templates/shop/**/*.html" required_patterns: - "{% extends ['\"]shop/base\\.html['\"] %}" exceptions: - "base.html" - "partials/" - id: "TPL-004" name: "Use x-text for dynamic text content (prevents XSS)" severity: "warning" description: | Use x-text directive for dynamic content to prevent XSS vulnerabilities pattern: file_pattern: "app/templates/**/*.html" recommended_pattern: '
' - id: "TPL-005" name: "Use x-html ONLY for safe content" severity: "error" description: | Use x-html only for trusted content like icons, never for user-generated content pattern: file_pattern: "app/templates/**/*.html" safe_usage: - 'x-html="\\$icon\\(' - id: "TPL-006" name: "Implement loading state for data loads" severity: "warning" description: | All templates that load data should show loading state pattern: file_pattern: "app/templates/**/*.html" recommended_pattern: 'Modal content here