diff --git a/deno.json b/deno.json index 5eb08b9..6b32982 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@moonlight-protocol/provider-console", - "version": "0.3.1", + "version": "0.3.2", "license": "MIT", "tasks": { "dev": "deno run --allow-all --allow-env --watch src/server.ts", diff --git a/src/app-styles.css b/src/app-styles.css index a5d41ba..3e219f4 100644 --- a/src/app-styles.css +++ b/src/app-styles.css @@ -46,6 +46,14 @@ th { border-radius: 4px; } +/* Neutral badge for UNVERIFIED entity status. The lib ships + .badge / .badge-active / .badge-pending / .badge-inactive; "unverified" has + no semantic color (not an error, not pending) so it gets a muted grey. */ +.badge-unverified { + background: rgba(148, 163, 184, 0.15); + color: var(--text-muted); +} + /* Empty-state extensions for code/details/summary (lib ships .empty-state base) */ .empty-state details { margin-top: 1rem; diff --git a/src/lib/api.ts b/src/lib/api.ts index da374ca..cfdf058 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -352,6 +352,28 @@ export async function getMetrics( return body.data as MetricsResponse; } +// --- Entities interacting with the provider (operator view) --- + +export interface EntityInteraction { + pubkey: string; + status: string; + name: string | null; + jurisdictions: string[] | null; + createdAt: string; + updatedAt: string; +} + +export async function getEntities( + ppPublicKey: string, +): Promise { + const res = await platformFetch( + `/providers/${encodeURIComponent(ppPublicKey)}/entities`, + ); + if (!res.ok) throw new Error("Failed to fetch entities"); + const { data } = await res.json(); + return data as EntityInteraction[]; +} + export type BundleOpKind = | "deposit" | "withdraw" diff --git a/src/views/provider.ts b/src/views/provider.ts index f8b0770..28a63f4 100644 --- a/src/views/provider.ts +++ b/src/views/provider.ts @@ -2,7 +2,9 @@ import { page } from "../components/page.ts"; import { escapeHtml } from "../lib/dom.ts"; import { type BundleDetail, + type EntityInteraction, getBundleDetail, + getEntities, getMetrics, getTreasury, listPps, @@ -121,6 +123,8 @@ async function renderContent(): Promise { wireHeader(root, pp); wireFund(root, pp.publicKey); wireCouncils(root); + // Best-effort: a fetch failure renders an error row, never blocks the page. + renderEntitiesSection(root, ppPublicKey); // v2 zones (counter strip / recent bundles / activity feed / sparklines). const zones = setupV2Zones({ @@ -215,6 +219,11 @@ function renderTemplate( ${renderSparklineBox("queue", "Queue depth")} + +

Entities

+
+
Loading entities…
+
`; } @@ -283,6 +292,90 @@ function renderCouncilCard(m: MembershipInfo): string { `; } +// ----------------------------------------------------------------------------- +// Entities section — every pubkey that has interacted with this PP +// ----------------------------------------------------------------------------- + +/** Maps a per-PP entity status to one of the lib's badge variants. UNVERIFIED + * uses a neutral local variant (badge-unverified, app-styles.css). */ +function entityStatusBadge(status: string): string { + const cls = status === "APPROVED" + ? "badge-active" + : status === "PENDING" + ? "badge-pending" + : status === "BLOCKED" + ? "badge-inactive" + : "badge-unverified"; // UNVERIFIED + any unknown future value + return `${escapeHtml(status)}`; +} + +const COPY_ICON = + ``; + +async function renderEntitiesSection( + root: HTMLElement, + ppPublicKey: string, +): Promise { + const body = root.querySelector("#entities-body") as HTMLElement | null; + if (!body) return; + + let entities: EntityInteraction[]; + try { + entities = await getEntities(ppPublicKey); + } catch (err) { + body.innerHTML = `${ + escapeHtml( + err instanceof Error ? err.message : "Failed to load entities", + ) + }`; + return; + } + + if (entities.length === 0) { + body.textContent = "No entities have interacted with this provider yet"; + return; + } + + const now = Date.now(); + const rows = entities.map((e) => { + const date = fmtRelativeTime(new Date(e.updatedAt).getTime(), now); + return ` + + ${ + escapeHtml(date) + } + + ${ + escapeHtml(truncate(e.pubkey)) + } + + + ${entityStatusBadge(e.status)} + `; + }).join(""); + + body.innerHTML = ` + + + + + ${rows} +
DatePublic KeyStatus
`; + + body.querySelectorAll(".entity-copy-btn").forEach((el) => { + const btn = el as HTMLButtonElement; + btn.addEventListener("click", () => { + const pubkey = btn.dataset.pubkey; + if (!pubkey) return; + navigator.clipboard.writeText(pubkey).then(() => + withBriefCopyFeedback(btn) + ); + }); + }); +} + // ----------------------------------------------------------------------------- // v1 header / fund / councils wiring (unchanged paths) // -----------------------------------------------------------------------------