| |
| |
| |
| |
| |
|
|
| 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; |
|
|
| |
| |
| |
| static async loadFeatureDetection() { |
| if (this.featureDetectionLoaded) return; |
| |
| |
| if (!window._hfWarningsSuppressed) { |
| const originalWarn = console.warn; |
| const originalError = console.error; |
| |
| |
| 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(); |
| |
| |
| if (msg.includes('unrecognized feature:')) { |
| return unrecognizedFeatures.some(feature => msg.includes(feature)); |
| } |
| |
| |
| if (msg.includes('permissions-policy') || msg.includes('feature-policy')) { |
| return unrecognizedFeatures.some(feature => msg.includes(feature)); |
| } |
| |
| |
| 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; |
| } |
| originalWarn.apply(console, args); |
| }; |
| |
| console.error = function(...args) { |
| const message = args[0]?.toString() || ''; |
| if (shouldSuppress(message)) { |
| return; |
| } |
| originalError.apply(console, args); |
| }; |
| |
| window._hfWarningsSuppressed = true; |
| } |
| |
| try { |
| |
| 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' |
| ]; |
| |
| |
| const script = document.createElement('script'); |
| |
| |
| script.src = possiblePaths[0]; |
| script.async = true; |
| script.onerror = () => { |
| |
| 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); |
| |
| } |
| } |
|
|
| |
| |
| |
| |
| static async init(pageName = null) { |
| |
| await this.loadFeatureDetection(); |
| await this.injectLayouts(); |
| if (pageName) { |
| this.setActivePage(pageName); |
| } |
| } |
|
|
| |
| |
| |
| |
| static setActivePage(pageName) { |
| this.setActiveNav(pageName); |
| } |
|
|
| |
| |
| |
| |
| static async injectLayouts() { |
| if (this.layoutsInjected) { |
| logger.debug('LayoutManager', 'Layouts already injected'); |
| return; |
| } |
|
|
| try { |
| |
| await this.injectHeader(); |
|
|
| |
| this.setupEventListeners(); |
|
|
| |
| this.checkApiStatus(); |
|
|
| |
| const loadNonCritical = () => { |
| |
| const defer = window.requestIdleCallback || ((fn) => setTimeout(fn, 50)); |
| defer(async () => { |
| try { |
| await this.injectSidebar(); |
| |
| |
| 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 }); |
| }; |
|
|
| |
| if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', loadNonCritical); |
| } else { |
| loadNonCritical(); |
| } |
|
|
| |
| this.apiStatusInterval = setInterval(() => { |
| |
| if (!this.isOffline && !document.hidden) { |
| this.checkApiStatus(); |
| } |
| }, 30000); |
|
|
| |
| document.addEventListener('visibilitychange', () => { |
| if (document.hidden) { |
| |
| } else if (!this.isOffline) { |
| |
| this.checkApiStatus(); |
| } |
| }); |
|
|
| |
| this.layoutsInjected = true; |
|
|
| logger.info('LayoutManager', 'Layouts injection initiated'); |
| } catch (error) { |
| logger.error('LayoutManager', 'Failed to inject layouts:', error); |
| throw error; |
| } |
| } |
|
|
| |
| |
| |
| 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'); |
| } |
|
|
| |
| 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'); |
| |
| |
| setTimeout(() => { |
| this.consecutiveFailures = 0; |
| this.isOffline = false; |
| this.checkApiStatus(); |
| if (!this.apiStatusInterval) { |
| this.apiStatusInterval = setInterval(() => { |
| if (!this.isOffline && !document.hidden) { |
| this.checkApiStatus(); |
| } |
| }, 30000); |
| } |
| }, 120000); |
| } |
| } |
| } |
|
|
| |
| |
| |
| static async injectSidebar() { |
| const container = document.getElementById('sidebar-container'); |
| if (!container) { |
| logger.warn('LayoutManager', 'Sidebar container not found'); |
| return; |
| } |
|
|
| try { |
| |
| let response = await fetch('/static/shared/layouts/sidebar.html'); |
| |
| |
| 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); |
| |
| container.innerHTML = this._createFallbackSidebar(); |
| } |
| } |
|
|
| |
| |
| |
| static async injectHeader() { |
| const container = document.getElementById('header-container'); |
| if (!container) { |
| logger.warn('LayoutManager', 'Header container not found'); |
| return; |
| } |
|
|
| try { |
| |
| let response = await fetch('/static/shared/layouts/header.html'); |
| |
| |
| 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; |
| |
| 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); |
| |
| container.innerHTML = this._createFallbackHeader(); |
| this.updateApiStatus('checking'); |
| } |
| } |
|
|
| |
| |
| |
| static async injectFooter() { |
| const container = document.getElementById('footer-container'); |
| if (!container) return; |
|
|
| try { |
| |
| let response = await fetch('/static/shared/layouts/footer.html'); |
| |
| |
| 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 { |
| |
| logger.warn('LayoutManager', 'Footer not available, skipping'); |
| } |
| } catch (error) { |
| |
| logger.warn('LayoutManager', 'Failed to load footer:', error); |
| } |
| } |
|
|
| |
| |
| |
| static setActiveNav(pageName) { |
| |
| document.querySelectorAll('.nav-link').forEach(link => { |
| link.classList.remove('active'); |
| }); |
|
|
| |
| const activeLink = document.querySelector(`.nav-link[data-page="${pageName}"]`); |
| if (activeLink) { |
| activeLink.classList.add('active'); |
| activeLink.setAttribute('aria-current', 'page'); |
| } |
|
|
| |
| const metadata = PAGE_METADATA.find(p => p.page === pageName); |
| if (metadata) { |
| document.title = metadata.title; |
| } |
| } |
|
|
| |
| |
| |
| 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); |
| } |
| } |
|
|
| |
| |
| |
| static getStatusText(status) { |
| const statusMap = { |
| 'online': '✅ System Active', |
| 'offline': '❌ Connection Failed', |
| 'checking': '⏳ Checking...', |
| 'degraded': '⚠️ Degraded', |
| }; |
| return statusMap[status] || 'Unknown'; |
| } |
|
|
| |
| |
| |
| static updateLastUpdate(text) { |
| const el = document.getElementById('header-last-update'); |
| if (!el) return; |
|
|
| const textEl = el.querySelector('.update-text'); |
| if (textEl) { |
| textEl.textContent = text; |
| } |
| } |
|
|
| |
| |
| |
| static setupEventListeners() { |
| |
| const sidebarToggle = document.getElementById('sidebar-toggle'); |
| if (sidebarToggle) { |
| sidebarToggle.addEventListener('click', () => { |
| this.toggleSidebar(); |
| }); |
| } |
|
|
| |
| const themeToggle = document.getElementById('theme-toggle-btn'); |
| if (themeToggle) { |
| themeToggle.addEventListener('click', () => { |
| this.toggleTheme(); |
| }); |
| } |
|
|
| |
| 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); |
| } |
| }); |
| } |
|
|
| |
| if (window.innerWidth <= 768) { |
| document.querySelectorAll('.nav-link').forEach(link => { |
| link.addEventListener('click', () => { |
| this.closeSidebar(); |
| }); |
| }); |
| } |
| } |
|
|
| |
| |
| |
| static toggleSidebar() { |
| const sidebar = document.querySelector('.sidebar'); |
| if (sidebar) { |
| sidebar.classList.toggle('open'); |
| } |
| } |
|
|
| |
| |
| |
| static closeSidebar() { |
| const sidebar = document.querySelector('.sidebar'); |
| if (sidebar) { |
| sidebar.classList.remove('open'); |
| } |
| } |
|
|
| |
| |
| |
| 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); |
|
|
| |
| this.updateThemeIcons(newTheme); |
| logger.debug('LayoutManager', 'Theme switched to:', newTheme); |
| } |
|
|
| |
| |
| |
| 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'; |
| } |
| } |
|
|
| |
| |
| |
| static initTheme() { |
| const savedTheme = localStorage.getItem('crypto_monitor_theme') || 'light'; |
| document.documentElement.setAttribute('data-theme', savedTheme); |
| this.updateThemeIcons(savedTheme); |
| } |
|
|
| |
| |
| |
| |
| static _createFallbackSidebar() { |
| |
| const basePath = window.location.pathname.includes('/static/') |
| ? window.location.pathname.split('/static/')[0] + '/static' |
| : '/static'; |
| |
| return ` |
| <nav class="sidebar" role="navigation"> |
| <div class="sidebar-header"> |
| <h2>Crypto Monitor</h2> |
| </div> |
| <ul class="nav-list"> |
| <li><a href="${basePath}/pages/dashboard/index.html" class="nav-link" data-page="dashboard">Dashboard</a></li> |
| <li><a href="${basePath}/pages/market/index.html" class="nav-link" data-page="market">Market</a></li> |
| <li><a href="${basePath}/pages/models/index.html" class="nav-link" data-page="models">AI Models</a></li> |
| <li><a href="${basePath}/pages/providers/index.html" class="nav-link" data-page="providers">Providers</a></li> |
| <li><a href="${basePath}/pages/sentiment/index.html" class="nav-link" data-page="sentiment">Sentiment</a></li> |
| <li><a href="${basePath}/pages/news/index.html" class="nav-link" data-page="news">News</a></li> |
| <li><a href="/system-monitor" class="nav-link" data-page="system-monitor">System Monitor</a></li> |
| </ul> |
| </nav> |
| `; |
| } |
|
|
| |
| |
| |
| |
| static _createFallbackHeader() { |
| return ` |
| <header class="header"> |
| <div class="header-content"> |
| <div class="header-left"> |
| <button id="sidebar-toggle" class="btn-icon" aria-label="Toggle sidebar">☰</button> |
| <h1 class="header-title">Crypto Monitor</h1> |
| </div> |
| <div class="header-right"> |
| <span id="api-status-badge" class="status-badge" data-status="checking"> |
| <span class="status-text">⏳ Checking...</span> |
| </span> |
| </div> |
| </div> |
| </header> |
| `; |
| } |
| } |
|
|
| |
| LayoutManager.initTheme(); |
|
|
| export default LayoutManager; |
|
|