From 9a828999fea3dab52620ac434c669d31c8b71f4c Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Tue, 27 Jan 2026 20:34:58 +0100 Subject: [PATCH] feat: add admin menu configuration and sidebar improvements - Add AdminMenuConfig model for per-platform menu customization - Add menu registry for centralized menu configuration - Add my-menu-config and platform-menu-config admin pages - Update sidebar with improved layout and Alpine.js interactions - Add FrontendType enum for admin/vendor menu separation - Document self-contained module patterns in session note Co-Authored-By: Claude Opus 4.5 --- .../za0k1l2m3n4o5_add_admin_menu_config.py | 128 ++++ ...n4o5p6_add_frontend_type_to_menu_config.py | 117 ++++ app/config/__init__.py | 18 + app/config/menu_registry.py | 546 +++++++++++++++++ app/services/platform_service.py | 24 +- app/templates/admin/my-menu-config.html | 174 ++++++ app/templates/admin/partials/sidebar.html | 152 +++-- app/templates/admin/platform-menu-config.html | 200 +++++++ ..._NOTE_2026-01-26_self-contained-modules.md | 300 ++++++++++ docs/proposals/TEMP.md | 557 ------------------ docs/proposals/humble-orbiting-otter.md | 433 ++++++++++++++ models/database/admin_menu_config.py | 244 ++++++++ models/database/user.py | 9 + static/admin/js/init-alpine.js | 63 ++ 14 files changed, 2346 insertions(+), 619 deletions(-) create mode 100644 alembic/versions/za0k1l2m3n4o5_add_admin_menu_config.py create mode 100644 alembic/versions/zb1l2m3n4o5p6_add_frontend_type_to_menu_config.py create mode 100644 app/config/__init__.py create mode 100644 app/config/menu_registry.py create mode 100644 app/templates/admin/my-menu-config.html create mode 100644 app/templates/admin/platform-menu-config.html create mode 100644 docs/proposals/SESSION_NOTE_2026-01-26_self-contained-modules.md delete mode 100644 docs/proposals/TEMP.md create mode 100644 docs/proposals/humble-orbiting-otter.md create mode 100644 models/database/admin_menu_config.py diff --git a/alembic/versions/za0k1l2m3n4o5_add_admin_menu_config.py b/alembic/versions/za0k1l2m3n4o5_add_admin_menu_config.py new file mode 100644 index 00000000..b2c18cba --- /dev/null +++ b/alembic/versions/za0k1l2m3n4o5_add_admin_menu_config.py @@ -0,0 +1,128 @@ +"""Add admin menu configuration table + +Revision ID: za0k1l2m3n4o5 +Revises: z9j0k1l2m3n4 +Create Date: 2026-01-25 + +Adds configurable admin sidebar menus: +- Platform-level config: Controls which menu items platform admins see +- User-level config: Controls which menu items super admins see +- Opt-out model: All items visible by default +- Mandatory items enforced at application level (companies, vendors, users, settings) +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "za0k1l2m3n4o5" +down_revision = "z9j0k1l2m3n4" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Create admin_menu_configs table + op.create_table( + "admin_menu_configs", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column( + "platform_id", + sa.Integer(), + nullable=True, + comment="Platform scope - applies to all platform admins of this platform", + ), + sa.Column( + "user_id", + sa.Integer(), + nullable=True, + comment="User scope - applies to this specific super admin", + ), + sa.Column( + "menu_item_id", + sa.String(50), + nullable=False, + comment="Menu item identifier from registry (e.g., 'products', 'inventory')", + ), + sa.Column( + "is_visible", + sa.Boolean(), + nullable=False, + server_default="true", + comment="Whether this menu item is visible (False = hidden)", + ), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.func.now(), + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.func.now(), + onupdate=sa.func.now(), + ), + # Foreign keys + sa.ForeignKeyConstraint( + ["platform_id"], + ["platforms.id"], + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.id"], + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("id"), + # Unique constraints + sa.UniqueConstraint("platform_id", "menu_item_id", name="uq_platform_menu_config"), + sa.UniqueConstraint("user_id", "menu_item_id", name="uq_user_menu_config"), + # Check constraint: exactly one scope must be set + sa.CheckConstraint( + "(platform_id IS NOT NULL AND user_id IS NULL) OR " + "(platform_id IS NULL AND user_id IS NOT NULL)", + name="ck_admin_menu_config_scope", + ), + ) + + # Create indexes for performance + op.create_index( + "idx_admin_menu_configs_platform_id", + "admin_menu_configs", + ["platform_id"], + ) + op.create_index( + "idx_admin_menu_configs_user_id", + "admin_menu_configs", + ["user_id"], + ) + op.create_index( + "idx_admin_menu_configs_menu_item_id", + "admin_menu_configs", + ["menu_item_id"], + ) + op.create_index( + "idx_admin_menu_config_platform_visible", + "admin_menu_configs", + ["platform_id", "is_visible"], + ) + op.create_index( + "idx_admin_menu_config_user_visible", + "admin_menu_configs", + ["user_id", "is_visible"], + ) + + +def downgrade() -> None: + # Drop indexes + op.drop_index("idx_admin_menu_config_user_visible", table_name="admin_menu_configs") + op.drop_index("idx_admin_menu_config_platform_visible", table_name="admin_menu_configs") + op.drop_index("idx_admin_menu_configs_menu_item_id", table_name="admin_menu_configs") + op.drop_index("idx_admin_menu_configs_user_id", table_name="admin_menu_configs") + op.drop_index("idx_admin_menu_configs_platform_id", table_name="admin_menu_configs") + + # Drop table + op.drop_table("admin_menu_configs") diff --git a/alembic/versions/zb1l2m3n4o5p6_add_frontend_type_to_menu_config.py b/alembic/versions/zb1l2m3n4o5p6_add_frontend_type_to_menu_config.py new file mode 100644 index 00000000..ded81419 --- /dev/null +++ b/alembic/versions/zb1l2m3n4o5p6_add_frontend_type_to_menu_config.py @@ -0,0 +1,117 @@ +"""Add frontend_type to admin_menu_configs + +Revision ID: zb1l2m3n4o5p6 +Revises: za0k1l2m3n4o5 +Create Date: 2026-01-25 + +Adds frontend_type column to support both admin and vendor menu configuration: +- 'admin': Admin panel menus (super admins, platform admins) +- 'vendor': Vendor dashboard menus (configured per platform) + +Also updates unique constraints to include frontend_type and adds +a check constraint ensuring user_id scope is only used for admin frontend. +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "zb1l2m3n4o5p6" +down_revision = "za0k1l2m3n4o5" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # 1. Create the enum type for frontend_type + frontend_type_enum = sa.Enum('admin', 'vendor', name='frontendtype') + frontend_type_enum.create(op.get_bind(), checkfirst=True) + + # 2. Add frontend_type column with default value + op.add_column( + "admin_menu_configs", + sa.Column( + "frontend_type", + sa.Enum('admin', 'vendor', name='frontendtype'), + nullable=False, + server_default="admin", + comment="Which frontend this config applies to (admin or vendor)", + ), + ) + + # 3. Create index on frontend_type + op.create_index( + "idx_admin_menu_configs_frontend_type", + "admin_menu_configs", + ["frontend_type"], + ) + + # 4. Drop old unique constraints + op.drop_constraint("uq_platform_menu_config", "admin_menu_configs", type_="unique") + op.drop_constraint("uq_user_menu_config", "admin_menu_configs", type_="unique") + + # 5. Create new unique constraints that include frontend_type + op.create_unique_constraint( + "uq_frontend_platform_menu_config", + "admin_menu_configs", + ["frontend_type", "platform_id", "menu_item_id"], + ) + op.create_unique_constraint( + "uq_frontend_user_menu_config", + "admin_menu_configs", + ["frontend_type", "user_id", "menu_item_id"], + ) + + # 6. Add check constraint: user_id scope only allowed for admin frontend + op.create_check_constraint( + "ck_user_scope_admin_only", + "admin_menu_configs", + "(user_id IS NULL) OR (frontend_type = 'admin')", + ) + + # 7. Create composite indexes for common queries + op.create_index( + "idx_admin_menu_config_frontend_platform", + "admin_menu_configs", + ["frontend_type", "platform_id"], + ) + op.create_index( + "idx_admin_menu_config_frontend_user", + "admin_menu_configs", + ["frontend_type", "user_id"], + ) + + +def downgrade() -> None: + # Drop new indexes + op.drop_index("idx_admin_menu_config_frontend_user", table_name="admin_menu_configs") + op.drop_index("idx_admin_menu_config_frontend_platform", table_name="admin_menu_configs") + + # Drop check constraint + op.drop_constraint("ck_user_scope_admin_only", "admin_menu_configs", type_="check") + + # Drop new unique constraints + op.drop_constraint("uq_frontend_user_menu_config", "admin_menu_configs", type_="unique") + op.drop_constraint("uq_frontend_platform_menu_config", "admin_menu_configs", type_="unique") + + # Restore old unique constraints + op.create_unique_constraint( + "uq_platform_menu_config", + "admin_menu_configs", + ["platform_id", "menu_item_id"], + ) + op.create_unique_constraint( + "uq_user_menu_config", + "admin_menu_configs", + ["user_id", "menu_item_id"], + ) + + # Drop frontend_type index + op.drop_index("idx_admin_menu_configs_frontend_type", table_name="admin_menu_configs") + + # Drop frontend_type column + op.drop_column("admin_menu_configs", "frontend_type") + + # Drop the enum type + sa.Enum('admin', 'vendor', name='frontendtype').drop(op.get_bind(), checkfirst=True) diff --git a/app/config/__init__.py b/app/config/__init__.py new file mode 100644 index 00000000..f0aa0e54 --- /dev/null +++ b/app/config/__init__.py @@ -0,0 +1,18 @@ +# app/config/__init__.py +"""Configuration modules for the application.""" + +from .menu_registry import ( + ADMIN_MENU_REGISTRY, + VENDOR_MENU_REGISTRY, + AdminMenuItem, + VendorMenuItem, + get_all_menu_item_ids, +) + +__all__ = [ + "ADMIN_MENU_REGISTRY", + "VENDOR_MENU_REGISTRY", + "AdminMenuItem", + "VendorMenuItem", + "get_all_menu_item_ids", +] diff --git a/app/config/menu_registry.py b/app/config/menu_registry.py new file mode 100644 index 00000000..386e6eab --- /dev/null +++ b/app/config/menu_registry.py @@ -0,0 +1,546 @@ +# app/config/menu_registry.py +""" +Menu registry for Admin and Vendor frontends. + +This module defines the complete menu structure for both frontends. +Menu items are identified by unique IDs that are used for: +- Storing visibility configuration in AdminMenuConfig +- Checking access in require_menu_access() dependency +- Rendering the sidebar dynamically + +The registry is the single source of truth for menu structure. +Database only stores visibility overrides (is_visible=False). +""" + +from enum import Enum + +from models.database.admin_menu_config import FrontendType + + +class AdminMenuItem(str, Enum): + """Admin frontend menu item identifiers.""" + + # Dashboard (always visible section) + DASHBOARD = "dashboard" + + # Super Admin section + ADMIN_USERS = "admin-users" + + # Platform Administration section + COMPANIES = "companies" + VENDORS = "vendors" + MESSAGES = "messages" + + # Vendor Operations section + VENDOR_PRODUCTS = "vendor-products" + CUSTOMERS = "customers" + INVENTORY = "inventory" + ORDERS = "orders" + + # Marketplace section + MARKETPLACE_LETZSHOP = "marketplace-letzshop" + + # Billing & Subscriptions section + SUBSCRIPTION_TIERS = "subscription-tiers" + SUBSCRIPTIONS = "subscriptions" + BILLING_HISTORY = "billing-history" + + # Content Management section + PLATFORMS = "platforms" + CONTENT_PAGES = "content-pages" + VENDOR_THEMES = "vendor-themes" + + # Developer Tools section + COMPONENTS = "components" + ICONS = "icons" + + # Platform Health section + PLATFORM_HEALTH = "platform-health" + TESTING = "testing" + CODE_QUALITY = "code-quality" + + # Platform Monitoring section + IMPORTS = "imports" + BACKGROUND_TASKS = "background-tasks" + LOGS = "logs" + NOTIFICATIONS = "notifications" + + # Platform Settings section + SETTINGS = "settings" + EMAIL_TEMPLATES = "email-templates" + MY_MENU = "my-menu" # Super admin only - personal menu configuration + + +class VendorMenuItem(str, Enum): + """Vendor frontend menu item identifiers.""" + + # Main section (always visible) + DASHBOARD = "dashboard" + ANALYTICS = "analytics" + + # Products & Inventory section + PRODUCTS = "products" + INVENTORY = "inventory" + MARKETPLACE = "marketplace" + + # Sales & Orders section + ORDERS = "orders" + LETZSHOP = "letzshop" + INVOICES = "invoices" + + # Customers section + CUSTOMERS = "customers" + MESSAGES = "messages" + NOTIFICATIONS = "notifications" + + # Shop & Content section + CONTENT_PAGES = "content-pages" + MEDIA = "media" + + # Account & Settings section + TEAM = "team" + PROFILE = "profile" + BILLING = "billing" + EMAIL_TEMPLATES = "email-templates" + SETTINGS = "settings" + + +# ============================================================================= +# Admin Menu Registry +# ============================================================================= + +ADMIN_MENU_REGISTRY = { + "frontend_type": FrontendType.ADMIN, + "sections": [ + { + "id": "main", + "label": None, # No header, always at top + "items": [ + { + "id": AdminMenuItem.DASHBOARD.value, + "label": "Dashboard", + "icon": "home", + "url": "/admin/dashboard", + }, + ], + }, + { + "id": "superAdmin", + "label": "Super Admin", + "super_admin_only": True, + "items": [ + { + "id": AdminMenuItem.ADMIN_USERS.value, + "label": "Admin Users", + "icon": "shield", + "url": "/admin/admin-users", + }, + ], + }, + { + "id": "platformAdmin", + "label": "Platform Administration", + "items": [ + { + "id": AdminMenuItem.COMPANIES.value, + "label": "Companies", + "icon": "office-building", + "url": "/admin/companies", + }, + { + "id": AdminMenuItem.VENDORS.value, + "label": "Vendors", + "icon": "shopping-bag", + "url": "/admin/vendors", + }, + { + "id": AdminMenuItem.MESSAGES.value, + "label": "Messages", + "icon": "chat-bubble-left-right", + "url": "/admin/messages", + }, + ], + }, + { + "id": "vendorOps", + "label": "Vendor Operations", + "items": [ + { + "id": AdminMenuItem.VENDOR_PRODUCTS.value, + "label": "Products", + "icon": "cube", + "url": "/admin/vendor-products", + }, + { + "id": AdminMenuItem.CUSTOMERS.value, + "label": "Customers", + "icon": "user-group", + "url": "/admin/customers", + }, + { + "id": AdminMenuItem.INVENTORY.value, + "label": "Inventory", + "icon": "archive", + "url": "/admin/inventory", + }, + { + "id": AdminMenuItem.ORDERS.value, + "label": "Orders", + "icon": "clipboard-list", + "url": "/admin/orders", + }, + ], + }, + { + "id": "marketplace", + "label": "Marketplace", + "items": [ + { + "id": AdminMenuItem.MARKETPLACE_LETZSHOP.value, + "label": "Letzshop", + "icon": "shopping-cart", + "url": "/admin/marketplace/letzshop", + }, + ], + }, + { + "id": "billing", + "label": "Billing & Subscriptions", + "items": [ + { + "id": AdminMenuItem.SUBSCRIPTION_TIERS.value, + "label": "Subscription Tiers", + "icon": "tag", + "url": "/admin/subscription-tiers", + }, + { + "id": AdminMenuItem.SUBSCRIPTIONS.value, + "label": "Vendor Subscriptions", + "icon": "credit-card", + "url": "/admin/subscriptions", + }, + { + "id": AdminMenuItem.BILLING_HISTORY.value, + "label": "Billing History", + "icon": "document-text", + "url": "/admin/billing-history", + }, + ], + }, + { + "id": "contentMgmt", + "label": "Content Management", + "items": [ + { + "id": AdminMenuItem.PLATFORMS.value, + "label": "Platforms", + "icon": "globe-alt", + "url": "/admin/platforms", + }, + { + "id": AdminMenuItem.CONTENT_PAGES.value, + "label": "Content Pages", + "icon": "document-text", + "url": "/admin/content-pages", + }, + { + "id": AdminMenuItem.VENDOR_THEMES.value, + "label": "Vendor Themes", + "icon": "color-swatch", + "url": "/admin/vendor-themes", + }, + ], + }, + { + "id": "devTools", + "label": "Developer Tools", + "items": [ + { + "id": AdminMenuItem.COMPONENTS.value, + "label": "Components", + "icon": "view-grid", + "url": "/admin/components", + }, + { + "id": AdminMenuItem.ICONS.value, + "label": "Icons", + "icon": "photograph", + "url": "/admin/icons", + }, + ], + }, + { + "id": "platformHealth", + "label": "Platform Health", + "items": [ + { + "id": AdminMenuItem.PLATFORM_HEALTH.value, + "label": "Capacity Monitor", + "icon": "chart-bar", + "url": "/admin/platform-health", + }, + { + "id": AdminMenuItem.TESTING.value, + "label": "Testing Hub", + "icon": "beaker", + "url": "/admin/testing", + }, + { + "id": AdminMenuItem.CODE_QUALITY.value, + "label": "Code Quality", + "icon": "shield-check", + "url": "/admin/code-quality", + }, + ], + }, + { + "id": "monitoring", + "label": "Platform Monitoring", + "items": [ + { + "id": AdminMenuItem.IMPORTS.value, + "label": "Import Jobs", + "icon": "cube", + "url": "/admin/imports", + }, + { + "id": AdminMenuItem.BACKGROUND_TASKS.value, + "label": "Background Tasks", + "icon": "collection", + "url": "/admin/background-tasks", + }, + { + "id": AdminMenuItem.LOGS.value, + "label": "Application Logs", + "icon": "document-text", + "url": "/admin/logs", + }, + { + "id": AdminMenuItem.NOTIFICATIONS.value, + "label": "Notifications", + "icon": "bell", + "url": "/admin/notifications", + }, + ], + }, + { + "id": "settingsSection", + "label": "Platform Settings", + "items": [ + { + "id": AdminMenuItem.SETTINGS.value, + "label": "General", + "icon": "cog", + "url": "/admin/settings", + }, + { + "id": AdminMenuItem.EMAIL_TEMPLATES.value, + "label": "Email Templates", + "icon": "mail", + "url": "/admin/email-templates", + }, + { + "id": AdminMenuItem.MY_MENU.value, + "label": "My Menu", + "icon": "view-grid", + "url": "/admin/my-menu", + "super_admin_only": True, # Only super admins can customize their menu + }, + ], + }, + ], +} + + +# ============================================================================= +# Vendor Menu Registry +# ============================================================================= + +VENDOR_MENU_REGISTRY = { + "frontend_type": FrontendType.VENDOR, + "sections": [ + { + "id": "main", + "label": None, # No header, always at top + "items": [ + { + "id": VendorMenuItem.DASHBOARD.value, + "label": "Dashboard", + "icon": "home", + "url": "/dashboard", # Relative to /vendor/{code}/ + }, + { + "id": VendorMenuItem.ANALYTICS.value, + "label": "Analytics", + "icon": "chart-bar", + "url": "/analytics", + }, + ], + }, + { + "id": "products", + "label": "Products & Inventory", + "items": [ + { + "id": VendorMenuItem.PRODUCTS.value, + "label": "All Products", + "icon": "shopping-bag", + "url": "/products", + }, + { + "id": VendorMenuItem.INVENTORY.value, + "label": "Inventory", + "icon": "clipboard-list", + "url": "/inventory", + }, + { + "id": VendorMenuItem.MARKETPLACE.value, + "label": "Marketplace Import", + "icon": "download", + "url": "/marketplace", + }, + ], + }, + { + "id": "sales", + "label": "Sales & Orders", + "items": [ + { + "id": VendorMenuItem.ORDERS.value, + "label": "Orders", + "icon": "document-text", + "url": "/orders", + }, + { + "id": VendorMenuItem.LETZSHOP.value, + "label": "Letzshop Orders", + "icon": "external-link", + "url": "/letzshop", + }, + { + "id": VendorMenuItem.INVOICES.value, + "label": "Invoices", + "icon": "currency-euro", + "url": "/invoices", + }, + ], + }, + { + "id": "customers", + "label": "Customers", + "items": [ + { + "id": VendorMenuItem.CUSTOMERS.value, + "label": "All Customers", + "icon": "user-group", + "url": "/customers", + }, + { + "id": VendorMenuItem.MESSAGES.value, + "label": "Messages", + "icon": "chat-bubble-left-right", + "url": "/messages", + }, + { + "id": VendorMenuItem.NOTIFICATIONS.value, + "label": "Notifications", + "icon": "bell", + "url": "/notifications", + }, + ], + }, + { + "id": "shop", + "label": "Shop & Content", + "items": [ + { + "id": VendorMenuItem.CONTENT_PAGES.value, + "label": "Content Pages", + "icon": "document-text", + "url": "/content-pages", + }, + { + "id": VendorMenuItem.MEDIA.value, + "label": "Media Library", + "icon": "photograph", + "url": "/media", + }, + ], + }, + { + "id": "account", + "label": "Account & Settings", + "items": [ + { + "id": VendorMenuItem.TEAM.value, + "label": "Team", + "icon": "user-group", + "url": "/team", + }, + { + "id": VendorMenuItem.PROFILE.value, + "label": "Profile", + "icon": "user", + "url": "/profile", + }, + { + "id": VendorMenuItem.BILLING.value, + "label": "Billing", + "icon": "credit-card", + "url": "/billing", + }, + { + "id": VendorMenuItem.EMAIL_TEMPLATES.value, + "label": "Email Templates", + "icon": "mail", + "url": "/email-templates", + }, + { + "id": VendorMenuItem.SETTINGS.value, + "label": "Settings", + "icon": "adjustments", + "url": "/settings", + }, + ], + }, + ], +} + + +# ============================================================================= +# Helper Functions +# ============================================================================= + + +def get_all_menu_item_ids(frontend_type: FrontendType) -> set[str]: + """Get all menu item IDs for a frontend type.""" + registry = ( + ADMIN_MENU_REGISTRY if frontend_type == FrontendType.ADMIN else VENDOR_MENU_REGISTRY + ) + items = set() + for section in registry["sections"]: + for item in section["items"]: + items.add(item["id"]) + return items + + +def get_menu_item(frontend_type: FrontendType, menu_item_id: str) -> dict | None: + """Get a menu item definition by ID.""" + registry = ( + ADMIN_MENU_REGISTRY if frontend_type == FrontendType.ADMIN else VENDOR_MENU_REGISTRY + ) + for section in registry["sections"]: + for item in section["items"]: + if item["id"] == menu_item_id: + return {**item, "section_id": section["id"], "section_label": section.get("label")} + return None + + +def is_super_admin_only_item(menu_item_id: str) -> bool: + """Check if a menu item is in a super_admin_only section.""" + for section in ADMIN_MENU_REGISTRY["sections"]: + if section.get("super_admin_only"): + for item in section["items"]: + if item["id"] == menu_item_id: + return True + return False diff --git a/app/services/platform_service.py b/app/services/platform_service.py index 248b12d9..ff8ccbb9 100644 --- a/app/services/platform_service.py +++ b/app/services/platform_service.py @@ -20,7 +20,7 @@ from sqlalchemy.orm import Session from app.exceptions.platform import ( PlatformNotFoundException, ) -from models.database.content_page import ContentPage +from app.modules.cms.models import ContentPage from models.database.platform import Platform from models.database.vendor_platform import VendorPlatform @@ -81,6 +81,28 @@ class PlatformService: """ return db.query(Platform).filter(Platform.code == code).first() + @staticmethod + def get_platform_by_id(db: Session, platform_id: int) -> Platform: + """ + Get platform by ID. + + Args: + db: Database session + platform_id: Platform ID + + Returns: + Platform object + + Raises: + PlatformNotFoundException: If platform not found + """ + platform = db.query(Platform).filter(Platform.id == platform_id).first() + + if not platform: + raise PlatformNotFoundException(str(platform_id)) + + return platform + @staticmethod def list_platforms( db: Session, include_inactive: bool = False diff --git a/app/templates/admin/my-menu-config.html b/app/templates/admin/my-menu-config.html new file mode 100644 index 00000000..a707936e --- /dev/null +++ b/app/templates/admin/my-menu-config.html @@ -0,0 +1,174 @@ +{# app/templates/admin/my-menu-config.html #} +{% extends "admin/base.html" %} +{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %} +{% from 'shared/macros/headers.html' import page_header %} + +{% block title %}My Menu{% endblock %} + +{% block alpine_data %}adminMyMenuConfig(){% endblock %} + +{% block content %} +{{ page_header('My Menu Configuration', subtitle='Customize your personal admin sidebar', back_url='/admin/settings') }} + +{{ alert_dynamic(type='success', title='Success', message_var='successMessage', show_condition='successMessage') }} +{{ error_state('Error', show_condition='error') }} + + +
+
+ +
+

