/**
* Layout Manager
* Handles injection and management of shared layout components
* Version: 2025-12-02-3 (Fixed syntax error - all methods inside class)
*/
import { PAGE_METADATA } from './config.js';
import logger from '../utils/logger.js';
export class LayoutManager {
static layoutsInjected = false;
static featureDetectionLoaded = false;
static apiStatusInterval = null;
static consecutiveFailures = 0;
static maxFailures = 3;
static isOffline = false;
/**
* Load feature detection utility (suppresses browser warnings)
*/
static async loadFeatureDetection() {
if (this.featureDetectionLoaded) return;
// Suppress warnings immediately (before loading script)
if (!window._hfWarningsSuppressed) {
const originalWarn = console.warn;
const originalError = console.error;
// List of unrecognized features that cause warnings (from HF Space container)
const unrecognizedFeatures = [
'ambient-light-sensor',
'battery',
'document-domain',
'layout-animations',
'legacy-image-formats',
'oversized-images',
'vr',
'wake-lock',
'screen-wake-lock',
'virtual-reality',
'cross-origin-isolated',
'execution-while-not-rendered',
'execution-while-out-of-viewport',
'keyboard-map',
'navigation-override',
'publickey-credentials-get',
'xr-spatial-tracking'
];
const shouldSuppress = (message) => {
if (!message) return false;
const msg = message.toString().toLowerCase();
// Check for "Unrecognized feature:" pattern
if (msg.includes('unrecognized feature:')) {
return unrecognizedFeatures.some(feature => msg.includes(feature));
}
// Also check for Permissions-Policy warnings
if (msg.includes('permissions-policy') || msg.includes('feature-policy')) {
return unrecognizedFeatures.some(feature => msg.includes(feature));
}
// Check for HF Space domain in warning
if (msg.includes('datasourceforcryptocurrency') &&
unrecognizedFeatures.some(feature => msg.includes(feature))) {
return true;
}
return false;
};
console.warn = function(...args) {
const message = args[0]?.toString() || '';
if (shouldSuppress(message)) {
return; // Suppress silently
}
originalWarn.apply(console, args);
};
console.error = function(...args) {
const message = args[0]?.toString() || '';
if (shouldSuppress(message)) {
return; // Suppress silently
}
originalError.apply(console, args);
};
window._hfWarningsSuppressed = true;
}
try {
// Try multiple paths for feature detection
const possiblePaths = [
'/static/shared/js/feature-detection.js',
'../shared/js/feature-detection.js',
'./shared/js/feature-detection.js',
window.location.pathname.includes('/static/')
? window.location.pathname.split('/static/')[0] + '/static/shared/js/feature-detection.js'
: '/static/shared/js/feature-detection.js'
];
// Load feature detection script to suppress console warnings
const script = document.createElement('script');
// Try first path, fallback to others if needed
script.src = possiblePaths[0];
script.async = true;
script.onerror = () => {
// Try fallback paths
for (let i = 1; i < possiblePaths.length; i++) {
const fallbackScript = document.createElement('script');
fallbackScript.src = possiblePaths[i];
fallbackScript.async = true;
fallbackScript.onerror = () => {
if (i === possiblePaths.length - 1) {
logger.warn('LayoutManager', 'Could not load feature detection from any path');
}
};
document.head.appendChild(fallbackScript);
break;
}
};
document.head.appendChild(script);
this.featureDetectionLoaded = true;
} catch (e) {
logger.warn('LayoutManager', 'Could not load feature detection:', e);
// Continue without feature detection - not critical
}
}
/**
* Initialize the layout manager - alias for injectLayouts
* @param {string} pageName - Optional page name to set as active
*/
static async init(pageName = null) {
// Load feature detection first to suppress warnings
await this.loadFeatureDetection();
await this.injectLayouts();
if (pageName) {
this.setActivePage(pageName);
}
}
/**
* Set active page in sidebar navigation
* @param {string} pageName - The page identifier
*/
static setActivePage(pageName) {
this.setActiveNav(pageName);
}
/**
* Inject all layouts (header, sidebar, footer) into current page
* Optimized: Lazy load non-critical components after initial render
*/
static async injectLayouts() {
if (this.layoutsInjected) {
logger.debug('LayoutManager', 'Layouts already injected');
return;
}
try {
// Inject critical header first (needed for initial render)
await this.injectHeader();
// Setup event listeners early
this.setupEventListeners();
// Check API status immediately (non-blocking)
this.checkApiStatus();
// Lazy load sidebar and footer after initial render
const loadNonCritical = () => {
// Use requestIdleCallback if available for better performance
const defer = window.requestIdleCallback || ((fn) => setTimeout(fn, 50));
defer(async () => {
try {
await this.injectSidebar();
// Inject footer (if container exists)
const footerContainer = document.getElementById('footer-container');
if (footerContainer) {
await this.injectFooter();
}
} catch (error) {
logger.warn('LayoutManager', 'Failed to load non-critical layouts:', error);
}
}, { timeout: 1000 });
};
// Load non-critical components after a short delay
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadNonCritical);
} else {
loadNonCritical();
}
// Auto-check API status every 30 seconds (only when online)
this.apiStatusInterval = setInterval(() => {
// Skip if offline or tab is hidden
if (!this.isOffline && !document.hidden) {
this.checkApiStatus();
}
}, 30000);
// Pause when tab is hidden, resume when visible
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// Tab hidden - pause checks
} else if (!this.isOffline) {
// Tab visible and online - resume checks
this.checkApiStatus();
}
});
// Mark as injected
this.layoutsInjected = true;
logger.info('LayoutManager', 'Layouts injection initiated');
} catch (error) {
logger.error('LayoutManager', 'Failed to inject layouts:', error);
throw error;
}
}
/**
* Check backend API health and update status badge
*/
static async checkApiStatus() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch('/api/health', {
signal: controller.signal,
cache: 'no-cache'
});
clearTimeout(timeoutId);
if (response.ok) {
this.consecutiveFailures = 0;
this.isOffline = false;
this.updateApiStatus('online', '✓ Online');
} else {
this.consecutiveFailures++;
this.updateApiStatus('degraded', `⚠ HTTP ${response.status}`);
}
} catch (error) {
this.consecutiveFailures++;
if (error.name === 'AbortError') {
this.updateApiStatus('degraded', '⚠ Timeout');
} else {
this.updateApiStatus('offline', '✗ Offline');
}
// Stop checking if too many consecutive failures
if (this.consecutiveFailures >= this.maxFailures) {
this.isOffline = true;
if (this.apiStatusInterval) {
clearInterval(this.apiStatusInterval);
this.apiStatusInterval = null;
}
logger.warn('LayoutManager', 'Too many failures, entering offline mode');
// Retry after 2 minutes
setTimeout(() => {
this.consecutiveFailures = 0;
this.isOffline = false;
this.checkApiStatus();
if (!this.apiStatusInterval) {
this.apiStatusInterval = setInterval(() => {
if (!this.isOffline && !document.hidden) {
this.checkApiStatus();
}
}, 30000);
}
}, 120000);
}
}
}
/**
* Inject sidebar HTML
*/
static async injectSidebar() {
const container = document.getElementById('sidebar-container');
if (!container) {
logger.warn('LayoutManager', 'Sidebar container not found');
return;
}
try {
// Try primary path
let response = await fetch('/static/shared/layouts/sidebar.html');
// Fallback to alternative paths if primary fails
if (!response.ok) {
const altPaths = [
'/static/shared/layouts/sidebar.html',
'../shared/layouts/sidebar.html',
'./shared/layouts/sidebar.html'
];
for (const path of altPaths) {
try {
response = await fetch(path);
if (response.ok) break;
} catch (e) {
continue;
}
}
}
if (response.ok) {
const html = await response.text();
container.innerHTML = html;
} else {
throw new Error(`Failed to load sidebar: ${response.status}`);
}
} catch (error) {
logger.error('LayoutManager', 'Failed to load sidebar, using fallback:', error);
// Fallback: Create minimal sidebar
container.innerHTML = this._createFallbackSidebar();
}
}
/**
* Inject header HTML
*/
static async injectHeader() {
const container = document.getElementById('header-container');
if (!container) {
logger.warn('LayoutManager', 'Header container not found');
return;
}
try {
// Try primary path
let response = await fetch('/static/shared/layouts/header.html');
// Fallback to alternative paths if primary fails
if (!response.ok) {
const altPaths = [
'/static/shared/layouts/header.html',
'../shared/layouts/header.html',
'./shared/layouts/header.html'
];
for (const path of altPaths) {
try {
response = await fetch(path);
if (response.ok) break;
} catch (e) {
continue;
}
}
}
if (response.ok) {
const html = await response.text();
container.innerHTML = html;
// Update API status
this.updateApiStatus('checking');
} else {
throw new Error(`Failed to load header: ${response.status}`);
}
} catch (error) {
logger.error('LayoutManager', 'Failed to load header, using fallback:', error);
// Fallback: Create minimal header
container.innerHTML = this._createFallbackHeader();
this.updateApiStatus('checking');
}
}
/**
* Inject footer HTML
*/
static async injectFooter() {
const container = document.getElementById('footer-container');
if (!container) return;
try {
// Try primary path
let response = await fetch('/static/shared/layouts/footer.html');
// Fallback to alternative paths if primary fails
if (!response.ok) {
const altPaths = [
'/static/shared/layouts/footer.html',
'../shared/layouts/footer.html',
'./shared/layouts/footer.html'
];
for (const path of altPaths) {
try {
response = await fetch(path);
if (response.ok) break;
} catch (e) {
continue;
}
}
}
if (response.ok) {
const html = await response.text();
container.innerHTML = html;
} else {
// Footer is optional, just log warning
logger.warn('LayoutManager', 'Footer not available, skipping');
}
} catch (error) {
// Footer is optional, just log warning
logger.warn('LayoutManager', 'Failed to load footer:', error);
}
}
/**
* Set active navigation item based on current page
*/
static setActiveNav(pageName) {
// Remove active class from all nav links
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
});
// Add active class to current page
const activeLink = document.querySelector(`.nav-link[data-page="${pageName}"]`);
if (activeLink) {
activeLink.classList.add('active');
activeLink.setAttribute('aria-current', 'page');
}
// Update page title
const metadata = PAGE_METADATA.find(p => p.page === pageName);
if (metadata) {
document.title = metadata.title;
}
}
/**
* Update API status badge in header
*/
static updateApiStatus(status, message = '') {
const badge = document.getElementById('api-status-badge');
if (!badge) return;
badge.setAttribute('data-status', status);
const statusText = badge.querySelector('.status-text');
if (statusText) {
statusText.textContent = message || this.getStatusText(status);
}
}
/**
* Get status text for badge
*/
static getStatusText(status) {
const statusMap = {
'online': '✅ System Active',
'offline': '❌ Connection Failed',
'checking': '⏳ Checking...',
'degraded': '⚠️ Degraded',
};
return statusMap[status] || 'Unknown';
}
/**
* Update last update timestamp in header
*/
static updateLastUpdate(text) {
const el = document.getElementById('header-last-update');
if (!el) return;
const textEl = el.querySelector('.update-text');
if (textEl) {
textEl.textContent = text;
}
}
/**
* Setup event listeners for layout interactions
*/
static setupEventListeners() {
// Mobile sidebar toggle
const sidebarToggle = document.getElementById('sidebar-toggle');
if (sidebarToggle) {
sidebarToggle.addEventListener('click', () => {
this.toggleSidebar();
});
}
// Theme toggle
const themeToggle = document.getElementById('theme-toggle-btn');
if (themeToggle) {
themeToggle.addEventListener('click', () => {
this.toggleTheme();
});
}
// Config Helper Modal
const configHelperBtn = document.getElementById('config-helper-btn');
if (configHelperBtn) {
configHelperBtn.addEventListener('click', async () => {
try {
const { ConfigHelperModal } = await import('/static/shared/components/config-helper-modal.js');
if (!window._configHelperModal) {
window._configHelperModal = new ConfigHelperModal();
}
window._configHelperModal.show();
} catch (error) {
logger.error('LayoutManager', 'Failed to load config helper:', error);
}
});
}
// Close sidebar on mobile when clicking a link
if (window.innerWidth <= 768) {
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', () => {
this.closeSidebar();
});
});
}
}
/**
* Toggle sidebar visibility (mobile)
*/
static toggleSidebar() {
const sidebar = document.querySelector('.sidebar');
if (sidebar) {
sidebar.classList.toggle('open');
}
}
/**
* Close sidebar (mobile)
*/
static closeSidebar() {
const sidebar = document.querySelector('.sidebar');
if (sidebar) {
sidebar.classList.remove('open');
}
}
/**
* Toggle theme (dark/light)
*/
static toggleTheme() {
const html = document.documentElement;
const currentTheme = html.getAttribute('data-theme') || 'light';
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('crypto_monitor_theme', newTheme);
// Update visibility of sun/moon icons
this.updateThemeIcons(newTheme);
logger.debug('LayoutManager', 'Theme switched to:', newTheme);
}
/**
* Update theme icons visibility
*/
static updateThemeIcons(theme) {
const sunIcon = document.querySelector('.icon-sun');
const moonIcon = document.querySelector('.icon-moon');
if (sunIcon && moonIcon) {
sunIcon.style.display = theme === 'light' ? 'block' : 'none';
moonIcon.style.display = theme === 'dark' ? 'block' : 'none';
}
}
/**
* Initialize theme from localStorage (default: light)
*/
static initTheme() {
const savedTheme = localStorage.getItem('crypto_monitor_theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
this.updateThemeIcons(savedTheme);
}
/**
* Create fallback sidebar when file can't be loaded
* @private
*/
static _createFallbackSidebar() {
// Use relative paths that work from any location
const basePath = window.location.pathname.includes('/static/')
? window.location.pathname.split('/static/')[0] + '/static'
: '/static';
return `
`;
}
/**
* Create fallback header when file can't be loaded
* @private
*/
static _createFallbackHeader() {
return `
`;
}
}
// Initialize theme immediately
LayoutManager.initTheme();
export default LayoutManager;