import { LitElement, html, css, state, customElement, deesComms } from './plugins.js'; import type { CSSResult, TemplateResult } from './plugins.js'; import { sharedStyles, terminalStyles, navStyles } from './sw-dash-styles.js'; import type { IMetricsData } from './sw-dash-overview.js'; import type { ICachedResource } from './sw-dash-urls.js'; import type { IDomainStats } from './sw-dash-domains.js'; import type { IContentTypeStats } from './sw-dash-types.js'; import type { serviceworker } from '../dist_ts_interfaces/index.js'; // Import components to register them import './sw-dash-overview.js'; import './sw-dash-urls.js'; import './sw-dash-domains.js'; import './sw-dash-types.js'; import './sw-dash-events.js'; import './sw-dash-requests.js'; import './sw-dash-table.js'; type ViewType = 'overview' | 'urls' | 'domains' | 'types' | 'events' | 'requests'; interface IResourceData { resources: ICachedResource[]; domains: IDomainStats[]; contentTypes: IContentTypeStats[]; resourceCount: number; } /** * Main SW Dashboard application shell */ @customElement('sw-dash-app') export class SwDashApp extends LitElement { public static styles: CSSResult[] = [ sharedStyles, terminalStyles, navStyles, css` :host { display: block; background: var(--bg-primary); min-height: 100vh; padding: var(--space-5); } .view { display: none; } .view.active { display: block; } .header-left { display: flex; align-items: center; gap: var(--space-3); } .logo { width: 24px; height: 24px; background: var(--accent-primary); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px; color: white; } .uptime-badge { display: inline-flex; align-items: center; gap: var(--space-1); padding: var(--space-1) var(--space-2); background: var(--bg-tertiary); border-radius: var(--radius-sm); font-size: 11px; color: var(--text-tertiary); } .uptime-badge .value { color: var(--text-primary); font-weight: 500; font-variant-numeric: tabular-nums; } .footer-left { display: flex; align-items: center; gap: var(--space-2); color: var(--text-tertiary); font-size: 11px; } .footer-right { display: flex; align-items: center; gap: var(--space-2); } .auto-refresh { display: inline-flex; align-items: center; gap: var(--space-1); padding: var(--space-1) var(--space-2); background: rgba(34, 197, 94, 0.1); color: var(--accent-success); border-radius: var(--radius-sm); font-size: 11px; font-weight: 500; } .auto-refresh .dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; animation: pulse 2s infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } ` ]; @state() accessor currentView: ViewType = 'overview'; @state() accessor metrics: IMetricsData | null = null; @state() accessor resourceData: IResourceData = { resources: [], domains: [], contentTypes: [], resourceCount: 0 }; @state() accessor lastRefresh = new Date().toLocaleTimeString(); @state() accessor isConnected = false; // DeesComms for receiving push updates from service worker private comms: deesComms.DeesComms | null = null; // Heartbeat interval (30 seconds) for SW health check private heartbeatInterval: ReturnType | null = null; private readonly HEARTBEAT_INTERVAL_MS = 30000; connectedCallback(): void { super.connectedCallback(); // Initial HTTP seed request to wake up SW and get initial data this.loadInitialData(); // Setup push listeners via DeesComms this.setupPushListeners(); // Start heartbeat for SW health check this.startHeartbeat(); } disconnectedCallback(): void { super.disconnectedCallback(); if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); } } /** * Initial HTTP request to seed data and wake up service worker */ private async loadInitialData(): Promise { try { // Fetch metrics (wakes up SW) const metricsResponse = await fetch('/sw-dash/metrics'); this.metrics = await metricsResponse.json(); this.lastRefresh = new Date().toLocaleTimeString(); this.isConnected = true; // Also load resources const resourcesResponse = await fetch('/sw-dash/resources'); this.resourceData = await resourcesResponse.json(); } catch (err) { console.error('Failed to load initial data:', err); this.isConnected = false; } } /** * Setup DeesComms handlers for receiving push updates */ private setupPushListeners(): void { this.comms = new deesComms.DeesComms(); // Handle metrics push updates this.comms.createTypedHandler( 'serviceworker_metricsUpdate', async (snapshot) => { // Update metrics from push if (this.metrics) { this.metrics = { ...this.metrics, cache: { ...this.metrics.cache, hits: snapshot.cache.hits, misses: snapshot.cache.misses, errors: snapshot.cache.errors, bytesServedFromCache: snapshot.cache.bytesServedFromCache, bytesFetched: snapshot.cache.bytesFetched, }, network: { ...this.metrics.network, totalRequests: snapshot.network.totalRequests, successfulRequests: snapshot.network.successfulRequests, failedRequests: snapshot.network.failedRequests, }, cacheHitRate: snapshot.cacheHitRate, networkSuccessRate: snapshot.networkSuccessRate, resourceCount: snapshot.resourceCount, uptime: snapshot.uptime, }; } else { // If no metrics yet, create minimal structure this.metrics = { cache: { hits: snapshot.cache.hits, misses: snapshot.cache.misses, errors: snapshot.cache.errors, bytesServedFromCache: snapshot.cache.bytesServedFromCache, bytesFetched: snapshot.cache.bytesFetched, averageResponseTime: 0, }, network: { totalRequests: snapshot.network.totalRequests, successfulRequests: snapshot.network.successfulRequests, failedRequests: snapshot.network.failedRequests, timeouts: 0, averageLatency: 0, totalBytesTransferred: 0, }, update: { totalChecks: 0, successfulChecks: 0, failedChecks: 0, updatesFound: 0, updatesApplied: 0, lastCheckTimestamp: 0, lastUpdateTimestamp: 0, }, connection: { connectedClients: 0, totalConnectionAttempts: 0, successfulConnections: 0, failedConnections: 0, }, speedtest: { lastDownloadSpeedMbps: 0, lastUploadSpeedMbps: 0, lastLatencyMs: 0, lastTestTimestamp: 0, testCount: 0, isOnline: true, }, startTime: Date.now() - snapshot.uptime, uptime: snapshot.uptime, cacheHitRate: snapshot.cacheHitRate, networkSuccessRate: snapshot.networkSuccessRate, resourceCount: snapshot.resourceCount, }; } this.lastRefresh = new Date().toLocaleTimeString(); this.isConnected = true; return {}; } ); // Handle event log push updates - dispatch to events component this.comms.createTypedHandler( 'serviceworker_eventLogged', async (entry) => { // Dispatch custom event for sw-dash-events component this.dispatchEvent(new CustomEvent('event-logged', { detail: entry, bubbles: true, composed: true, })); return {}; } ); // Handle resource cached push updates this.comms.createTypedHandler( 'serviceworker_resourceCached', async (resource) => { // Update resource count optimistically if (resource.cached && this.metrics) { this.metrics = { ...this.metrics, resourceCount: this.metrics.resourceCount + 1, }; } return {}; } ); // Handle TypedRequest logged push updates - dispatch to requests component this.comms.createTypedHandler( 'serviceworker_typedRequestLogged', async (entry) => { // Dispatch custom event for sw-dash-requests component this.dispatchEvent(new CustomEvent('typedrequest-logged', { detail: entry, bubbles: true, composed: true, })); return {}; } ); } /** * Heartbeat to check SW health periodically */ private startHeartbeat(): void { this.heartbeatInterval = setInterval(async () => { try { const response = await fetch('/sw-dash/metrics'); if (response.ok) { this.isConnected = true; // Optionally refresh full metrics periodically this.metrics = await response.json(); this.lastRefresh = new Date().toLocaleTimeString(); } else { this.isConnected = false; } } catch { this.isConnected = false; } }, this.HEARTBEAT_INTERVAL_MS); } /** * Load resource data on demand (when switching to urls/domains/types view) */ private async loadResourceData(): Promise { try { const response = await fetch('/sw-dash/resources'); this.resourceData = await response.json(); } catch (err) { console.error('Failed to load resources:', err); } } private setView(view: ViewType): void { this.currentView = view; if (view !== 'overview') { this.loadResourceData(); } } private handleSpeedtestComplete(_e: CustomEvent): void { // Refresh metrics after speedtest via HTTP this.loadInitialData(); } private formatUptime(ms: number): string { const s = Math.floor(ms / 1000); const m = Math.floor(s / 60); const h = Math.floor(m / 60); const d = Math.floor(h / 24); if (d > 0) return `${d}d ${h % 24}h`; if (h > 0) return `${h}h ${m % 60}m`; if (m > 0) return `${m}m ${s % 60}s`; return `${s}s`; } public render(): TemplateResult { return html`
Service Worker Dashboard
Uptime: ${this.metrics ? this.formatUptime(this.metrics.uptime) : '--'}
`; } }