V1 Web PWA · Surface walkthrough

Library — Collection deep link

A per-surface walkthrough of the V1 Web PWA customer surface: layout, states, interactions, data, and cross-references.

walked
9sections4 minread

On this page

Context. surface customer · domain discovery · route /library/collection/[collectionId] · auth signed-in · source apps/oshun/web/src/app/library/collection/[collectionId]/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; content re-verified 2026-06-03 against current source; focused direct-route state addendum 2026-06-25 covers found/missing/custom/corrupt-storage collection states, query seeding, edit persistence, and add/remove membership with real BFF-seeded Playwright coverage

Purpose#

A bookmarkable deep link into one Library collection. Renders the same LibraryDashboard as /library, but pre-selects the collection identified by the URL segment and pre-filters by domain / kind if those query parameters are present.

Entry points#

  • Library collection card/library collection cards push /library/collection/<id> via buildLibraryCollectionHref and router.push
  • "Create collection from filters"handleCreateCollectionFromFilters produces a collection and navigates here
  • Direct URL / bookmark — yes; collectionId is decoded via decodeURIComponent
  • Query overrides?domain=<all|tara|veritas|nyx|arete|nisaba|metis> and ?kind=<libraryKindFilter> seed initialDomainFilter and initialKindFilter
  • Shell middleware — auth required; this view does not declare its own surface for shell-state preview (it does not call resolveShellRouteSurfaceContent)

Layout regions#

page.tsx mounts ShellLayout with active="library" (no breadcrumbs prop) and renders only <LibraryDashboard ... /> configured with initialDomainFilter, initialKindFilter, and initialCollectionId = decodeURIComponent(params.collectionId). There is no sr-only <h1> on this page — headings are provided by LibraryDashboard.

All visible layout regions are owned by LibraryDashboard. See library.md — Layout regions — for the full surface map.

Key collection-mode differences (read from LibraryDashboard.tsx):

  • Frame title and summary are sourced from selectedCollection.label and selectedCollection.description instead of the filter-derived strings
  • Followup copy on the collection card uses selectedCollection.followUp
  • Editor state (collectionEditor) is initialized from the resolved collection on mount

States#

  • Loading (pre-hydration)hasMounted === false; only the ShellRouteAnchor placeholder ("Loading saved continuity, collections, and the next best return point.") renders
  • Collection foundresolveInitialCollectionId(collections, initialCollectionId) matches; collection editor + items populate
  • Collection missingresolveInitialCollectionId returns null (unknown id); selectedCollectionId falls back to null and the view degrades to the default /library dashboard (same fallback logic as navigating with no filter); URL still reads /library/collection/<id>
  • Custom (user-created) collection — hydrated from localStorage['oshun.library.collections.v1']
  • Query-driven filter pre-population?domain= / ?kind= are converted via isDomainFilter and normalizeLibraryKindFilter; invalid values default to 'all'
  • Edits persist — label / description / followUp edits write back to localStorage (via the same effect chain as /library)
  • Empty collection — the collection has no items; "Create collection from filters" remains the primary repair path

Interactions#

All interactions match library.md — Interactions. Diffs specific to the collection-mode entry:

  • Back-to-all-saved navigation — clearing the selected collection in the dashboard calls navigateToCollection(null)router.push(WEB_SHELL_ROUTE_PATHS.library) (route returns to /library)
  • Selecting a different collection from the lanenavigateToCollection (collectionId) calls router.push(buildLibraryCollectionHref(...)); URL updates to /library/collection/<newId>
  • Collection editor inputs (label / description / followUp) — drive collectionEditor and propagate via syncCollection
  • Remove item from collectionsetCollectionItemIds mutates the collection's item-id list; metrics recompute via withCollectionMetrics

E2E coverage#

  • Backed by apps/oshun/web/e2e/library-collection-management.spec.ts — real authenticated shell + BFF-seeded saved items drive the collection-found route, unknown collection fallback with query-seeded filters, user-created collection hydration from localStorage with invalid item-id sanitization, corrupt collection-storage fallback to defaults, collection editor persistence across reload, create-from-filter navigation, add/remove membership count recomputation, archive, and desktop return-lane layout.
  • Backed by apps/oshun/web/src/components/library/__tests__/LibraryDashboard.test.tsx — component coverage asserts the same collection-id resolution, custom storage hydration/sanitization, corrupted storage fallback, edit persistence, and membership toggling without network.

Data & contracts#

  • Reads (same as /library):
    • useOshunWebLibraryStore() — saved items
    • localStorage['oshun.library.collections.v1'] — stored collection payload
  • Route params:
    • params.collectionId: string — URL segment, decodeURIComponentd before use
  • Query params:
    • searchParams.domainLibraryDomainFilter ('all' if invalid)
    • searchParams.kindLibraryKindFilter ('all' if invalid)
  • Writes: same as /library — local store + localStorage; no BFF writes observed
  • Realtime: None.
  • Caching: client store + localStorage
  • Auth/role check: shell middleware (no in-page guard)

Cross-references#

Open questions / known gaps#

  • Document the missing-collection behavior — page.tsx does not 404; the dashboard keeps the unknown collection URL, clears selected collection state, preserves valid query-seeded filters, and renders the default library dashboard without collection management.
  • Confirm whether ?domain= and ?kind= are stable in the URL after the user changes filters from inside the dashboard (filters mutate state but this page.tsx does not write them back to the URL)
  • Document collection sharing semantics — the deep link is bookmarkable but uses localStorage-derived state, so a shared link will show the target user's own collections, not the sender's