|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|