V1 Web PWA · Surface walkthrough

Nisaba · notebooks (index)

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

walked
9sections6 minread

On this page

Context. surface customer · domain nisaba · route /nisaba/notebooks · auth signed-in · source apps/oshun/web/src/app/nisaba/notebooks/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. 2026-06-29 notebooks-index direct-route addendum — Playwright real-dev-infra coverage now seeds real BFF notebooks, proves the explicit BFF-empty state, forces the authenticated fixture fallback when the BFF refuses, verifies mobile and standalone containment, replays the cached document offline through the real service worker, asserts no telemetry hook attributes or dispatched analytics events on card/footer activations, and confirms anonymous redirect-before-render. Evidence: WALKTHROUGH/results/v1-real-infra-run-2026-06-22.md §217; spec: apps/oshun/web/e2e/nisaba-notebooks-index.spec.ts.

Purpose#

The reading desk: every notebook the caller owns, each a thread of passages and annotations. The page reads /v1/nisaba/notebooks and, when the authenticated endpoint refuses (so the surface still renders for layout review), falls back to a small fixture set. Open one to read or compose, or start a new one. See V1/features.md#nisaba.

Entry points#

  • Nisaba hub (/nisaba) — verify a "Notebooks" link from the hub sub-nav
  • /nisaba/notebook/new confirmation — the "All notebooks" button → here
  • /nisaba/notebook/new form — the "Back" button → /nisaba/notebooks
  • Direct URL / bookmark — yes (alternates.canonical = '/nisaba/notebooks'); signed-in
  • Shell nav — arrives under the library tab (LCustomerNav active="library")

Layout regions#

page.tsx is async: await bffGet<NotebooksResponse>('/v1/nisaba/notebooks'), then a remote array is normalized from the BFF's notebookId / updatedAt / status contract into the route's card view model; a refused or malformed BFF response falls back to NOTEBOOK_FIXTURE. Centered column, maxWidth: 880, data-responsive-page, data-nisaba-notebooks-page.

  • Top chrome: LCustomerNav (Library active) with "Lilith" wordmark + "⌕ Search"
  • Masthead (LMasthead): left Nisaba · notebooks, right ${notebooks.length} held, kicker "The reading desk", title "The notebooks."
  • "How this opens" note: "Notebooks are the long-form companions to your reading. Each one threads passages from primary texts with your own annotations."
  • Main: data-nisaba-notebooks-list, a grid (gap: 12) of notebook rows (each a Link card) — or the empty-state card
  • Footer button row: "← Back to Nisaba", "Scholarly read", "Start a notebook" (data-nisaba-notebooks-actions)

States#

  • Loadingpage.tsx is async; no adjacent loading.tsx, so the route blocks server-side on bffGet (5 s timeout)
  • Populated (BFF)/v1/nisaba/notebooks returns notebooks[]; each renders as an "Open →" card with passage count, last-edited date, and notebook status. Verified against a real BFF-created notebook in apps/oshun/web/e2e/nisaba-notebooks-index.spec.ts.
  • Populated (fixture fallback) — when bffGet returns null or a non-array notebooks, the page shows NOTEBOOK_FIXTURE: "On the citadel" (18 passages, private, 1 day ago), "The night watch" (6 passages, private, 9 days ago), "Quiet hour" (11 passages, unlisted, 32 days ago); masthead reads "3 held". Verified by signing in without an access-token cookie so the BFF read returns 401 before the route falls back.
  • Empty — only reachable when the BFF returns { notebooks: [] } (an explicit empty array): renders a dashed-border card "No notebooks yet. Start one to thread your first passage." Note: a null BFF response does NOT reach empty — it falls back to the fixture instead. Verified against a fresh real BFF user with zero notebooks.
  • Populated (long) — 50+ notebooks render as a flat vertical grid; no pagination or virtualization in code (verify scroll perf — see gaps)
  • Error (recoverable BFF refusal)bffGet swallows non-2xx → null → fixture fallback; no retry surfaced (and the fixture masks the error). Verified for the signed-in/no-bearer 401 path.
  • Error (unrecoverable) — no error.tsx adjacent; unexpected throw bubbles to a higher boundary (verify)
  • Offline — served from the real SW cache where present; the cached signed-in document replays the same BFF-backed card and does not show the generic offline fallback
  • Gated — middleware signed-in; anonymous users redirect to /welcome before the notebooks page renders. If the authenticated BFF refuses, the fixture renders for layout (no information leak — fixture data is generic)
  • Standalone PWA — renders cleanly at 390 px with standalone display-mode mocked and no horizontal overflow

