From c6d42a68b5943d4091afe917bb05fd9c2d1d5a92 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Mon, 16 Mar 2026 09:37:33 +0000 Subject: [PATCH 1/3] feat(cli): redesign dashboard for ATmosphereConf demo Two-column layout (Repository | Network) instead of three columns for narrow terminals and split-screen use. Key changes: - Account status (ACTIVE/INACTIVE) displayed prominently in header - Identity checks (handle resolution, DID document) in Network panel - Per-relay host status (active/idle/offline) replaces commit sync check - Separated activation beats: [a] activates only, [r] and [e] are independent steps with contextual keybinding visibility - Firehose commit events trigger debounced repo refetch for faster UI --- packages/pds/src/cli/commands/dashboard.ts | 364 +++++++++++++-------- 1 file changed, 229 insertions(+), 135 deletions(-) diff --git a/packages/pds/src/cli/commands/dashboard.ts b/packages/pds/src/cli/commands/dashboard.ts index 393132d..99699e6 100644 --- a/packages/pds/src/cli/commands/dashboard.ts +++ b/packages/pds/src/cli/commands/dashboard.ts @@ -8,6 +8,10 @@ import { getVars } from "../utils/wrangler.js"; import { readDevVars } from "../utils/dotenv.js"; import { PDSClient } from "../utils/pds-client.js"; import { getTargetUrl } from "../utils/cli-helpers.js"; +import { + checkHandleResolutionDetailed, + checkDidResolution, +} from "../utils/checks.js"; // ============================================ // ANSI string utilities @@ -197,9 +201,17 @@ function shortenIP(ip: string): string { // Strip leading zeros from each group return parts.map((p) => p.replace(/^0+(?=.)/, "")).join(":"); } - const before = parts.slice(0, bestStart).map((p) => p.replace(/^0+(?=.)/, "")); - const after = parts.slice(bestStart + bestLen).map((p) => p.replace(/^0+(?=.)/, "")); - return (before.length ? before.join(":") : "") + "::" + (after.length ? after.join(":") : ""); + const before = parts + .slice(0, bestStart) + .map((p) => p.replace(/^0+(?=.)/, "")); + const after = parts + .slice(bestStart + bestLen) + .map((p) => p.replace(/^0+(?=.)/, "")); + return ( + (before.length ? before.join(":") : "") + + "::" + + (after.length ? after.join(":") : "") + ); } // ============================================ @@ -263,28 +275,33 @@ interface Notification { interface DashboardState { collections: CollectionInfo[]; - syncStatus: "checking" | "synced" | "behind" | "unknown" | "error"; - relayRev: string | null; pdsRev: string | null; subscribers: number; latestSeq: number | null; - subscriberDetails: Array<{ connectedAt: number; cursor: number; ip: string | null }>; + subscriberDetails: Array<{ + connectedAt: number; + cursor: number; + ip: string | null; + }>; events: DashboardEvent[]; notifications: Notification[]; accountActive: boolean; wsConnected: boolean; statusMessage: string | null; statusMessageTimeout: ReturnType | null; + handleCheck: { ok: boolean; methods: string[] } | null; + didCheck: { ok: boolean; pdsEndpoint: string | null } | null; + relayHostStatus: Array<{ + status: "active" | "idle" | "offline" | "throttled" | "banned"; + relay: string; + }>; } const MAX_EVENTS = 100; -const MAX_NOTIFICATIONS = 50; function createInitialState(): DashboardState { return { collections: [], - syncStatus: "checking", - relayRev: null, pdsRev: null, subscribers: 0, latestSeq: null, @@ -295,6 +312,9 @@ function createInitialState(): DashboardState { wsConnected: false, statusMessage: null, statusMessageTimeout: null, + handleCheck: null, + didCheck: null, + relayHostStatus: [], }; } @@ -343,32 +363,6 @@ async function fetchRepo( } } -async function fetchRelaySync( - did: string, - state: DashboardState, - render: () => void, -): Promise { - if (!state.pdsRev) return; - try { - const res = await fetch( - `https://bsky.network/xrpc/com.atproto.sync.getLatestCommit?did=${encodeURIComponent(did)}`, - ); - if (res.status === 404) { - state.syncStatus = "unknown"; - } else if (res.ok) { - const data = (await res.json()) as { rev: string }; - state.syncStatus = data.rev === state.pdsRev ? "synced" : "behind"; - state.relayRev = data.rev; - } else { - state.syncStatus = "error"; - } - render(); - } catch { - state.syncStatus = "error"; - render(); - } -} - async function fetchFirehoseStatus( client: PDSClient, state: DashboardState, @@ -423,6 +417,48 @@ async function fetchAccountStatus( } } +async function fetchIdentityChecks( + client: PDSClient, + handle: string, + did: string, + pdsHostname: string, + state: DashboardState, + render: () => void, +): Promise { + try { + const [handleResult, didResult] = await Promise.all([ + checkHandleResolutionDetailed(client, handle, did), + checkDidResolution(client, did, pdsHostname), + ]); + state.handleCheck = { + ok: handleResult.ok, + methods: handleResult.methods, + }; + state.didCheck = { + ok: didResult.ok, + pdsEndpoint: didResult.pdsEndpoint, + }; + render(); + } catch { + // Silently retry + } +} + +async function fetchRelayHostStatus( + client: PDSClient, + pdsHostname: string, + state: DashboardState, + render: () => void, +): Promise { + try { + const statuses = await client.getAllRelayHostStatus(pdsHostname); + state.relayHostStatus = statuses; + render(); + } catch { + // Silently retry + } +} + // ============================================ // WebSocket firehose connection // ============================================ @@ -431,6 +467,7 @@ function connectFirehose( targetUrl: string, state: DashboardState, render: () => void, + onCommit?: () => void, ): { close: () => void } { let ws: WebSocket | null = null; let reconnectTimer: ReturnType | null = null; @@ -455,7 +492,9 @@ function connectFirehose( new Uint8Array(e.data as ArrayBuffer), ); if (!event) return; - const time = new Date().toLocaleTimeString("en-GB", { hour12: false }); + const time = new Date().toLocaleTimeString("en-GB", { + hour12: false, + }); if (event.type === "identity") { state.events.unshift({ time, @@ -472,6 +511,7 @@ function connectFirehose( path: op.path, }); } + onCommit?.(); } if (state.events.length > MAX_EVENTS) { state.events.length = MAX_EVENTS; @@ -529,23 +569,26 @@ function renderDashboard( const lines: string[] = []; const indent = " "; - // Header + // Header with account status + const accountDot = state.accountActive + ? pc.green("\u25cf") + " " + pc.green("ACTIVE") + : pc.yellow("\u25cb") + " " + pc.yellow("INACTIVE"); lines.push(""); lines.push( `${indent}${pc.bold("\u2601 CIRRUS")} ${pc.dim("\u00b7")} ${pc.cyan(config.hostname)} ${pc.dim("\u00b7")} ${pc.dim("v" + config.version)}`, ); lines.push( - `${indent} ${pc.white("@" + config.handle)} ${pc.dim("\u00b7")} ${pc.dim(config.did)}`, + `${indent} ${pc.white("@" + config.handle)} ${pc.dim("\u00b7")} ${pc.dim(config.did)} ${pc.dim("\u00b7")} ${accountDot}`, ); lines.push(""); - // Three-column panels - const colWidth = Math.floor((cols - 6) / 3); + // Two-column layout: REPOSITORY | NETWORK + const colWidth = Math.floor((cols - 6) / 2); // Column 1: Repository const col1: string[] = [pc.dim("REPOSITORY"), ""]; if (state.collections.length === 0) { - col1.push(pc.dim("Loading\u2026")); + col1.push(pc.dim("No records")); } else { for (const c of state.collections) { const name = c.friendlyName.padEnd(16); @@ -555,92 +598,93 @@ function renderDashboard( } } - // Column 2: Federation - const col2: string[] = [pc.dim("FEDERATION"), ""]; - col2.push(pc.dim("bsky.network")); - const statusColors: Record string> = { - synced: pc.green, - behind: pc.yellow, - error: pc.red, - checking: pc.dim, - unknown: pc.dim, + // Column 2: Network + const col2: string[] = [pc.dim("NETWORK"), ""]; + + // Identity checks + if (state.handleCheck) { + const icon = state.handleCheck.ok ? pc.green("\u2713") : pc.red("\u2717"); + const methods = + state.handleCheck.methods.length > 0 + ? pc.dim(` ${state.handleCheck.methods.join(" ")}`) + : ""; + col2.push(`${icon} handle${methods}`); + } else { + col2.push(pc.dim("\u25cb handle checking\u2026")); + } + + if (state.didCheck) { + const icon = state.didCheck.ok ? pc.green("\u2713") : pc.red("\u2717"); + col2.push(`${icon} did document`); + } else { + col2.push(pc.dim("\u25cb did doc checking\u2026")); + } + + col2.push(""); + + // Relay host status + const relayStatusColors: Record string> = { + active: pc.green, + idle: pc.yellow, + offline: pc.red, + throttled: pc.red, + banned: pc.red, }; - const dotColors: Record = { - synced: pc.green("\u25cf"), - behind: pc.yellow("\u25cf"), - error: pc.red("\u25cf"), - checking: pc.dim("\u25cb"), - unknown: pc.dim("\u25cb"), + const relayDotColors: Record = { + active: pc.green("\u25cf"), + idle: pc.yellow("\u25cf"), + offline: pc.red("\u25cf"), + throttled: pc.red("\u25cf"), + banned: pc.red("\u25cf"), }; - const colorFn = statusColors[state.syncStatus] ?? pc.dim; - col2.push( - `${dotColors[state.syncStatus] ?? pc.dim("\u25cb")} ${colorFn(state.syncStatus.toUpperCase())}`, - ); - if (state.relayRev) { - col2.push(pc.dim(`rev: ${state.relayRev.slice(0, 12)}`)); + + if (state.relayHostStatus.length > 0) { + for (const relay of state.relayHostStatus) { + const name = relay.relay + .replace("https://relay1.", "") + .replace(".bsky.network", ""); + const colorFn = relayStatusColors[relay.status] ?? pc.dim; + const dot = relayDotColors[relay.status] ?? pc.dim("\u25cb"); + col2.push(`${dot} ${name} ${colorFn(relay.status)}`); + } + } else { + col2.push(pc.dim("\u25cb relay unknown")); } - // Column 3: Firehose - const col3: string[] = [pc.dim("FIREHOSE"), ""]; + col2.push(""); + + // Firehose subscribers const subDot = state.subscribers > 0 ? pc.green("\u25cf") : pc.dim("\u25cb"); - col3.push( - `${subDot} ${pc.bold(String(state.subscribers))} subscriber${state.subscribers !== 1 ? "s" : ""}`, - ); - col3.push( - pc.dim(`seq: ${state.latestSeq != null ? state.latestSeq : "\u2014"}`), + col2.push( + `${subDot} ${pc.bold(String(state.subscribers))} subscriber${state.subscribers !== 1 ? "s" : ""} ${pc.dim("seq:")} ${state.latestSeq != null ? state.latestSeq : pc.dim("\u2014")}`, ); if (state.subscriberDetails.length > 0) { - col3.push(""); - for (const sub of state.subscriberDetails.slice(0, 5)) { - const ip = sub.ip ? ` ${shortenIP(sub.ip)}` : ""; - col3.push( - pc.dim(`${relativeTime(sub.connectedAt)} cursor: ${sub.cursor}${ip}`), + for (const sub of state.subscriberDetails.slice(0, 3)) { + const ip = sub.ip ? shortenIP(sub.ip) : ""; + col2.push( + pc.dim(` ${relativeTime(sub.connectedAt)} cursor: ${sub.cursor} ${ip}`), ); } } - const columnLines = renderColumns( - [col1, col2, col3], - [colWidth, colWidth, colWidth], - ); + const columnLines = renderColumns([col1, col2], [colWidth, colWidth]); for (const line of columnLines) { lines.push(indent + line); } lines.push(""); - // Calculate remaining space for notifications + events + footer + // Calculate remaining space for events + notifications + footer const usedLines = lines.length; const footerLines = 3; // blank + keybindings + blank const remaining = rows - usedLines - footerLines; - const notifHeight = Math.max(3, Math.floor(remaining * 0.4)); + const notifHeight = Math.max(3, Math.floor(remaining * 0.35)); const eventsHeight = Math.max(3, remaining - notifHeight); - // Notifications panel - const notifSeparator = "\u2500".repeat( - Math.max(0, cols - visibleLength(indent + "NOTIFICATIONS ") - 2), - ); - lines.push(`${indent}${pc.dim("NOTIFICATIONS " + notifSeparator)}`); - if (state.notifications.length === 0) { - lines.push(`${indent}${pc.dim("No notifications yet")}`); - for (let i = 1; i < notifHeight - 1; i++) lines.push(""); - } else { - const visibleNotifs = state.notifications.slice(0, notifHeight - 1); - for (const n of visibleNotifs) { - const readDim = n.isRead ? pc.dim : (s: string) => s; - const line = `${indent}${pc.dim(n.time)} ${n.icon} ${readDim(n.author)} ${readDim(pc.dim(n.text))}`; - lines.push(truncate(line, cols)); - } - // Pad remaining - for (let i = visibleNotifs.length; i < notifHeight - 1; i++) { - lines.push(""); - } - } - - lines.push(""); - - // Events panel - const wsStatusText = state.wsConnected ? "\u25cf connected" : "\u25cb disconnected"; + // Events panel (full width) + const wsStatusText = state.wsConnected + ? "\u25cf connected" + : "\u25cb disconnected"; const eventsPrefix = indent + "EVENTS "; const eventsSuffix = " " + wsStatusText + " "; const eventsSeparator = "\u2500".repeat( @@ -663,7 +707,7 @@ function renderDashboard( identity: pc.cyan, }; const actionColor = actionColors[ev.action] ?? pc.dim; - const line = `${indent}${pc.dim(ev.time)} ${pc.dim("#" + String(ev.seq).padStart(4))} ${actionColor(ev.action.toUpperCase().padEnd(7))} ${ev.path}`; + const line = `${indent}${pc.dim(ev.time)} ${pc.dim("#" + String(ev.seq).padStart(4))} ${actionColor(ev.action.toUpperCase().padEnd(8))} ${ev.path}`; lines.push(truncate(line, cols)); } for (let i = visibleEvents.length; i < eventsHeight - 1; i++) { @@ -671,17 +715,40 @@ function renderDashboard( } } - // Footer + // Notifications panel + const notifSeparator = "\u2500".repeat( + Math.max(0, cols - visibleLength(indent + "NOTIFICATIONS ") - 2), + ); + lines.push(`${indent}${pc.dim("NOTIFICATIONS " + notifSeparator)}`); + if (state.notifications.length === 0) { + lines.push(`${indent}${pc.dim("No notifications yet")}`); + for (let i = 1; i < notifHeight - 1; i++) lines.push(""); + } else { + const visibleNotifs = state.notifications.slice(0, notifHeight - 1); + for (const n of visibleNotifs) { + const readDim = n.isRead ? pc.dim : (s: string) => s; + const line = `${indent}${pc.dim(n.time)} ${n.icon} ${readDim(n.author)} ${readDim(pc.dim(n.text))}`; + lines.push(truncate(line, cols)); + } + // Pad remaining + for (let i = visibleNotifs.length; i < notifHeight - 1; i++) { + lines.push(""); + } + } + + // Footer with contextual keybindings lines.push(""); - const accountStatus = state.accountActive - ? pc.green("\u25cf active") - : pc.yellow("\u25cb deactivated"); - let footer = `${indent}${pc.dim("[a]")} activate ${pc.dim("\u00b7")} ${pc.dim("[r]")} crawl ${pc.dim("\u00b7")} ${pc.dim("[e]")} emit identity ${pc.dim("\u00b7")} ${pc.dim("[q]")} quit`; - // Add status message or account status to the right + const keys: string[] = []; + if (!state.accountActive) { + keys.push(`${pc.dim("[a]")} activate`); + } else { + keys.push(`${pc.dim("[r]")} crawl`); + keys.push(`${pc.dim("[e]")} emit identity`); + } + keys.push(`${pc.dim("[q]")} quit`); + let footer = `${indent}${keys.join(` ${pc.dim("\u00b7")} `)}`; if (state.statusMessage) { footer += ` ${pc.yellow(state.statusMessage)}`; - } else { - footer += ` ${accountStatus}`; } lines.push(footer); @@ -755,6 +822,7 @@ export const dashboardCommand = defineCommand({ const authToken = config.AUTH_TOKEN; const handle = config.HANDLE ?? ""; const did = config.DID ?? ""; + const pdsHostname = config.PDS_HOSTNAME ?? ""; if (!authToken) { console.error( @@ -776,7 +844,7 @@ export const dashboardCommand = defineCommand({ // Initialize state const state = createInitialState(); const dashConfig = { - hostname: config.PDS_HOSTNAME ?? targetUrl, + hostname: pdsHostname || targetUrl, handle, did, version: "0.10.6", @@ -793,10 +861,12 @@ export const dashboardCommand = defineCommand({ // Cleanup function const intervals: ReturnType[] = []; let firehose: { close: () => void } | null = null; + let repoRefetchTimer: ReturnType | null = null; function cleanup(): void { for (const interval of intervals) clearInterval(interval); if (firehose) firehose.close(); + if (repoRefetchTimer) clearTimeout(repoRefetchTimer); if (state.statusMessageTimeout) clearTimeout(state.statusMessageTimeout); if (process.stdin.isTTY) { process.stdin.setRawMode(false); @@ -814,6 +884,14 @@ export const dashboardCommand = defineCommand({ process.exit(0); }); + // Debounced repo refetch triggered by firehose events + const scheduleRepoRefetch = () => { + if (repoRefetchTimer) clearTimeout(repoRefetchTimer); + repoRefetchTimer = setTimeout(() => { + fetchRepo(client, did, state, render); + }, 1000); + }; + // Initial data fetch render(); await Promise.all([ @@ -821,15 +899,21 @@ export const dashboardCommand = defineCommand({ fetchFirehoseStatus(client, state, render), fetchAccountStatus(client, state, render), fetchNotifications(client, state, render), + fetchIdentityChecks(client, handle, did, pdsHostname, state, render), ]); - // Start relay sync after repo data is available (needs pdsRev) - await fetchRelaySync(did, state, render); + // Relay host status can run in parallel too + await fetchRelayHostStatus(client, pdsHostname, state, render); // Set up polling intervals intervals.push( setInterval(() => fetchRepo(client, did, state, render), 30000), ); - intervals.push(setInterval(() => fetchRelaySync(did, state, render), 5000)); + intervals.push( + setInterval( + () => fetchRelayHostStatus(client, pdsHostname, state, render), + 5000, + ), + ); intervals.push( setInterval(() => fetchFirehoseStatus(client, state, render), 10000), ); @@ -839,9 +923,23 @@ export const dashboardCommand = defineCommand({ intervals.push( setInterval(() => fetchAccountStatus(client, state, render), 30000), ); + intervals.push( + setInterval( + () => + fetchIdentityChecks( + client, + handle, + did, + pdsHostname, + state, + render, + ), + 15000, + ), + ); // Connect to firehose for real-time events - firehose = connectFirehose(targetUrl, state, render); + firehose = connectFirehose(targetUrl, state, render, scheduleRepoRefetch); // Handle resize process.stdout.on("resize", render); @@ -868,27 +966,20 @@ export const dashboardCommand = defineCommand({ process.exit(0); } - // a = activate (with inline confirmation) + // a = activate (only when inactive, with inline confirmation) if (key === "a" || key === "A") { + if (state.accountActive) return; if (awaitingActivateConfirm) { awaitingActivateConfirm = false; - if (activateConfirmTimeout) clearTimeout(activateConfirmTimeout); + if (activateConfirmTimeout) + clearTimeout(activateConfirmTimeout); setStatusMessage(state, "Activating\u2026", render, 10000); try { await client.activateAccount(); state.accountActive = true; - const pdsHostname = config.PDS_HOSTNAME; - if (pdsHostname && !isDev) { - await client.requestCrawl(pdsHostname); - } - try { - await client.emitIdentity(); - } catch { - // Non-critical - } setStatusMessage( state, - pc.green("\u2713 Activated! Crawl requested."), + pc.green("\u2713 Account activated"), render, 5000, ); @@ -919,9 +1010,9 @@ export const dashboardCommand = defineCommand({ return; } - // r = request crawl + // r = request crawl (only when active) if (key === "r" || key === "R") { - const pdsHostname = config.PDS_HOSTNAME; + if (!state.accountActive) return; if (!pdsHostname || isDev) { setStatusMessage( state, @@ -942,8 +1033,9 @@ export const dashboardCommand = defineCommand({ return; } - // e = emit identity + // e = emit identity (only when active) if (key === "e" || key === "E") { + if (!state.accountActive) return; setStatusMessage(state, "Emitting identity\u2026", render, 10000); try { const result = await client.emitIdentity(); @@ -955,7 +1047,9 @@ export const dashboardCommand = defineCommand({ } catch (err) { setStatusMessage( state, - pc.red(`\u2717 ${err instanceof Error ? err.message : "Failed"}`), + pc.red( + `\u2717 ${err instanceof Error ? err.message : "Failed"}`, + ), render, ); } From f1e8b0330c206ea21ca5c8287b51e470a8e1ac32 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sat, 21 Mar 2026 12:03:58 +0000 Subject: [PATCH 2/3] Change ascii art --- packages/create-pds/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/create-pds/src/index.ts b/packages/create-pds/src/index.ts index f0aad8c..bdcd12f 100644 --- a/packages/create-pds/src/index.ts +++ b/packages/create-pds/src/index.ts @@ -23,8 +23,9 @@ const BANNER = ` ██║ ██║██╔══██╗██╔══██╗██║ ██║╚════██║ ╚██████╗██║██║ ██║██║ ██║╚██████╔╝███████║ ╚═════╝╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ - - ☁️ The lightest PDS in the Atmosphere ☁️ + ☁️ ☁️ ☁️ ☁️ ☁️ ☁️ ☁️ ☁️ + ☁️ ☁️ Your personal data server ☁️ ☁️ + ☁️ ☁️ ☁️ ☁️ ☁️ ☁️ ☁️ `; type PackageManager = "npm" | "yarn" | "pnpm" | "bun"; From 67a6c8a3311025b88eebd906ef5e2230d11e254b Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sat, 21 Mar 2026 14:23:45 +0000 Subject: [PATCH 3/3] changeset --- .changeset/dashboard-demo.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/dashboard-demo.md diff --git a/.changeset/dashboard-demo.md b/.changeset/dashboard-demo.md new file mode 100644 index 0000000..3bdc078 --- /dev/null +++ b/.changeset/dashboard-demo.md @@ -0,0 +1,6 @@ +--- +"@getcirrus/pds": minor +"create-pds": patch +--- + +Improve CLI dashboard design