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/newconfirmation — the "All notebooks" button → here/nisaba/notebook/newform — the "Back" button →/nisaba/notebooks- Direct URL / bookmark — yes
(
alternates.canonical = '/nisaba/notebooks'); signed-in - Shell nav — arrives under the
librarytab (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): leftNisaba · 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 aLinkcard) — or the empty-state card - Footer button row: "← Back to Nisaba", "Scholarly read", "Start a
notebook" (
data-nisaba-notebooks-actions)
States#
- Loading —
page.tsxis async; no adjacentloading.tsx, so the route blocks server-side onbffGet(5 s timeout) - Populated (BFF) —
/v1/nisaba/notebooksreturnsnotebooks[]; each renders as an "Open →" card with passage count, last-edited date, and notebook status. Verified against a real BFF-created notebook inapps/oshun/web/e2e/nisaba-notebooks-index.spec.ts. - Populated (fixture fallback) — when
bffGetreturnsnullor a non-arraynotebooks, the page showsNOTEBOOK_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: anullBFF 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) —
bffGetswallows 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.tsxadjacent; 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
/welcomebefore 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;idis normalized from BFFnotebookIdfirst, then fixtureid - 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 autogrid 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:eventdispatch on activation
- Function: →
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)
Footer buttons#
- "← Back to Nisaba" (
LBtnghost) →/nisaba - "Scholarly read" (
LBtnghost) →/nisaba/scholar - "Start a notebook" (
LBtnprimary) →/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 fromnotebookId/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 normalizesnotebookId ?? idinto the href id,updatedAt ?? updatedAtIsointo the last-edited date, andstatus ?? visibility ?? 'active'into the meta label. LocalNOTEBOOK_FIXTUREis 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:
bffGetnext: { revalidate: 60 }(60 s ISR) - Auth/role check: middleware signed-in;
bffGetforwards theoshun-accesscookie asAuthorization: Bearerand session cookies ascookie; refusal → fixture for layout review - Privacy: status or fixture visibility shown per row;
metadatahere does NOT setrobots: noindex(unlike/nisaba/notebook) — verify whether the index should be search-excluded (see gaps)
Cross-references#
- Feature spec:
V1/features.md#nisaba - Architecture:
V1/ARCHITECTURE.md#nisaba - Sibling Nisaba routes:
nisaba.md,nisaba-notebook.md,nisaba-notebook-new.md,nisaba-scholar.md - Journeys:
journeys/nisaba-notebook-capture-and-cite.md(this index → open/create a notebook) - Component / data sources:
apps/oshun/web/src/lib/server/bff-fetch.ts(bffGet)
Open questions / known gaps#
- The fixture fallback fires on any
bffGetfailure (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: noindexon 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:eventdispatch on static card/footer activation