269 lines
7.4 KiB
TypeScript
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>
|
|
`;
|
|
}
|
|
}
|