revamping frontend logging system and reorganising documentation

This commit is contained in:
2025-10-28 21:07:26 +01:00
parent 5c80ba17c5
commit b0cc0385f8
68 changed files with 3481 additions and 624 deletions

6
.env
View File

@@ -6,9 +6,9 @@ DESCRIPTION=Advanced product management system with JWT authentication
VERSION=0.0.1
# Database Configuration
# DATABASE_URL=postgresql://username:password@localhost:5432/ecommerce_db
# DATABASE_URL=postgresql://username:password@localhost:5432/wizamart_db
# For development, you can use SQLite:
DATABASE_URL=sqlite:///./ecommerce.db
DATABASE_URL=sqlite:///./wizamart.db
# Documentation
# .env.development
@@ -49,5 +49,5 @@ SSL_PROVIDER=letsencrypt # or "cloudflare", "manual"
AUTO_PROVISION_SSL=False # Set to True if using automated SSL
# DNS verification
DNS_VERIFICATION_PREFIX=_letzshop-verify
DNS_VERIFICATION_PREFIX=_wizamart-verify
DNS_VERIFICATION_TTL=3600

View File

@@ -6,9 +6,9 @@ DESCRIPTION=Advanced product management system with JWT authentication
VERSION=0.0.1
# Database Configuration
# DATABASE_URL=postgresql://username:password@localhost:5432/ecommerce_db
# DATABASE_URL=postgresql://username:password@localhost:5432/wizamart_db
# For development, you can use SQLite:
DATABASE_URL=sqlite:///./ecommerce.db
DATABASE_URL=sqlite:///./wizamart.db
# Documentation
# .env.development
@@ -49,5 +49,5 @@ SSL_PROVIDER=letsencrypt # or "cloudflare", "manual"
AUTO_PROVISION_SSL=False # Set to True if using automated SSL
# DNS verification
DNS_VERIFICATION_PREFIX=_letzshop-verify
DNS_VERIFICATION_PREFIX=_wizamart-verify
DNS_VERIFICATION_TTL=3600

View File

