Context. surface customer · domain atelier · route /atelier/camera-obscura · auth signed-in · source apps/oshun/web/src/app/atelier/camera-obscura/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#
The camera-obscura room's job queue — the render pipeline that composites
footage with a light box (composite / denoise / stabilise). The page reads the
whole atelier job feed and shows only the jobs whose
room === 'camera-obscura', each with its run state, start time, and ETA.
Unlike the static atelier-image.md specimen view, this
is a live, data-backed list server-rendered from /v1/atelier/jobs.
Entry points#
- Atelier index (
/atelier) — verify a room/queue card links here /atelier/newsuccess card — after a scene is created, the composer/queue is where work surfaces (verify the queue link is reachable from the flow)- "← Back to Atelier" within this page returns to
/atelier - "Start a new scene" within this page goes to
/atelier/new - Direct URL / bookmark — yes (signed-in)
Layout regions#
page.tsx is an async server component: await bffGet('/v1/atelier/jobs') →
filter to camera-obscura → render inside LWebShell with
LCustomerNav active="library". Max width 880, centered.
- Header: Lilith customer nav (Library tab active)
- Masthead (
LMasthead): left "Atelier · camera obscura", right job count ("N job"/"N jobs"), kicker "The render pipeline", title "Camera obscura." - Job list (grid, gap 12):
- Empty row (dashed border, italic serif): "No camera-obscura jobs in flight." when the filtered list is empty
- Job card per job (
1fr autogrid): left side has a mono uppercase status line —<state> · started <localeTime> · eta <N>m | no eta(color perSTATE_COLOUR: queued=muted, running=accent, done=green#1f4f3a, failed=red#a30f0f) — and the serifjob.label; right side shows the monojob.id. Running jobs get an accent border, others a rule border.
- Action row (flex, wraps): ghost "← Back to Atelier" (→
/atelier), primary "Start a new scene" (→/atelier/new)
States#
- Loading — server component awaits
bffGet; no client skeleton. Verify whether aloading.tsxexists for this segment - Empty —
bffGetreturns null/no camera-obscura jobs → "No camera-obscura jobs in flight." dashed row; masthead reads "0 jobs" - Populated (short) — 1–5 camera-obscura jobs render as cards
- Populated (long) — 50+ jobs; list is a plain grid (no pagination/virtualization in code) — verify scroll behaviour
- Job running — accent border + accent status word
- Job done — green status word
done - Job failed — red status word
failed; verify there is no retry/detail affordance (none coded) - Error (recoverable) —
bffGetreturnsnullon non-2xx/network fail (helper swallows errors), so this collapses into the Empty state — no distinct error UI; verify whether that is the intended behaviour - Offline —
bffGetaborts after 5s and returns null → Empty state; SW-cached HTML may show last render — verify - Standalone PWA —
data-responsive-pagewrapper; verify safe-area and narrow-width rendering of the1fr autojob cards - Gated — N/A in code beyond signed-in (no role gate on
/atelier/*)
Interactions#
The job cards themselves are non-interactive (no link/handler in code) — the only interactive elements are the two footer buttons.
- Job card (
<div>) — non-interactive display only- Function: shows state, start time, ETA, label, id; no click target
- Screen reader: verify the mono status line + label read coherently (no
explicit list semantics — cards are sibling
<div>s, not a<ul>/<ol>) - Telemetry: none coded
- "← Back to Atelier" (
LBtn kind="ghost",href="/atelier")- Function: navigates to
/atelier - Keyboard: Enter activates; in tab order
- Touch target: verify
LBtn size="md"meets ≥ 44×44 px
- Function: navigates to
- "Start a new scene" (
LBtn kind="primary",href="/atelier/new")- Function: navigates to
/atelier/new - Keyboard: Enter activates
- Mobile (≤ 640 px): action row
flexWrap: 'wrap'allows buttons to stack
- Function: navigates to
Data & contracts#
- Reads:
bffGet<JobsResponse>('/v1/atelier/jobs')from@/lib/server/bff-fetch; returns{ generatedAt, jobs: Job[] }whereJob = { id, room: 'camera-obscura'|'scene-composer'|'mastering', state: 'queued'|'running'|'done'|'failed', label, startedAtIso, etaSeconds }. Filtered client-of-server toroom === 'camera-obscura' - Writes: none in this view
- Realtime: none — running jobs do not poll/subscribe in code (ETA is static at render time); verify whether a refresh mechanism is planned
- Caching:
bffGetdefaultnext.revalidate = 60s; session cookie (oshun-session/__session) forwarded for caller resolution - Auth/role check: proxy enforces signed-in (
/atelier/*not inPUBLIC_PATHS); no extra role gate
Cross-references#
- Atelier index:
atelier.md - New scene:
atelier-new.md - Specimen/static image room:
atelier-image.md - Sibling rooms:
atelier-audio.md,atelier-video.md,atelier-forms.md,atelier-motion.md - Journey:
../../journeys/atelier-image-to-library.md(the queue is where in-flight renders surface) - Feature spec:
V1/features.md - Architecture:
V1/ARCHITECTURE.md
Open questions / known gaps#
- No realtime/poll: running jobs and ETAs freeze at SSR time. Confirm
whether a 60s revalidate (the
bffGetdefault) is the intended refresh cadence or whether a WebSocket/SSE subscription is planned - Job cards have no detail link — a failed job offers no way to inspect or retry. Confirm where job detail/diagnostics live
-
bffGetreturningnullflattens both "no jobs" and "BFF down" into the same Empty row — no distinct error affordance. Verify intended UX - Cards are bare
<div>s, not list items — confirm assistive-tech list semantics are acceptable - Nav uses
active="library"while sibling/lilith-studio/scene/newusesactive="explore"— confirm the canonical tab for Atelier surfaces