Files
typedserver/ts_swdash/sw-dash-app.ts

269 lines
7.4 KiB
TypeScript

import { LitElement, html, css, state, customElement } 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 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-table.js';
type ViewType = 'overview' | 'urls' | 'domains' | 'types';
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();
private refreshInterval: ReturnType<typeof setInterval> | null = null;
connectedCallback(): void {
super.connectedCallback();
this.loadMetrics();
this.loadResourceData();
// Auto-refresh every 2 seconds
this.refreshInterval = setInterval(() => {
this.loadMetrics();
if (this.currentView !== 'overview') {
this.loadResourceData();
}
}, 2000);
}
disconnectedCallback(): void {
super.disconnectedCallback();
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
}
private async loadMetrics(): Promise<void> {
try {
const response = await fetch('/sw-dash/metrics');
this.metrics = await response.json();
this.lastRefresh = new Date().toLocaleTimeString();
} catch (err) {
console.error('Failed to load metrics:', err);
}
}
private async loadResourceData(): Promise<void> {
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
this.loadMetrics();
}
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`
<div class="terminal">
<div class="header">
<div class="header-left">
<div class="logo">SW</div>
<span class="title">Service Worker Dashboard</span>
</div>
<div class="uptime-badge">
Uptime: <span class="value">${this.metrics ? this.formatUptime(this.metrics.uptime) : '--'}</span>
</div>
</div>
<nav class="nav">
<button
class="nav-tab ${this.currentView === 'overview' ? 'active' : ''}"
@click="${() => this.setView('overview')}"
>Overview</button>
<button
class="nav-tab ${this.currentView === 'urls' ? 'active' : ''}"
@click="${() => this.setView('urls')}"
>URLs <span class="count">${this.resourceData.resourceCount}</span></button>
<button
class="nav-tab ${this.currentView === 'domains' ? 'active' : ''}"
@click="${() => this.setView('domains')}"
>Domains</button>
<button
class="nav-tab ${this.currentView === 'types' ? 'active' : ''}"
@click="${() => this.setView('types')}"
>Types</button>
</nav>
<div class="content">
<div class="view ${this.currentView === 'overview' ? 'active' : ''}">
<sw-dash-overview
.metrics="${this.metrics}"
@speedtest-complete="${this.handleSpeedtestComplete}"
></sw-dash-overview>
</div>
<div class="view ${this.currentView === 'urls' ? 'active' : ''}">
<sw-dash-urls .resources="${this.resourceData.resources}"></sw-dash-urls>
</div>
<div class="view ${this.currentView === 'domains' ? 'active' : ''}">
<sw-dash-domains .domains="${this.resourceData.domains}"></sw-dash-domains>
</div>
<div class="view ${this.currentView === 'types' ? 'active' : ''}">
<sw-dash-types .contentTypes="${this.resourceData.contentTypes}"></sw-dash-types>
</div>
</div>
<div class="footer">
<div class="footer-left">
Last updated: ${this.lastRefresh}
</div>
<div class="footer-right">
<div class="auto-refresh">
<span class="dot"></span>
Live
</div>
</div>
</div>
</div>
`;
}
}