@@ -1,4 +1,15 @@
# alembic/env.py
"""
Alembic migration environment configuration.
This file is responsible for:
1. Importing ALL database models so Alembic can detect schema changes
2. Configuring the database connection
3. Running migrations in online/offline mode
CRITICAL: Every model in models/database/__init__.py must be imported here!
"""
import os
import sys
from logging.config import fileConfig
@@ -12,93 +23,160 @@ sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from app.core.config import settings
from models.database.base import Base
# === IMPORTANT: Import all your DATABASE models here ===
# Only import SQLAlchemy models, not Pydantic API models
print("[ALEMBIC] Importing database models...")
# ============================================================================
# IMPORT ALL DATABASE MODELS
# ============================================================================
# CRITICAL: Every model must be imported here so Alembic can detect tables!
# If a model is not imported, Alembic will not create/update its table.
#
# Models list must match: models/database/__init__.py
# ============================================================================
# Core models
print("[ALEMBIC] Importing database models...")
print("=" * 70)
# ----------------------------------------------------------------------------
# ADMIN MODELS
# ----------------------------------------------------------------------------
try:
from models.database.admin import (
AdminAuditLog,
AdminNotification,
AdminSetting,
PlatformAlert,
AdminSession
)
print(" ✓ Admin models imported (5 models)")
print(" - AdminAuditLog")
print(" - AdminNotification")
print(" - AdminSetting")
print(" - PlatformAlert")
print(" - AdminSession")
except ImportError as e:
print(f" ✗ Admin models failed: {e}")
# ----------------------------------------------------------------------------
# USER MODEL
# ----------------------------------------------------------------------------
try:
from models.database.user import User
print(" ✓ User model imported")
except ImportError as e:
print(f" ✗ User model failed: {e}")
# ----------------------------------------------------------------------------
# VENDOR MODELS
# ----------------------------------------------------------------------------
try:
from models.database.vendor import Vendor, VendorUser, Role
print(" ✓ Vendor models imported (Vendor, VendorUser, Role)")
print(" ✓ Vendor models imported (3 models)")
print(" - Vendor")
print(" - VendorUser")
print(" - Role")
except ImportError as e:
print(f" ✗ Vendor models failed: {e}")
try:
from models.database.vendor_domain import VendorDomain
print(" ✓ VendorDomain model imported")
except ImportError as e:
print(f" ✗ VendorDomain model failed: {e}")
try:
from models.database.vendor_theme import VendorTheme
print(" ✓ VendorTheme model imported")
except ImportError as e:
print(f" ✗ VendorTheme model failed: {e}")
# Product models
try:
from models.database.marketplace_product import MarketplaceProduct
print(" ✓ MarketplaceProduct model imported")
except ImportError as e:
print(f" ✗ MarketplaceProduct model failed: {e}")
# ----------------------------------------------------------------------------
# PRODUCT MODELS
# ----------------------------------------------------------------------------
try:
from models.database.product import Product
print(" ✓ Product model imported")
except ImportError as e:
print(f" ✗ Product model failed: {e}")
# Inventory
try:
from models.database.marketplace_product import MarketplaceProduct
print(" ✓ MarketplaceProduct model imported")
except ImportError as e:
print(f" ✗ MarketplaceProduct model failed: {e}")
# ----------------------------------------------------------------------------
# INVENTORY MODEL
# ----------------------------------------------------------------------------
try:
from models.database.inventory import Inventory
print(" ✓ Inventory model imported")
except ImportError as e:
print(f" ✗ Inventory model failed: {e}")
# Marketplace imports
# ----------------------------------------------------------------------------
# MARKETPLACE IMPORT
# ----------------------------------------------------------------------------
try:
from models.database.marketplace_import_job import MarketplaceImportJob
print(" ✓ MarketplaceImportJob model imported")
except ImportError as e:
print(f" ✗ MarketplaceImportJob model failed: {e}")
# Customer models
# ----------------------------------------------------------------------------
# CUSTOMER MODELS
# ----------------------------------------------------------------------------
try:
from models.database.customer import Customer, CustomerAddress
print(" ✓ Customer models imported (Customer, CustomerAddress)")
print(" ✓ Customer models imported (2 models)")
print(" - Customer")
print(" - CustomerAddress")
except ImportError as e:
print(f" ✗ Customer models failed: {e}")
# Order models
# ----------------------------------------------------------------------------
# ORDER MODELS
# ----------------------------------------------------------------------------
try:
from models.database.order import Order, OrderItem
print(" ✓ Order models imported (Order, OrderItem)")
print(" ✓ Order models imported (2 models)")
print(" - Order")
print(" - OrderItem")
except ImportError as e:
print(f" ✗ Order models failed: {e}")
# Payment models (if you have them)
try:
from models.database.payment import Payment
print(" ✓ Payment model imported")
except ImportError as e:
print(f" - Payment model not found (optional)")
# ============================================================================
# SUMMARY
# ============================================================================
print("=" * 70)
print(f"[ALEMBIC] Model import completed")
print(f"[ALEMBIC] Tables detected in metadata:")
print("=" * 70)
# Shipping models (if you have them)
try:
from models.database.shipping import ShippingAddress
print(" ✓ Shipping model imported")
except ImportError as e:
print(f" - Shipping model not found (optional)")
if Base.metadata.tables:
for i, table_name in enumerate(sorted(Base.metadata.tables.keys()), 1):
print(f" {i:2d}. {table_name}")
print("=" * 70)
print(f"[ALEMBIC] Total tables: {len(Base.metadata.tables)}")
else:
print(" ⚠️ WARNING: No tables found in metadata!")
print(" This usually means models are not being imported correctly.")
print(f"\n[ALEMBIC] Model import completed.")
print(f"[ALEMBIC] Tables found: {list(Base.metadata.tables.keys())}")
print(f"[ALEMBIC] Total tables to create: {len(Base.metadata.tables)}\n")
print("=" * 70)
print()
# ============================================================================
# ALEMBIC CONFIGURATION
# ============================================================================
# Alembic Config object
config = context.config
@@ -109,11 +187,20 @@ config.set_main_option("sqlalchemy.url", settings.database_url)
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# Set target metadata from Base
target_metadata = Base.metadata
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode."""
"""
Run migrations in 'offline' mode.
This configures the context with just a URL and not an Engine,
though an Engine is acceptable here as well. By skipping the Engine
creation we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
@@ -127,7 +214,12 @@ def run_migrations_offline() -> None:
def run_migrations_online() -> None:
"""Run migrations in 'online' mode."""
"""
Run migrations in 'online' mode.
In this scenario we need to create an Engine and associate a connection
with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
@@ -135,12 +227,19 @@ def run_migrations_online() -> None:
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
context.configure(
connection=connection,
target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
# ============================================================================
# MAIN EXECUTION
# ============================================================================
if context.is_offline_mode():
run_migrations_offline()
else:

View File

@@ -1,8 +1,8 @@
"""initial_schema_with_proper_relationships
"""Initial migration - all tables
Revision ID: 6fe45d3d84c4
Revision ID: 4951b2e50581
Revises:
Create Date: 2025-10-07 22:11:56.036486
Create Date: 2025-10-27 22:28:33.137564
"""
from typing import Sequence, Union
@@ -12,7 +12,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '6fe45d3d84c4'
revision: str = '4951b2e50581'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
@@ -92,6 +92,112 @@ def upgrade() -> None:
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
op.create_table('admin_audit_logs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('admin_user_id', sa.Integer(), nullable=False),
sa.Column('action', sa.String(length=100), nullable=False),
sa.Column('target_type', sa.String(length=50), nullable=False),
sa.Column('target_id', sa.String(length=100), nullable=False),
sa.Column('details', sa.JSON(), nullable=True),
sa.Column('ip_address', sa.String(length=45), nullable=True),
sa.Column('user_agent', sa.Text(), nullable=True),
sa.Column('request_id', sa.String(length=100), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['admin_user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_admin_audit_logs_action'), 'admin_audit_logs', ['action'], unique=False)
op.create_index(op.f('ix_admin_audit_logs_admin_user_id'), 'admin_audit_logs', ['admin_user_id'], unique=False)
op.create_index(op.f('ix_admin_audit_logs_id'), 'admin_audit_logs', ['id'], unique=False)
op.create_index(op.f('ix_admin_audit_logs_target_id'), 'admin_audit_logs', ['target_id'], unique=False)
op.create_index(op.f('ix_admin_audit_logs_target_type'), 'admin_audit_logs', ['target_type'], unique=False)
op.create_table('admin_notifications',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('type', sa.String(length=50), nullable=False),
sa.Column('priority', sa.String(length=20), nullable=True),
sa.Column('title', sa.String(length=200), nullable=False),
sa.Column('message', sa.Text(), nullable=False),
sa.Column('is_read', sa.Boolean(), nullable=True),
sa.Column('read_at', sa.DateTime(), nullable=True),
sa.Column('read_by_user_id', sa.Integer(), nullable=True),
sa.Column('action_required', sa.Boolean(), nullable=True),
sa.Column('action_url', sa.String(length=500), nullable=True),
sa.Column('notification_metadata', sa.JSON(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['read_by_user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_admin_notifications_action_required'), 'admin_notifications', ['action_required'], unique=False)
op.create_index(op.f('ix_admin_notifications_id'), 'admin_notifications', ['id'], unique=False)
op.create_index(op.f('ix_admin_notifications_is_read'), 'admin_notifications', ['is_read'], unique=False)
op.create_index(op.f('ix_admin_notifications_priority'), 'admin_notifications', ['priority'], unique=False)
op.create_index(op.f('ix_admin_notifications_type'), 'admin_notifications', ['type'], unique=False)
op.create_table('admin_sessions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('admin_user_id', sa.Integer(), nullable=False),
sa.Column('session_token', sa.String(length=255), nullable=False),
sa.Column('ip_address', sa.String(length=45), nullable=False),
sa.Column('user_agent', sa.Text(), nullable=True),
sa.Column('login_at', sa.DateTime(), nullable=False),
sa.Column('last_activity_at', sa.DateTime(), nullable=False),
sa.Column('logout_at', sa.DateTime(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('logout_reason', sa.String(length=50), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['admin_user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_admin_sessions_admin_user_id'), 'admin_sessions', ['admin_user_id'], unique=False)
op.create_index(op.f('ix_admin_sessions_id'), 'admin_sessions', ['id'], unique=False)
op.create_index(op.f('ix_admin_sessions_is_active'), 'admin_sessions', ['is_active'], unique=False)
op.create_index(op.f('ix_admin_sessions_login_at'), 'admin_sessions', ['login_at'], unique=False)
op.create_index(op.f('ix_admin_sessions_session_token'), 'admin_sessions', ['session_token'], unique=True)
op.create_table('admin_settings',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('key', sa.String(length=100), nullable=False),
sa.Column('value', sa.Text(), nullable=False),
sa.Column('value_type', sa.String(length=20), nullable=True),
sa.Column('category', sa.String(length=50), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('is_encrypted', sa.Boolean(), nullable=True),
sa.Column('is_public', sa.Boolean(), nullable=True),
sa.Column('last_modified_by_user_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['last_modified_by_user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_admin_settings_category'), 'admin_settings', ['category'], unique=False)
op.create_index(op.f('ix_admin_settings_id'), 'admin_settings', ['id'], unique=False)
op.create_index(op.f('ix_admin_settings_key'), 'admin_settings', ['key'], unique=True)
op.create_table('platform_alerts',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('alert_type', sa.String(length=50), nullable=False),
sa.Column('severity', sa.String(length=20), nullable=False),
sa.Column('title', sa.String(length=200), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('affected_vendors', sa.JSON(), nullable=True),
sa.Column('affected_systems', sa.JSON(), nullable=True),
sa.Column('is_resolved', sa.Boolean(), nullable=True),
sa.Column('resolved_at', sa.DateTime(), nullable=True),
sa.Column('resolved_by_user_id', sa.Integer(), nullable=True),
sa.Column('resolution_notes', sa.Text(), nullable=True),
sa.Column('auto_generated', sa.Boolean(), nullable=True),
sa.Column('occurrence_count', sa.Integer(), nullable=True),
sa.Column('first_occurred_at', sa.DateTime(), nullable=False),
sa.Column('last_occurred_at', sa.DateTime(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['resolved_by_user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_platform_alerts_alert_type'), 'platform_alerts', ['alert_type'], unique=False)
op.create_index(op.f('ix_platform_alerts_id'), 'platform_alerts', ['id'], unique=False)
op.create_index(op.f('ix_platform_alerts_is_resolved'), 'platform_alerts', ['is_resolved'], unique=False)
op.create_index(op.f('ix_platform_alerts_severity'), 'platform_alerts', ['severity'], unique=False)
op.create_table('vendors',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('vendor_code', sa.String(), nullable=False),
@@ -99,7 +205,6 @@ def upgrade() -> None:
sa.Column('name', sa.String(), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('owner_user_id', sa.Integer(), nullable=False),
sa.Column('theme_config', sa.JSON(), nullable=True),
sa.Column('contact_email', sa.String(), nullable=True),
sa.Column('contact_phone', sa.String(), nullable=True),
sa.Column('website', sa.String(), nullable=True),
@@ -203,6 +308,54 @@ def upgrade() -> None:
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_roles_id'), 'roles', ['id'], unique=False)
op.create_table('vendor_domains',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('vendor_id', sa.Integer(), nullable=False),
sa.Column('domain', sa.String(length=255), nullable=False),
sa.Column('is_primary', sa.Boolean(), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('ssl_status', sa.String(length=50), nullable=True),
sa.Column('ssl_verified_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('verification_token', sa.String(length=100), nullable=True),
sa.Column('is_verified', sa.Boolean(), nullable=False),
sa.Column('verified_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('vendor_id', 'domain', name='uq_vendor_domain'),
sa.UniqueConstraint('verification_token')
)
op.create_index('idx_domain_active', 'vendor_domains', ['domain', 'is_active'], unique=False)
op.create_index('idx_vendor_primary', 'vendor_domains', ['vendor_id', 'is_primary'], unique=False)
op.create_index(op.f('ix_vendor_domains_domain'), 'vendor_domains', ['domain'], unique=True)
op.create_index(op.f('ix_vendor_domains_id'), 'vendor_domains', ['id'], unique=False)
op.create_table('vendor_themes',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('vendor_id', sa.Integer(), nullable=False),
sa.Column('theme_name', sa.String(length=100), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('colors', sa.JSON(), nullable=True),
sa.Column('font_family_heading', sa.String(length=100), nullable=True),
sa.Column('font_family_body', sa.String(length=100), nullable=True),
sa.Column('logo_url', sa.String(length=500), nullable=True),
sa.Column('logo_dark_url', sa.String(length=500), nullable=True),
sa.Column('favicon_url', sa.String(length=500), nullable=True),
sa.Column('banner_url', sa.String(length=500), nullable=True),
sa.Column('layout_style', sa.String(length=50), nullable=True),
sa.Column('header_style', sa.String(length=50), nullable=True),
sa.Column('product_card_style', sa.String(length=50), nullable=True),
sa.Column('custom_css', sa.Text(), nullable=True),
sa.Column('social_links', sa.JSON(), nullable=True),
sa.Column('meta_title_template', sa.String(length=200), nullable=True),
sa.Column('meta_description', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('vendor_id')
)
op.create_index(op.f('ix_vendor_themes_id'), 'vendor_themes', ['id'], unique=False)
op.create_table('customer_addresses',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('vendor_id', sa.Integer(), nullable=False),
@@ -342,6 +495,13 @@ def downgrade() -> None:
op.drop_table('inventory')
op.drop_index(op.f('ix_customer_addresses_id'), table_name='customer_addresses')
op.drop_table('customer_addresses')
op.drop_index(op.f('ix_vendor_themes_id'), table_name='vendor_themes')
op.drop_table('vendor_themes')
op.drop_index(op.f('ix_vendor_domains_id'), table_name='vendor_domains')
op.drop_index(op.f('ix_vendor_domains_domain'), table_name='vendor_domains')
op.drop_index('idx_vendor_primary', table_name='vendor_domains')
op.drop_index('idx_domain_active', table_name='vendor_domains')
op.drop_table('vendor_domains')
op.drop_index(op.f('ix_roles_id'), table_name='roles')
op.drop_table('roles')
op.drop_index(op.f('ix_products_id'), table_name='products')
@@ -363,6 +523,33 @@ def downgrade() -> None:
op.drop_index(op.f('ix_vendors_subdomain'), table_name='vendors')
op.drop_index(op.f('ix_vendors_id'), table_name='vendors')
op.drop_table('vendors')
op.drop_index(op.f('ix_platform_alerts_severity'), table_name='platform_alerts')
op.drop_index(op.f('ix_platform_alerts_is_resolved'), table_name='platform_alerts')
op.drop_index(op.f('ix_platform_alerts_id'), table_name='platform_alerts')
op.drop_index(op.f('ix_platform_alerts_alert_type'), table_name='platform_alerts')
op.drop_table('platform_alerts')
op.drop_index(op.f('ix_admin_settings_key'), table_name='admin_settings')
op.drop_index(op.f('ix_admin_settings_id'), table_name='admin_settings')
op.drop_index(op.f('ix_admin_settings_category'), table_name='admin_settings')
op.drop_table('admin_settings')
op.drop_index(op.f('ix_admin_sessions_session_token'), table_name='admin_sessions')
op.drop_index(op.f('ix_admin_sessions_login_at'), table_name='admin_sessions')
op.drop_index(op.f('ix_admin_sessions_is_active'), table_name='admin_sessions')
op.drop_index(op.f('ix_admin_sessions_id'), table_name='admin_sessions')
op.drop_index(op.f('ix_admin_sessions_admin_user_id'), table_name='admin_sessions')
op.drop_table('admin_sessions')
op.drop_index(op.f('ix_admin_notifications_type'), table_name='admin_notifications')
op.drop_index(op.f('ix_admin_notifications_priority'), table_name='admin_notifications')
op.drop_index(op.f('ix_admin_notifications_is_read'), table_name='admin_notifications')
op.drop_index(op.f('ix_admin_notifications_id'), table_name='admin_notifications')
op.drop_index(op.f('ix_admin_notifications_action_required'), table_name='admin_notifications')
op.drop_table('admin_notifications')
op.drop_index(op.f('ix_admin_audit_logs_target_type'), table_name='admin_audit_logs')
op.drop_index(op.f('ix_admin_audit_logs_target_id'), table_name='admin_audit_logs')
op.drop_index(op.f('ix_admin_audit_logs_id'), table_name='admin_audit_logs')
op.drop_index(op.f('ix_admin_audit_logs_admin_user_id'), table_name='admin_audit_logs')
op.drop_index(op.f('ix_admin_audit_logs_action'), table_name='admin_audit_logs')
op.drop_table('admin_audit_logs')
op.drop_index(op.f('ix_users_username'), table_name='users')
op.drop_index(op.f('ix_users_id'), table_name='users')
op.drop_index(op.f('ix_users_email'), table_name='users')

View File

@@ -1,30 +0,0 @@
"""Add vendor and role tables for slice 1
Revision ID: 9189d3baaea1
Revises: 6fe45d3d84c4
Create Date: 2025-10-08 22:39:53.101668
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '9189d3baaea1'
down_revision: Union[str, None] = '6fe45d3d84c4'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

View File

@@ -266,14 +266,14 @@ def verify_domain_ownership(
Verify domain ownership via DNS TXT record (Admin only).
**Verification Process:**
1. Queries DNS for TXT record: `_letzshop-verify.{domain}`
1. Queries DNS for TXT record: `_wizamart-verify.{domain}`
2. Checks if verification token matches
3. If found, marks domain as verified
**Requirements:**
- Vendor must have added TXT record to their DNS
- DNS propagation may take 5-15 minutes
- Record format: `_letzshop-verify.domain.com` TXT `{token}`
- Record format: `_wizamart-verify.domain.com` TXT `{token}`
**After verification:**
- Domain can be activated

View File

@@ -73,7 +73,7 @@ class Settings(BaseSettings):
auto_provision_ssl: bool = False # Set to True if using automated SSL
# DNS verification
dns_verification_prefix: str = "_letzshop-verify"
dns_verification_prefix: str = "_wizamart-verify"
dns_verification_ttl: int = 3600
model_config = {"env_file": ".env"} # Updated syntax for Pydantic v2

View File

@@ -286,7 +286,7 @@ class VendorDomainService:
Verify domain ownership via DNS TXT record.
The vendor must add a TXT record:
Name: _letzshop-verify.{domain}
Name: _wizamart-verify.{domain}
Value: {verification_token}
Args:
@@ -313,7 +313,7 @@ class VendorDomainService:
# Query DNS TXT records
try:
txt_records = dns.resolver.resolve(
f"_letzshop-verify.{domain.domain}",
f"_wizamart-verify.{domain.domain}",
'TXT'
)
@@ -339,7 +339,7 @@ class VendorDomainService:
except dns.resolver.NXDOMAIN:
raise DomainVerificationFailedException(
domain.domain,
f"DNS record _letzshop-verify.{domain.domain} not found"
f"DNS record _wizamart-verify.{domain.domain} not found"
)
except dns.resolver.NoAnswer:
raise DomainVerificationFailedException(
@@ -394,7 +394,7 @@ class VendorDomainService:
},
"txt_record": {
"type": "TXT",
"name": "_letzshop-verify",
"name": "_wizamart-verify",
"value": domain.verification_token,
"ttl": 3600
},

View File

@@ -40,7 +40,7 @@
<!-- Core Scripts - ORDER MATTERS! -->
<!-- 1. FIRST: Log Configuration -->
<script src="{{ url_for('static', path='admin/js/log-config.js') }}"></script>
<script src="{{ url_for('static', path='shared/js/log-config.js') }}"></script>
<!-- 2. SECOND: Icons (before Alpine.js) -->
<script src="{{ url_for('static', path='shared/js/icons.js') }}"></script>

View File

@@ -243,7 +243,7 @@
<!-- Typography Section -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
<span x-html="$icon('document-text', 'inline w-5 h-5 mr-2')"></span>
<span x-html="$icon('document', 'inline w-5 h-5 mr-2')"></span>
Typography
</h3>
<div class="grid gap-4 md:grid-cols-2">

View File

@@ -0,0 +1,562 @@
# Alpine.js Page Template - Quick Reference (WITH CENTRALIZED LOGGING)
## ✅ Correct Page Structure
```javascript
// static/admin/js/your-page.js (or vendor/shop)
// 1. ✅ Use centralized logger (ONE LINE!)
const yourPageLog = window.LogConfig.loggers.yourPage;
// OR create custom if not pre-configured
// const yourPageLog = window.LogConfig.createLogger('YOUR-PAGE', window.LogConfig.logLevel);
// 2. Create your Alpine.js component
function yourPageComponent() {
return {
// ✅ CRITICAL: Inherit base layout functionality
...data(),
// ✅ CRITICAL: Set page identifier
currentPage: 'your-page',
// Your page-specific state
items: [],
loading: false,
error: null,
// ✅ CRITICAL: Proper initialization with guard
async init() {
yourPageLog.info('=== YOUR PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._yourPageInitialized) {
yourPageLog.warn('Page already initialized, skipping...');
return;
}
window._yourPageInitialized = true;
// Load your data
await this.loadData();
yourPageLog.info('=== YOUR PAGE INITIALIZATION COMPLETE ===');
},
// Your methods
async loadData() {
yourPageLog.info('Loading data...');
this.loading = true;
this.error = null;
try {
const startTime = performance.now();
// Log API request
const url = '/your/endpoint';
window.LogConfig.logApiCall('GET', url, null, 'request');
// ✅ CRITICAL: Use lowercase apiClient
const response = await apiClient.get(url);
// Log API response
window.LogConfig.logApiCall('GET', url, response, 'response');
this.items = response.items || [];
// Log performance
const duration = performance.now() - startTime;
window.LogConfig.logPerformance('Load Data', duration);
yourPageLog.info(`Data loaded successfully`, {
count: this.items.length,
duration: `${duration}ms`
});
} catch (error) {
// Use centralized error logging
window.LogConfig.logError(error, 'Load Data');
this.error = error.message;
Utils.showToast('Failed to load data', 'error');
} finally {
this.loading = false;
}
},
// Format date helper (if needed)
formatDate(dateString) {
if (!dateString) return '-';
return Utils.formatDate(dateString);
},
// Your other methods...
};
}
yourPageLog.info('Your page module loaded');
```
---
## 🎯 Checklist for New Pages
### HTML Template
```jinja2
{# app/templates/admin/your-page.html #}
{% extends "admin/base.html" %}
{% block title %}Your Page{% endblock %}
{# ✅ CRITICAL: Link to your Alpine.js component #}
{% block alpine_data %}yourPageComponent(){% endblock %}
{% block content %}
{% endblock %}
{% block extra_scripts %}
{# ✅ CRITICAL: Load your JavaScript file #}
{% endblock %}
```
### JavaScript File Checklist
- [ ] ✅ Use centralized logger (ONE line instead of 15!)
- [ ] ✅ Function name matches `alpine_data` in template
- [ ]`...data(),` at start of return object
- [ ]`currentPage: 'your-page'` set
- [ ] ✅ Initialization guard in `init()`
- [ ] ✅ Use lowercase `apiClient` for API calls
- [ ] ✅ Use `window.LogConfig.logApiCall()` for API logging
- [ ] ✅ Use `window.LogConfig.logPerformance()` for performance
- [ ] ✅ Use `window.LogConfig.logError()` for errors
- [ ] ✅ Module loaded log at end
---
## 📦 Pre-configured Loggers by Frontend
### Admin Frontend
```javascript
window.LogConfig.loggers.vendors // Vendor management
window.LogConfig.loggers.vendorTheme // Theme customization
window.LogConfig.loggers.vendorUsers // Vendor users
window.LogConfig.loggers.products // Product management
window.LogConfig.loggers.inventory // Inventory
window.LogConfig.loggers.orders // Order management
window.LogConfig.loggers.users // User management
window.LogConfig.loggers.audit // Audit logs
window.LogConfig.loggers.dashboard // Dashboard
window.LogConfig.loggers.imports // Import operations
```
### Vendor Frontend
```javascript
window.LogConfig.loggers.dashboard // Vendor dashboard
window.LogConfig.loggers.products // Product management
window.LogConfig.loggers.inventory // Inventory
window.LogConfig.loggers.orders // Order management
window.LogConfig.loggers.theme // Theme customization
window.LogConfig.loggers.settings // Settings
window.LogConfig.loggers.analytics // Analytics
```
### Shop Frontend
```javascript
window.LogConfig.loggers.catalog // Product browsing
window.LogConfig.loggers.product // Product details
window.LogConfig.loggers.search // Search
window.LogConfig.loggers.cart // Shopping cart
window.LogConfig.loggers.checkout // Checkout
window.LogConfig.loggers.account // User account
window.LogConfig.loggers.orders // Order history
window.LogConfig.loggers.wishlist // Wishlist
```
---
## ❌ Common Mistakes to Avoid
### 1. Old Way vs New Way
```javascript
// ❌ OLD WAY - 15 lines of duplicate code
const YOUR_PAGE_LOG_LEVEL = 3;
const yourPageLog = {
error: (...args) => YOUR_PAGE_LOG_LEVEL >= 1 && console.error('❌ [YOUR_PAGE ERROR]', ...args),
warn: (...args) => YOUR_PAGE_LOG_LEVEL >= 2 && console.warn('⚠️ [YOUR_PAGE WARN]', ...args),
info: (...args) => YOUR_PAGE_LOG_LEVEL >= 3 && console.info(' [YOUR_PAGE INFO]', ...args),
debug: (...args) => YOUR_PAGE_LOG_LEVEL >= 4 && console.log('🔍 [YOUR_PAGE DEBUG]', ...args)
};
// ✅ NEW WAY - 1 line!
const yourPageLog = window.LogConfig.loggers.yourPage;
```
### 2. Missing Base Inheritance
```javascript
// ❌ WRONG
function myPage() {
return {
items: [],
// Missing ...data()
};
}
// ✅ CORRECT
function myPage() {
return {
...data(), // Must be first!
items: [],
};
}
```
### 3. Wrong API Client Name
```javascript
// ❌ WRONG - Capital letters
await ApiClient.get('/endpoint');
await API_CLIENT.get('/endpoint');
// ✅ CORRECT - lowercase
await apiClient.get('/endpoint');
```
### 4. Missing Init Guard
```javascript
// ❌ WRONG
async init() {
await this.loadData();
}
// ✅ CORRECT
async init() {
if (window._myPageInitialized) return;
window._myPageInitialized = true;
await this.loadData();
}
```
### 5. Missing currentPage
```javascript
// ❌ WRONG
return {
...data(),
items: [],
// Missing currentPage
};
// ✅ CORRECT
return {
...data(),
currentPage: 'my-page', // Must set this!
items: [],
};
```
---
## 🔧 API Client Pattern
### GET Request
```javascript
try {
const response = await apiClient.get('/endpoint');
this.data = response;
} catch (error) {
console.error('Failed:', error);
Utils.showToast('Failed to load', 'error');
}
```
### POST Request
```javascript
try {
const response = await apiClient.post('/endpoint', {
name: 'value',
// ... data
});
Utils.showToast('Created successfully', 'success');
} catch (error) {
console.error('Failed:', error);
Utils.showToast('Failed to create', 'error');
}
```
### PUT Request
```javascript
try {
const response = await apiClient.put('/endpoint/123', {
name: 'updated value'
});
Utils.showToast('Updated successfully', 'success');
} catch (error) {
console.error('Failed:', error);
Utils.showToast('Failed to update', 'error');
}
```
### DELETE Request
```javascript
try {
await apiClient.delete('/endpoint/123');
Utils.showToast('Deleted successfully', 'success');
await this.reloadData();
} catch (error) {
console.error('Failed:', error);
Utils.showToast('Failed to delete', 'error');
}
```
---
## 🔧 Centralized Logging Patterns
### Basic Logging
```javascript
const log = window.LogConfig.loggers.yourPage;
log.info('Page loaded');
log.warn('Connection slow');
log.error('Failed to load data', error);
log.debug('User data:', userData);
```
### Grouped Logging
```javascript
log.group('Loading Theme Data');
log.info('Fetching vendor...');
log.info('Fetching theme...');
log.info('Fetching presets...');
log.groupEnd();
```
### API Call Logging
```javascript
const url = '/api/vendors';
// Before request
window.LogConfig.logApiCall('GET', url, null, 'request');
// Make request
const data = await apiClient.get(url);
// After response
window.LogConfig.logApiCall('GET', url, data, 'response');
```
### Error Logging
```javascript
try {
await saveTheme();
} catch (error) {
window.LogConfig.logError(error, 'Save Theme');
}
```
### Performance Logging
```javascript
const start = performance.now();
await loadThemeData();
const duration = performance.now() - start;
window.LogConfig.logPerformance('Load Theme Data', duration);
```
### Table Logging
```javascript
log.table([
{ id: 1, name: 'Vendor A', status: 'active' },
{ id: 2, name: 'Vendor B', status: 'inactive' }
]);
```
---
## 📚 Benefits of Centralized Logging
| Aspect | Old Way | New Way |
|--------|---------|---------|
| **Lines of code** | ~15 per file | 1 line per file |
| **Consistency** | Varies by file | Unified across all frontends |
| **Maintenance** | Update each file | Update one shared file |
| **Features** | Basic logging | Advanced (groups, perf, API) |
| **Environment** | Manual config | Auto-detected |
| **Frontend aware** | No | Yes (admin/vendor/shop) |
| **Log levels** | Per file | Per frontend + environment |
---
## 🎨 Common UI Patterns
### Loading State
```javascript
async loadData() {
this.loading = true;
try {
const data = await apiClient.get('/endpoint');
this.items = data;
} finally {
this.loading = false;
}
}
```
### Refresh/Reload
```javascript
async refresh() {
console.info('Refreshing...');
await this.loadData();
Utils.showToast('Refreshed successfully', 'success');
}
```
---
## 📚 Available Utilities
### From `init-alpine.js` (via `...data()`)
- `this.dark` - Dark mode state
- `this.toggleTheme()` - Toggle theme
- `this.isSideMenuOpen` - Side menu state
- `this.toggleSideMenu()` - Toggle side menu
- `this.closeSideMenu()` - Close side menu
- `this.isNotificationsMenuOpen` - Notifications menu state
- `this.toggleNotificationsMenu()` - Toggle notifications
- `this.closeNotificationsMenu()` - Close notifications
- `this.isProfileMenuOpen` - Profile menu state
- `this.toggleProfileMenu()` - Toggle profile menu
- `this.closeProfileMenu()` - Close profile menu
- `this.isPagesMenuOpen` - Pages menu state
- `this.togglePagesMenu()` - Toggle pages menu
### From `Utils` (global)
- `Utils.showToast(message, type, duration)` - Show toast notification
- `Utils.formatDate(dateString)` - Format date for display
- `Utils.confirm(message, title)` - Show confirmation dialog (if available)
### From `apiClient` (global)
- `apiClient.get(url)` - GET request
- `apiClient.post(url, data)` - POST request
- `apiClient.put(url, data)` - PUT request
- `apiClient.delete(url)` - DELETE request
---
## 🎨 Complete Example
```javascript
// static/admin/js/vendor-theme.js
// 1. Use centralized logger
const themeLog = window.LogConfig.loggers.vendorTheme;
// 2. Create component
function adminVendorTheme() {
return {
...data(),
currentPage: 'vendor-theme',
vendor: null,
themeData: {},
loading: true,
async init() {
themeLog.info('Initializing vendor theme editor');
if (window._vendorThemeInitialized) {
themeLog.warn('Already initialized, skipping...');
return;
}
window._vendorThemeInitialized = true;
const startTime = performance.now();
try {
themeLog.group('Loading theme data');
await Promise.all([
this.loadVendor(),
this.loadTheme()
]);
themeLog.groupEnd();
const duration = performance.now() - startTime;
window.LogConfig.logPerformance('Theme Editor Init', duration);
themeLog.info('Theme editor initialized successfully');
} catch (error) {
window.LogConfig.logError(error, 'Theme Editor Init');
Utils.showToast('Failed to initialize', 'error');
} finally {
this.loading = false;
}
},
async loadVendor() {
const url = `/admin/vendors/${this.vendorCode}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const response = await apiClient.get(url);
this.vendor = response;
window.LogConfig.logApiCall('GET', url, response, 'response');
themeLog.debug('Vendor loaded:', this.vendor);
},
async saveTheme() {
themeLog.info('Saving theme changes');
try {
const url = `/admin/vendor-themes/${this.vendorCode}`;
window.LogConfig.logApiCall('PUT', url, this.themeData, 'request');
const response = await apiClient.put(url, this.themeData);
window.LogConfig.logApiCall('PUT', url, response, 'response');
themeLog.info('Theme saved successfully');
Utils.showToast('Theme saved', 'success');
} catch (error) {
window.LogConfig.logError(error, 'Save Theme');
Utils.showToast('Failed to save theme', 'error');
}
}
};
}
themeLog.info('Vendor theme editor module loaded');
```
---
## 🚀 Quick Start Template Files
Copy these to create a new page:
1. **Copy base file:** `dashboard.js` → rename to `your-page.js`
2. **Update logger:**
```javascript
// Change from:
const dashLog = window.LogConfig.loggers.dashboard;
// To:
const yourPageLog = window.LogConfig.loggers.yourPage;
// Or create custom:
const yourPageLog = window.LogConfig.createLogger('YOUR-PAGE');
```
3. **Replace function name:** `adminDashboard()` → `yourPageComponent()`
4. **Update init flag:** `_dashboardInitialized` → `_yourPageInitialized`
5. **Update page identifier:** `currentPage: 'dashboard'` → `currentPage: 'your-page'`
6. **Replace data loading logic** with your API endpoints
7. **Update HTML template** to use your function name:
```jinja2
{% block alpine_data %}yourPageComponent(){% endblock %}
```
8. **Load your script** in the template:
```jinja2
{% block extra_scripts %}
<script src="{{ url_for('static', path='admin/js/your-page.js') }}"></script>
{% endblock %}
```
Done! ✅

View File

@@ -0,0 +1,410 @@
# Quick Reference: Centralized Logging System
## 🚀 Quick Start
### In Any Page File (Admin/Vendor/Shop):
```javascript
// 1. Use pre-configured logger (RECOMMENDED)
const pageLog = window.LogConfig.loggers.yourPage;
// 2. Or create custom logger
const pageLog = window.LogConfig.createLogger('PAGE-NAME');
// 3. Use it
pageLog.info('Page loaded');
pageLog.debug('Data:', data);
pageLog.warn('Warning message');
pageLog.error('Error occurred', error);
```
---
## 📦 Pre-configured Loggers
### Admin Frontend
```javascript
window.LogConfig.loggers.vendors
window.LogConfig.loggers.vendorTheme
window.LogConfig.loggers.products
window.LogConfig.loggers.orders
window.LogConfig.loggers.users
window.LogConfig.loggers.dashboard
window.LogConfig.loggers.imports
```
### Vendor Frontend
```javascript
window.LogConfig.loggers.dashboard
window.LogConfig.loggers.products
window.LogConfig.loggers.orders
window.LogConfig.loggers.theme
window.LogConfig.loggers.analytics
```
### Shop Frontend
```javascript
window.LogConfig.loggers.catalog
window.LogConfig.loggers.cart
window.LogConfig.loggers.checkout
window.LogConfig.loggers.product
```
---
## 🎨 Basic Logging
```javascript
const log = window.LogConfig.loggers.myPage;
// Simple messages
log.info('Operation started');
log.warn('Slow connection detected');
log.error('Operation failed');
log.debug('Debug data:', { user: 'John', id: 123 });
// With multiple arguments
log.info('User logged in:', username, 'at', timestamp);
```
---
## 🔥 Advanced Features
### API Call Logging
```javascript
const url = '/api/vendors';
// Before request
window.LogConfig.logApiCall('GET', url, null, 'request');
// Make request
const data = await apiClient.get(url);
// After response
window.LogConfig.logApiCall('GET', url, data, 'response');
```
### Performance Logging
```javascript
const start = performance.now();
await expensiveOperation();
const duration = performance.now() - start;
window.LogConfig.logPerformance('Operation Name', duration);
// Output: ⚡ Operation Name took 45ms (fast)
// Output: ⏱️ Operation Name took 250ms (medium)
// Output: 🐌 Operation Name took 750ms (slow)
```
### Error Logging
```javascript
try {
await riskyOperation();
} catch (error) {
window.LogConfig.logError(error, 'Operation Context');
// Automatically logs error message and stack trace
}
```
### Grouped Logging
```javascript
log.group('Loading Data');
log.info('Fetching users...');
log.info('Fetching products...');
log.info('Fetching orders...');
log.groupEnd();
```
### Table Logging
```javascript
const users = [
{ id: 1, name: 'John', status: 'active' },
{ id: 2, name: 'Jane', status: 'inactive' }
];
log.table(users);
```
### Timer Logging
```javascript
log.time('Data Processing');
// ... long operation ...
log.timeEnd('Data Processing');
// Output: ⏱️ [ADMIN:PAGE TIME] Data Processing: 1234.56ms
```
---
## 📋 Complete Page Template
```javascript
// static/admin/js/my-page.js
// 1. Setup logger
const pageLog = window.LogConfig.loggers.myPage;
// 2. Create component
function myPageComponent() {
return {
...data(),
currentPage: 'my-page',
items: [],
loading: false,
async init() {
pageLog.info('Initializing page');
// Prevent double init
if (window._myPageInitialized) return;
window._myPageInitialized = true;
const start = performance.now();
try {
pageLog.group('Loading Data');
await this.loadData();
pageLog.groupEnd();
const duration = performance.now() - start;
window.LogConfig.logPerformance('Page Init', duration);
pageLog.info('Page initialized successfully');
} catch (error) {
window.LogConfig.logError(error, 'Page Init');
}
},
async loadData() {
const url = '/api/my-data';
window.LogConfig.logApiCall('GET', url, null, 'request');
const data = await apiClient.get(url);
window.LogConfig.logApiCall('GET', url, data, 'response');
this.items = data;
}
};
}
pageLog.info('Page module loaded');
```
---
## 🎯 Log Levels
```javascript
LOG_LEVELS = {
ERROR: 1, // Only errors
WARN: 2, // Errors + warnings
INFO: 3, // Errors + warnings + info (default)
DEBUG: 4 // Everything
}
```
**Auto-detected:**
- `localhost` → DEBUG (4)
- Production → Varies by frontend
**Manual override:**
```javascript
const myLog = window.LogConfig.createLogger('MY-PAGE', 4); // Force DEBUG
```
---
## 🔍 Debugging Tips
### Check Current Config
```javascript
console.log(window.LogConfig.frontend); // 'admin' | 'vendor' | 'shop'
console.log(window.LogConfig.environment); // 'development' | 'production'
console.log(window.LogConfig.logLevel); // 1 | 2 | 3 | 4
```
### Test Logger
```javascript
const test = window.LogConfig.createLogger('TEST', 4);
test.info('This is a test');
test.debug('Debug info:', { test: true });
```
### Enable Debug Mode
```javascript
// In browser console
window.LogConfig.loggers.myPage = window.LogConfig.createLogger('MY-PAGE', 4);
// Now reload page to see debug logs
```
---
## ⚠️ Common Mistakes
### ❌ WRONG
```javascript
// Old way (don't do this!)
const LOG_LEVEL = 3;
const log = {
info: (...args) => LOG_LEVEL >= 3 && console.log(...args)
};
// Wrong case
ApiClient.get(url) // Should be apiClient
Logger.info() // Should be window.LogConfig
```
### ✅ CORRECT
```javascript
// New way
const log = window.LogConfig.loggers.myPage;
// Correct case
apiClient.get(url)
window.LogConfig.loggers.myPage.info()
```
---
## 🎨 Console Output Examples
### Development Mode
```
🎛️ Admin Frontend Logging System Initialized
Environment: Development
Log Level: 4 (DEBUG)
[ADMIN:VENDORS INFO] Vendors module loaded
[ADMIN:VENDORS INFO] Initializing vendors page
📤 [ADMIN:API DEBUG] GET /admin/vendors
📥 [ADMIN:API DEBUG] GET /admin/vendors {data...}
⚡ [ADMIN:PERF DEBUG] Load Vendors took 45ms
[ADMIN:VENDORS INFO] Vendors loaded: 25 items
```
### Production Mode (Admin)
```
🎛️ Admin Frontend Logging System Initialized
Environment: Production
Log Level: 2 (WARN)
⚠️ [ADMIN:API WARN] Slow API response: 2.5s
❌ [ADMIN:VENDORS ERROR] Failed to load vendor
```
---
## 📱 Frontend Detection
The system automatically detects which frontend based on URL:
| URL Path | Frontend | Prefix |
|----------|----------|--------|
| `/admin/*` | admin | `ADMIN:` |
| `/vendor/*` | vendor | `VENDOR:` |
| `/shop/*` | shop | `SHOP:` |
---
## 🛠️ Customization
### Add New Pre-configured Logger
Edit `log-config.js`:
```javascript
const adminLoggers = {
// ... existing loggers
myNewPage: createLogger('MY-NEW-PAGE', ACTIVE_LOG_LEVEL)
};
```
### Change Log Level for Frontend
Edit `log-config.js`:
```javascript
const DEFAULT_LOG_LEVELS = {
admin: {
development: LOG_LEVELS.DEBUG,
production: LOG_LEVELS.INFO // Change this
}
};
```
---
## 📚 Full API Reference
```javascript
window.LogConfig = {
// Log levels
LOG_LEVELS: { ERROR: 1, WARN: 2, INFO: 3, DEBUG: 4 },
// Current config
frontend: 'admin' | 'vendor' | 'shop',
environment: 'development' | 'production',
logLevel: 1 | 2 | 3 | 4,
// Default logger
log: { error(), warn(), info(), debug(), group(), groupEnd(), table(), time(), timeEnd() },
// Pre-configured loggers
loggers: { vendors, products, orders, ... },
// Create custom logger
createLogger(prefix, level?),
// Utility functions
logApiCall(method, url, data?, status),
logError(error, context),
logPerformance(operation, duration)
}
```
---
## 💡 Tips & Tricks
1. **Use Pre-configured Loggers**
- Faster to write
- Consistent naming
- Already configured
2. **Log API Calls**
- Easy to track requests
- See request/response data
- Debug API issues
3. **Track Performance**
- Find slow operations
- Optimize bottlenecks
- Monitor page load times
4. **Group Related Logs**
- Cleaner console
- Easier to follow
- Better debugging
5. **Use Debug Level in Development**
- See everything
- Find issues faster
- Learn how code flows
---
## 📖 More Documentation
- [Migration Guide](MIGRATION_GUIDE.md)
- [Alpine.js Template V2](ALPINE_PAGE_TEMPLATE_V2.md)
- [Recommendation Summary](RECOMMENDATION_SUMMARY.md)
---
**Print this page and keep it handy!** 📌
Quick pattern to remember:
```javascript
const log = window.LogConfig.loggers.myPage;
log.info('Hello from my page!');
```
That's it! 🎉

View File

@@ -402,7 +402,7 @@ Value: 123.45.67.89
TTL: 3600
Type: TXT
Name: _letzshop-verify
Name: _wizamart-verify
Value: abc123xyz (verification token from your platform)
TTL: 3600
```

View File

@@ -161,7 +161,7 @@ To prevent domain hijacking, verify the vendor owns the domain:
2. System generates verification token: `abc123xyz`
3. Vendor adds DNS TXT record:
```
Name: _letzshop-verify.customdomain1.com
Name: _wizamart-verify.customdomain1.com
Type: TXT
Value: abc123xyz
```
@@ -177,7 +177,7 @@ def verify_domain(domain_id: int, db: Session):
# Query DNS for TXT record
txt_records = dns.resolver.resolve(
f"_letzshop-verify.{domain.domain}",
f"_wizamart-verify.{domain.domain}",
'TXT'
)

View File

@@ -116,7 +116,7 @@ Name: @
Value: 123.45.67.89 (your server IP)
Type: TXT
Name: _letzshop-verify
Name: _wizamart-verify
Value: abc123xyz
```

View File

@@ -125,7 +125,7 @@ pip install dnspython
import dns.resolver
# Test querying TXT record
answers = dns.resolver.resolve("_letzshop-verify.example.com", "TXT")
answers = dns.resolver.resolve("_wizamart-verify.example.com", "TXT")
for txt in answers:
print(txt.to_text())
```
@@ -259,7 +259,7 @@ TTL: 3600
**Verification TXT Record:**
```
Type: TXT
Name: _letzshop-verify
Name: _wizamart-verify
Value: [token from step 9.1]
TTL: 3600
```
@@ -351,7 +351,7 @@ HAVING COUNT(*) > 1;
**Check DNS propagation:**
```bash
# Check if TXT record exists
dig _letzshop-verify.customdomain1.com TXT
dig _wizamart-verify.customdomain1.com TXT
# Should show verification token
```

View File

@@ -203,7 +203,7 @@ When a vendor wants to use `customdomain1.com`:
Value: 123.45.67.89 (your server IP)
2. Verification TXT Record:
Name: _letzshop-verify
Name: _wizamart-verify
Value: [token from your platform]
3. Wait 5-15 minutes for DNS propagation

View File

@@ -331,7 +331,7 @@ Step 2: Get Instructions
┌────────────────────────────────────┐
│ System returns instructions: │
│ "Add TXT record: │
│ _letzshop-verify.myshop.com │
│ _wizamart-verify.myshop.com │
│ Value: abc123..." │
└────────────────────────────────────┘
@@ -343,7 +343,7 @@ Step 3: Vendor Adds DNS Record
┌────────────────────────────────────┐
│ DNS Provider (GoDaddy/etc) │
│ _letzshop-verify.myshop.com TXT │
│ _wizamart-verify.myshop.com TXT │
│ "abc123..." │
└────────────────────────────────────┘

View File

@@ -356,14 +356,14 @@ GET /api/v1/admin/vendors/domains/1/verification-instructions
},
"txt_record": {
"type": "TXT",
"name": "_letzshop-verify",
"name": "_wizamart-verify",
"value": "abc123xyz...",
"ttl": 3600
}
}
# Step 2: Vendor adds DNS record
# _letzshop-verify.myshop.com TXT "abc123xyz..."
# _wizamart-verify.myshop.com TXT "abc123xyz..."
# Step 3: Verify domain
POST /api/v1/admin/vendors/domains/1/verify

View File

@@ -1,331 +0,0 @@
# Alpine.js Page Template - Quick Reference
## ✅ Correct Page Structure
```javascript
// static/admin/js/your-page.js
// 1. Setup logging (optional but recommended)
const YOUR_PAGE_LOG_LEVEL = 3;
const yourPageLog = {
error: (...args) => YOUR_PAGE_LOG_LEVEL >= 1 && console.error('❌ [YOUR_PAGE ERROR]', ...args),
warn: (...args) => YOUR_PAGE_LOG_LEVEL >= 2 && console.warn('⚠️ [YOUR_PAGE WARN]', ...args),
info: (...args) => YOUR_PAGE_LOG_LEVEL >= 3 && console.info(' [YOUR_PAGE INFO]', ...args),
debug: (...args) => YOUR_PAGE_LOG_LEVEL >= 4 && console.log('🔍 [YOUR_PAGE DEBUG]', ...args)
};
// 2. Create your Alpine.js component
function yourPageComponent() {
return {
// ✅ CRITICAL: Inherit base layout functionality
...data(),
// ✅ CRITICAL: Set page identifier
currentPage: 'your-page',
// Your page-specific state
items: [],
loading: false,
error: null,
// ✅ CRITICAL: Proper initialization with guard
async init() {
yourPageLog.info('=== YOUR PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._yourPageInitialized) {
yourPageLog.warn('Page already initialized, skipping...');
return;
}
window._yourPageInitialized = true;
// Load your data
await this.loadData();
yourPageLog.info('=== YOUR PAGE INITIALIZATION COMPLETE ===');
},
// Your methods
async loadData() {
yourPageLog.info('Loading data...');
this.loading = true;
this.error = null;
try {
const startTime = Date.now();
// ✅ CRITICAL: Use lowercase apiClient
const response = await apiClient.get('/your/endpoint');
const duration = Date.now() - startTime;
this.items = response.items || [];
yourPageLog.info(`Data loaded in ${duration}ms`, {
count: this.items.length
});
} catch (error) {
yourPageLog.error('Failed to load data:', error);
this.error = error.message;
Utils.showToast('Failed to load data', 'error');
} finally {
this.loading = false;
}
},
// Format date helper (if needed)
formatDate(dateString) {
if (!dateString) return '-';
return Utils.formatDate(dateString);
},
// Your other methods...
};
}
yourPageLog.info('Your page module loaded');
```
---
## 🎯 Checklist for New Pages
### HTML Template
```jinja2
{# app/templates/admin/your-page.html #}
{% extends "admin/base.html" %}
{% block title %}Your Page{% endblock %}
{# ✅ CRITICAL: Link to your Alpine.js component #}
{% block alpine_data %}yourPageComponent(){% endblock %}
{% block content %}
<!-- Your page content -->
{% endblock %}
{% block extra_scripts %}
{# ✅ CRITICAL: Load your JavaScript file #}
<script src="{{ url_for('static', path='admin/js/your-page.js') }}"></script>
{% endblock %}
```
### JavaScript File Checklist
- [ ] ✅ Logging setup (optional)
- [ ] ✅ Function name matches `alpine_data` in template
- [ ]`...data(),` at start of return object
- [ ]`currentPage: 'your-page'` set
- [ ] ✅ Initialization guard in `init()`
- [ ] ✅ Use lowercase `apiClient` for API calls
- [ ] ✅ Use your custom logger (not `Logger`)
- [ ] ✅ Performance tracking with `Date.now()` (optional)
- [ ] ✅ Module loaded log at end
---
## ❌ Common Mistakes to Avoid
### 1. Missing Base Inheritance
```javascript
// ❌ WRONG
function myPage() {
return {
items: [],
// Missing ...data()
};
}
// ✅ CORRECT
function myPage() {
return {
...data(), // Must be first!
items: [],
};
}
```
### 2. Wrong API Client Name
```javascript
// ❌ WRONG - Capital letters
await ApiClient.get('/endpoint');
await API_CLIENT.get('/endpoint');
// ✅ CORRECT - lowercase
await apiClient.get('/endpoint');
```
### 3. Missing Init Guard
```javascript
// ❌ WRONG
async init() {
await this.loadData();
}
// ✅ CORRECT
async init() {
if (window._myPageInitialized) return;
window._myPageInitialized = true;
await this.loadData();
}
```
### 4. Missing currentPage
```javascript
// ❌ WRONG
return {
...data(),
items: [],
// Missing currentPage
};
// ✅ CORRECT
return {
...data(),
currentPage: 'my-page', // Must set this!
items: [],
};
```
---
## 🔧 API Client Pattern
### GET Request
```javascript
try {
const response = await apiClient.get('/endpoint');
this.data = response;
} catch (error) {
console.error('Failed:', error);
Utils.showToast('Failed to load', 'error');
}
```
### POST Request
```javascript
try {
const response = await apiClient.post('/endpoint', {
name: 'value',
// ... data
});
Utils.showToast('Created successfully', 'success');
} catch (error) {
console.error('Failed:', error);
Utils.showToast('Failed to create', 'error');
}
```
### PUT Request
```javascript
try {
const response = await apiClient.put('/endpoint/123', {
name: 'updated value'
});
Utils.showToast('Updated successfully', 'success');
} catch (error) {
console.error('Failed:', error);
Utils.showToast('Failed to update', 'error');
}
```
### DELETE Request
```javascript
try {
await apiClient.delete('/endpoint/123');
Utils.showToast('Deleted successfully', 'success');
await this.reloadData();
} catch (error) {
console.error('Failed:', error);
Utils.showToast('Failed to delete', 'error');
}
```
---
## 🎨 Common UI Patterns
### Loading State
```javascript
async loadData() {
this.loading = true;
try {
const data = await apiClient.get('/endpoint');
this.items = data;
} finally {
this.loading = false;
}
}
```
### Error Handling
```javascript
async loadData() {
this.loading = true;
this.error = null;
try {
const data = await apiClient.get('/endpoint');
this.items = data;
} catch (error) {
this.error = error.message;
Utils.showToast('Failed to load', 'error');
} finally {
this.loading = false;
}
}
```
### Refresh/Reload
```javascript
async refresh() {
console.info('Refreshing...');
await this.loadData();
Utils.showToast('Refreshed successfully', 'success');
}
```
---
## 📚 Available Utilities
### From `init-alpine.js` (via `...data()`)
- `this.dark` - Dark mode state
- `this.toggleTheme()` - Toggle theme
- `this.isSideMenuOpen` - Side menu state
- `this.toggleSideMenu()` - Toggle side menu
- `this.closeSideMenu()` - Close side menu
- `this.isNotificationsMenuOpen` - Notifications menu state
- `this.toggleNotificationsMenu()` - Toggle notifications
- `this.closeNotificationsMenu()` - Close notifications
- `this.isProfileMenuOpen` - Profile menu state
- `this.toggleProfileMenu()` - Toggle profile menu
- `this.closeProfileMenu()` - Close profile menu
- `this.isPagesMenuOpen` - Pages menu state
- `this.togglePagesMenu()` - Toggle pages menu
### From `Utils` (global)
- `Utils.showToast(message, type, duration)` - Show toast notification
- `Utils.formatDate(dateString)` - Format date for display
- `Utils.confirm(message, title)` - Show confirmation dialog (if available)
### From `apiClient` (global)
- `apiClient.get(url)` - GET request
- `apiClient.post(url, data)` - POST request
- `apiClient.put(url, data)` - PUT request
- `apiClient.delete(url)` - DELETE request
---
## 🚀 Quick Start Template Files
Copy these to create a new page:
1. Copy `dashboard.js` → rename to `your-page.js`
2. Replace function name: `adminDashboard()``yourPageComponent()`
3. Update logging prefix: `dashLog``yourPageLog`
4. Update init flag: `_dashboardInitialized``_yourPageInitialized`
5. Update `currentPage`: `'dashboard'``'your-page'`
6. Replace data loading logic with your endpoints
7. Update HTML template to use your function name
Done! ✅

View File

@@ -0,0 +1,235 @@
# Database Quick Reference Card
Quick reference for common database operations. See `DATABASE_SETUP_GUIDE.md` for detailed instructions.
---
## 🚀 Quick Start (New Developer)
```bash
# 1. Clone and setup
git clone <repo>
cd letzshop
python -m venv venv
venv\Scripts\activate # Windows
pip install -r requirements.txt
# 2. Initialize database
alembic upgrade head
python scripts/seed_database.py
# 3. Start server
python -m uvicorn app.main:app --reload
# 4. Login
# http://localhost:8000/admin/login
# Username: admin | Password: admin123
```
---
## 🔄 Reset Database (Clean Slate)
**Windows:**
```powershell
.\scripts\reset_database.ps1
```
**Linux/Mac:**
```bash
./scripts/reset_database.sh
```
**Manual:**
```bash
# 1. Delete migrations
Remove-Item alembic\versions\*.py -Exclude __init__.py
# 2. Delete database
Remove-Item wizamart.db
# 3. Regenerate
alembic revision --autogenerate -m "Initial migration"
alembic upgrade head
python scripts/seed_database.py
```
---
## 📝 Creating Migrations
```bash
# After modifying models in models/database/
# 1. Generate migration
alembic revision --autogenerate -m "Add field to vendor_themes"
# 2. Review the file
# Check: alembic/versions/<id>_add_field_to_vendor_themes.py
# 3. Apply migration
alembic upgrade head
```
---
## 🔍 Checking Status
```bash
# Current migration version
alembic current
# Migration history
alembic history --verbose
# List all tables
python -c "import sqlite3; conn = sqlite3.connect('wizamart.db'); cursor = conn.cursor(); cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\" ORDER BY name;'); [print(t[0]) for t in cursor.fetchall()]"
# Check specific table
python -c "import sqlite3; conn = sqlite3.connect('wizamart.db'); cursor = conn.cursor(); cursor.execute('PRAGMA table_info(vendor_themes)'); [print(col) for col in cursor.fetchall()]"
```
---
## ⬆️ Upgrading Database
```bash
# Upgrade to latest
alembic upgrade head
# Upgrade one version
alembic upgrade +1
# Upgrade to specific revision
alembic upgrade <revision_id>
```
---
## ⬇️ Rolling Back
```bash
# Downgrade one version
alembic downgrade -1
# Downgrade to specific revision
alembic downgrade <revision_id>
# Downgrade to empty database
alembic downgrade base
```
---
## 🌱 Seeding Data
```bash
# Seed development data
python scripts/seed_database.py
# Manual seed (Python shell)
python
>>> from scripts.seed_database import seed_database
>>> seed_database()
```
---
## 🐛 Common Fixes
### "No such table" Error
```bash
# Check model imported in alembic/env.py
# Then regenerate:
alembic revision --autogenerate -m "Add missing table"
alembic upgrade head
```
### "Database locked"
```bash
# Stop server
taskkill /F /IM python.exe # Windows
pkill python # Linux/Mac
# Try again
alembic upgrade head
```
### "Target database not up to date"
```bash
alembic stamp head
alembic upgrade head
```
### "Multiple head revisions"
```bash
alembic merge heads -m "Merge migrations"
alembic upgrade head
```
---
## 📦 Adding New Models
**Checklist:**
```bash
# 1. Create model file
# models/database/my_model.py
# 2. Import in alembic/env.py
# from models.database.my_model import MyModel
# 3. Generate migration
alembic revision --autogenerate -m "Add MyModel table"
# 4. Review migration file
# alembic/versions/<id>_add_mymodel_table.py
# 5. Apply migration
alembic upgrade head
# 6. Test
python -c "from models.database.my_model import MyModel; print('✓ Import works')"
```
---
## 🔗 Important Files
```
├── alembic/
│ ├── env.py ← Import ALL models here!
│ └── versions/ ← Migration files
├── app/core/
│ └── database.py ← Database connection
├── models/database/ ← SQLAlchemy models
├── scripts/
│ ├── seed_database.py ← Seed script
│ └── reset_database.ps1 ← Reset script
├── alembic.ini ← Alembic config
└── wizamart.db ← SQLite database
```
---
## 🆘 Getting Help
1. Check full guide: `docs/DATABASE_SETUP_GUIDE.md`
2. Check Alembic docs: https://alembic.sqlalchemy.org/
3. Ask team lead
---
## ⚠️ Important Notes
- ✅ Always review migrations before applying
- ✅ Test on local database first
- ✅ Keep migrations in version control
- ✅ Backup before destructive operations
- ❌ Never edit applied migrations
- ❌ Never delete migration files
---
**Last Updated:** 2025-10-27

View File

@@ -0,0 +1,883 @@
# Database Setup & Initialization Guide
## Overview
This guide walks you through setting up the LetzShop database from scratch. Whether you're a new developer joining the team or need to reset your local database, this document covers everything you need to know.
---
## Table of Contents
1. [Prerequisites](#prerequisites)
2. [First-Time Setup](#first-time-setup)
3. [Database Reset (Clean Slate)](#database-reset-clean-slate)
4. [Verifying Your Setup](#verifying-your-setup)
5. [Common Issues & Solutions](#common-issues--solutions)
6. [Database Migrations](#database-migrations)
7. [Seeding Data](#seeding-data)
---
## Prerequisites
### Required Software
- **Python 3.11+**
- **Alembic** (installed via `requirements.txt`)
- **SQLite** (built into Python) or **PostgreSQL** (for production-like setup)
### Environment Setup
```bash
# 1. Clone the repository
git clone <repository-url>
cd letzshop
# 2. Create virtual environment
python -m venv venv
# 3. Activate virtual environment
# Windows:
venv\Scripts\activate
# Linux/Mac:
source venv/bin/activate
# 4. Install dependencies
pip install -r requirements.txt
```
---
## First-Time Setup
### Step 1: Configure Database Connection
Create or update `.env` file in project root:
```env
# Database Configuration
DATABASE_URL=sqlite:///./wizamart.db
# For PostgreSQL (production):
# DATABASE_URL=postgresql://user:password@localhost:5432/letzshop
# Other required settings
SECRET_KEY=your-secret-key-here-change-in-production
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
```
---
### Step 2: Initialize Alembic (First Time Only)
**If `alembic/` folder doesn't exist:**
```bash
# Initialize Alembic
alembic init alembic
```
**Configure `alembic.ini`:**
```ini
# alembic.ini
# Find this line and update:
sqlalchemy.url = sqlite:///./wizamart.db
# Or for PostgreSQL:
# sqlalchemy.url = postgresql://user:password@localhost:5432/letzshop
```
**Configure `alembic/env.py`:**
```python
# alembic/env.py
# Add this near the top (after imports)
import sys
from pathlib import Path
# Add project root to Python path
sys.path.append(str(Path(__file__).parents[1]))
# ============================================================================
# CRITICAL: Import ALL database models here
# ============================================================================
from models.database.user import User
from models.database.vendor import Vendor
from models.database.vendor_domain import VendorDomain
from models.database.vendor_theme import VendorTheme
from models.database.customer import Customer
from models.database.team import Team, TeamMember, TeamInvitation
from models.database.product import Product
from models.database.marketplace_product import MarketplaceProduct
from models.database.inventory import Inventory
from models.database.order import Order
from models.database.marketplace_import_job import MarketplaceImportJob
# Import Base
from app.core.database import Base
# Set target metadata
target_metadata = Base.metadata
```
---
### Step 3: Generate Initial Migration
```bash
# Generate migration from your models
alembic revision --autogenerate -m "Initial migration - all tables"
# You should see output listing all detected tables:
# INFO [alembic.autogenerate.compare] Detected added table 'users'
# INFO [alembic.autogenerate.compare] Detected added table 'vendors'
# INFO [alembic.autogenerate.compare] Detected added table 'vendor_themes'
# etc.
```
---
### Step 4: Apply Migration
```bash
# Apply the migration to create all tables
alembic upgrade head
# You should see:
# INFO [alembic.runtime.migration] Running upgrade -> abc123def456, Initial migration - all tables
```
---
### Step 5: Seed Initial Data
Create an admin user and test vendor:
```bash
python scripts/seed_database.py
```
**Or manually:**
```python
# Run Python interactive shell
python
# Then execute:
from app.core.database import SessionLocal
from models.database.user import User
from models.database.vendor import Vendor
from middleware.auth import AuthManager
from datetime import datetime, timezone
db = SessionLocal()
auth_manager = AuthManager()
# Create admin user
admin = User(
username="admin",
email="admin@letzshop.com",
hashed_password=auth_manager.hash_password("admin123"),
role="admin",
is_active=True,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.add(admin)
db.flush()
# Create test vendor
vendor = Vendor(
vendor_code="TESTVENDOR",
subdomain="testvendor",
name="Test Vendor",
description="Development test vendor",
owner_user_id=admin.id,
contact_email="contact@testvendor.com",
is_active=True,
is_verified=True,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.add(vendor)
db.commit()
print("✅ Admin user and test vendor created!")
db.close()
exit()
```
---
### Step 6: Start the Application
```bash
# Start FastAPI server
python -m uvicorn app.main:app --reload
# Server should start on http://localhost:8000
```
**Test the setup:**
- Admin login: http://localhost:8000/admin/login
- Username: `admin`
- Password: `admin123`
---
## Database Reset (Clean Slate)
Use this when you want to completely reset your database and start fresh.
⚠️ **WARNING:** This will delete ALL data! Make a backup if needed.
---
### Quick Reset Script (Windows PowerShell)
**Create `scripts/reset_database.ps1`:**
```powershell
# scripts/reset_database.ps1
Write-Host "🗑️ Database Reset Script" -ForegroundColor Cyan
Write-Host "This will delete ALL data!" -ForegroundColor Red
$confirm = Read-Host "Type 'YES' to continue"
if ($confirm -ne "YES") {
Write-Host "Aborted." -ForegroundColor Yellow
exit
}
Write-Host ""
Write-Host "Step 1: Backing up current database..." -ForegroundColor Yellow
if (Test-Path wizamart.db) {
$backupName = "wizamart_backup_$(Get-Date -Format 'yyyyMMdd_HHmmss').db"
Copy-Item wizamart.db $backupName
Write-Host "✓ Backup created: $backupName" -ForegroundColor Green
}
Write-Host ""
Write-Host "Step 2: Removing old migrations..." -ForegroundColor Yellow
Remove-Item alembic\versions\*.py -Exclude __init__.py -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force alembic\versions\__pycache__ -ErrorAction SilentlyContinue
# Ensure __init__.py exists
if (-not (Test-Path alembic\versions\__init__.py)) {
New-Item -Path alembic\versions\__init__.py -ItemType File | Out-Null
}
Write-Host "✓ Old migrations removed" -ForegroundColor Green
Write-Host ""
Write-Host "Step 3: Deleting database..." -ForegroundColor Yellow
Remove-Item wizamart.db -ErrorAction SilentlyContinue
Write-Host "✓ Database deleted" -ForegroundColor Green
Write-Host ""
Write-Host "Step 4: Generating new migration..." -ForegroundColor Yellow
alembic revision --autogenerate -m "Initial migration - all tables"
Write-Host "✓ Migration generated" -ForegroundColor Green
Write-Host ""
Write-Host "Step 5: Applying migration..." -ForegroundColor Yellow
alembic upgrade head
Write-Host "✓ Migration applied" -ForegroundColor Green
Write-Host ""
Write-Host "Step 6: Verifying tables..." -ForegroundColor Yellow
python -c @"
import sqlite3
conn = sqlite3.connect('wizamart.db')
cursor = conn.cursor()
cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\" ORDER BY name;')
tables = [t[0] for t in cursor.fetchall()]
print('\n📊 Tables created:')
for t in tables:
print(f' {t}')
print(f'\n Total: {len(tables)} tables')
if 'vendor_themes' in tables:
print(' vendor_themes table confirmed!')
conn.close()
"@
Write-Host ""
Write-Host "Step 7: Seeding initial data..." -ForegroundColor Yellow
python scripts/seed_database.py
Write-Host ""
Write-Host "✅ Database reset complete!" -ForegroundColor Green
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Cyan
Write-Host " 1. Start server: python -m uvicorn app.main:app --reload" -ForegroundColor White
Write-Host " 2. Login: http://localhost:8000/admin/login" -ForegroundColor White
Write-Host " 3. Username: admin | Password: admin123" -ForegroundColor White
```
**Run it:**
```powershell
# First time only - allow script execution
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
# Run the reset
.\scripts\reset_database.ps1
```
---
### Quick Reset Script (Linux/Mac Bash)
**Create `scripts/reset_database.sh`:**
```bash
#!/bin/bash
# scripts/reset_database.sh
echo "🗑️ Database Reset Script"
echo "⚠️ This will delete ALL data!"
read -p "Type 'YES' to continue: " confirm
if [ "$confirm" != "YES" ]; then
echo "Aborted."
exit 1
fi
echo ""
echo "Step 1: Backing up current database..."
if [ -f wizamart.db ]; then
backup_name="wizamart_backup_$(date +%Y%m%d_%H%M%S).db"
cp wizamart.db "$backup_name"
echo "✓ Backup created: $backup_name"
fi
echo ""
echo "Step 2: Removing old migrations..."
find alembic/versions -type f -name "*.py" ! -name "__init__.py" -delete
rm -rf alembic/versions/__pycache__
touch alembic/versions/__init__.py
echo "✓ Old migrations removed"
echo ""
echo "Step 3: Deleting database..."
rm -f wizamart.db
echo "✓ Database deleted"
echo ""
echo "Step 4: Generating new migration..."
alembic revision --autogenerate -m "Initial migration - all tables"
echo "✓ Migration generated"
echo ""
echo "Step 5: Applying migration..."
alembic upgrade head
echo "✓ Migration applied"
echo ""
echo "Step 6: Verifying tables..."
python -c "
import sqlite3
conn = sqlite3.connect('wizamart.db')
cursor = conn.cursor()
cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\" ORDER BY name;')
tables = [t[0] for t in cursor.fetchall()]
print('\n📊 Tables created:')
for t in tables:
print(f' ✓ {t}')
print(f'\n✅ Total: {len(tables)} tables')
if 'vendor_themes' in tables:
print('✅ vendor_themes table confirmed!')
conn.close()
"
echo ""
echo "Step 7: Seeding initial data..."
python scripts/seed_database.py
echo ""
echo "✅ Database reset complete!"
echo ""
echo "Next steps:"
echo " 1. Start server: python -m uvicorn app.main:app --reload"
echo " 2. Login: http://localhost:8000/admin/login"
echo " 3. Username: admin | Password: admin123"
```
**Run it:**
```bash
chmod +x scripts/reset_database.sh
./scripts/reset_database.sh
```
---
### Manual Reset Steps
If you prefer to run commands manually:
```bash
# 1. Backup (optional)
cp wizamart.db wizamart_backup.db # Windows: copy wizamart.db wizamart_backup.db
# 2. Remove migrations
rm alembic/versions/*.py # Windows: Remove-Item alembic\versions\*.py -Exclude __init__.py
touch alembic/versions/__init__.py # Windows: New-Item -Path alembic\versions\__init__.py
# 3. Delete database
rm wizamart.db # Windows: Remove-Item wizamart.db
# 4. Generate migration
alembic revision --autogenerate -m "Initial migration - all tables"
# 5. Apply migration
alembic upgrade head
# 6. Seed data
python scripts/seed_database.py
```
---
## Verifying Your Setup
### Check Alembic Status
```bash
# Check current migration version
alembic current
# Should show:
# <revision_id> (head)
```
### Verify Tables Exist
**Using Python:**
```python
import sqlite3
conn = sqlite3.connect('wizamart.db')
cursor = conn.cursor()
# List all tables
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;")
tables = cursor.fetchall()
print("📊 Database Tables:")
for table in tables:
print(f"{table[0]}")
# Check specific table structure
cursor.execute("PRAGMA table_info(vendor_themes);")
columns = cursor.fetchall()
print("\n📋 vendor_themes columns:")
for col in columns:
print(f" - {col[1]} ({col[2]})")
conn.close()
```
### Test Database Connection
```bash
# Start Python shell
python
# Test connection
from app.core.database import SessionLocal, engine
from sqlalchemy import text
db = SessionLocal()
result = db.execute(text("SELECT name FROM sqlite_master WHERE type='table';"))
tables = [row[0] for row in result]
print("✅ Database connection successful!")
print(f"📊 Tables found: {len(tables)}")
for table in tables:
print(f" - {table}")
db.close()
exit()
```
### Test Application Startup
```bash
# Start server
python -m uvicorn app.main:app --reload
# Should see:
# INFO: Uvicorn running on http://127.0.0.1:8000
# INFO: Application startup complete.
```
**No errors = successful setup!**
---
## Common Issues & Solutions
### Issue 1: "No such table" Error
**Symptom:**
```
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: vendor_themes
```
**Solution:**
```bash
# 1. Check if model is imported in alembic/env.py
# Open alembic/env.py and verify:
from models.database.vendor_theme import VendorTheme
# 2. Regenerate migration
alembic revision --autogenerate -m "Add missing tables"
# 3. Apply migration
alembic upgrade head
```
---
### Issue 2: "Target database is not up to date"
**Symptom:**
```
ERROR [alembic.util.messaging] Target database is not up to date.
```
**Solution:**
```bash
# Option 1: Stamp to current head
alembic stamp head
# Option 2: Check and upgrade
alembic current # See current version
alembic upgrade head # Upgrade to latest
# Option 3: Full reset (see Database Reset section)
```
---
### Issue 3: Database Locked
**Symptom:**
```
sqlite3.OperationalError: database is locked
```
**Solution:**
```bash
# 1. Stop the FastAPI server (Ctrl+C)
# 2. Kill any Python processes
# Windows:
taskkill /F /IM python.exe
# Linux/Mac:
pkill python
# 3. Close any database browser applications
# 4. Try again
alembic upgrade head
```
---
### Issue 4: Migration File Conflicts
**Symptom:**
```
Multiple head revisions are present
```
**Solution:**
```bash
# Option 1: Merge migrations
alembic merge heads -m "Merge migrations"
alembic upgrade head
# Option 2: Clean reset (recommended for development)
# See Database Reset section above
```
---
### Issue 5: Import Errors in alembic/env.py
**Symptom:**
```
ModuleNotFoundError: No module named 'models'
```
**Solution:**
Check `alembic/env.py` has correct path setup:
```python
# alembic/env.py (top of file)
import sys
from pathlib import Path
# Add project root to Python path
sys.path.append(str(Path(__file__).parents[1]))
```
---
## Database Migrations
### Creating New Migrations
When you add/modify models:
```bash
# 1. Update your model file (e.g., models/database/vendor_theme.py)
# 2. Generate migration
alembic revision --autogenerate -m "Add new field to vendor_themes"
# 3. Review the generated migration
# Check: alembic/versions/<revision>_add_new_field_to_vendor_themes.py
# 4. Apply migration
alembic upgrade head
```
---
### Migration Best Practices
**DO:**
- Review generated migrations before applying
- Use descriptive migration messages
- Test migrations on development database first
- Keep migrations small and focused
- Commit migrations to version control
**DON'T:**
- Edit applied migrations (create new ones instead)
- Delete migration files from version control
- Skip migration review
- Combine unrelated changes in one migration
---
### Rolling Back Migrations
```bash
# Downgrade one version
alembic downgrade -1
# Downgrade to specific revision
alembic downgrade <revision_id>
# Downgrade to base (empty database)
alembic downgrade base
```
---
## Seeding Data
### Development Data Seed
**Create `scripts/seed_database.py`:**
```python
# scripts/seed_database.py
"""
Seed the database with initial development data.
"""
from app.core.database import SessionLocal
from models.database.user import User
from models.database.vendor import Vendor
from app.core.security import get_password_hash
from datetime import datetime, timezone
def seed_database():
"""Seed database with initial data."""
db = SessionLocal()
try:
print("🌱 Seeding database...")
# Check if admin already exists
existing_admin = db.query(User).filter(User.username == "admin").first()
if existing_admin:
print("⚠️ Admin user already exists, skipping...")
return
# Create admin user
admin = User(
username="admin",
email="admin@letzshop.com",
hashed_password=get_password_hash("admin123"),
is_admin=True,
is_active=True,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.add(admin)
db.flush()
print(f"✓ Admin user created (ID: {admin.id})")
# Create test vendor
vendor = Vendor(
vendor_code="TESTVENDOR",
subdomain="testvendor",
name="Test Vendor",
description="Development test vendor",
owner_user_id=admin.id,
contact_email="contact@testvendor.com",
is_active=True,
is_verified=True,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.add(vendor)
db.flush()
print(f"✓ Test vendor created: {vendor.vendor_code}")
db.commit()
print("\n✅ Database seeded successfully!")
print("\n📝 Login Credentials:")
print(" URL: http://localhost:8000/admin/login")
print(" Username: admin")
print(" Password: admin123")
except Exception as e:
db.rollback()
print(f"❌ Error seeding database: {e}")
raise
finally:
db.close()
if __name__ == "__main__":
seed_database()
```
**Run it:**
```bash
python scripts/seed_database.py
```
---
### Production Data Migration
For production, create separate seed scripts:
```python
# scripts/seed_production.py
# - Create only essential admin user
# - No test data
# - Strong passwords from environment variables
```
---
## Quick Reference
### Essential Commands
```bash
# Check migration status
alembic current
# Generate new migration
alembic revision --autogenerate -m "Description"
# Apply migrations
alembic upgrade head
# Rollback one migration
alembic downgrade -1
# View migration history
alembic history
# Seed database
python scripts/seed_database.py
# Start server
python -m uvicorn app.main:app --reload
```
---
### File Locations
```
project/
├── alembic/
│ ├── versions/ # Migration files here
│ │ └── __init__.py
│ ├── env.py # Alembic configuration (import models here!)
│ └── script.py.mako
├── app/
│ └── core/
│ └── database.py # Database connection
├── models/
│ └── database/ # SQLAlchemy models
│ ├── user.py
│ ├── vendor.py
│ ├── vendor_theme.py
│ └── ...
├── scripts/
│ ├── seed_database.py # Development seed
│ └── reset_database.ps1 # Reset script
├── alembic.ini # Alembic config
├── wizamart.db # SQLite database (gitignored)
└── .env # Environment variables (gitignored)
```
---
## Getting Help
### Internal Resources
- **Architecture Guide:** `docs/PROPER_ARCHITECTURE_GUIDE.md`
- **Exception Handling:** `docs/EXCEPTION_PATTERN_EXPLAINED.md`
- **Admin Feature Guide:** `docs/ADMIN_FEATURE_INTEGRATION_GUIDE.md`
### External Resources
- [Alembic Documentation](https://alembic.sqlalchemy.org/)
- [SQLAlchemy Documentation](https://docs.sqlalchemy.org/)
- [FastAPI Documentation](https://fastapi.tiangolo.com/)
---
## Contributing
When adding new models:
1. ✅ Create the model in `models/database/`
2. ✅ Import it in `alembic/env.py`
3. ✅ Generate migration: `alembic revision --autogenerate -m "Add XYZ model"`
4. ✅ Review the migration file
5. ✅ Test locally: `alembic upgrade head`
6. ✅ Commit both model and migration files
---
**Last Updated:** 2025-10-27
**Maintainer:** Development Team
**Questions?** Contact the team lead or check internal documentation.

View File

@@ -1,29 +1,38 @@
# models/database/__init__.py
"""Database models package."""
from .admin import AdminAuditLog, AdminNotification, AdminSetting, PlatformAlert, AdminSession
from .base import Base
from .customer import Customer
from .order import Order
from .customer import Customer, CustomerAddress
from .order import Order, OrderItem
from .user import User
from .marketplace_product import MarketplaceProduct
from .inventory import Inventory
from .vendor import Vendor
from .vendor import Vendor, Role, VendorUser
from .vendor_domain import VendorDomain
from .vendor_theme import VendorTheme
from .product import Product
from .marketplace_import_job import MarketplaceImportJob
__all__ = [
# Admin-specific models
"AdminAuditLog",
"AdminNotification",
"AdminSetting",
"PlatformAlert",
"AdminSession",
"Base",
"User",
"MarketplaceProduct",
"Inventory",
"Customer",
"CustomerAddress",
"Order",
"OrderItem",
"Vendor",
"VendorUser",
"Role",
"Product",
"MarketplaceImportJob",
"MarketplaceProduct",
"VendorDomain",
"VendorTheme"
]

319
scripts/seed_database.py Normal file
View File

@@ -0,0 +1,319 @@
#!/usr/bin/env python3
"""
Seed the database with initial development data.
Creates:
- Admin user (admin/admin123)
- Test vendors (TESTVENDOR, WIZAMART)
Usage:
python scripts/seed_database.py
This script is idempotent - safe to run multiple times.
"""
import sys
from pathlib import Path
# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from sqlalchemy.orm import Session
from sqlalchemy import select
from app.core.database import SessionLocal, engine
from models.database.user import User
from models.database.vendor import Vendor
from middleware.auth import AuthManager
from datetime import datetime, timezone
# Default credentials
DEFAULT_ADMIN_EMAIL = "admin@letzshop.com"
DEFAULT_ADMIN_USERNAME = "admin"
DEFAULT_ADMIN_PASSWORD = "admin123" # Change in production!
def verify_database_ready() -> bool:
"""
Verify that database tables exist.
Returns:
bool: True if database is ready, False otherwise
"""
try:
with engine.connect() as conn:
from sqlalchemy import text
# Check for users table
result = conn.execute(
text("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")
)
users_table = result.fetchall()
# Check for vendors table
result = conn.execute(
text("SELECT name FROM sqlite_master WHERE type='table' AND name='vendors'")
)
vendors_table = result.fetchall()
return len(users_table) > 0 and len(vendors_table) > 0
except Exception as e:
print(f"❌ Error checking database: {e}")
return False
def create_admin_user(db: Session, auth_manager: AuthManager) -> tuple[User, bool]:
"""
Create admin user if it doesn't exist.
Args:
db: Database session
auth_manager: AuthManager instance for password hashing
Returns:
tuple: (User object, was_created boolean)
"""
# Check if admin already exists
existing_admin = db.execute(
select(User).where(User.username == DEFAULT_ADMIN_USERNAME)
).scalar_one_or_none()
if existing_admin:
return existing_admin, False
# Create new admin user
admin = User(
email=DEFAULT_ADMIN_EMAIL,
username=DEFAULT_ADMIN_USERNAME,
hashed_password=auth_manager.hash_password(DEFAULT_ADMIN_PASSWORD),
role="admin",
is_active=True,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.add(admin)
db.flush() # Get the ID
return admin, True
def create_vendor(
db: Session,
vendor_code: str,
name: str,
subdomain: str,
owner_user_id: int,
description: str = None
) -> tuple[Vendor, bool]:
"""
Create vendor if it doesn't exist.
Args:
db: Database session
vendor_code: Unique vendor code
name: Vendor name
subdomain: Subdomain for the vendor
owner_user_id: ID of the owner user
description: Optional description
Returns:
tuple: (Vendor object, was_created boolean)
"""
# Check if vendor already exists
existing_vendor = db.execute(
select(Vendor).where(Vendor.vendor_code == vendor_code)
).scalar_one_or_none()
if existing_vendor:
return existing_vendor, False
# Create new vendor
vendor = Vendor(
vendor_code=vendor_code,
subdomain=subdomain,
name=name,
description=description or f"{name} - Development vendor",
owner_user_id=owner_user_id,
contact_email=f"contact@{subdomain}.com",
contact_phone="+352 123 456 789",
website=f"https://{subdomain}.com",
is_active=True,
is_verified=True,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.add(vendor)
db.flush()
return vendor, True
def seed_database():
"""Main seeding function."""
print("\n" + "" + "" * 68 + "")
print("" + " " * 18 + "DATABASE SEEDING SCRIPT" + " " * 27 + "")
print("" + "" * 68 + "\n")
# ========================================================================
# STEP 1: VERIFY DATABASE
# ========================================================================
print("STEP 1: Verifying database...")
if not verify_database_ready():
print("\n❌ ERROR: Database not ready!")
print("\n Required tables don't exist yet.")
print(" Please run database migrations first:\n")
print(" alembic upgrade head\n")
sys.exit(1)
print(" ✓ Database tables verified\n")
# ========================================================================
# STEP 2: CREATE ADMIN USER
# ========================================================================
db = SessionLocal()
auth_manager = AuthManager()
try:
print("STEP 2: Creating admin user...")
admin, admin_created = create_admin_user(db, auth_manager)
if admin_created:
print(f" ✓ Admin user created (ID: {admin.id})")
print(f" Username: {admin.username}")
print(f" Email: {admin.email}")
else:
print(f" Admin user already exists (ID: {admin.id})")
print(f" Username: {admin.username}")
print(f" Email: {admin.email}")
# ====================================================================
# STEP 3: CREATE VENDORS
# ====================================================================
print("\nSTEP 3: Creating vendors...")
# Create TESTVENDOR
test_vendor, test_created = create_vendor(
db,
vendor_code="TESTVENDOR",
name="Test Vendor",
subdomain="testvendor",
owner_user_id=admin.id,
description="Development test vendor for general testing"
)
if test_created:
print(f" ✓ TESTVENDOR created (ID: {test_vendor.id})")
else:
print(f" TESTVENDOR already exists (ID: {test_vendor.id})")
# Create WIZAMART
wizamart, wizamart_created = create_vendor(
db,
vendor_code="WIZAMART",
name="WizaMart",
subdomain="wizamart",
owner_user_id=admin.id,
description="Primary development vendor for theme customization"
)
if wizamart_created:
print(f" ✓ WIZAMART created (ID: {wizamart.id})")
else:
print(f" WIZAMART already exists (ID: {wizamart.id})")
# Commit all changes
db.commit()
# ====================================================================
# SUMMARY
# ====================================================================
print("\n" + "" + "" * 68 + "")
print("" + " " * 22 + "SEEDING COMPLETE!" + " " * 28 + "")
print("" + "" * 68 + "\n")
if admin_created or test_created or wizamart_created:
print("✅ New items created:")
if admin_created:
print(" • Admin user")
if test_created:
print(" • TESTVENDOR")
if wizamart_created:
print(" • WIZAMART")
else:
print(" All items already existed - no changes made")
print("\n" + "" * 70)
print("📝 ADMIN LOGIN CREDENTIALS")
print("" * 70)
print(f" URL: http://localhost:8000/admin/login")
print(f" Username: {DEFAULT_ADMIN_USERNAME}")
print(f" Password: {DEFAULT_ADMIN_PASSWORD}")
print("" * 70)
print("\n" + "" * 70)
print("🏪 VENDORS")
print("" * 70)
vendors = db.execute(select(Vendor)).scalars().all()
for v in vendors:
print(f"\n {v.vendor_code}")
print(f" Name: {v.name}")
print(f" Subdomain: {v.subdomain}.letzshop.com")
print(f" Theme URL: http://localhost:8000/admin/vendors/{v.vendor_code}/theme")
print(f" Verified: {'' if v.is_verified else ''}")
print(f" Active: {'' if v.is_active else ''}")
print("\n" + "" * 70)
print("🚀 NEXT STEPS")
print("" * 70)
print(" 1. Start the server:")
print(" python -m uvicorn app.main:app --reload\n")
print(" 2. Login to admin panel:")
print(" http://localhost:8000/admin/login\n")
print(" 3. Test theme editor:")
print(" http://localhost:8000/admin/vendors/WIZAMART/theme\n")
if admin_created:
print("⚠️ SECURITY: Change the default admin password after first login!")
print()
except Exception as e:
db.rollback()
print("\n" + "" + "" * 68 + "")
print("" + " " * 28 + "ERROR" + " " * 35 + "")
print("" + "" * 68 + "\n")
print(f"❌ Failed to seed database: {e}")
print(f"\nError type: {type(e).__name__}")
print("\nCommon issues:")
print(" - Database migrations not run (run: alembic upgrade head)")
print(" - Database connection issues")
print(" - Foreign key constraint violations")
print()
import traceback
traceback.print_exc()
sys.exit(1)
finally:
db.close()
if __name__ == "__main__":
try:
seed_database()
except KeyboardInterrupt:
print("\n\n⚠️ Seeding interrupted by user")
sys.exit(1)
except Exception as e:
print(f"\n❌ Fatal error: {e}")
sys.exit(1)

View File

@@ -1,22 +1,17 @@
// static/admin/js/vendor-theme.js
// static/admin/js/vendor-theme.js (FIXED VERSION)
/**
* Vendor Theme Editor - Alpine.js Component
* Manages theme customization for vendor shops
*
* REQUIRES: log-config.js to be loaded first
*/
// ============================================================================
// LOGGING CONFIGURATION
// LOGGING CONFIGURATION (using centralized logger)
// ============================================================================
const THEME_LOG_LEVEL = 3; // 1=error, 2=warn, 3=info, 4=debug
const themeLog = {
error: (...args) => THEME_LOG_LEVEL >= 1 && console.error('❌ [THEME ERROR]', ...args),
warn: (...args) => THEME_LOG_LEVEL >= 2 && console.warn('⚠️ [THEME WARN]', ...args),
info: (...args) => THEME_LOG_LEVEL >= 3 && console.info(' [THEME INFO]', ...args),
debug: (...args) => THEME_LOG_LEVEL >= 4 && console.log('🔍 [THEME DEBUG]', ...args)
};
// Use the pre-configured theme logger from centralized log-config.js
const themeLog = window.LogConfig.loggers.vendorTheme;
// ============================================================================
// ALPINE.JS COMPONENT
@@ -50,254 +45,278 @@ function adminVendorTheme() {
},
fonts: {
heading: 'Inter, sans-serif',
body: 'Inter, sans-serif'
body: 'Inter, sans-serif',
size_base: '16px',
size_heading: '2rem'
},
layout: {
style: 'grid',
header: 'fixed',
product_card: 'modern'
header_position: 'fixed',
product_card_style: 'card',
sidebar_position: 'left'
},
branding: {
logo: null,
logo_dark: null,
favicon: null,
banner: null
logo_url: '',
favicon_url: '',
banner_url: ''
},
custom_css: ''
custom_css: '',
social_links: {
facebook: '',
instagram: '',
twitter: '',
linkedin: ''
}
},
// Available presets
presets: [],
selectedPreset: null,
// ============================================================================
// ====================================================================
// INITIALIZATION
// ============================================================================
// ====================================================================
async init() {
themeLog.info('=== VENDOR THEME EDITOR INITIALIZING ===');
themeLog.info('Initializing vendor theme editor');
// ✅ CRITICAL: Prevent multiple initializations
if (window._vendorThemeInitialized) {
themeLog.warn('Theme editor already initialized, skipping...');
return;
}
window._vendorThemeInitialized = true;
const startTime = Date.now();
// Get vendor code from URL
this.vendorCode = this.getVendorCodeFromURL();
themeLog.info('Vendor code:', this.vendorCode);
// Load data
await Promise.all([
this.loadVendorData(),
this.loadTheme(),
this.loadPresets()
]);
const duration = Date.now() - startTime;
themeLog.info(`=== THEME EDITOR INITIALIZATION COMPLETE (${duration}ms) ===`);
},
// ============================================================================
// URL HELPERS
// ============================================================================
getVendorCodeFromURL() {
const pathParts = window.location.pathname.split('/');
const vendorIndex = pathParts.indexOf('vendors');
return pathParts[vendorIndex + 1];
},
// ============================================================================
// DATA LOADING
// ============================================================================
async loadVendorData() {
themeLog.info('Loading vendor data...');
// Start performance timer
const startTime = performance.now();
try {
const startTime = Date.now();
const response = await apiClient.get(`/admin/vendors/${this.vendorCode}`);
const duration = Date.now() - startTime;
// Extract vendor code from URL
const urlParts = window.location.pathname.split('/');
this.vendorCode = urlParts[urlParts.indexOf('vendors') + 1];
this.vendor = response;
themeLog.info(`Vendor loaded in ${duration}ms:`, this.vendor.name);
themeLog.debug('Vendor code from URL:', this.vendorCode);
} catch (error) {
themeLog.error('Failed to load vendor:', error);
this.error = 'Failed to load vendor data';
Utils.showToast('Failed to load vendor data', 'error');
}
},
async loadTheme() {
themeLog.info('Loading theme...');
this.loading = true;
this.error = null;
try {
const startTime = Date.now();
const response = await apiClient.get(`/admin/vendor-themes/${this.vendorCode}`);
const duration = Date.now() - startTime;
if (response) {
// Merge loaded theme with defaults
this.themeData = {
theme_name: response.theme_name || 'default',
colors: {
...this.themeData.colors,
...(response.colors || {})
},
fonts: {
heading: response.fonts?.heading || this.themeData.fonts.heading,
body: response.fonts?.body || this.themeData.fonts.body
},
layout: {
style: response.layout?.style || this.themeData.layout.style,
header: response.layout?.header || this.themeData.layout.header,
product_card: response.layout?.product_card || this.themeData.layout.product_card
},
branding: {
...this.themeData.branding,
...(response.branding || {})
},
custom_css: response.custom_css || ''
};
themeLog.info(`Theme loaded in ${duration}ms:`, this.themeData.theme_name);
if (!this.vendorCode) {
throw new Error('Vendor code not found in URL');
}
} catch (error) {
themeLog.error('Failed to load theme:', error);
this.error = 'Failed to load theme';
Utils.showToast('Failed to load theme', 'error');
// Load data in parallel
themeLog.group('Loading theme data');
await Promise.all([
this.loadVendor(),
this.loadTheme(),
this.loadPresets()
]);
themeLog.groupEnd();
// Log performance
const duration = performance.now() - startTime;
window.LogConfig.logPerformance('Theme Editor Init', duration);
themeLog.info('Theme editor initialized successfully');
} catch (error) {
// Use centralized error logger
window.LogConfig.logError(error, 'Theme Editor Init');
this.error = error.message || 'Failed to initialize theme editor';
Utils.showToast(this.error, 'error');
} finally {
this.loading = false;
}
},
async loadPresets() {
themeLog.info('Loading presets...');
// ====================================================================
// DATA LOADING
// ====================================================================
async loadVendor() {
themeLog.info('Loading vendor data');
const url = `/admin/vendors/${this.vendorCode}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
try {
const startTime = Date.now();
const response = await apiClient.get('/admin/vendor-themes/presets');
const duration = Date.now() - startTime;
// ✅ FIX: apiClient returns data directly, not response.data
const response = await apiClient.get(url);
this.presets = response.presets || [];
themeLog.info(`${this.presets.length} presets loaded in ${duration}ms`);
// ✅ Direct assignment - response IS the data
this.vendor = response;
window.LogConfig.logApiCall('GET', url, this.vendor, 'response');
themeLog.debug('Vendor loaded:', this.vendor);
} catch (error) {
themeLog.warn('Failed to load presets:', error);
// Non-critical error, continue without presets
themeLog.error('Failed to load vendor:', error);
throw error;
}
},
// ============================================================================
// PRESET OPERATIONS
// ============================================================================
async loadTheme() {
themeLog.info('Loading theme data');
const url = `/admin/vendor-themes/${this.vendorCode}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
try {
// ✅ FIX: apiClient returns data directly
const response = await apiClient.get(url);
// Merge with default theme data
this.themeData = {
...this.themeData,
...response
};
window.LogConfig.logApiCall('GET', url, this.themeData, 'response');
themeLog.debug('Theme loaded:', this.themeData);
} catch (error) {
themeLog.warn('Failed to load theme, using defaults:', error);
// Continue with default theme
}
},
async loadPresets() {
themeLog.info('Loading theme presets');
const url = '/admin/vendor-themes/presets';
window.LogConfig.logApiCall('GET', url, null, 'request');
try {
// ✅ FIX: apiClient returns data directly
const response = await apiClient.get(url);
// ✅ Access presets directly from response, not response.data.presets
this.presets = response.presets || [];
window.LogConfig.logApiCall('GET', url, response, 'response');
themeLog.debug(`Loaded ${this.presets.length} presets`);
} catch (error) {
themeLog.error('Failed to load presets:', error);
this.presets = [];
}
},
// ====================================================================
// THEME OPERATIONS
// ====================================================================
async saveTheme() {
if (this.saving) return;
themeLog.info('Saving theme changes');
this.saving = true;
this.error = null;
const startTime = performance.now();
try {
const url = `/admin/vendor-themes/${this.vendorCode}`;
window.LogConfig.logApiCall('PUT', url, this.themeData, 'request');
// ✅ FIX: apiClient returns data directly
const response = await apiClient.put(url, this.themeData);
window.LogConfig.logApiCall('PUT', url, response, 'response');
const duration = performance.now() - startTime;
window.LogConfig.logPerformance('Save Theme', duration);
themeLog.info('Theme saved successfully');
Utils.showToast('Theme saved successfully', 'success');
} catch (error) {
window.LogConfig.logError(error, 'Save Theme');
this.error = 'Failed to save theme';
Utils.showToast(this.error, 'error');
} finally {
this.saving = false;
}
},
async applyPreset(presetName) {
themeLog.info(`Applying preset: ${presetName}`);
this.saving = true;
try {
const startTime = Date.now();
const response = await apiClient.post(
`/admin/vendor-themes/${this.vendorCode}/preset/${presetName}`
);
const duration = Date.now() - startTime;
const url = `/admin/vendor-themes/${this.vendorCode}/preset/${presetName}`;
window.LogConfig.logApiCall('POST', url, null, 'request');
// ✅ FIX: apiClient returns data directly
const response = await apiClient.post(url);
window.LogConfig.logApiCall('POST', url, response, 'response');
// ✅ FIX: Access theme directly from response, not response.data.theme
if (response && response.theme) {
// Update theme data with preset
this.themeData = {
theme_name: response.theme.theme_name,
colors: response.theme.colors || this.themeData.colors,
fonts: response.theme.fonts || this.themeData.fonts,
layout: response.theme.layout || this.themeData.layout,
branding: response.theme.branding || this.themeData.branding,
custom_css: response.theme.custom_css || ''
...this.themeData,
...response.theme
};
Utils.showToast(`Applied ${presetName} preset successfully`, 'success');
themeLog.info(`Preset applied in ${duration}ms`);
}
} catch (error) {
themeLog.error('Failed to apply preset:', error);
const message = error.response?.data?.detail || 'Failed to apply preset';
Utils.showToast(message, 'error');
themeLog.info(`Preset '${presetName}' applied successfully`);
Utils.showToast(`Applied ${presetName} preset`, 'success');
} catch (error) {
window.LogConfig.logError(error, 'Apply Preset');
Utils.showToast('Failed to apply preset', 'error');
} finally {
this.saving = false;
}
},
async resetToDefault() {
if (!confirm('Are you sure you want to reset to default theme? This will discard all customizations.')) {
async resetTheme() {
if (!confirm('Reset theme to default? This cannot be undone.')) {
return;
}
themeLog.info('Resetting to default theme');
await this.applyPreset('default');
},
// ============================================================================
// SAVE OPERATIONS
// ============================================================================
async saveTheme() {
themeLog.info('Saving theme:', this.themeData);
themeLog.warn('Resetting theme to default');
this.saving = true;
this.error = null;
try {
const startTime = Date.now();
const response = await apiClient.put(
`/admin/vendor-themes/${this.vendorCode}`,
this.themeData
);
const duration = Date.now() - startTime;
const url = `/admin/vendor-themes/${this.vendorCode}`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
if (response) {
Utils.showToast('Theme saved successfully', 'success');
themeLog.info(`Theme saved in ${duration}ms`);
}
await apiClient.delete(url);
window.LogConfig.logApiCall('DELETE', url, null, 'response');
// Reload theme data
await this.loadTheme();
themeLog.info('Theme reset successfully');
Utils.showToast('Theme reset to default', 'success');
} catch (error) {
themeLog.error('Failed to save theme:', error);
const message = error.response?.data?.detail || 'Failed to save theme';
Utils.showToast(message, 'error');
this.error = message;
window.LogConfig.logError(error, 'Reset Theme');
Utils.showToast('Failed to reset theme', 'error');
} finally {
this.saving = false;
}
},
// ============================================================================
// HELPER METHODS
// ============================================================================
// ====================================================================
// UTILITY METHODS
// ====================================================================
formatDate(dateString) {
if (!dateString) return '-';
return Utils.formatDate(dateString);
previewTheme() {
themeLog.debug('Opening theme preview');
const previewUrl = `/vendor/${this.vendor?.subdomain || this.vendorCode}`;
window.open(previewUrl, '_blank');
},
getPreviewStyle() {
return {
'--color-primary': this.themeData.colors.primary,
'--color-secondary': this.themeData.colors.secondary,
'--color-accent': this.themeData.colors.accent,
'--color-background': this.themeData.colors.background,
'--color-text': this.themeData.colors.text,
'--color-border': this.themeData.colors.border,
'--font-heading': this.themeData.fonts.heading,
'--font-body': this.themeData.fonts.body,
};
updateColor(key, value) {
themeLog.debug(`Color updated: ${key} = ${value}`);
this.themeData.colors[key] = value;
},
updateFont(type, value) {
themeLog.debug(`Font updated: ${type} = ${value}`);
this.themeData.fonts[type] = value;
},
updateLayout(key, value) {
themeLog.debug(`Layout updated: ${key} = ${value}`);
this.themeData.layout[key] = value;
}
};
}

View File

@@ -69,6 +69,7 @@ const Icons = {
'folder-open': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z"/></svg>`,
'download': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>`,
'upload': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>`,
'save': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"/></svg>`,
// Settings & Tools
'cog': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>`,
@@ -77,6 +78,10 @@ const Icons = {
'moon': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/></svg>`,
'sun': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg>`,
// Design & Theming
'palette': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"/></svg>`,
'color-swatch': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"/></svg>`,
// Location
'location-marker': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg>`,
'globe': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>`,

View File

@@ -0,0 +1,490 @@
// static/shared/js/log-config.js
/**
* Centralized Logging Configuration for ALL Frontends
*
* This file provides a consistent logging system across:
* - Admin Frontend
* - Vendor Frontend
* - Shop Frontend
*
* Each frontend can customize log levels while sharing the same logging infrastructure.
*
* Usage in any frontend:
* ```javascript
* // Use the global logger
* log.info('Page loaded');
* log.error('Something went wrong', error);
*
* // Or use a pre-configured logger
* const vendorLog = window.LogConfig.loggers.vendors;
* vendorLog.info('Vendors loaded');
*
* // Or create a custom logger
* const pageLog = window.LogConfig.createLogger('MY-PAGE', 3);
* pageLog.info('Page initialized');
* ```
*/
// ============================================================================
// LOG LEVELS
// ============================================================================
const LOG_LEVELS = {
ERROR: 1, // Only errors
WARN: 2, // Errors and warnings
INFO: 3, // Errors, warnings, and info (default)
DEBUG: 4 // Everything including debug messages
};
// ============================================================================
// FRONTEND DETECTION
// ============================================================================
/**
* Detect which frontend we're in based on URL path
* @returns {string} 'admin' | 'vendor' | 'shop' | 'unknown'
*/
function detectFrontend() {
const path = window.location.pathname;
if (path.startsWith('/admin')) return 'admin';
if (path.startsWith('/vendor')) return 'vendor';
if (path.startsWith('/shop')) return 'shop';
return 'unknown';
}
// ============================================================================
// ENVIRONMENT DETECTION
// ============================================================================
/**
* Detect environment based on hostname
* @returns {string} 'development' | 'production'
*/
function detectEnvironment() {
const hostname = window.location.hostname;
// Development environments
if (hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname.startsWith('192.168.') ||
hostname.endsWith('.local')) {
return 'development';
}
return 'production';
}
// ============================================================================
// LOG LEVEL CONFIGURATION
// ============================================================================
/**
* Default log levels per frontend
* Can be overridden by environment
*/
const DEFAULT_LOG_LEVELS = {
admin: {
development: LOG_LEVELS.DEBUG, // Show everything in development
production: LOG_LEVELS.WARN // Only warnings and errors in production
},
vendor: {
development: LOG_LEVELS.DEBUG,
production: LOG_LEVELS.INFO // Vendors might need more logging
},
shop: {
development: LOG_LEVELS.DEBUG,
production: LOG_LEVELS.ERROR // Shop frontend: minimal logging in production
},
unknown: {
development: LOG_LEVELS.DEBUG,
production: LOG_LEVELS.WARN
}
};
// ============================================================================
// ACTIVE LOG LEVEL DETERMINATION
// ============================================================================
const CURRENT_FRONTEND = detectFrontend();
const CURRENT_ENVIRONMENT = detectEnvironment();
const ACTIVE_LOG_LEVEL = DEFAULT_LOG_LEVELS[CURRENT_FRONTEND][CURRENT_ENVIRONMENT];
// ============================================================================
// LOGGING UTILITIES
// ============================================================================
/**
* Create a logger with a specific prefix and log level
*
* @param {string} prefix - Logger prefix (e.g., 'VENDORS', 'THEME', 'PRODUCTS')
* @param {number} level - Log level (1-4, defaults to ACTIVE_LOG_LEVEL)
* @param {string} context - Optional frontend context (defaults to CURRENT_FRONTEND)
* @returns {Object} Logger object with error, warn, info, debug methods
*/
function createLogger(prefix = 'APP', level = ACTIVE_LOG_LEVEL, context = CURRENT_FRONTEND) {
const frontendPrefix = context.toUpperCase();
const formatPrefix = (emoji, label) => `${emoji} [${frontendPrefix}:${prefix} ${label}]`;
return {
error: (...args) => {
if (level >= LOG_LEVELS.ERROR) {
console.error(formatPrefix('❌', 'ERROR'), ...args);
}
},
warn: (...args) => {
if (level >= LOG_LEVELS.WARN) {
console.warn(formatPrefix('⚠️', 'WARN'), ...args);
}
},
info: (...args) => {
if (level >= LOG_LEVELS.INFO) {
console.info(formatPrefix('', 'INFO'), ...args);
}
},
debug: (...args) => {
if (level >= LOG_LEVELS.DEBUG) {
console.log(formatPrefix('🔍', 'DEBUG'), ...args);
}
},
// Additional utility methods
group: (label) => {
if (level >= LOG_LEVELS.INFO) {
console.group(formatPrefix('📂', 'GROUP') + ' ' + label);
}
},
groupEnd: () => {
if (level >= LOG_LEVELS.INFO) {
console.groupEnd();
}
},
table: (data) => {
if (level >= LOG_LEVELS.DEBUG) {
console.table(data);
}
},
time: (label) => {
if (level >= LOG_LEVELS.DEBUG) {
console.time(formatPrefix('⏱️', 'TIME') + ' ' + label);
}
},
timeEnd: (label) => {
if (level >= LOG_LEVELS.DEBUG) {
console.timeEnd(formatPrefix('⏱️', 'TIME') + ' ' + label);
}
}
};
}
// ============================================================================
// DEFAULT GLOBAL LOGGER
// ============================================================================
/**
* Default logger for general operations
* Automatically prefixed with current frontend
*/
const log = createLogger('APP', ACTIVE_LOG_LEVEL);
// ============================================================================
// PRE-CONFIGURED LOGGERS FOR ADMIN FRONTEND
// ============================================================================
const adminLoggers = {
// Vendor management
vendors: createLogger('VENDORS', ACTIVE_LOG_LEVEL),
vendorTheme: createLogger('THEME', ACTIVE_LOG_LEVEL),
vendorUsers: createLogger('VENDOR-USERS', ACTIVE_LOG_LEVEL),
// Product management
products: createLogger('PRODUCTS', ACTIVE_LOG_LEVEL),
inventory: createLogger('INVENTORY', ACTIVE_LOG_LEVEL),
// Order management
orders: createLogger('ORDERS', ACTIVE_LOG_LEVEL),
// User management
users: createLogger('USERS', ACTIVE_LOG_LEVEL),
// Admin operations
audit: createLogger('AUDIT', ACTIVE_LOG_LEVEL),
dashboard: createLogger('DASHBOARD', ACTIVE_LOG_LEVEL),
// Import operations
imports: createLogger('IMPORTS', ACTIVE_LOG_LEVEL)
};
// ============================================================================
// PRE-CONFIGURED LOGGERS FOR VENDOR FRONTEND
// ============================================================================
const vendorLoggers = {
// Vendor dashboard
dashboard: createLogger('DASHBOARD', ACTIVE_LOG_LEVEL),
// Product management
products: createLogger('PRODUCTS', ACTIVE_LOG_LEVEL),
inventory: createLogger('INVENTORY', ACTIVE_LOG_LEVEL),
// Order management
orders: createLogger('ORDERS', ACTIVE_LOG_LEVEL),
// Theme customization
theme: createLogger('THEME', ACTIVE_LOG_LEVEL),
// Settings
settings: createLogger('SETTINGS', ACTIVE_LOG_LEVEL),
// Analytics
analytics: createLogger('ANALYTICS', ACTIVE_LOG_LEVEL)
};
// ============================================================================
// PRE-CONFIGURED LOGGERS FOR SHOP FRONTEND
// ============================================================================
const shopLoggers = {
// Product browsing
catalog: createLogger('CATALOG', ACTIVE_LOG_LEVEL),
product: createLogger('PRODUCT', ACTIVE_LOG_LEVEL),
search: createLogger('SEARCH', ACTIVE_LOG_LEVEL),
// Shopping cart
cart: createLogger('CART', ACTIVE_LOG_LEVEL),
checkout: createLogger('CHECKOUT', ACTIVE_LOG_LEVEL),
// User account
account: createLogger('ACCOUNT', ACTIVE_LOG_LEVEL),
orders: createLogger('ORDERS', ACTIVE_LOG_LEVEL),
// Wishlist
wishlist: createLogger('WISHLIST', ACTIVE_LOG_LEVEL)
};
// ============================================================================
// SMART LOGGER SELECTION
// ============================================================================
/**
* Get the appropriate logger set for current frontend
*/
function getLoggers() {
switch (CURRENT_FRONTEND) {
case 'admin':
return adminLoggers;
case 'vendor':
return vendorLoggers;
case 'shop':
return shopLoggers;
default:
return {}; // Empty object, use createLogger instead
}
}
// Export frontend-specific loggers
const loggers = getLoggers();
// ============================================================================
// API CALL LOGGING
// ============================================================================
/**
* Log API calls with consistent formatting
*
* @param {string} method - HTTP method (GET, POST, PUT, DELETE)
* @param {string} url - API endpoint URL
* @param {*} data - Request/response data (optional)
* @param {string} status - 'request' or 'response'
*/
function logApiCall(method, url, data = null, status = 'request') {
const apiLogger = createLogger('API', ACTIVE_LOG_LEVEL);
const emoji = status === 'request' ? '📤' : '📥';
const message = `${emoji} ${method} ${url}`;
if (ACTIVE_LOG_LEVEL >= LOG_LEVELS.DEBUG) {
if (data) {
apiLogger.debug(message, data);
} else {
apiLogger.debug(message);
}
} else if (ACTIVE_LOG_LEVEL >= LOG_LEVELS.INFO) {
apiLogger.info(message);
}
}
// ============================================================================
// ERROR LOGGING
// ============================================================================
/**
* Log errors with stack traces
*
* @param {Error} error - Error object
* @param {string} context - Context where error occurred
*/
function logError(error, context = 'Unknown') {
const errorLogger = createLogger('ERROR', ACTIVE_LOG_LEVEL);
errorLogger.error(`Error in ${context}:`, error.message);
if (ACTIVE_LOG_LEVEL >= LOG_LEVELS.DEBUG && error.stack) {
console.error('Stack trace:', error.stack);
}
}
// ============================================================================
// PERFORMANCE LOGGING
// ============================================================================
/**
* Log performance metrics
*
* @param {string} operation - Operation name
* @param {number} duration - Duration in milliseconds
*/
function logPerformance(operation, duration) {
const perfLogger = createLogger('PERF', ACTIVE_LOG_LEVEL);
if (ACTIVE_LOG_LEVEL >= LOG_LEVELS.DEBUG) {
const emoji = duration < 100 ? '⚡' : duration < 500 ? '⏱️' : '🐌';
perfLogger.debug(`${emoji} ${operation} took ${duration}ms`);
}
}
// ============================================================================
// EXPORTS (for modules)
// ============================================================================
// For pages that use module imports
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
LOG_LEVELS,
log,
loggers,
createLogger,
logApiCall,
logError,
logPerformance,
detectFrontend,
detectEnvironment
};
}
// ============================================================================
// GLOBAL ACCESS (for inline scripts)
// ============================================================================
// Make available globally for inline scripts
window.LogConfig = {
LOG_LEVELS,
log,
loggers,
createLogger,
logApiCall,
logError,
logPerformance,
// Expose frontend/environment info
frontend: CURRENT_FRONTEND,
environment: CURRENT_ENVIRONMENT,
logLevel: ACTIVE_LOG_LEVEL
};
// ============================================================================
// INITIALIZATION
// ============================================================================
// Log that logging system is initialized
if (ACTIVE_LOG_LEVEL >= LOG_LEVELS.INFO) {
const frontendName = CURRENT_FRONTEND.charAt(0).toUpperCase() + CURRENT_FRONTEND.slice(1);
const envName = CURRENT_ENVIRONMENT.charAt(0).toUpperCase() + CURRENT_ENVIRONMENT.slice(1);
const levelName = Object.keys(LOG_LEVELS).find(k => LOG_LEVELS[k] === ACTIVE_LOG_LEVEL);
console.log(
`%c🎛 ${frontendName} Frontend Logging System Initialized`,
'font-weight: bold; font-size: 14px; color: #6366f1;'
);
console.log(
`%c Environment: ${envName}`,
'color: #8b5cf6;'
);
console.log(
`%c Log Level: ${ACTIVE_LOG_LEVEL} (${levelName})`,
'color: #8b5cf6;'
);
}
// ============================================================================
// USAGE EXAMPLES (for developers)
// ============================================================================
/*
EXAMPLE 1: Use global logger
=============================
window.LogConfig.log.info('Application started');
window.LogConfig.log.error('Failed to load data', error);
EXAMPLE 2: Use pre-configured logger (RECOMMENDED)
===================================================
// In admin frontend
const themeLog = window.LogConfig.loggers.vendorTheme;
themeLog.info('Theme editor loaded');
// In vendor frontend
const dashLog = window.LogConfig.loggers.dashboard;
dashLog.info('Dashboard initialized');
// In shop frontend
const cartLog = window.LogConfig.loggers.cart;
cartLog.info('Cart updated');
EXAMPLE 3: Create custom logger
================================
const myLog = window.LogConfig.createLogger('MY-FEATURE', 4);
myLog.info('Feature initialized');
EXAMPLE 4: Check current frontend
==================================
if (window.LogConfig.frontend === 'admin') {
// Admin-specific code
}
EXAMPLE 5: API call logging
============================
window.LogConfig.logApiCall('GET', '/api/vendors', null, 'request');
const data = await apiClient.get('/api/vendors');
window.LogConfig.logApiCall('GET', '/api/vendors', data, 'response');
EXAMPLE 6: Performance logging
===============================
const start = performance.now();
await loadData();
const duration = performance.now() - start;
window.LogConfig.logPerformance('Load Data', duration);
EXAMPLE 7: Error logging
=========================
try {
await saveData();
} catch (error) {
window.LogConfig.logError(error, 'Save Operation');
}
*/