fix(ui): inject window.FRONTEND_TYPE from server + rename SHOP→STOREFRONT

Server now injects window.FRONTEND_TYPE in all base templates via
get_context_for_frontend(). Both log-config.js and dev-toolbar.js read
this instead of guessing from URL paths, fixing:
- UNKNOWN prefix on merchant pages
- Incorrect detection on custom domains/subdomains in prod

Also adds frontend_type to login page contexts (admin, merchant, store).

Renames all [SHOP] logger prefixes to [STOREFRONT] across 7 files
(storefront-layout.js + 6 storefront templates).

Adds 'merchant' and 'storefront' to log-config.js frontend detection,
log levels, and logger selection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-28 21:08:59 +01:00
parent 9bceeaac9c
commit b4f01210d9
16 changed files with 99 additions and 79 deletions

View File

@@ -208,7 +208,7 @@ document.addEventListener('alpine:init', () => {
// Initialize
async init() {
console.log('[SHOP] Cart page initializing...');
console.log('[STOREFRONT] Cart page initializing...');
// Call parent init to set up sessionId
if (baseData.init) {
@@ -223,17 +223,17 @@ document.addEventListener('alpine:init', () => {
this.loading = true;
try {
console.log(`[SHOP] Loading cart for session ${this.sessionId}...`);
console.log(`[STOREFRONT] Loading cart for session ${this.sessionId}...`);
const response = await fetch(`/api/v1/storefront/cart/${this.sessionId}`);
if (response.ok) {
const data = await response.json();
this.items = data.items || [];
this.cartCount = this.totalItems;
console.log('[SHOP] Cart loaded:', this.items.length, 'items');
console.log('[STOREFRONT] Cart loaded:', this.items.length, 'items');
}
} catch (error) {
console.error('[SHOP] Failed to load cart:', error);
console.error('[STOREFRONT] Failed to load cart:', error);
this.showToast('Failed to load cart', 'error');
} finally {
this.loading = false;
@@ -249,7 +249,7 @@ document.addEventListener('alpine:init', () => {
this.updating = true;
try {
console.log('[SHOP] Updating quantity:', productId, newQuantity);
console.log('[STOREFRONT] Updating quantity:', productId, newQuantity);
const response = await fetch(
`/api/v1/storefront/cart/${this.sessionId}/items/${productId}`,
{
@@ -268,7 +268,7 @@ document.addEventListener('alpine:init', () => {
throw new Error('Failed to update quantity');
}
} catch (error) {
console.error('[SHOP] Update quantity error:', error);
console.error('[STOREFRONT] Update quantity error:', error);
this.showToast('Failed to update quantity', 'error');
} finally {
this.updating = false;
@@ -280,7 +280,7 @@ document.addEventListener('alpine:init', () => {
this.updating = true;
try {
console.log('[SHOP] Removing item:', productId);
console.log('[STOREFRONT] Removing item:', productId);
const response = await fetch(
`/api/v1/storefront/cart/${this.sessionId}/items/${productId}`,
{
@@ -295,7 +295,7 @@ document.addEventListener('alpine:init', () => {
throw new Error('Failed to remove item');
}
} catch (error) {
console.error('[SHOP] Remove item error:', error);
console.error('[STOREFRONT] Remove item error:', error);
this.showToast('Failed to remove item', 'error');
} finally {
this.updating = false;

View File

@@ -187,8 +187,8 @@ document.addEventListener('alpine:init', () => {
},
async init() {
console.log('[SHOP] Category page initializing...');
console.log('[SHOP] Category slug:', this.categorySlug);
console.log('[STOREFRONT] Category page initializing...');
console.log('[STOREFRONT] Category slug:', this.categorySlug);
// Convert slug to display name
this.categoryName = this.categorySlug
@@ -213,7 +213,7 @@ document.addEventListener('alpine:init', () => {
params.append('sort', this.sortBy);
}
console.log(`[SHOP] Loading category products from /api/v1/storefront/products?${params}`);
console.log(`[STOREFRONT] Loading category products from /api/v1/storefront/products?${params}`);
const response = await fetch(`/api/v1/storefront/products?${params}`);
@@ -223,12 +223,12 @@ document.addEventListener('alpine:init', () => {
const data = await response.json();
console.log(`[SHOP] Loaded ${data.products.length} products (total: ${data.total})`);
console.log(`[STOREFRONT] Loaded ${data.products.length} products (total: ${data.total})`);
this.products = data.products;
this.total = data.total;
} catch (error) {
console.error('[SHOP] Failed to load category products:', error);
console.error('[STOREFRONT] Failed to load category products:', error);
this.showToast('Failed to load products', 'error');
} finally {
this.loading = false;
@@ -243,7 +243,7 @@ document.addEventListener('alpine:init', () => {
},
async addToCart(product) {
console.log('[SHOP] Adding to cart:', product);
console.log('[STOREFRONT] Adding to cart:', product);
try {
const url = `/api/v1/storefront/cart/${this.sessionId}/items`;
@@ -262,16 +262,16 @@ document.addEventListener('alpine:init', () => {
if (response.ok) {
const result = await response.json();
console.log('[SHOP] Add to cart success:', result);
console.log('[STOREFRONT] Add to cart success:', result);
this.cartCount += 1;
this.showToast(`${product.marketplace_product.title} added to cart`, 'success');
} else {
const error = await response.json();
console.error('[SHOP] Add to cart error:', error);
console.error('[STOREFRONT] Add to cart error:', error);
this.showToast(error.message || 'Failed to add to cart', 'error');
}
} catch (error) {
console.error('[SHOP] Add to cart exception:', error);
console.error('[STOREFRONT] Add to cart exception:', error);
this.showToast('Failed to add to cart', 'error');
}
}

View File

@@ -256,16 +256,16 @@ document.addEventListener('alpine:init', () => {
// Initialize
async init() {
console.log('[SHOP] Product detail page initializing...');
console.log('[STOREFRONT] Product detail page initializing...');
// Call parent init to set up sessionId
if (baseData.init) {
baseData.init.call(this);
}
console.log('[SHOP] Product ID:', this.productId);
console.log('[SHOP] Store ID:', this.storeId);
console.log('[SHOP] Session ID:', this.sessionId);
console.log('[STOREFRONT] Product ID:', this.productId);
console.log('[STOREFRONT] Store ID:', this.storeId);
console.log('[STOREFRONT] Session ID:', this.sessionId);
await this.loadProduct();
},
@@ -275,7 +275,7 @@ document.addEventListener('alpine:init', () => {
this.loading = true;
try {
console.log(`[SHOP] Loading product ${this.productId}...`);
console.log(`[STOREFRONT] Loading product ${this.productId}...`);
const response = await fetch(`/api/v1/storefront/products/${this.productId}`);
if (!response.ok) {
@@ -283,7 +283,7 @@ document.addEventListener('alpine:init', () => {
}
this.product = await response.json();
console.log('[SHOP] Product loaded:', this.product);
console.log('[STOREFRONT] Product loaded:', this.product);
// Set default image
if (this.product?.marketplace_product?.image_link) {
@@ -297,7 +297,7 @@ document.addEventListener('alpine:init', () => {
await this.loadRelatedProducts();
} catch (error) {
console.error('[SHOP] Failed to load product:', error);
console.error('[STOREFRONT] Failed to load product:', error);
this.showToast('Failed to load product', 'error');
// Redirect back to products after error
setTimeout(() => {
@@ -320,10 +320,10 @@ document.addEventListener('alpine:init', () => {
.filter(p => p.id !== parseInt(this.productId))
.slice(0, 4);
console.log('[SHOP] Loaded related products:', this.relatedProducts.length);
console.log('[STOREFRONT] Loaded related products:', this.relatedProducts.length);
}
} catch (error) {
console.error('[SHOP] Failed to load related products:', error);
console.error('[STOREFRONT] Failed to load related products:', error);
}
},
@@ -356,7 +356,7 @@ document.addEventListener('alpine:init', () => {
// Add to cart
async addToCart() {
if (!this.canAddToCart) {
console.warn('[SHOP] Cannot add to cart:', {
console.warn('[STOREFRONT] Cannot add to cart:', {
canAddToCart: this.canAddToCart,
isActive: this.product?.is_active,
inventory: this.product?.available_inventory,
@@ -374,7 +374,7 @@ document.addEventListener('alpine:init', () => {
quantity: this.quantity
};
console.log('[SHOP] Adding to cart:', {
console.log('[STOREFRONT] Adding to cart:', {
url,
sessionId: this.sessionId,
productId: this.productId,
@@ -390,14 +390,14 @@ document.addEventListener('alpine:init', () => {
body: JSON.stringify(payload)
});
console.log('[SHOP] Add to cart response:', {
console.log('[STOREFRONT] Add to cart response:', {
status: response.status,
ok: response.ok
});
if (response.ok) {
const result = await response.json();
console.log('[SHOP] Add to cart success:', result);
console.log('[STOREFRONT] Add to cart success:', result);
this.cartCount += this.quantity;
this.showToast(
@@ -409,11 +409,11 @@ document.addEventListener('alpine:init', () => {
this.quantity = this.product?.min_quantity || 1;
} else {
const error = await response.json();
console.error('[SHOP] Add to cart error response:', error);
console.error('[STOREFRONT] Add to cart error response:', error);
throw new Error(error.detail || 'Failed to add to cart');
}
} catch (error) {
console.error('[SHOP] Add to cart exception:', error);
console.error('[STOREFRONT] Add to cart exception:', error);
this.showToast(error.message || 'Failed to add to cart', 'error');
} finally {
this.addingToCart = false;

View File

@@ -160,7 +160,7 @@ document.addEventListener('alpine:init', () => {
},
async init() {
console.log('[SHOP] Products page initializing...');
console.log('[STOREFRONT] Products page initializing...');
await this.loadProducts();
},
@@ -178,7 +178,7 @@ document.addEventListener('alpine:init', () => {
params.append('search', this.filters.search);
}
console.log(`[SHOP] Loading products from /api/v1/storefront/products?${params}`);
console.log(`[STOREFRONT] Loading products from /api/v1/storefront/products?${params}`);
const response = await fetch(`/api/v1/storefront/products?${params}`);
@@ -188,12 +188,12 @@ document.addEventListener('alpine:init', () => {
const data = await response.json();
console.log(`[SHOP] Loaded ${data.products.length} products (total: ${data.total})`);
console.log(`[STOREFRONT] Loaded ${data.products.length} products (total: ${data.total})`);
this.products = data.products;
this.pagination.total = data.total;
} catch (error) {
console.error('[SHOP] Failed to load products:', error);
console.error('[STOREFRONT] Failed to load products:', error);
this.showToast('Failed to load products', 'error');
} finally {
this.loading = false;
@@ -208,7 +208,7 @@ document.addEventListener('alpine:init', () => {
// formatPrice is inherited from storefrontLayoutData() via spread operator
async addToCart(product) {
console.log('[SHOP] Adding to cart:', product);
console.log('[STOREFRONT] Adding to cart:', product);
try {
const url = `/api/v1/storefront/cart/${this.sessionId}/items`;
@@ -227,16 +227,16 @@ document.addEventListener('alpine:init', () => {
if (response.ok) {
const result = await response.json();
console.log('[SHOP] Add to cart success:', result);
console.log('[STOREFRONT] Add to cart success:', result);
this.cartCount += 1;
this.showToast(`${product.marketplace_product.title} added to cart`, 'success');
} else {
const error = await response.json();
console.error('[SHOP] Add to cart error:', error);
console.error('[STOREFRONT] Add to cart error:', error);
this.showToast(error.message || 'Failed to add to cart', 'error');
}
} catch (error) {
console.error('[SHOP] Add to cart exception:', error);
console.error('[STOREFRONT] Add to cart exception:', error);
this.showToast('Failed to add to cart', 'error');
}
}

View File

@@ -212,7 +212,7 @@ document.addEventListener('alpine:init', () => {
},
async init() {
console.log('[SHOP] Search page initializing...');
console.log('[STOREFRONT] Search page initializing...');
// Check for query parameter in URL
const urlParams = new URLSearchParams(window.location.search);
@@ -254,7 +254,7 @@ document.addEventListener('alpine:init', () => {
limit: this.perPage
});
console.log(`[SHOP] Searching: /api/v1/storefront/products/search?${params}`);
console.log(`[STOREFRONT] Searching: /api/v1/storefront/products/search?${params}`);
const response = await fetch(`/api/v1/storefront/products/search?${params}`);
@@ -264,12 +264,12 @@ document.addEventListener('alpine:init', () => {
const data = await response.json();
console.log(`[SHOP] Search found ${data.total} results`);
console.log(`[STOREFRONT] Search found ${data.total} results`);
this.products = data.products;
this.total = data.total;
} catch (error) {
console.error('[SHOP] Search failed:', error);
console.error('[STOREFRONT] Search failed:', error);
this.showToast('Search failed. Please try again.', 'error');
this.products = [];
this.total = 0;
@@ -289,7 +289,7 @@ document.addEventListener('alpine:init', () => {
},
async addToCart(product) {
console.log('[SHOP] Adding to cart:', product);
console.log('[STOREFRONT] Adding to cart:', product);
try {
const url = `/api/v1/storefront/cart/${this.sessionId}/items`;
@@ -308,16 +308,16 @@ document.addEventListener('alpine:init', () => {
if (response.ok) {
const result = await response.json();
console.log('[SHOP] Add to cart success:', result);
console.log('[STOREFRONT] Add to cart success:', result);
this.cartCount += 1;
this.showToast(`${product.marketplace_product?.title || 'Product'} added to cart`, 'success');
} else {
const error = await response.json();
console.error('[SHOP] Add to cart error:', error);
console.error('[STOREFRONT] Add to cart error:', error);
this.showToast(error.message || 'Failed to add to cart', 'error');
}
} catch (error) {
console.error('[SHOP] Add to cart exception:', error);
console.error('[STOREFRONT] Add to cart exception:', error);
this.showToast('Failed to add to cart', 'error');
}
}

View File

@@ -143,7 +143,7 @@ document.addEventListener('alpine:init', () => {
isLoggedIn: false,
async init() {
console.log('[SHOP] Wishlist page initializing...');
console.log('[STOREFRONT] Wishlist page initializing...');
// Check if user is logged in
this.isLoggedIn = await this.checkLoginStatus();
@@ -168,7 +168,7 @@ document.addEventListener('alpine:init', () => {
this.loading = true;
try {
console.log('[SHOP] Loading wishlist...');
console.log('[STOREFRONT] Loading wishlist...');
const response = await fetch('/api/v1/storefront/wishlist');
@@ -182,11 +182,11 @@ document.addEventListener('alpine:init', () => {
const data = await response.json();
console.log(`[SHOP] Loaded ${data.items?.length || 0} wishlist items`);
console.log(`[STOREFRONT] Loaded ${data.items?.length || 0} wishlist items`);
this.items = data.items || [];
} catch (error) {
console.error('[SHOP] Failed to load wishlist:', error);
console.error('[STOREFRONT] Failed to load wishlist:', error);
this.showToast('Failed to load wishlist', 'error');
} finally {
this.loading = false;
@@ -195,7 +195,7 @@ document.addEventListener('alpine:init', () => {
async removeFromWishlist(item) {
try {
console.log('[SHOP] Removing from wishlist:', item);
console.log('[STOREFRONT] Removing from wishlist:', item);
const response = await fetch(`/api/v1/storefront/wishlist/${item.id}`, {
method: 'DELETE'
@@ -208,13 +208,13 @@ document.addEventListener('alpine:init', () => {
throw new Error('Failed to remove from wishlist');
}
} catch (error) {
console.error('[SHOP] Failed to remove from wishlist:', error);
console.error('[STOREFRONT] Failed to remove from wishlist:', error);
this.showToast('Failed to remove from wishlist', 'error');
}
},
async addToCart(product) {
console.log('[SHOP] Adding to cart:', product);
console.log('[STOREFRONT] Adding to cart:', product);
try {
const url = `/api/v1/storefront/cart/${this.sessionId}/items`;
@@ -233,16 +233,16 @@ document.addEventListener('alpine:init', () => {
if (response.ok) {
const result = await response.json();
console.log('[SHOP] Add to cart success:', result);
console.log('[STOREFRONT] Add to cart success:', result);
this.cartCount += 1;
this.showToast(`${product.marketplace_product?.title || 'Product'} added to cart`, 'success');
} else {
const error = await response.json();
console.error('[SHOP] Add to cart error:', error);
console.error('[STOREFRONT] Add to cart error:', error);
this.showToast(error.message || 'Failed to add to cart', 'error');
}
} catch (error) {
console.error('[SHOP] Add to cart exception:', error);
console.error('[STOREFRONT] Add to cart exception:', error);
this.showToast('Failed to add to cart', 'error');
}
}

View File

@@ -62,6 +62,7 @@ async def admin_login_page(
context = {
"request": request,
"current_language": language,
"frontend_type": "admin",
**get_jinja2_globals(language),
}
return templates.TemplateResponse("tenancy/admin/login.html", context)

View File

@@ -72,6 +72,7 @@ async def merchant_login_page(
context = {
"request": request,
"current_language": language,
"frontend_type": "merchant",
**get_jinja2_globals(language),
}
return templates.TemplateResponse("tenancy/merchant/login.html", context)

View File

@@ -5,11 +5,11 @@
* Works with store-specific themes
*/
const shopLog = {
info: (...args) => console.info('🛒 [SHOP]', ...args),
warn: (...args) => console.warn('⚠️ [SHOP]', ...args),
error: (...args) => console.error('❌ [SHOP]', ...args),
debug: (...args) => console.log('🔍 [SHOP]', ...args)
const shopLog = window.LogConfig?.createLogger('STOREFRONT') || {
info: (...args) => console.info('🛒 [STOREFRONT]', ...args),
warn: (...args) => console.warn('⚠️ [STOREFRONT]', ...args),
error: (...args) => console.error('❌ [STOREFRONT]', ...args),
debug: (...args) => console.log('🔍 [STOREFRONT]', ...args)
};
/**

View File

@@ -149,6 +149,9 @@ def get_context_for_frontend(
# Pass enabled module codes to templates for conditional rendering
context["enabled_modules"] = enabled_module_codes
# Pass frontend type to templates (used by JS for logging, dev toolbar, etc.)
context["frontend_type"] = frontend_type.value
# For storefront, build nav menu structure from module declarations
if frontend_type == FrontendType.STOREFRONT:
from app.modules.core.services.menu_discovery_service import (

View File

@@ -100,6 +100,9 @@
<!-- Core Scripts - ORDER MATTERS! -->
<!-- 0. Frontend type (server-injected, used by log-config and dev-toolbar) -->
<script>window.FRONTEND_TYPE = '{{ frontend_type | default("admin") }}';</script>
<!-- 1. FIRST: Log Configuration -->
<script defer src="{{ url_for('static', path='shared/js/log-config.js') }}"></script>

View File

@@ -50,6 +50,9 @@
<!-- Core Scripts - ORDER MATTERS! -->
<!-- 0. Frontend type (server-injected, used by log-config and dev-toolbar) -->
<script>window.FRONTEND_TYPE = '{{ frontend_type | default("merchant") }}';</script>
<!-- 1. FIRST: Log Configuration -->
<script defer src="{{ url_for('static', path='shared/js/log-config.js') }}"></script>

View File

@@ -54,6 +54,9 @@
<!-- Core Scripts - ORDER MATTERS! -->
<!-- 0. Frontend type (server-injected, used by log-config and dev-toolbar) -->
<script>window.FRONTEND_TYPE = '{{ frontend_type | default("store") }}';</script>
<!-- 1. FIRST: Log Configuration -->
<script defer src="{{ url_for('static', path='shared/js/log-config.js') }}"></script>

View File

@@ -349,6 +349,9 @@
{# JavaScript Loading Order (CRITICAL - must be in this order) #}
{# 0. Frontend type (server-injected, used by log-config and dev-toolbar) #}
<script>window.FRONTEND_TYPE = '{{ frontend_type | default("storefront") }}';</script>
{# 1. Log Configuration (must load first) #}
<script defer src="{{ url_for('static', path='shared/js/log-config.js') }}"></script>

View File

@@ -286,11 +286,11 @@
}
function detectFrontend() {
// Prefer server-injected value (set in base templates)
if (window.FRONTEND_TYPE) return window.FRONTEND_TYPE;
// Fallback for pages without base template (e.g., API docs)
var path = window.location.pathname;
if (path.startsWith('/store/') || path === '/store') return 'store';
if (path.startsWith('/admin/') || path === '/admin') return 'admin';
if (path.indexOf('/merchants/') !== -1) return 'merchant';
if (path.indexOf('/storefront/') !== -1) return 'storefront';
if (path.startsWith('/api/')) return 'api';
return 'unknown';
}

View File

@@ -43,14 +43,11 @@ const LOG_LEVELS = {
/**
* Detect which frontend we're in based on URL path
* @returns {string} 'admin' | 'store' | 'shop' | 'unknown'
* @returns {string} 'admin' | 'store' | 'merchant' | 'storefront' | 'unknown'
*/
function detectFrontend() {
const path = window.location.pathname;
if (path.startsWith('/admin')) return 'admin';
if (path.startsWith('/store')) return 'store';
if (path.startsWith('/shop')) return 'shop';
// Prefer server-injected value (set in base templates before this script loads)
if (window.FRONTEND_TYPE) return window.FRONTEND_TYPE;
return 'unknown';
}
@@ -94,9 +91,13 @@ const DEFAULT_LOG_LEVELS = {
development: LOG_LEVELS.DEBUG,
production: LOG_LEVELS.INFO // Stores might need more logging
},
shop: {
merchant: {
development: LOG_LEVELS.DEBUG,
production: LOG_LEVELS.ERROR // Shop frontend: minimal logging in production
production: LOG_LEVELS.INFO // Merchant portal: same as store
},
storefront: {
development: LOG_LEVELS.DEBUG,
production: LOG_LEVELS.ERROR // Storefront: minimal logging in production
},
unknown: {
development: LOG_LEVELS.DEBUG,
@@ -275,10 +276,10 @@ const storeLoggers = {
};
// ============================================================================
// PRE-CONFIGURED LOGGERS FOR SHOP FRONTEND
// PRE-CONFIGURED LOGGERS FOR STOREFRONT
// ============================================================================
const shopLoggers = {
const storefrontLoggers = {
// Product browsing
catalog: createLogger('CATALOG', ACTIVE_LOG_LEVEL),
product: createLogger('PRODUCT', ACTIVE_LOG_LEVEL),
@@ -309,8 +310,10 @@ function getLoggers() {
return adminLoggers;
case 'store':
return storeLoggers;
case 'shop':
return shopLoggers;
case 'merchant':
return storeLoggers; // Merchant portal reuses store logger set
case 'storefront':
return storefrontLoggers;
default:
return {}; // Empty object, use createLogger instead
}