+ This configures your personal admin sidebar menu. These settings only affect your view. +

+

+ To configure menus for platform admins or vendors, go to Platforms and select a platform's Menu Configuration. +

+
+
+
+ + +
+
+
+ +
+
+

Total Items

+

+
+
+ +
+
+ +
+
+

Visible

+

+
+
+ +
+
+ +
+
+

Hidden

+

+
+
+
+ + +
+

+ Toggle visibility for menu items. Mandatory items cannot be hidden. +

+
+ + +
+
+ + +
+ + Loading menu configuration... +
+ + +
+ + + +
+ +

No menu items available.

+
+
+ +{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/app/templates/admin/partials/sidebar.html b/app/templates/admin/partials/sidebar.html index ee2b4b67..d1d372a9 100644 --- a/app/templates/admin/partials/sidebar.html +++ b/app/templates/admin/partials/sidebar.html @@ -38,9 +38,9 @@ {% endmacro %} -{# Macro for menu item #} +{# Macro for menu item with visibility check #} {% macro menu_item(page_id, url, icon, label) %} -
  • +
  • +
    Admin Portal @@ -77,80 +77,110 @@ - {{ section_header('Platform Administration', 'platformAdmin') }} - {% call section_content('platformAdmin') %} - {{ menu_item('companies', '/admin/companies', 'office-building', 'Companies') }} - {{ menu_item('vendors', '/admin/vendors', 'shopping-bag', 'Vendors') }} - {{ menu_item('messages', '/admin/messages', 'chat-bubble-left-right', 'Messages') }} - {% endcall %} +
    + {{ section_header('Platform Administration', 'platformAdmin') }} + {% call section_content('platformAdmin') %} + {{ menu_item('companies', '/admin/companies', 'office-building', 'Companies') }} + {{ menu_item('vendors', '/admin/vendors', 'shopping-bag', 'Vendors') }} + {{ menu_item('messages', '/admin/messages', 'chat-bubble-left-right', 'Messages') }} + {% endcall %} +
    - {{ section_header('Vendor Operations', 'vendorOps') }} - {% call section_content('vendorOps') %} - {{ menu_item('vendor-products', '/admin/vendor-products', 'cube', 'Products') }} - {{ menu_item('customers', '/admin/customers', 'user-group', 'Customers') }} - {{ menu_item('inventory', '/admin/inventory', 'archive', 'Inventory') }} - {{ menu_item('orders', '/admin/orders', 'clipboard-list', 'Orders') }} - {# Future items - uncomment when implemented: - {{ menu_item('shipping', '/admin/shipping', 'truck', 'Shipping') }} - #} - {% endcall %} +
    + {{ section_header('Vendor Operations', 'vendorOps') }} + {% call section_content('vendorOps') %} + {{ menu_item('vendor-products', '/admin/vendor-products', 'cube', 'Products') }} + {{ menu_item('customers', '/admin/customers', 'user-group', 'Customers') }} + {{ menu_item('inventory', '/admin/inventory', 'archive', 'Inventory') }} + {{ menu_item('orders', '/admin/orders', 'clipboard-list', 'Orders') }} + {# Future items - uncomment when implemented: + {{ menu_item('shipping', '/admin/shipping', 'truck', 'Shipping') }} + #} + {% endcall %} +
    - {{ section_header('Marketplace', 'marketplace') }} - {% call section_content('marketplace') %} - {{ menu_item('marketplace-letzshop', '/admin/marketplace/letzshop', 'shopping-cart', 'Letzshop') }} - {% endcall %} +
    + {{ section_header('Marketplace', 'marketplace') }} + {% call section_content('marketplace') %} + {{ menu_item('marketplace-letzshop', '/admin/marketplace/letzshop', 'shopping-cart', 'Letzshop') }} + {% endcall %} +
    - {{ section_header('Billing & Subscriptions', 'billing') }} - {% call section_content('billing') %} - {{ menu_item('subscription-tiers', '/admin/subscription-tiers', 'tag', 'Subscription Tiers') }} - {{ menu_item('subscriptions', '/admin/subscriptions', 'credit-card', 'Vendor Subscriptions') }} - {{ menu_item('billing-history', '/admin/billing-history', 'document-text', 'Billing History') }} - {% endcall %} +
    + {{ section_header('Billing & Subscriptions', 'billing') }} + {% call section_content('billing') %} + {{ menu_item('subscription-tiers', '/admin/subscription-tiers', 'tag', 'Subscription Tiers') }} + {{ menu_item('subscriptions', '/admin/subscriptions', 'credit-card', 'Vendor Subscriptions') }} + {{ menu_item('billing-history', '/admin/billing-history', 'document-text', 'Billing History') }} + {% endcall %} +
    - {{ section_header('Content Management', 'contentMgmt') }} - {% call section_content('contentMgmt') %} - {{ menu_item('platforms', '/admin/platforms', 'globe-alt', 'Platforms') }} - {{ menu_item('content-pages', '/admin/content-pages', 'document-text', 'Content Pages') }} - {{ menu_item('vendor-theme', '/admin/vendor-themes', 'color-swatch', 'Vendor Themes') }} - {% endcall %} +
    + {{ section_header('Content Management', 'contentMgmt') }} + {% call section_content('contentMgmt') %} + {{ menu_item('platforms', '/admin/platforms', 'globe-alt', 'Platforms') }} + {{ menu_item('content-pages', '/admin/content-pages', 'document-text', 'Content Pages') }} + {{ menu_item('vendor-themes', '/admin/vendor-themes', 'color-swatch', 'Vendor Themes') }} + {% endcall %} +
    - {{ section_header('Developer Tools', 'devTools') }} - {% call section_content('devTools') %} - {{ menu_item('components', '/admin/components', 'view-grid', 'Components') }} - {{ menu_item('icons', '/admin/icons', 'photograph', 'Icons') }} - {% endcall %} +
    + {{ section_header('Developer Tools', 'devTools') }} + {% call section_content('devTools') %} + {{ menu_item('components', '/admin/components', 'view-grid', 'Components') }} + {{ menu_item('icons', '/admin/icons', 'photograph', 'Icons') }} + {% endcall %} +
    - {{ section_header('Platform Health', 'platformHealth') }} - {% call section_content('platformHealth') %} - {{ menu_item('platform-health', '/admin/platform-health', 'chart-bar', 'Capacity Monitor') }} - {{ menu_item('testing', '/admin/testing', 'beaker', 'Testing Hub') }} - {{ menu_item('code-quality', '/admin/code-quality', 'shield-check', 'Code Quality') }} - {% endcall %} +
    + {{ section_header('Platform Health', 'platformHealth') }} + {% call section_content('platformHealth') %} + {{ menu_item('platform-health', '/admin/platform-health', 'chart-bar', 'Capacity Monitor') }} + {{ menu_item('testing', '/admin/testing', 'beaker', 'Testing Hub') }} + {{ menu_item('code-quality', '/admin/code-quality', 'shield-check', 'Code Quality') }} + {% endcall %} +
    - {{ section_header('Platform Monitoring', 'monitoring') }} - {% call section_content('monitoring') %} - {{ menu_item('imports', '/admin/imports', 'cube', 'Import Jobs') }} - {{ menu_item('background-tasks', '/admin/background-tasks', 'collection', 'Background Tasks') }} - {{ menu_item('logs', '/admin/logs', 'document-text', 'Application Logs') }} - {{ menu_item('notifications', '/admin/notifications', 'bell', 'Notifications') }} - {% endcall %} +
    + {{ section_header('Platform Monitoring', 'monitoring') }} + {% call section_content('monitoring') %} + {{ menu_item('imports', '/admin/imports', 'cube', 'Import Jobs') }} + {{ menu_item('background-tasks', '/admin/background-tasks', 'collection', 'Background Tasks') }} + {{ menu_item('logs', '/admin/logs', 'document-text', 'Application Logs') }} + {{ menu_item('notifications', '/admin/notifications', 'bell', 'Notifications') }} + {% endcall %} +
    - {{ section_header('Platform Settings', 'settingsSection') }} - {% call section_content('settingsSection') %} - {{ menu_item('settings', '/admin/settings', 'cog', 'General') }} - {{ menu_item('email-templates', '/admin/email-templates', 'mail', 'Email Templates') }} - {# TODO: Implement profile and API keys pages #} - {# {{ menu_item('profile', '/admin/profile', 'user-circle', 'Profile') }} #} - {# {{ menu_item('api-keys', '/admin/api-keys', 'key', 'API Keys') }} #} - {% endcall %} +
    + {{ section_header('Platform Settings', 'settingsSection') }} + {% call section_content('settingsSection') %} + {{ menu_item('settings', '/admin/settings', 'cog', 'General') }} + {{ menu_item('email-templates', '/admin/email-templates', 'mail', 'Email Templates') }} + + + {# TODO: Implement profile and API keys pages #} + {# {{ menu_item('profile', '/admin/profile', 'user-circle', 'Profile') }} #} + {# {{ menu_item('api-keys', '/admin/api-keys', 'key', 'API Keys') }} #} + {% endcall %} +
    {% endmacro %} diff --git a/app/templates/admin/platform-menu-config.html b/app/templates/admin/platform-menu-config.html new file mode 100644 index 00000000..0be972a2 --- /dev/null +++ b/app/templates/admin/platform-menu-config.html @@ -0,0 +1,200 @@ +{# app/templates/admin/platform-menu-config.html #} +{% extends "admin/base.html" %} +{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %} +{% from 'shared/macros/headers.html' import page_header %} + +{% block title %}Menu Configuration{% endblock %} + +{% block alpine_data %}adminPlatformMenuConfig('{{ platform_code }}'){% endblock %} + +{% block content %} +{{ page_header('Menu Configuration', back_url='/admin/platforms/' + platform_code) }} + +{{ alert_dynamic(type='success', title='Success', message_var='successMessage', show_condition='successMessage') }} +{{ error_state('Error', show_condition='error') }} + + +
    +
    +
    +

    +

    + Configure which menu items are visible for admins and vendors on this platform. +

    +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +
    +

    Total Items

    +

    +
    +
    + +
    +
    + +
    +
    +

    Visible

    +

    +
    +
    + +
    +
    + +
    +
    +

    Hidden

    +

    +
    +
    +
    + + +
    +

    + Toggle visibility for menu items. Mandatory items cannot be hidden. +

    +
    + + +
    +
    + + +
    + + Loading menu configuration... +
    + + +
    + + + +
    + +

    No menu items configured for this frontend type.

    +
    +
    + +{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/docs/proposals/SESSION_NOTE_2026-01-26_self-contained-modules.md b/docs/proposals/SESSION_NOTE_2026-01-26_self-contained-modules.md new file mode 100644 index 00000000..49617192 --- /dev/null +++ b/docs/proposals/SESSION_NOTE_2026-01-26_self-contained-modules.md @@ -0,0 +1,300 @@ +# Session Note: Self-Contained Module Architecture + +**Date:** 2026-01-26 +**Plan Reference:** `docs/proposals/TEMP.md` (now this file) +**Previous Session:** `docs/proposals/SESSION_NOTE_2026-01-25_modular-platform-architecture.md` + +--- + +## Summary + +Transformed thin module wrappers into fully self-contained modules, using CMS as the pilot. Each self-contained module is an autonomous unit with its own services, models, schemas, templates, exceptions, and locales. + +--- + +## Completed Phases + +### Phase 1: Foundation + +Created infrastructure for self-contained modules: + +| File | Purpose | +|------|---------| +| `app/modules/contracts/` | Protocol definitions for cross-module dependencies | +| `app/templates_config.py` | Multi-directory template loader for module templates | +| `app/modules/base.py` | Enhanced ModuleDefinition with self-contained flags | + +**Git tag:** `pre-modular-architecture` + +### Phase 2: CMS Pilot (Full Self-Contained Module) + +Migrated CMS to be the first fully self-contained module: + +| Component | Location | Status | +|-----------|----------|--------| +| Services | `app/modules/cms/services/content_page_service.py` | ✅ | +| Models | `app/modules/cms/models/content_page.py` | ✅ | +| Schemas | `app/modules/cms/schemas/content_page.py` | ✅ | +| Exceptions | `app/modules/cms/exceptions.py` | ✅ | +| Locales | `app/modules/cms/locales/{en,fr,de,lb}.json` | ✅ | +| Templates | `app/modules/cms/templates/cms/{admin,vendor}/` | ✅ | +| Static | `app/modules/cms/static/{admin,vendor}/js/` | ✅ | +| Routes | `app/modules/cms/routes/{api,pages}/` | ✅ | + +--- + +## CMS Module Structure + +``` +app/modules/cms/ +├── __init__.py # Lazy getter to avoid circular imports +├── definition.py # ModuleDefinition with self-contained config +├── exceptions.py # CMSException, ContentPageNotFoundError +├── locales/ +│ ├── en.json +│ ├── fr.json +│ ├── de.json +│ └── lb.json +├── models/ +│ ├── __init__.py # Exports: ContentPage, MediaFile, ProductMedia +│ └── content_page.py # ContentPage model (canonical location) +├── routes/ +│ ├── __init__.py +│ ├── admin.py # Admin router wrapper +│ ├── vendor.py # Vendor router wrapper +│ ├── api/ +│ │ ├── admin.py # Admin API endpoints +│ │ ├── vendor.py # Vendor API endpoints +│ │ └── shop.py # Shop/public API endpoints +│ └── pages/ +│ ├── admin.py # Admin page routes +│ └── vendor.py # Vendor page routes +├── schemas/ +│ ├── __init__.py +│ └── content_page.py # Pydantic schemas +├── services/ +│ ├── __init__.py +│ └── content_page_service.py +├── static/ +│ ├── admin/js/ +│ │ ├── content-pages.js +│ │ └── content-page-edit.js +│ └── vendor/js/ +│ ├── content-pages.js +│ └── content-page-edit.js +└── templates/ + └── cms/ + ├── admin/ + │ ├── content-pages.html + │ └── content-page-edit.html + └── vendor/ + ├── content-pages.html + └── content-page-edit.html +``` + +--- + +## Key Patterns Established + +### 1. Module-First Models + +Models live in module folders and are dynamically loaded at startup: + +```python +# app/modules/cms/models/content_page.py (canonical location) +from app.core.database import Base + +class ContentPage(Base): + __tablename__ = "content_pages" + ... + +# models/database/__init__.py (dynamic loader) +def _discover_module_models(): + for module_dir in sorted(modules_dir.iterdir()): + models_init = module_dir / "models" / "__init__.py" + if models_init.exists(): + importlib.import_module(f"app.modules.{module_dir.name}.models") + +_discover_module_models() +``` + +### 2. Shared Templates Instance + +Route files must import from `app.templates_config`: + +```python +# CORRECT +from app.templates_config import templates + +# WRONG - creates local instance without module loaders +templates = Jinja2Templates(directory="app/templates") +``` + +### 3. Template Namespacing + +Module templates use namespace prefix to avoid collisions: + +```python +# Module templates at: app/modules/cms/templates/cms/admin/content-pages.html +# Rendered as: +templates.TemplateResponse("cms/admin/content-pages.html", ...) +``` + +### 4. Import Pattern + +All code should import from module: + +```python +# Models +from app.modules.cms.models import ContentPage + +# Services +from app.modules.cms.services import content_page_service + +# Exceptions +from app.modules.cms.exceptions import ContentPageNotFoundException +``` + +### 5. Lazy Imports for Circular Import Prevention + +```python +# app/modules/cms/__init__.py +def get_cms_module(): + """Lazy getter for cms_module to avoid circular imports.""" + from app.modules.cms.definition import cms_module + return cms_module +``` + +--- + +## Decisions Made + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Pilot Module | CMS | Simplest, minimal dependencies | +| Cross-Module Pattern | Protocol pattern | Type-safe interfaces | +| Timeline | Incremental | Alongside feature work | +| Backwards Compatibility | No shims | Pre-launch, can delete old files | +| Template Namespace | `{module}/admin/`, `{module}/vendor/` | Prevent collisions | + +--- + +## Verification Completed + +- [x] `python -c "from main import app"` succeeds +- [x] ContentPage model in `app/modules/cms/models/content_page.py` +- [x] Dynamic model loader in `models/database/__init__.py` +- [x] `content_pages` table in Base.metadata (67 total tables) +- [x] Template files in correct locations +- [x] Route files use shared templates instance +- [x] Admin CMS pages render correctly +- [x] Vendor CMS pages render correctly + +--- + +## What Stays in Core vs Moves to Modules + +### Core (Stays in Place) + +| Component | Location | Reason | +|-----------|----------|--------| +| User, Vendor, Company, Platform models | `models/database/` | Foundational entities | +| Auth service | `app/services/` | Cross-cutting concern | +| Storage, Cache, Email services | `app/services/` | Infrastructure utilities | +| Base exceptions | `app/exceptions/` | Shared error types | +| Shared macros, partials | `app/templates/shared/` | Reusable UI components | +| API dependencies | `app/api/deps.py` | Auth, module access checks | + +### Modules (Move to Self-Contained) + +| Module | Services | Models | Status | +|--------|----------|--------|--------| +| cms | content_page, media | content_page, media | ✅ Complete | +| billing | billing, stripe, invoice, subscription | subscription, invoice, payment | Pending | +| inventory | inventory, inventory_transaction | inventory, inventory_transaction | Pending | +| orders | order, cart, order_item_exception | order, order_item_exception | Pending | +| marketplace | marketplace, marketplace_product, letzshop_export | marketplace_product, import_job | Pending | +| customers | customer, customer_address | customer | Pending | +| messaging | messaging, notification | message, notification | Pending | +| analytics | stats, capacity_forecast | (uses other models) | Pending | +| monitoring | background_tasks, test_runner, log | test_run, architecture_scan | Pending | + +--- + +## Pending/Next Steps + +### Phase 3: Simple Modules Migration + +- [ ] Migrate analytics module +- [ ] Migrate monitoring module +- [ ] Migrate messaging module +- [ ] Migrate customers module + +### Phase 4: Complex Modules Migration + +- [ ] Migrate billing (with Stripe integration) +- [ ] Migrate inventory +- [ ] Migrate orders +- [ ] Migrate marketplace + +### Phase 5: Cleanup + +- [ ] Remove deprecated shims (if any created) +- [ ] Update all imports across codebase +- [ ] Delete `app/platforms/` directory +- [ ] Update architecture documentation + +### Other Pending Items + +- [ ] Wire up vendor module routers to `app/api/v1/vendor/__init__.py` +- [ ] PlatformModule database table (optional - for audit trail) +- [ ] Module-specific configuration UI +- [ ] Integration tests for `/api/v1/admin/modules/*` endpoints + +--- + +## Git Commits + +``` +ec4ec04 feat: complete CMS as fully autonomous self-contained module +0b65864 fix: resolve circular import in CMS module +3ffa890 fix: correct static file mount order and update architecture validator +3307205 feat: add module info and configuration pages to admin panel +``` + +--- + +## Key Files Reference + +### Self-Contained Module Infrastructure + +- `app/modules/base.py` - ModuleDefinition with self-contained flags +- `app/modules/contracts/` - Protocol definitions for cross-module deps +- `app/templates_config.py` - Multi-directory template loader +- `models/database/__init__.py` - Dynamic module model discovery + +### CMS Module (Pilot) + +- `app/modules/cms/definition.py` - Module metadata +- `app/modules/cms/models/content_page.py` - ContentPage model +- `app/modules/cms/services/content_page_service.py` - Business logic +- `app/modules/cms/exceptions.py` - Module-specific exceptions +- `app/modules/cms/templates/cms/` - Namespaced templates + +--- + +## Testing + +```bash +# Verify app starts +python -c "from main import app; print('OK')" + +# Run module service tests +python -m pytest tests/unit/services/test_module_service.py -v + +# Verify CMS model is loaded +python -c "from app.modules.cms.models import ContentPage; print(ContentPage.__tablename__)" + +# Verify template loading +python -c "from app.templates_config import templates; print(templates.env.loader)" +``` diff --git a/docs/proposals/TEMP.md b/docs/proposals/TEMP.md deleted file mode 100644 index 5fb22f14..00000000 --- a/docs/proposals/TEMP.md +++ /dev/null @@ -1,557 +0,0 @@ -● Platform Homepage Configuration Analysis - - Summary: Current Flexibility - ┌──────────────────────┬─────────────┬──────────────────────────────────────────────────┐ - │ Feature │ Flexibility │ Notes │ - ├──────────────────────┼─────────────┼──────────────────────────────────────────────────┤ - │ Homepage Content │ ✅ High │ Fully CMS-driven via ContentPage model │ - ├──────────────────────┼─────────────┼──────────────────────────────────────────────────┤ - │ Header/Footer Menus │ ⚠️ Medium │ Driven by ContentPage flags, flat structure only │ - ├──────────────────────┼─────────────┼──────────────────────────────────────────────────┤ - │ Platform Branding │ ✅ High │ Logo, favicon, colors via Platform model │ - ├──────────────────────┼─────────────┼──────────────────────────────────────────────────┤ - │ Vendor Themes │ ✅ High │ 7 presets + custom CSS + full color control │ - ├──────────────────────┼─────────────┼──────────────────────────────────────────────────┤ - │ Pricing/Features │ ❌ Low │ Hardcoded in TIER_LIMITS │ - ├──────────────────────┼─────────────┼──────────────────────────────────────────────────┤ - │ Navigation Structure │ ❌ Low │ No nested menus, no icons │ - └──────────────────────┴─────────────┴──────────────────────────────────────────────────┘ - --- - 1. Homepage Content Configuration - - Model: ContentPage with three-tier hierarchy - - Platform Marketing Pages (is_platform_page=True, vendor_id=NULL) - ↓ e.g., /pricing, /about, /features for oms.lu - - Vendor Default Pages (is_platform_page=False, vendor_id=NULL) - ↓ Fallback for all vendors (About, Shipping Policy, etc.) - - Vendor Override Pages (is_platform_page=False, vendor_id=set) - ↓ Vendor-specific customizations - - Configurable per page: - - title, content (HTML/Markdown), slug - - template (default, minimal, modern, full) - - meta_description, meta_keywords (SEO) - - show_in_header, show_in_footer, show_in_legal - - display_order, is_published - - --- - 2. Menu Configuration - - Current approach: Content-driven (no separate Menu model) - ┌───────────────┬─────────────┬─────────────────────┐ - │ Menu Location │ Source │ Filter │ - ├───────────────┼─────────────┼─────────────────────┤ - │ Header │ ContentPage │ show_in_header=True │ - ├───────────────┼─────────────┼─────────────────────┤ - │ Footer │ ContentPage │ show_in_footer=True │ - ├───────────────┼─────────────┼─────────────────────┤ - │ Legal bar │ ContentPage │ show_in_legal=True │ - └───────────────┴─────────────┴─────────────────────┘ - Limitations: - - Flat structure only (no dropdowns/submenus) - - No custom menu items (only links to content pages) - - No menu icons or special styling - - No external URLs - - --- - 3. Platform Model - - File: models/database/platform.py - - Platform: - code # 'main', 'oms', 'loyalty' - name # Display name - domain # Production: 'oms.lu' - path_prefix # Dev: '/oms/' - logo # Light mode logo URL - logo_dark # Dark mode logo URL - favicon # Favicon URL - theme_config # JSON: colors, fonts, etc. - default_language # 'fr', 'en', 'de' - supported_languages # ['fr', 'de', 'en'] - settings # JSON: feature flags - - --- - 4. Theme System - - Vendor-level only (not platform-level defaults) - ┌───────────────┬────────┬─────────────────────────────────────────────────────────────┐ - │ Property │ Type │ Options │ - ├───────────────┼────────┼─────────────────────────────────────────────────────────────┤ - │ Colors │ JSON │ primary, secondary, accent, background, text, border │ - ├───────────────┼────────┼─────────────────────────────────────────────────────────────┤ - │ Fonts │ String │ font_family_heading, font_family_body │ - ├───────────────┼────────┼─────────────────────────────────────────────────────────────┤ - │ Layout │ String │ grid, list, masonry │ - ├───────────────┼────────┼─────────────────────────────────────────────────────────────┤ - │ Header │ String │ fixed, static, transparent │ - ├───────────────┼────────┼─────────────────────────────────────────────────────────────┤ - │ Product cards │ String │ modern, classic, minimal │ - ├───────────────┼────────┼─────────────────────────────────────────────────────────────┤ - │ Custom CSS │ Text │ Injected into