Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
8 changes: 8 additions & 0 deletions src/app-styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
22 changes: 22 additions & 0 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<EntityInteraction[]> {
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"
Expand Down
93 changes: 93 additions & 0 deletions src/views/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -121,6 +123,8 @@ async function renderContent(): Promise<HTMLElement> {
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({
Expand Down Expand Up @@ -215,6 +219,11 @@ function renderTemplate(
${renderSparklineBox("queue", "Queue depth")}
</div>
</div>

<h3 style="margin:2rem 0 0.5rem">Entities</h3>
<div id="entities-section" class="stat-card" style="padding:0.75rem 1rem;margin-bottom:2rem">
<div id="entities-body" style="color:var(--text-muted);font-size:0.85rem">Loading entities…</div>
</div>
`;
}

Expand Down Expand Up @@ -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 `<span class="badge ${cls}">${escapeHtml(status)}</span>`;
}

const COPY_ICON =
`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`;

async function renderEntitiesSection(
root: HTMLElement,
ppPublicKey: string,
): Promise<void> {
const body = root.querySelector("#entities-body") as HTMLElement | null;
if (!body) return;

let entities: EntityInteraction[];
try {
entities = await getEntities(ppPublicKey);
} catch (err) {
body.innerHTML = `<span class="error-text">${
escapeHtml(
err instanceof Error ? err.message : "Failed to load entities",
)
}</span>`;
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 `
<tr>
<td style="white-space:nowrap;color:var(--text-muted)">${
escapeHtml(date)
}</td>
<td>
<span style="font-family:monospace">${
escapeHtml(truncate(e.pubkey))
}</span>
<button class="icon-btn entity-copy-btn" data-pubkey="${
escapeHtml(e.pubkey)
}" title="Copy public key" style="vertical-align:middle">${COPY_ICON}</button>
</td>
<td>${entityStatusBadge(e.status)}</td>
</tr>`;
}).join("");

body.innerHTML = `
<table style="margin:0">
<thead>
<tr><th>Date</th><th>Public Key</th><th>Status</th></tr>
</thead>
<tbody>${rows}</tbody>
</table>`;

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)
// -----------------------------------------------------------------------------
Expand Down
Loading