Context. surface customer · domain discovery · route /library/collections · auth signed-in · source apps/oshun/web/src/app/library/collections/page.tsx
Last walked. 2026-05-29 automated runtime walk (Playwright headless) — render, /v1 data (2xx), console/page-errors, expected content, screenshot verified; live screen-reader, touch, offline, and telemetry-delivery checks pending a manual AT pass. Evidence: WALKTHROUGH/results/runtime-sweep-2026-05-29.md
Purpose#
A flat list of every named collection the user has made — themed gatherings of
saved items (passages, sits, observations, lessons). Each row links into the
library filtered to that collection; the foot CTA opens the new-collection form.
Backed by GET /v1/library/collections.
Entry points#
- Shell nav: Library tab —
LCustomerNav active="library" - From
/library— the collections lane / "Create from filters" affordance inLibraryDashboardleads here (seelibrary.md) ← Back to Libraryghost button at page foot returns to/library- After creating a collection — the success card in
library-collections-newoffers "All collections" → this page - Direct URL / bookmark — yes;
canonical = '/library/collections'; auth via shell middleware
Layout regions#
page.tsx is an async server component:
await bffGet<CollectionsResponse>( '/v1/library/collections') (falling back to
{ collections: [] } on null) →
<LWebShell top={<LCustomerNav active="library" />}> wrapping a centered column
(maxWidth: 880).
- Header:
LCustomerNav(Library active) - Masthead:
LMasthead— left "Library · collections", right${data.collections.length} held, kicker "Gatherings", title "The collections." - "How this opens" note:
LEyebrow+ serif-italic copy ("Collections are themed gatherings of saved items. Drag and drop, reorder, share or keep private — they live in your library, never indexed.") - Main: a grid (
display: grid; gap: 12) of collection rows OR the empty card - Footer action row:
LBtnghost "← Back to Library" (href/library) andLBtnprimary "Start a collection" (href/library/collections/new)
States#
- Loading — server-side
await bffGet; blocks render (no skeleton observed — verifyloading.tsx) - Empty —
data.collections.length === 0; dashed-border card "No collections yet. Start one to gather your first few saved items." Masthead right reads "0 held" - Populated (short) — 1–5 rows; each is a
Linkshowing item count (with singular/plural "item"/"items"), optional "· updated", the label, and a mono "Open →" affordance - Populated (long) — 50+ rows in a single grid; no pagination in code (verify behavior with a large roster)
- BFF unreachable / non-2xx —
bffGetreturnsnull; page substitutes{ generatedAt, collections: [] }→ renders the empty state (no fabricated collections, unlike the events fixture) -
lastUpdatedAtIsoabsent — row omits the "· updated …" suffix - Offline — RSC fetch (
revalidate 60); SW-cached HTML serves last render; rows are plain links - Standalone PWA — renders cleanly; safe-area via shell (verify)
Interactions#
Masthead#
- Masthead labels — non-interactive
Collection row (per col in data.collections)#
- Collection row (
Link)- Function: navigates to
/library?collectionId=<encodeURIComponent(col.id)>— opens the Library dashboard pre-filtered to this collection - Keyboard: Enter activates; tab order top-to-bottom
- Screen reader: announces the item-count line + label + "Open →" (the whole card is one link; verify the count line reads naturally)
- Touch target: full card row; verify ≥ 44 px height on coarse pointers
- Mobile (≤ 640 px): two-column grid (
1fr auto) — confirm the "Open →" chip doesn't crowd the label - Telemetry: none observed
- Sub-elements (all non-interactive within the link): item-count + updated line, label, "Open →" accent text
- Function: navigates to
Footer actions#
- "← Back to Library" (
LBtnghost, link) →/library - "Start a collection" (
LBtnprimary, link) →/library/collections/new
Data & contracts#
- Reads:
bffGet<CollectionsResponse>('/v1/library/collections')—{ generatedAt: string; collections: Collection[] }whereCollection = { id, label, itemCount, lastUpdatedAtIso? }. Onnull(network failure / non-2xx) substitutes an empty list - Writes: None. (creation happens on
/library/collections/new) - Realtime: None.
- Caching: RSC fetch,
next.revalidate = 60; 5 s abort; SW-cached HTML - Auth/role check: shell middleware;
bffGetforwards session cookie - Note: this index reads the BFF, whereas
/libraryreads a client store + localStorage — confirm the two sources agree on collection membership
Cross-references#
- Feature spec:
V1/features.md - Architecture:
V1/ARCHITECTURE.md - Sibling routes:
library.md— the library dashboard (collection target)library-collections-new.md— create formlibrary-collection-collectionId.md— single-collection entry
- Component sources:
apps/oshun/web/src/lib/server/bff-fetch.ts(bffGet)
- Journeys traversing this view:
Open questions / known gaps#
- Reconcile the data source: this page reads
GET /v1/library/collections(BFF) while/libraryreadslocalStorage['oshun.library.collections.v1']— document which is authoritative and how they sync - Collection rows link to
/library?collectionId=butlibrary.mddocuments/library/collection/<id>— verify which collection-open route is canonical - No share / delete / reorder affordance on this index despite the "share or keep private … drag and drop, reorder" copy; confirm those live on the detail view
- Confirm whether a
loading.tsxskeleton exists for this segment