# 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"
- 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;
- 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-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-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 apiEndpoint = '/admin/vendors'
WRONG (causes double prefix /api/v1/api/v1/...):
apiClient.get('/api/v1/admin/vendors')
const apiEndpoint = '/api/v1/admin/vendors'
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"
- "apiEndpoint.*=.*['\"`]/api/v1"
exceptions:
- "init-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: '
Loading...
'
- id: "TPL-007"
name: "Implement empty state when no data"
severity: "warning"
description: |
Show empty state when lists have no items
pattern:
file_pattern: "app/templates/**/*.html"
recommended_pattern: 'No items'
- id: "TPL-008"
name: "Use table_header_custom for custom headers, not table_header"
severity: "error"
description: |
When using {% call %} to create custom table headers with th_sortable
or custom
elements, you MUST use table_header_custom(), not table_header().
The table_header() macro takes a columns list and does NOT support caller().
Using {% call table_header() %} causes a Jinja2 error:
"macro 'table_header' was invoked with two values for the special caller argument"
WRONG (causes 500 error):
{% call table_header() %}
{{ th_sortable('name', 'Name', 'sortBy', 'sortOrder') }}