V1 Web PWA · Surface walkthrough

Atelier · Camera obscura (render queue)

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

walked
8sections4 minread

On this page

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/new success 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 auto grid): left side has a mono uppercase status line — <state> · started <localeTime> · eta <N>m | no eta (color per STATE_COLOUR: queued=muted, running=accent, done=green #1f4f3a, failed=red #a30f0f) — and the serif job.label; right side shows the mono job.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 a loading.tsx exists for this segment
  • EmptybffGet returns 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)bffGet returns null on 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
  • OfflinebffGet aborts after 5s and returns null → Empty state; SW-cached HTML may show last render — verify
  • Standalone PWAdata-responsive-page wrapper; verify safe-area and narrow-width rendering of the 1fr auto job 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
  • "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

Data & contracts#

  • Reads: bffGet<JobsResponse>('/v1/atelier/jobs') from @/lib/server/bff-fetch; returns { generatedAt, jobs: Job[] } where Job = { id, room: 'camera-obscura'|'scene-composer'|'mastering', state: 'queued'|'running'|'done'|'failed', label, startedAtIso, etaSeconds }. Filtered client-of-server to room === '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: bffGet default next.revalidate = 60 s; session cookie (oshun-session / __session) forwarded for caller resolution
  • Auth/role check: proxy enforces signed-in (/atelier/* not in PUBLIC_PATHS); no extra role gate

Cross-references#

Open questions / known gaps#

  • No realtime/poll: running jobs and ETAs freeze at SSR time. Confirm whether a 60s revalidate (the bffGet default) 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
  • bffGet returning null flattens 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/new uses active="explore" — confirm the canonical tab for Atelier surfaces