Interactions#

Notebook card#

  • Notebook card (Link, one per notebook, data-nisaba-notebook-card="<id>")
    • Function: → /nisaba/notebook?id=<encodeURIComponent(id)> to open in the reading desk; id is normalized from BFF notebookId first, then fixture id
    • Content: mono meta line " passage(s) · last edited · " (singular/plural handled), serif title, accent "Open →" marker
    • Screen reader: announces the meta line + title + "Open"
    • Touch target: full-card click area (16–18 px padding) — verified ≥ 44 px height at 390 px
    • Mobile (≤ 640 px): 1fr auto grid keeps title and "Open →" aligned; no horizontal overflow at 390 px in the focused spec
    • Telemetry: explicitly absent; no hook attributes and no oshun-analytics:event dispatch on activation

Empty-state card#

  • "No notebooks yet…" card (data-nisaba-notebooks-empty) — non-interactive text only; verify whether it should link to /nisaba/notebook/new (currently plain copy — see gaps)
  • "← Back to Nisaba" (LBtn ghost) → /nisaba
  • "Scholarly read" (LBtn ghost) → /nisaba/scholar
  • "Start a notebook" (LBtn primary) → /nisaba/notebook/new

All footer buttons are verified as ≥ 44 px tall at 390 px and explicitly telemetry-silent: no hook attributes and no oshun-analytics:event dispatch when click-guarded.

E2E coverage#

  • apps/oshun/web/e2e/nisaba-notebooks-index.spec.ts — real BFF create/list readback into the server-rendered index, explicit BFF-empty state, authenticated fixture fallback on BFF refusal, card href/meta normalization from notebookId / updatedAt / status, mobile 44 px activation targets and no horizontal overflow, standalone display-mode launch, real service-worker cached-document offline replay, explicit absence of card and footer telemetry hooks/click-dispatched analytics events, and anonymous redirect-before-render.

Data & contracts#

  • Reads: bffGet<NotebooksResponse>('/v1/nisaba/notebooks'). Live BFF notebooks are serialized as { notebookId, title, summary, tags, status, itemCount, passageCount, annotationCount, evidenceViewCount, compareItemCount, updatedAt, launch }. The page normalizes notebookId ?? id into the href id, updatedAt ?? updatedAtIso into the last-edited date, and status ?? visibility ?? 'active' into the meta label. Local NOTEBOOK_FIXTURE is the fallback and still uses generic private/unlisted labels for layout review.
  • Writes: none on this view (creation happens at /nisaba/notebook/new)
  • Realtime: none
  • Caching: bffGet next: { revalidate: 60 } (60 s ISR)
  • Auth/role check: middleware signed-in; bffGet forwards the oshun-access cookie as Authorization: Bearer and session cookies as cookie; refusal → fixture for layout review
  • Privacy: status or fixture visibility shown per row; metadata here does NOT set robots: noindex (unlike /nisaba/notebook) — verify whether the index should be search-excluded (see gaps)

Cross-references#

Open questions / known gaps#

  • The fixture fallback fires on any bffGet failure (including a real outage), so an authenticated user could be shown three sample notebooks that are not theirs — decide whether a refusal should render empty/error rather than generic fixtures
  • The empty state is only reachable via an explicit { notebooks: [] }; the focused real-BFF spec confirms a fresh user with zero notebooks returns an empty array and renders the dashed empty-state card
  • Empty-state copy is not a link to /nisaba/notebook/new — verify the productive next-action affordance
  • No pagination/virtualization for long lists — verify acceptable for V1 notebook counts
  • No robots: noindex on the notebooks index (the individual notebook sets it); confirm whether the listing of titles/visibilities should be search-excluded
  • No telemetry on card opens or footer buttons — current V1 behavior is explicitly no hook attributes and no oshun-analytics:event dispatch on static card/footer activation