wifi-densepose/frontend/src/pages/Services.ts

87 lines
3.8 KiB
TypeScript

/**
* Services page — lists every registered service grouped by domain.
* Reads from `/api/services` (HA-wire-compat).
*/
import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { HomecoreClient } from '../api/client.js';
import type { ServiceDomainView } from '../api/types.js';
function resolveToken(): string {
if (typeof localStorage !== 'undefined') {
const stored = localStorage.getItem('homecore.token');
if (stored) return stored;
}
const qs = new URL(window.location.href).searchParams.get('token');
return qs ?? 'dev-token';
}
@customElement('hc-services')
export class ServicesPage extends LitElement {
static styles = css`
:host { display: block; padding: 24px; color: var(--hc-text, #e6eaee); font-family: var(--hc-font-sans, 'Outfit', system-ui, sans-serif); }
h1 { font-size: 18px; font-weight: 600; margin: 0 0 16px 0; }
.domain { background: hsl(220 20% 10%); border: 1px solid var(--hc-border, #2a323e); border-radius: 8px; margin-bottom: 12px; padding: 14px 16px; }
.domain h2 { font-size: 14px; font-weight: 600; margin: 0 0 8px 0; color: var(--hc-primary, #19d4e5); font-family: var(--hc-font-mono, 'JetBrains Mono', monospace); }
ul { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 6px; }
li { background: hsl(220 25% 14%); padding: 4px 10px; border-radius: 4px; font-family: var(--hc-font-mono, 'JetBrains Mono', monospace); font-size: 12px; color: var(--hc-text-muted, #7b899d); }
.empty { padding: 24px; border: 1px dashed var(--hc-border, #2a323e); border-radius: 8px; text-align: center; color: var(--hc-text-muted, #7b899d); }
.err { padding: 16px; border: 1px dashed #b35a5a; border-radius: 8px; color: #f0c0c0; font-size: 13px; }
`;
@state() private domains: ServiceDomainView[] = [];
@state() private error: string | null = null;
@state() private loading = true;
private client = new HomecoreClient({ token: resolveToken() });
connectedCallback(): void {
super.connectedCallback();
void this.refresh();
}
private async refresh(): Promise<void> {
try {
const r = await fetch('/api/services', { headers: { 'Authorization': `Bearer ${resolveToken()}` } });
if (!r.ok) throw new Error(`/api/services -> HTTP ${r.status}`);
this.domains = await r.json();
this.error = null;
} catch (e) {
this.error = e instanceof Error ? e.message : String(e);
} finally {
this.loading = false;
}
void this.client; // suppress unused warning while keeping the import shape consistent
}
render() {
if (this.error) return html`<div class="err">backend unreachable — ${this.error}</div>`;
if (this.loading) return html`<div>loading services…</div>`;
if (this.domains.length === 0) {
return html`
<h1>Services (0 domains)</h1>
<div class="empty">
No services registered. Services are registered by plugins
(Wasmtime or InProcess) or by integrations that call
<code>services::register()</code> on boot.
</div>
`;
}
return html`
<h1>Services (${this.domains.length} domain${this.domains.length === 1 ? '' : 's'})</h1>
${this.domains.map(d => html`
<div class="domain">
<h2>${d.domain}</h2>
<ul>
${Object.keys(d.services).map(name => html`<li>${name}</li>`)}
</ul>
</div>
`)}
`;
}
}
declare global { interface HTMLElementTagNameMap { 'hc-services': ServicesPage; } }