From 1d3f90d844083844ccefe1172c76882c3ba8f26d Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Tue, 16 Jun 2026 21:04:47 -0500 Subject: [PATCH] feat: add repo route helpers and browser routes * expand routing and app roadmap --- TODO.md | 120 ++++- pnpm-workspace.yaml | 4 + src/lib/atproto/repo.svelte.ts | 1 + src/lib/atproto/routes.test.ts | 20 + src/lib/atproto/routes.ts | 12 +- src/lib/components/WelcomeScreen.svelte | 10 +- src/routes/+layout.svelte | 44 +- src/routes/docs/getting-started.md | 2 +- .../+page.svelte => repos/+layout.svelte} | 3 + .../[rkey]/+page.ts => repos/+layout.ts} | 0 src/routes/repos/[did]/+page.svelte | 1 + .../collections/[collection]/+page.svelte | 1 + .../[collection]/[rkey]/+page.svelte | 1 + .../icons/humanity/apps/archive-manager.svg | 61 +++ .../humanity/apps/identity-inspector.svg | 410 ++++++++++++++++++ .../apps/utilities-system-monitor.svg | 65 +++ 16 files changed, 709 insertions(+), 46 deletions(-) create mode 100644 src/lib/atproto/routes.test.ts rename src/routes/{records/[did]/[collection]/[rkey]/+page.svelte => repos/+layout.svelte} (70%) rename src/routes/{records/[did]/[collection]/[rkey]/+page.ts => repos/+layout.ts} (100%) create mode 100644 src/routes/repos/[did]/+page.svelte create mode 100644 src/routes/repos/[did]/collections/[collection]/+page.svelte create mode 100644 src/routes/repos/[did]/collections/[collection]/[rkey]/+page.svelte create mode 100644 static/icons/humanity/apps/archive-manager.svg create mode 100644 static/icons/humanity/apps/identity-inspector.svg create mode 100644 static/icons/humanity/apps/utilities-system-monitor.svg diff --git a/TODO.md b/TODO.md index 9b89ca1..e4f247a 100644 --- a/TODO.md +++ b/TODO.md @@ -6,46 +6,125 @@ no authentication or server. ## Routing -- Done: shareable record routes at `/records/:did/:collection/:rkey`. -- Done: direct record routes boot the desktop shell, select the repo/collection in - Nautilus, and open the record in gedit. -- Done: record routes fetch client-side with public ATProto and use DIDs as the - canonical identity. -- Done: record routes opportunistically hydrate public identity metadata for nicer - labels: handle, display name, avatar, DID, and PDS. - -## Browser - -- Preserve the first-run public handle setup flow for browsing a repo. -- Add handle typeahead to setup using `app.bsky.actor.searchActorsTypeahead`. -- Keep repository collection browsing in Nautilus. -- Keep cursor-based pagination / “Load more records” for large collections. -- Keep public record JSON viewing in gedit. -- Keep local record caching and full-text search across cached records. -- Keep user-readable network and cache errors. +- Keep `/` as the desktop home. +- Keep `/browse` as the configured/default public repo in Nautilus. +- Add canonical repo routes under `/repos/:did`. +- Add collection routes at `/repos/:did/collections/:collection`. +- Add record routes at `/repos/:did/collections/:collection/:rkey`. +- Add app routes that open the matching GNOME-style window: + - `/repos/:did/map` -> Network Map. + - `/repos/:did/identity` -> Identity Inspector. + - `/repos/:did/logs` -> Log Viewer. + - `/repos/:did/blobs` and `/repos/:did/blobs/:cid` -> Eye of GNOME. + - `/live`, `/live/jetstream`, `/live/firehose`, `/live/spacedust` -> System Monitor. + - `/labels` -> Label Browser. + - `/car` -> Archive Manager. + - `/servers/:host` -> Network Servers. +- Route loaders should stay browser-only and public/read-only. +- DIDs remain canonical in URLs; handles are accepted as lookup/input affordances and + resolved client-side. ## Apps Every new surface should feel like a GNOME 2 app that happens to speak `at://`. - **Nautilus** + - Preserve the first-run public handle setup flow for browsing a repo. + - Add handle typeahead to setup using `app.bsky.actor.searchActorsTypeahead`. - Browse public repo collections. - Select collections and records. - Open records in gedit or media blobs in Eye of GNOME. + - Keep cursor-based pagination / “Load more records” for large collections. + - Keep local record caching and full-text search across cached records. + - Keep user-readable network and cache errors. - Surface cached/search states clearly. + - Add collection filtering, reverse order, page-size controls, and preview field + selection. + - Add collection schema tabs and links into System Monitor for the selected repo or + collection. - **gedit** - Read-only JSON viewer for records. - Preserve copy, wrapping, syntax highlighting, and native GTK-style window behavior. + - Add record tabs for JSON, schema, backlinks, and info. + - Show AT URI, CID, raw PDS link, external app links, and read-only verification status. - **Eye of GNOME (eog)** - - Add image/blob viewing. + - List public repo blob CIDs via `com.atproto.sync.listBlobs`. - Open from Nautilus when a record contains embedded images or media blobs. - - Support `app.bsky.feed.post` embedded images as the first target. + - Preview image/video blobs, with `app.bsky.feed.post` embedded images as the first + target. + - Link each blob back to its repo and raw PDS URL when possible. +- **Network Map** + - Visualize an account at the center with app namespaces around it. + - Show app count, record-type count, PDS hosting status, and app/domain validation. + - Let users hide unresolved apps and jump from an app namespace into Nautilus. +- **System Monitor** + - Icon: `/icons/humanity/apps/utilities-system-monitor.svg`. + - Read-only live ATProto monitor for Jetstream, Firehose, and Spacedust-style backlink + streams. + - Filter by DID, collection, cursor, and stream instance where supported. + - Show event rate, total events, top collections/sources, expandable JSON, and copy. +- **Identity Inspector** + - Icon: `/icons/humanity/apps/identity-inspector.svg`. + - Show DID document, aliases, services, verification methods, and rotation keys. + - Validate handles through DNS TXT and `.well-known` when possible. + - Link out to PDS endpoints and copy identity fields. +- **Log Viewer** + - Show PLC audit history for `did:plc` identities. + - Present alias, service, verification method, and rotation-key diffs. + - Include event filters, validation status, and hash links to individual log entries. +- **Archive Manager** + - Icon: `/icons/humanity/apps/archive-manager.svg`. + - Open local CAR files without requiring a live PDS connection. + - Browse archive collections and records in the same read-only style as Nautilus/gedit. + - Export archive contents to a ZIP of JSON files. +- **Label Browser** + - Query public labeler services with URI patterns. + - Show label value, target, CID, negation, created/expiry timestamps, and pagination. + - Add client-side label filtering with wildcard include and `-exclude` support. +- **Network Servers** + - Inspect public PDS host info, version, available domains, and repo list. + - Link repos into Nautilus and open server firehose views in System Monitor. - **About This Computer** - Show OS/theme info and current public repo identity metadata when available. - **Desktop shell** - Preserve brown panels, tan chrome, Humanity icons, small dense spacing, task buttons, tray affordances, and movable/resizable windows. +## Priority Order + +- [x] Add canonical `/repos/:did/...` routing. +- [ ] Deepen Nautilus and gedit with collection controls plus record JSON/schema/info tabs. +- [ ] Add Identity Inspector. +- [ ] Add System Monitor with Jetstream first. +- [ ] Add Network Map. +- [ ] Add Archive Manager. +- [ ] Add Eye of GNOME blob browsing and previews. +- [ ] Add Label Browser. +- [ ] Add Log Viewer. +- [ ] Add Network Servers. + +## Tests + +- Vitest unit tests for pure helpers and data behavior: + - ATProto route helpers. + - identity normalization and DID/PDS resolution helpers. + - repo record summarization and collection grouping. + - pagination behavior. + - local DB migrations, cache repositories, and search. +- Browser/component tests for desktop UI behavior: + - first-run setup and handle typeahead states. + - Nautilus collection selection, search, pagination, and record opening. + - gedit JSON display, copy behavior, and record metadata tabs. + - window manager open, focus, minimize, maximize, and route-driven gedit opening. +- Playwright E2E/smoke tests for critical flows: + - `/` boots the desktop. + - `/browse` opens the configured repo after setup. + - `/repos/:did` hydrates identity and opens Nautilus. + - `/repos/:did/collections/:collection` selects the collection. + - `/repos/:did/collections/:collection/:rkey` opens Nautilus and gedit around the record. + - Mock external ATProto boundaries in tests unless the test is explicitly an integration + smoke test. + ## V2 - **gnome-terminal** @@ -58,9 +137,6 @@ Every new surface should feel like a GNOME 2 app that happens to speak `at://`. - **Contacts** - Follows / followers graph browser built on `app.bsky.graph.follow`; show avatar, handle, and mutual-follow status in an address-book layout. -- **File Roller** - - Export any collection to a `.json` archive or CAR file for local backup. - - Import archives to preview records without a live PDS connection. - Local starred/bookmarked records surfaced as a “Bookmarks” sidebar item in Nautilus. - Sync indicator in the panel showing last-synced time per repo/collection. - Multi-repo switching from the panel without implying authentication. diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5ba62b2..d4d28e2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,7 @@ +allowBuilds: + esbuild: true + sharp: true + workerd: true onlyBuiltDependencies: - esbuild - sharp diff --git a/src/lib/atproto/repo.svelte.ts b/src/lib/atproto/repo.svelte.ts index 2537930..e992087 100644 --- a/src/lib/atproto/repo.svelte.ts +++ b/src/lib/atproto/repo.svelte.ts @@ -63,6 +63,7 @@ class RepoBrowserState { async selectCollection(identity: AccountIdentity, collectionName: string) { this.selectedCollection = collectionName; + this.selectedRecord = null; this.searchQuery = ''; this.isLoadingRecords = true; this.canLoadMoreRecords = false; diff --git a/src/lib/atproto/routes.test.ts b/src/lib/atproto/routes.test.ts new file mode 100644 index 0000000..de201e7 --- /dev/null +++ b/src/lib/atproto/routes.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; +import { collectionPath, recordPath, repoPath } from './routes'; + +describe('ATProto route helpers', () => { + it('builds canonical repo routes', () => { + expect(repoPath('did:plc:abc123')).toBe('/repos/did:plc:abc123'); + }); + + it('builds canonical collection routes', () => { + expect(collectionPath({ did: 'did:plc:abc123', collection: 'app.bsky.feed.post' })).toBe( + '/repos/did:plc:abc123/collections/app.bsky.feed.post' + ); + }); + + it('encodes record keys in canonical record routes', () => { + expect(recordPath({ did: 'did:plc:abc123', collection: 'app.bsky.feed.post', rkey: 'post/key' })).toBe( + '/repos/did:plc:abc123/collections/app.bsky.feed.post/post%2Fkey' + ); + }); +}); diff --git a/src/lib/atproto/routes.ts b/src/lib/atproto/routes.ts index 8ed735e..1814f22 100644 --- a/src/lib/atproto/routes.ts +++ b/src/lib/atproto/routes.ts @@ -1,5 +1,15 @@ export type RecordRouteParams = { did: string; collection: string; rkey: string }; +export type CollectionRouteParams = Pick; + +export function repoPath(did: string) { + return `/repos/${did}`; +} + +export function collectionPath({ did, collection }: CollectionRouteParams) { + return `${repoPath(did)}/collections/${collection}`; +} + export function recordPath({ did, collection, rkey }: RecordRouteParams) { - return `/records/${did}/${collection}/${encodeURIComponent(rkey)}`; + return `${collectionPath({ did, collection })}/${encodeURIComponent(rkey)}`; } diff --git a/src/lib/components/WelcomeScreen.svelte b/src/lib/components/WelcomeScreen.svelte index a01d800..68c6d4c 100644 --- a/src/lib/components/WelcomeScreen.svelte +++ b/src/lib/components/WelcomeScreen.svelte @@ -84,12 +84,12 @@
  1. Choose a public ATProto repo by handle.
  2. Open Nautilus to browse collections and records.
  3. -
  4. Open records in gedit, then share stable DID-based URLs.
  5. +
  6. Open records in gedit to view or share them.
