diff --git a/ts_interfaces/serviceworker.ts b/ts_interfaces/serviceworker.ts index 26e608c..7833da6 100644 --- a/ts_interfaces/serviceworker.ts +++ b/ts_interfaces/serviceworker.ts @@ -386,6 +386,7 @@ export interface IRequest_Serviceworker_GetEventLog limit?: number; type?: TEventType; since?: number; + before?: number; // For pagination: get events before this timestamp }; response: { events: IEventLogEntry[]; @@ -579,6 +580,7 @@ export interface IRequest_Serviceworker_GetTypedRequestLogs limit?: number; method?: string; since?: number; + before?: number; // For pagination: get logs before this timestamp }; response: { logs: ITypedRequestLogEntry[]; diff --git a/ts_swdash/sw-dash-app.ts b/ts_swdash/sw-dash-app.ts index c82a2de..4166487 100644 --- a/ts_swdash/sw-dash-app.ts +++ b/ts_swdash/sw-dash-app.ts @@ -27,6 +27,11 @@ interface IResourceData { /** * Main SW Dashboard application shell + * + * Architecture: + * - ONE initial HTTP seed request to /sw-dash/metrics (provides ALL data) + * - HTTP heartbeat every 30s for SW health check + * - Everything else via DeesComms (push from SW, requests to SW) */ @customElement('sw-dash-app') export class SwDashApp extends LitElement { @@ -127,18 +132,32 @@ export class SwDashApp extends LitElement { ` ]; + // Core metrics @state() accessor currentView: ViewType = 'overview'; @state() accessor metrics: IMetricsData | null = null; + @state() accessor lastRefresh = new Date().toLocaleTimeString(); + @state() accessor isConnected = false; + + // Resource data (from initial seed) @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 + // Events data (from initial seed + push updates) + @state() accessor events: serviceworker.IEventLogEntry[] = []; + @state() accessor eventTotalCount = 0; + @state() accessor eventCountLastHour = 0; + + // Request logs data (from initial seed + push updates) + @state() accessor requestLogs: serviceworker.ITypedRequestLogEntry[] = []; + @state() accessor requestTotalCount = 0; + @state() accessor requestStats: serviceworker.ITypedRequestStats | null = null; + @state() accessor requestMethods: string[] = []; + + // DeesComms for communication with service worker private comms: deesComms.DeesComms | null = null; // Heartbeat interval (30 seconds) for SW health check @@ -147,7 +166,7 @@ export class SwDashApp extends LitElement { connectedCallback(): void { super.connectedCallback(); - // Initial HTTP seed request to wake up SW and get initial data + // Initial HTTP seed request to wake up SW and get ALL initial data this.loadInitialData(); // Setup push listeners via DeesComms this.setupPushListeners(); @@ -163,19 +182,42 @@ export class SwDashApp extends LitElement { } /** - * Initial HTTP request to seed data and wake up service worker + * Initial HTTP request to seed ALL data and wake up service worker + * This is the ONE HTTP request that provides everything: + * - Core metrics + * - Resources, domains, content types + * - Events (initial 50) + * - Request logs (initial 50), stats, methods */ private async loadInitialData(): Promise { try { - // Fetch metrics (wakes up SW) - const metricsResponse = await fetch('/sw-dash/metrics'); - this.metrics = await metricsResponse.json(); + const response = await fetch('/sw-dash/metrics'); + const data = await response.json(); + + // Core metrics + this.metrics = data; + + // Resource data + this.resourceData = { + resources: data.resources || [], + domains: data.domains || [], + contentTypes: data.contentTypes || [], + resourceCount: data.resourceCount || 0, + }; + + // Events data + this.events = data.events || []; + this.eventTotalCount = data.eventTotalCount || 0; + this.eventCountLastHour = data.eventCountLastHour || 0; + + // Request logs data + this.requestLogs = data.requestLogs || []; + this.requestTotalCount = data.requestTotalCount || 0; + this.requestStats = data.requestStats || null; + this.requestMethods = data.requestMethods || []; + 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; @@ -183,7 +225,8 @@ export class SwDashApp extends LitElement { } /** - * Setup DeesComms handlers for receiving push updates + * Setup DeesComms handlers for receiving push updates from SW + * All real-time updates come through here */ private setupPushListeners(): void { this.comms = new deesComms.DeesComms(); @@ -215,54 +258,6 @@ export class SwDashApp extends LitElement { 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; @@ -270,16 +265,18 @@ export class SwDashApp extends LitElement { } ); - // Handle event log push updates - dispatch to events component + // Handle new event logged - add to our events array 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, - })); + // Prepend new event to array + this.events = [entry, ...this.events]; + this.eventTotalCount++; + // Check if event is within last hour + const oneHourAgo = Date.now() - 3600000; + if (entry.timestamp >= oneHourAgo) { + this.eventCountLastHour++; + } return {}; } ); @@ -299,23 +296,51 @@ export class SwDashApp extends LitElement { } ); - // Handle TypedRequest logged push updates - dispatch to requests component + // Handle new TypedRequest logged - add to our logs array 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, - })); + // Prepend new log to array + this.requestLogs = [entry, ...this.requestLogs]; + this.requestTotalCount++; + + // Update stats optimistically + if (this.requestStats) { + const newStats = { ...this.requestStats }; + if (entry.phase === 'request') { + newStats.totalRequests++; + } else { + newStats.totalResponses++; + } + if (entry.error) { + newStats.errorCount++; + } + // Update method counts + if (!newStats.methodCounts[entry.method]) { + newStats.methodCounts[entry.method] = { requests: 0, responses: 0, errors: 0, avgDurationMs: 0 }; + // Add to methods list if new + if (!this.requestMethods.includes(entry.method)) { + this.requestMethods = [...this.requestMethods, entry.method]; + } + } + if (entry.phase === 'request') { + newStats.methodCounts[entry.method].requests++; + } else { + newStats.methodCounts[entry.method].responses++; + } + if (entry.error) { + newStats.methodCounts[entry.method].errors++; + } + this.requestStats = newStats; + } return {}; } ); } /** - * Heartbeat to check SW health periodically + * Heartbeat to check SW health periodically (HTTP) + * This is the ONLY periodic HTTP request */ private startHeartbeat(): void { this.heartbeatInterval = setInterval(async () => { @@ -323,8 +348,22 @@ export class SwDashApp extends LitElement { const response = await fetch('/sw-dash/metrics'); if (response.ok) { this.isConnected = true; - // Optionally refresh full metrics periodically - this.metrics = await response.json(); + // Refresh all data from heartbeat response + const data = await response.json(); + this.metrics = data; + this.resourceData = { + resources: data.resources || [], + domains: data.domains || [], + contentTypes: data.contentTypes || [], + resourceCount: data.resourceCount || 0, + }; + this.events = data.events || []; + this.eventTotalCount = data.eventTotalCount || 0; + this.eventCountLastHour = data.eventCountLastHour || 0; + this.requestLogs = data.requestLogs || []; + this.requestTotalCount = data.requestTotalCount || 0; + this.requestStats = data.requestStats || null; + this.requestMethods = data.requestMethods || []; this.lastRefresh = new Date().toLocaleTimeString(); } else { this.isConnected = false; @@ -336,22 +375,96 @@ export class SwDashApp extends LitElement { } /** - * Load resource data on demand (when switching to urls/domains/types view) + * Handle "load more events" request from sw-dash-events component + * Uses DeesComms to request older events from SW */ - private async loadResourceData(): Promise { + private async handleLoadMoreEvents(e: CustomEvent<{ before: number }>): Promise { + if (!this.comms) return; + try { - const response = await fetch('/sw-dash/resources'); - this.resourceData = await response.json(); + const tr = this.comms.createTypedRequest('serviceworker_getEventLog'); + const result = await tr.fire({ + limit: 50, + before: e.detail.before, + }); + // Append older events to existing array + this.events = [...this.events, ...result.events]; + this.eventTotalCount = result.totalCount; } catch (err) { - console.error('Failed to load resources:', err); + console.error('Failed to load more events:', err); + } + } + + /** + * Handle "clear events" request from sw-dash-events component + * Uses DeesComms to clear event log in SW + */ + private async handleClearEvents(): Promise { + if (!this.comms) return; + + try { + const tr = this.comms.createTypedRequest('serviceworker_clearEventLog'); + await tr.fire({}); + // Clear local state + this.events = []; + this.eventTotalCount = 0; + this.eventCountLastHour = 0; + } catch (err) { + console.error('Failed to clear events:', err); + } + } + + /** + * Handle "load more requests" from sw-dash-requests component + * Uses DeesComms to request older request logs from SW + */ + private async handleLoadMoreRequests(e: CustomEvent<{ before: number; method?: string }>): Promise { + if (!this.comms) return; + + try { + const tr = this.comms.createTypedRequest('serviceworker_getTypedRequestLogs'); + const result = await tr.fire({ + limit: 50, + before: e.detail.before, + method: e.detail.method, + }); + // Append older logs to existing array + this.requestLogs = [...this.requestLogs, ...result.logs]; + this.requestTotalCount = result.totalCount; + } catch (err) { + console.error('Failed to load more requests:', err); + } + } + + /** + * Handle "clear requests" from sw-dash-requests component + * Uses DeesComms to clear request logs in SW + */ + private async handleClearRequests(): Promise { + if (!this.comms) return; + + try { + const tr = this.comms.createTypedRequest('serviceworker_clearTypedRequestLogs'); + await tr.fire({}); + // Clear local state + this.requestLogs = []; + this.requestTotalCount = 0; + this.requestStats = { + totalRequests: 0, + totalResponses: 0, + methodCounts: {}, + errorCount: 0, + avgDurationMs: 0, + }; + this.requestMethods = []; + } catch (err) { + console.error('Failed to clear requests:', err); } } private setView(view: ViewType): void { this.currentView = view; - if (view !== 'overview') { - this.loadResourceData(); - } + // No HTTP fetch on view change - data is already loaded from initial seed } private handleSpeedtestComplete(_e: CustomEvent): void { @@ -414,6 +527,7 @@ export class SwDashApp extends LitElement {
@@ -431,11 +545,23 @@ export class SwDashApp extends LitElement {
- +
- +
diff --git a/ts_swdash/sw-dash-events.ts b/ts_swdash/sw-dash-events.ts index 0706ff4..ab32633 100644 --- a/ts_swdash/sw-dash-events.ts +++ b/ts_swdash/sw-dash-events.ts @@ -18,6 +18,10 @@ type TEventFilter = 'all' | 'sw_installed' | 'sw_activated' | 'sw_updated' | 'sw /** * Events panel component for sw-dash + * + * Receives events via property from parent (sw-dash-app). + * Filtering is done locally. + * Load more and clear operations dispatch events to parent. */ @customElement('sw-dash-events') export class SwDashEvents extends LitElement { @@ -189,97 +193,52 @@ export class SwDashEvents extends LitElement { ` ]; + // Received from parent (sw-dash-app) @property({ type: Array }) accessor events: IEventLogEntry[] = []; + @property({ type: Number }) accessor totalCount = 0; + + // Local state for filtering @state() accessor filter: TEventFilter = 'all'; @state() accessor searchText = ''; - @state() accessor totalCount = 0; - @state() accessor isLoading = true; - @state() accessor page = 1; - private readonly pageSize = 50; - - // Bound event handler reference for cleanup - private boundEventHandler: ((e: Event) => void) | null = null; - - connectedCallback(): void { - super.connectedCallback(); - this.loadEvents(); - // Listen for pushed events from parent - this.setupPushEventListener(); - } - - disconnectedCallback(): void { - super.disconnectedCallback(); - // Clean up event listener - if (this.boundEventHandler) { - window.removeEventListener('event-logged', this.boundEventHandler); - } - } - - /** - * Sets up listener for pushed events from service worker (via sw-dash-app) - */ - private setupPushEventListener(): void { - this.boundEventHandler = (e: Event) => { - const customEvent = e as CustomEvent; - const newEvent = customEvent.detail; - - // Only add if it matches current filter (or filter is 'all') - if (this.filter === 'all' || newEvent.type === this.filter) { - // Prepend new event to the list - this.events = [newEvent, ...this.events]; - this.totalCount++; - } - }; - - // Listen at window level since events bubble up with composed: true - window.addEventListener('event-logged', this.boundEventHandler); - } - - private async loadEvents(): Promise { - this.isLoading = true; - try { - const params = new URLSearchParams(); - params.set('limit', String(this.pageSize * this.page)); - if (this.filter !== 'all') { - params.set('type', this.filter); - } - - const response = await fetch(`/sw-dash/events?${params}`); - const data = await response.json(); - this.events = data.events; - this.totalCount = data.totalCount; - } catch (err) { - console.error('Failed to load events:', err); - } finally { - this.isLoading = false; - } - } + @state() accessor isLoadingMore = false; private handleFilterChange(e: Event): void { this.filter = (e.target as HTMLSelectElement).value as TEventFilter; - this.page = 1; - this.loadEvents(); + // Local filtering - no HTTP request } private handleSearch(e: Event): void { this.searchText = (e.target as HTMLInputElement).value.toLowerCase(); } - private async handleClear(): Promise { + private handleClear(): void { if (!confirm('Are you sure you want to clear the event log? This cannot be undone.')) { return; } - try { - await fetch('/sw-dash/events', { method: 'DELETE' }); - this.loadEvents(); - } catch (err) { - console.error('Failed to clear events:', err); - } + // Dispatch event to parent to clear via DeesComms + this.dispatchEvent(new CustomEvent('clear-events', { + bubbles: true, + composed: true, + })); } private loadMore(): void { - this.page++; - this.loadEvents(); + if (this.isLoadingMore || this.events.length === 0) return; + + this.isLoadingMore = true; + const oldestEvent = this.events[this.events.length - 1]; + + // Dispatch event to parent to load more via DeesComms + this.dispatchEvent(new CustomEvent('load-more-events', { + detail: { before: oldestEvent.timestamp }, + bubbles: true, + composed: true, + })); + + // Reset loading state after a short delay (parent will update events prop) + setTimeout(() => { + this.isLoadingMore = false; + }, 1000); } private getTypeClass(type: string): string { @@ -300,13 +259,27 @@ export class SwDashEvents extends LitElement { return type.replace(/_/g, ' '); } + /** + * Filter events locally based on type and search text + */ private getFilteredEvents(): IEventLogEntry[] { - if (!this.searchText) return this.events; - return this.events.filter(e => - e.message.toLowerCase().includes(this.searchText) || - e.type.toLowerCase().includes(this.searchText) || - (e.details && JSON.stringify(e.details).toLowerCase().includes(this.searchText)) - ); + let result = this.events; + + // Filter by type + if (this.filter !== 'all') { + result = result.filter(e => e.type === this.filter); + } + + // Filter by search text + if (this.searchText) { + result = result.filter(e => + e.message.toLowerCase().includes(this.searchText) || + e.type.toLowerCase().includes(this.searchText) || + (e.details && JSON.stringify(e.details).toLowerCase().includes(this.searchText)) + ); + } + + return result; } public render(): TemplateResult { @@ -352,10 +325,10 @@ export class SwDashEvents extends LitElement { - ${this.isLoading && this.events.length === 0 ? html` -
Loading events...
+ ${this.events.length === 0 ? html` +
No events recorded
` : filteredEvents.length === 0 ? html` -
No events found
+
No events match filter
` : html`
${filteredEvents.map(event => html` @@ -374,8 +347,8 @@ export class SwDashEvents extends LitElement { ${this.events.length < this.totalCount ? html` diff --git a/ts_swdash/sw-dash-overview.ts b/ts_swdash/sw-dash-overview.ts index 9662b33..e4346b7 100644 --- a/ts_swdash/sw-dash-overview.ts +++ b/ts_swdash/sw-dash-overview.ts @@ -79,42 +79,15 @@ export class SwDashOverview extends LitElement { ]; @property({ type: Object }) accessor metrics: IMetricsData | null = null; + @property({ type: Number }) accessor eventCountLastHour = 0; @state() accessor speedtestRunning = false; @state() accessor speedtestPhase: 'idle' | 'latency' | 'download' | 'upload' | 'complete' = 'idle'; @state() accessor speedtestProgress = 0; @state() accessor speedtestElapsed = 0; - @state() accessor eventCountLastHour = 0; // Speedtest timing constants (must match service worker) private static readonly TEST_DURATION_MS = 5000; // 5 seconds per test private progressInterval: number | null = null; - private eventCountInterval: number | null = null; - - connectedCallback(): void { - super.connectedCallback(); - this.fetchEventCount(); - // Refresh event count every 30 seconds - this.eventCountInterval = window.setInterval(() => this.fetchEventCount(), 30000); - } - - disconnectedCallback(): void { - super.disconnectedCallback(); - if (this.eventCountInterval) { - window.clearInterval(this.eventCountInterval); - this.eventCountInterval = null; - } - } - - private async fetchEventCount(): Promise { - try { - const oneHourAgo = Date.now() - 3600000; - const response = await fetch(`/sw-dash/events/count?since=${oneHourAgo}`); - const data = await response.json(); - this.eventCountLastHour = data.count; - } catch (err) { - console.error('Failed to fetch event count:', err); - } - } private async runSpeedtest(): Promise { if (this.speedtestRunning) return; diff --git a/ts_swdash/sw-dash-requests.ts b/ts_swdash/sw-dash-requests.ts index c599c06..8bfd53e 100644 --- a/ts_swdash/sw-dash-requests.ts +++ b/ts_swdash/sw-dash-requests.ts @@ -26,6 +26,10 @@ type TPhaseFilter = 'all' | 'request' | 'response'; /** * TypedRequest traffic monitoring panel for sw-dash + * + * Receives logs, stats, and methods via properties from parent (sw-dash-app). + * Filtering is done locally. + * Load more and clear operations dispatch events to parent. */ @customElement('sw-dash-requests') export class SwDashRequests extends LitElement { @@ -303,137 +307,70 @@ export class SwDashRequests extends LitElement { ` ]; + // Received from parent (sw-dash-app) @property({ type: Array }) accessor logs: ITypedRequestLogEntry[] = []; - @state() accessor stats: ITypedRequestStats | null = null; + @property({ type: Number }) accessor totalCount = 0; + @property({ type: Object }) accessor stats: ITypedRequestStats | null = null; + @property({ type: Array }) accessor methods: string[] = []; + + // Local state for filtering @state() accessor directionFilter: TRequestFilter = 'all'; @state() accessor phaseFilter: TPhaseFilter = 'all'; @state() accessor methodFilter = ''; @state() accessor searchText = ''; - @state() accessor totalCount = 0; - @state() accessor isLoading = true; - @state() accessor page = 1; @state() accessor expandedPayloads: Set = new Set(); - @state() accessor availableMethods: string[] = []; - private readonly pageSize = 50; - - // Bound event handler reference for cleanup - private boundLogHandler: ((e: Event) => void) | null = null; - - connectedCallback(): void { - super.connectedCallback(); - this.loadLogs(); - this.loadStats(); - this.loadMethods(); - this.setupPushListener(); - } - - disconnectedCallback(): void { - super.disconnectedCallback(); - if (this.boundLogHandler) { - window.removeEventListener('typedrequest-logged', this.boundLogHandler); - } - } - - private setupPushListener(): void { - this.boundLogHandler = (e: Event) => { - const customEvent = e as CustomEvent; - const newLog = customEvent.detail; - - // Apply filters - if (this.directionFilter !== 'all' && newLog.direction !== this.directionFilter) return; - if (this.phaseFilter !== 'all' && newLog.phase !== this.phaseFilter) return; - if (this.methodFilter && newLog.method !== this.methodFilter) return; - - // Prepend new log - this.logs = [newLog, ...this.logs]; - this.totalCount++; - - // Update available methods if new - if (!this.availableMethods.includes(newLog.method)) { - this.availableMethods = [...this.availableMethods, newLog.method]; - } - }; - - window.addEventListener('typedrequest-logged', this.boundLogHandler); - } - - private async loadLogs(): Promise { - this.isLoading = true; - try { - const params = new URLSearchParams(); - params.set('limit', String(this.pageSize * this.page)); - if (this.methodFilter) { - params.set('method', this.methodFilter); - } - - const response = await fetch(`/sw-dash/requests?${params}`); - const data = await response.json(); - this.logs = data.logs; - this.totalCount = data.totalCount; - } catch (err) { - console.error('Failed to load request logs:', err); - } finally { - this.isLoading = false; - } - } - - private async loadStats(): Promise { - try { - const response = await fetch('/sw-dash/requests/stats'); - this.stats = await response.json(); - } catch (err) { - console.error('Failed to load request stats:', err); - } - } - - private async loadMethods(): Promise { - try { - const response = await fetch('/sw-dash/requests/methods'); - const data = await response.json(); - this.availableMethods = data.methods; - } catch (err) { - console.error('Failed to load methods:', err); - } - } + @state() accessor isLoadingMore = false; private handleDirectionFilterChange(e: Event): void { this.directionFilter = (e.target as HTMLSelectElement).value as TRequestFilter; - this.page = 1; - this.loadLogs(); + // Local filtering - no HTTP request } private handlePhaseFilterChange(e: Event): void { this.phaseFilter = (e.target as HTMLSelectElement).value as TPhaseFilter; - this.page = 1; - this.loadLogs(); + // Local filtering - no HTTP request } private handleMethodFilterChange(e: Event): void { this.methodFilter = (e.target as HTMLSelectElement).value; - this.page = 1; - this.loadLogs(); + // Local filtering - no HTTP request } private handleSearch(e: Event): void { this.searchText = (e.target as HTMLInputElement).value.toLowerCase(); } - private async handleClear(): Promise { + private handleClear(): void { if (!confirm('Are you sure you want to clear the request logs? This cannot be undone.')) { return; } - try { - await fetch('/sw-dash/requests', { method: 'DELETE' }); - this.loadLogs(); - this.loadStats(); - } catch (err) { - console.error('Failed to clear request logs:', err); - } + // Dispatch event to parent to clear via DeesComms + this.dispatchEvent(new CustomEvent('clear-requests', { + bubbles: true, + composed: true, + })); } private loadMore(): void { - this.page++; - this.loadLogs(); + if (this.isLoadingMore || this.logs.length === 0) return; + + this.isLoadingMore = true; + const oldestLog = this.logs[this.logs.length - 1]; + + // Dispatch event to parent to load more via DeesComms + this.dispatchEvent(new CustomEvent('load-more-requests', { + detail: { + before: oldestLog.timestamp, + method: this.methodFilter || undefined, + }, + bubbles: true, + composed: true, + })); + + // Reset loading state after a short delay (parent will update logs prop) + setTimeout(() => { + this.isLoadingMore = false; + }, 1000); } private togglePayload(correlationId: string): void { @@ -469,6 +406,9 @@ export class SwDashRequests extends LitElement { return `${(durationMs / 1000).toFixed(2)}s`; } + /** + * Filter logs locally based on direction, phase, method, and search text + */ private getFilteredLogs(): ITypedRequestLogEntry[] { let result = this.logs; @@ -482,6 +422,11 @@ export class SwDashRequests extends LitElement { result = result.filter(l => l.phase === this.phaseFilter); } + // Apply method filter + if (this.methodFilter) { + result = result.filter(l => l.method === this.methodFilter); + } + // Apply search if (this.searchText) { result = result.filter(l => @@ -563,7 +508,7 @@ export class SwDashRequests extends LitElement { Method: - ${this.isLoading && this.logs.length === 0 ? html` -
Loading request logs...
- ` : filteredLogs.length === 0 ? html` + ${this.logs.length === 0 ? html`
No request logs found. Traffic will appear here as TypedRequests are made.
+ ` : filteredLogs.length === 0 ? html` +
No logs match filter
` : html`
${filteredLogs.map(log => html` @@ -624,8 +569,8 @@ export class SwDashRequests extends LitElement { ${this.logs.length < this.totalCount ? html` diff --git a/ts_web_serviceworker/classes.backend.ts b/ts_web_serviceworker/classes.backend.ts index d793328..0e30466 100644 --- a/ts_web_serviceworker/classes.backend.ts +++ b/ts_web_serviceworker/classes.backend.ts @@ -120,6 +120,7 @@ export class ServiceworkerBackend { limit: reqArg.limit, type: reqArg.type, since: reqArg.since, + before: reqArg.before, }); }); @@ -164,6 +165,7 @@ export class ServiceworkerBackend { limit: reqArg.limit, method: reqArg.method, since: reqArg.since, + before: reqArg.before, }); const totalCount = requestLogStore.getTotalCount({ method: reqArg.method, diff --git a/ts_web_serviceworker/classes.cachemanager.ts b/ts_web_serviceworker/classes.cachemanager.ts index 6f5e6e0..c2e57ef 100644 --- a/ts_web_serviceworker/classes.cachemanager.ts +++ b/ts_web_serviceworker/classes.cachemanager.ts @@ -210,65 +210,27 @@ export class CacheManager { fetchEventArg.respondWith(Promise.resolve(dashboard.serveDashboard())); return; } + // /sw-dash/metrics - THE initial seed endpoint (provides ALL data) if (parsedUrl.pathname === '/sw-dash/metrics') { const dashboard = getDashboardGenerator(); - fetchEventArg.respondWith(Promise.resolve(dashboard.serveMetrics())); + fetchEventArg.respondWith(dashboard.serveMetrics()); return; } + // /sw-dash/speedtest - user-triggered speedtest if (parsedUrl.pathname === '/sw-dash/speedtest') { const dashboard = getDashboardGenerator(); fetchEventArg.respondWith(dashboard.runSpeedtest()); return; } + // /sw-dash/resources - resource data (kept for now, could be merged into metrics) if (parsedUrl.pathname === '/sw-dash/resources') { const dashboard = getDashboardGenerator(); fetchEventArg.respondWith(Promise.resolve(dashboard.serveResources())); return; } - if (parsedUrl.pathname === '/sw-dash/events') { - const dashboard = getDashboardGenerator(); - fetchEventArg.respondWith(dashboard.serveEventLog(parsedUrl.searchParams)); - return; - } - if (parsedUrl.pathname === '/sw-dash/events/count') { - const dashboard = getDashboardGenerator(); - fetchEventArg.respondWith(dashboard.serveEventCount(parsedUrl.searchParams)); - return; - } - if (parsedUrl.pathname === '/sw-dash/cumulative-metrics') { - const dashboard = getDashboardGenerator(); - fetchEventArg.respondWith(dashboard.serveCumulativeMetrics()); - return; - } - // DELETE method for clearing events - if (parsedUrl.pathname === '/sw-dash/events' && originalRequest.method === 'DELETE') { - const dashboard = getDashboardGenerator(); - fetchEventArg.respondWith(dashboard.clearEventLog()); - return; - } - - // TypedRequest traffic monitoring endpoints - if (parsedUrl.pathname === '/sw-dash/requests' && originalRequest.method === 'GET') { - const dashboard = getDashboardGenerator(); - fetchEventArg.respondWith(Promise.resolve(dashboard.serveTypedRequestLogs(parsedUrl.searchParams))); - return; - } - if (parsedUrl.pathname === '/sw-dash/requests/stats') { - const dashboard = getDashboardGenerator(); - fetchEventArg.respondWith(Promise.resolve(dashboard.serveTypedRequestStats())); - return; - } - if (parsedUrl.pathname === '/sw-dash/requests/methods') { - const dashboard = getDashboardGenerator(); - fetchEventArg.respondWith(Promise.resolve(dashboard.serveTypedRequestMethods())); - return; - } - // DELETE method for clearing TypedRequest logs - if (parsedUrl.pathname === '/sw-dash/requests' && originalRequest.method === 'DELETE') { - const dashboard = getDashboardGenerator(); - fetchEventArg.respondWith(Promise.resolve(dashboard.clearTypedRequestLogs())); - return; - } + // All other /sw-dash/* routes removed - use DeesComms instead: + // - Events: via serviceworker_getEventLog, serviceworker_clearEventLog + // - Requests: via serviceworker_getTypedRequestLogs, serviceworker_clearTypedRequestLogs // Block requests that we don't want the service worker to handle. if ( diff --git a/ts_web_serviceworker/classes.dashboard.ts b/ts_web_serviceworker/classes.dashboard.ts index defe75b..9fab77d 100644 --- a/ts_web_serviceworker/classes.dashboard.ts +++ b/ts_web_serviceworker/classes.dashboard.ts @@ -25,10 +25,52 @@ export class DashboardGenerator { } /** - * Serves the metrics JSON endpoint + * Serves the metrics JSON endpoint with ALL initial data + * This is the single HTTP seed request that provides: + * - Current metrics + * - Initial events (last 50) + * - Initial request logs (last 50) + * - Request stats and methods + * - Resource data */ - public serveMetrics(): Response { - return new Response(this.generateMetricsJson(), { + public async serveMetrics(): Promise { + const metrics = getMetricsCollector(); + const persistentStore = getPersistentStore(); + await persistentStore.init(); + const requestLogStore = getRequestLogStore(); + + // Get event data + const eventResult = await persistentStore.getEventLog({ limit: 50 }); + const oneHourAgo = Date.now() - 3600000; + const eventCountLastHour = await persistentStore.getEventCount(oneHourAgo); + + // Build comprehensive initial response + const data = { + // Core metrics + ...metrics.getMetrics(), + cacheHitRate: metrics.getCacheHitRate(), + networkSuccessRate: metrics.getNetworkSuccessRate(), + resourceCount: metrics.getResourceCount(), + summary: metrics.getSummary(), + + // Resources data + resources: metrics.getCachedResources(), + domains: metrics.getDomainStats(), + contentTypes: metrics.getContentTypeStats(), + + // Events data (initial 50) + events: eventResult.events, + eventTotalCount: eventResult.totalCount, + eventCountLastHour, + + // Request logs data (initial 50) + requestLogs: requestLogStore.getEntries({ limit: 50 }), + requestTotalCount: requestLogStore.getTotalCount(), + requestStats: requestLogStore.getStats(), + requestMethods: requestLogStore.getMethods(), + }; + + return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', diff --git a/ts_web_serviceworker/classes.persistentstore.ts b/ts_web_serviceworker/classes.persistentstore.ts index 14efe97..97380bc 100644 --- a/ts_web_serviceworker/classes.persistentstore.ts +++ b/ts_web_serviceworker/classes.persistentstore.ts @@ -316,6 +316,7 @@ export class PersistentStore { limit?: number; type?: TEventType; since?: number; + before?: number; }): Promise<{ events: IEventLogEntry[]; totalCount: number }> { try { let events: IEventLogEntry[] = []; @@ -336,6 +337,11 @@ export class PersistentStore { events = events.filter(e => e.timestamp >= options.since); } + // Filter by before timestamp (for pagination) + if (options?.before) { + events = events.filter(e => e.timestamp < options.before); + } + // Sort by timestamp (newest first) events.sort((a, b) => b.timestamp - a.timestamp); diff --git a/ts_web_serviceworker/classes.requestlogstore.ts b/ts_web_serviceworker/classes.requestlogstore.ts index 3ba5181..fc60c28 100644 --- a/ts_web_serviceworker/classes.requestlogstore.ts +++ b/ts_web_serviceworker/classes.requestlogstore.ts @@ -103,6 +103,7 @@ export class RequestLogStore { limit?: number; method?: string; since?: number; + before?: number; }): interfaces.serviceworker.ITypedRequestLogEntry[] { let result = [...this.logs]; @@ -111,11 +112,16 @@ export class RequestLogStore { result = result.filter((e) => e.method === options.method); } - // Filter by timestamp + // Filter by timestamp (since) if (options?.since) { result = result.filter((e) => e.timestamp >= options.since); } + // Filter by timestamp (before - for pagination) + if (options?.before) { + result = result.filter((e) => e.timestamp < options.before); + } + // Sort by timestamp descending (newest first) result.sort((a, b) => b.timestamp - a.timestamp);