Skip to content

Commit 287c971

Browse files
ascorbicclaude
andauthored
Add dashboard on web and cli (#136)
* feat(pds): add live status dashboard at /status Dark-themed dashboard for conference demos. Shows server identity, repository record counts, relay federation sync status, firehose subscriber details, and a live event stream with inline CBOR decoding. - GET /status serves the dashboard HTML - GET /xrpc/gg.mk.experimental.getSubscribers exposes sanitized firehose subscriber metadata (connectedAt, cursor) - Relay sync polling (bsky.network) with animated status transitions - WebSocket firehose connection with minimal inline CBOR decoder * Add live terminal dashboard for PDS monitoring (#135) * feat(pds): add CLI dashboard command for live PDS monitoring Adds `pds dashboard` command that shows a real-time terminal UI with: - Repository panel (collection counts) - Federation panel (relay sync status) - Firehose panel (subscribers, sequence number) - Notifications panel (likes, reposts, follows via AppView proxy) - Events panel (live firehose events via WebSocket) - Keybindings: [a] activate, [r] request crawl, [e] emit identity, [q] quit Uses ANSI escape codes + picocolors for rendering (no new dependencies). Firehose frames decoded with existing @atproto/lex-cbor decodeAll. https://claude.ai/code/session_01XjTynB9skS7w4f14XUzTrC * style: apply prettier formatting https://claude.ai/code/session_01XjTynB9skS7w4f14XUzTrC * fix(pds): fix events panel separator overflow in CLI dashboard The separator line didn't account for the WebSocket status text width, causing it to overflow and merge with the status indicator. https://claude.ai/code/session_01XjTynB9skS7w4f14XUzTrC * fix(pds): fix dashboard seq display, collection sorting, and separator padding - Fetch latestSeq from authenticated getFirehoseStatus endpoint as primary source, falling back to getSubscribers - Sort collections by priority (posts, likes, follows first) and filter out empty collections - Add missing collection names (chat, postgates, labeler) - Add right padding to events separator so "connected" isn't flush with edge https://claude.ai/code/session_01XjTynB9skS7w4f14XUzTrC * feat(pds): show identity events in dashboard firehose log Parse #identity frames from the firehose in addition to #commit frames. Identity events display with cyan "IDENTITY" action and the handle. https://claude.ai/code/session_01XjTynB9skS7w4f14XUzTrC --------- Co-authored-by: Claude <noreply@anthropic.com> * Remove firehose status from web dash * Changeset --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent dd9ad5e commit 287c971

14 files changed

Lines changed: 1829 additions & 56 deletions

File tree

.changeset/pds-dashboard.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@getcirrus/pds": minor
3+
---
4+
5+
Add live terminal dashboard for PDS monitoring via `pds dashboard`. Shows repository stats, federation sync status, firehose subscribers with IPs, real-time event log, and notifications. Also adds a web dashboard at `/status`.

packages/oauth-provider/test/helpers.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,7 @@ export async function createDpopProof(
8686
* @param alg The algorithm (default: ES256)
8787
* @returns The key pair and public JWK
8888
*/
89-
export async function generateDpopKeyPair(
90-
alg: string = "ES256",
91-
): Promise<{
89+
export async function generateDpopKeyPair(alg: string = "ES256"): Promise<{
9290
privateKey: CryptoKey;
9391
publicKey: CryptoKey;
9492
publicJwk: JsonWebKey;

packages/pds/src/account-do.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -755,9 +755,7 @@ export class AccountDurableObject extends DurableObject<PDSEnv> {
755755

756756
// Lazily iterate SQLite rows — the cursor is already lazy,
757757
// only .toArray() would materialize everything in memory.
758-
const cursor = this.ctx.storage.sql.exec(
759-
"SELECT cid, bytes FROM blocks",
760-
);
758+
const cursor = this.ctx.storage.sql.exec("SELECT cid, bytes FROM blocks");
761759

762760
async function* blocks(): AsyncGenerator<CarBlock> {
763761
for (const row of cursor) {
@@ -1090,10 +1088,11 @@ export class AccountDurableObject extends DurableObject<PDSEnv> {
10901088
// Accept with hibernation
10911089
this.ctx.acceptWebSocket(server);
10921090

1093-
// Store cursor in attachment
1091+
// Store cursor and client metadata in attachment
10941092
server.serializeAttachment({
10951093
cursor: cursor ?? 0,
10961094
connectedAt: Date.now(),
1095+
ip: request.headers.get("CF-Connecting-IP") ?? null,
10971096
});
10981097

10991098
// Backfill if cursor provided
@@ -1345,15 +1344,29 @@ export class AccountDurableObject extends DurableObject<PDSEnv> {
13451344
* RPC method: Firehose status - returns subscriber count and latest sequence
13461345
*/
13471346
async rpcGetFirehoseStatus(): Promise<{
1348-
subscribers: number;
1347+
subscribers: Array<{
1348+
connectedAt: number;
1349+
cursor: number;
1350+
ip: string | null;
1351+
}>;
13491352
latestSeq: number | null;
13501353
}> {
13511354
const sockets = this.ctx.getWebSockets();
13521355
await this.ensureStorageInitialized();
1353-
const storage = await this.getStorage();
1354-
const seq = await storage.getSeq();
1356+
const seq = this.sequencer!.getLatestSeq();
13551357
return {
1356-
subscribers: sockets.length,
1358+
subscribers: sockets.map((ws) => {
1359+
const attachment = ws.deserializeAttachment() as {
1360+
cursor: number;
1361+
connectedAt: number;
1362+
ip: string | null;
1363+
};
1364+
return {
1365+
connectedAt: attachment.connectedAt,
1366+
cursor: attachment.cursor,
1367+
ip: attachment.ip ?? null,
1368+
};
1369+
}),
13571370
latestSeq: seq || null,
13581371
};
13591372
}

0 commit comments

Comments
 (0)