-

- Direct links look like - /records/:did/:collection/:rkey and reopen the desktop around that record. -

+

You can direct link a record with the following structure:

+
+				/repos/:did/collections/:collection/:rkey
+			
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index cd7f88d..725c55c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -30,7 +30,7 @@ let bootStep = $state('Loading database'); let bootError = $state(null); let cacheDisabled = $state(false); - let handledRecordRoute = $state(null); + let handledRepoRoute = $state(null); const routeRequiresSetup = $derived(page.route.id === '/browse' && !accountSetup.isConfigured); const routeUsesNativeWindow = $derived(page.route.id === '/' || page.route.id?.startsWith('/docs')); @@ -41,17 +41,18 @@ if (page.route.id?.startsWith('/docs')) return 'Document Viewer'; return 'AT Protocol Collections - Intrepid Ibex'; }); + const windowIcon = $derived.by(() => { if (routeRequiresSetup) return '/icons/humanity/places/user-home.svg'; if (page.route.id === '/') return '/icons/humanity/devices/computer.svg'; if (page.route.id?.startsWith('/docs')) return '/icons/humanity/mimes/gnome-mime-application-pdf.svg'; return '/icons/humanity/apps/internet-feed-reader.svg'; }); + const mainWindow = $derived(windowManager.getWindow('main')); const aboutWindow = $derived(windowManager.getWindow('about-computer')); const geditWindow = $derived(windowManager.getWindow('gedit')); const documentViewerWindow = $derived(windowManager.getWindow('document-viewer')); - const shortcuts = $derived([ { label: 'ibex Home', @@ -65,7 +66,7 @@ { label: 'Collections', icon: '/icons/humanity/places/folder.svg', - selected: page.route.id === '/browse' || page.route.id?.startsWith('/records'), + selected: page.route.id === '/browse' || page.route.id?.startsWith('/repos'), onactivate: () => { windowManager.restore('main'); void goto(resolve('/browse')); @@ -111,14 +112,14 @@ }); $effect(() => { - const route = recordRouteFromParams(); + const route = repoRouteFromParams(); if (bootStatus !== 'ready' || !route) return; - const routeKey = `${route.did}/${route.collection}/${route.rkey}`; - if (handledRecordRoute === routeKey) return; + const routeKey = [route.did, route.collection, route.rkey].filter(Boolean).join('/'); + if (handledRepoRoute === routeKey) return; - handledRecordRoute = routeKey; - void openRecordRoute(route); + handledRepoRoute = routeKey; + void openRepoRoute(route); }); onMount(() => { @@ -176,28 +177,37 @@ accountSetup.load(); } - async function openRecordRoute(route: { did: string; collection: string; rkey: string }) { + async function openRepoRoute(route: { did: string; collection?: string; rkey?: string }) { try { const { hydratePublicIdentity } = await import('$lib/atproto/identity'); const identity = await hydratePublicIdentity(route.did); - await repoBrowser.openRecordRoute(identity, route.collection, route.rkey); accountSetup.save(identity); - windowManager.restore('main'); - if (repoBrowser.selectedRecord) { - windowManager.open('gedit'); + + if (route.collection && route.rkey) { + await repoBrowser.openRecordRoute(identity, route.collection, route.rkey); + if (repoBrowser.selectedRecord) { + windowManager.open('gedit'); + } + return; + } + + await repoBrowser.load(identity); + + if (route.collection) { + await repoBrowser.selectCollection(identity, route.collection); } } catch (unknownError) { - repoBrowser.error = errorMessage(unknownError, 'Could not open that record route.'); + repoBrowser.error = errorMessage(unknownError, 'Could not open that repository route.'); } } - function recordRouteFromParams() { - if (page.route.id !== '/records/[did]/[collection]/[rkey]') return null; + function repoRouteFromParams() { + if (!page.route.id?.startsWith('/repos/[did]')) return null; const { did, collection, rkey } = page.params; - if (!did || !collection || !rkey) return null; + if (!did) return null; return { did, collection, rkey }; } diff --git a/src/routes/docs/getting-started.md b/src/routes/docs/getting-started.md index 28604d6..83ca541 100644 --- a/src/routes/docs/getting-started.md +++ b/src/routes/docs/getting-started.md @@ -16,7 +16,7 @@ records, then open any record in gedit for a formatted JSON view. Record windows use stable DID-based URLs: ```txt -/records/:did/:collection/:rkey +/repos/:did/collections/:collection/:rkey ``` Opening one of those links boots the desktop, selects the collection in Nautilus, and diff --git a/src/routes/records/[did]/[collection]/[rkey]/+page.svelte b/src/routes/repos/+layout.svelte similarity index 70% rename from src/routes/records/[did]/[collection]/[rkey]/+page.svelte rename to src/routes/repos/+layout.svelte index 6090d40..39dded1 100644 --- a/src/routes/records/[did]/[collection]/[rkey]/+page.svelte +++ b/src/routes/repos/+layout.svelte @@ -1,5 +1,8 @@ +{@render children()} diff --git a/src/routes/records/[did]/[collection]/[rkey]/+page.ts b/src/routes/repos/+layout.ts similarity index 100% rename from src/routes/records/[did]/[collection]/[rkey]/+page.ts rename to src/routes/repos/+layout.ts diff --git a/src/routes/repos/[did]/+page.svelte b/src/routes/repos/[did]/+page.svelte new file mode 100644 index 0000000..5bf03f4 --- /dev/null +++ b/src/routes/repos/[did]/+page.svelte @@ -0,0 +1 @@ + diff --git a/src/routes/repos/[did]/collections/[collection]/+page.svelte b/src/routes/repos/[did]/collections/[collection]/+page.svelte new file mode 100644 index 0000000..5bf03f4 --- /dev/null +++ b/src/routes/repos/[did]/collections/[collection]/+page.svelte @@ -0,0 +1 @@ + diff --git a/src/routes/repos/[did]/collections/[collection]/[rkey]/+page.svelte b/src/routes/repos/[did]/collections/[collection]/[rkey]/+page.svelte new file mode 100644 index 0000000..5bf03f4 --- /dev/null +++ b/src/routes/repos/[did]/collections/[collection]/[rkey]/+page.svelte @@ -0,0 +1 @@ + diff --git a/static/icons/humanity/apps/archive-manager.svg b/static/icons/humanity/apps/archive-manager.svg new file mode 100644 index 0000000..65f58ca --- /dev/null +++ b/static/icons/humanity/apps/archive-manager.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/humanity/apps/identity-inspector.svg b/static/icons/humanity/apps/identity-inspector.svg new file mode 100644 index 0000000..37df949 --- /dev/null +++ b/static/icons/humanity/apps/identity-inspector.svg @@ -0,0 +1,410 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/humanity/apps/utilities-system-monitor.svg b/static/icons/humanity/apps/utilities-system-monitor.svg new file mode 100644 index 0000000..95ef90c --- /dev/null +++ b/static/icons/humanity/apps/utilities-system-monitor.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -- 2.51.2