V1 Web PWA · Surface walkthrough

Studio · Generation · Curated Cards

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

walked
8sections6 minread

On this page

Context. surface studio · domain generation · route /studio/generation/curated-cards · auth signed-in + creator (proxy-gated; not anonymous, not search-indexed) · source apps/oshun/web/src/app/studio/generation/curated-cards/page.tsx

Last walked. 2026-06-03 automated runtime probe (Playwright headless) — prod build + mock BFF; render, HTTP 200, server-rendered <h1>, 0 page errors verified. Evidence: WALKTHROUGH/results/walkthrough-runtime-probe-2026-06-03.md

Purpose#

The approved curated creator-card generation surface (§24.6). Renders the six server-authoritative curated cards (illustration, narration, ambient-audio, explainer, caption-dub, accessibility-pass) from GET /v1/isis/curated-cards/catalog and submits each through POST /v1/isis/curated-cards/generate. The backend runs entitlement + raw-graph-leak audit + per-card field validation, then enqueues generation; the surface never fabricates a success or a cost — it reports entitlement-denied, invalid-inputs, provider-unconfigured (503), and accepted outcomes honestly.

Entry points#

  • Direct URL / bookmark/studio/generation/curated-cards (proxy gates to authed creators; the page sets robots: noindex/nofollow/noarchive/nosnippet so a crawler that discovers it via an inbound link does not index it)
  • Studio shell navactive="studio" sidebar entry (shortcut 6), via the generation sub-area
  • Sibling generation surfaces — cross-linked from studio-generation-music.md, studio-generation-nyx-3d.md, studio-generation-living-scene.md

Layout regions#

  • Header: shell header (ShellLayout active="studio")
  • Main: <section data-curated-cards-studio> containing
    • <h1>Curated Cards</h1> — page heading, rendered in all three states (error → inside data-curated-cards-error; loading → inside data-curated-cards-loading; populated → top of the studio section), plus a lede <p> "Generate from the six approved curated creator cards. Every submission is entitlement-checked and provenance-tracked server-side."
    • Consent panel (.panel, data-curated-cards-consent) — a single rights/terms confirmation checkbox at the top, shared by every card
    • Card list — one wrapper (data-curated-card-wrapper="<cardId>") per catalog entry, each rendering the kind-specific card (via CardShell) and, below it, a per-card status line (data-curated-card-status="<cardId>") once a submission has been attempted
  • Footer / sticky bottom: None. (shell footer only)

States#

  • Loadingdata-curated-cards-loading panel "Loading approved cards…" while GET /v1/isis/curated-cards/catalog is in flight; no card list yet
  • Empty — N/A: the catalog is server-authoritative; an empty cards[] simply renders the consent panel with no cards (the approved set is expected to be non-empty in a configured deployment)
  • Populated — consent panel + up to six cards, each in its data-card-status="available" shell with the kind-specific inputs panel, preflight cost/latency meta, provenance line, and Generate control
  • Per-card entitlement-denied — a card whose checkCardEntitlement verdict is deny renders data-card-status="entitlement-denied" with "This card is not available for your tier or tenant." + the reason; other cards stay interactive
  • Per-card feature-unavailable — kill-switch / frozen workflow class renders data-card-status="feature-unavailable" ("temporarily unavailable. Underlying workflow is frozen.")
  • Submit accepted — status line "Accepted — queued for generation."
  • Submit rejected (provider unconfigured) — 503 → "Generation provider is not configured for this deployment yet."
  • Submit rejected (entitlement) — 403 → "Not available: "
  • Submit rejected (validation) — 422 → "Please fix the highlighted fields: "
  • Submit rejected (not in catalog) — 404 → "This card is not in the approved catalog."
  • Error (catalog load failed)data-curated-cards-error panel "Curated cards are unavailable right now: "
  • Offline — catalog GET fails → error panel; submit POSTs surface the generic "Submission failed." when the network is unreachable
  • Gated — non-creator tiers are blocked by the proxy before the page renders (route-level), and per-card by checkCardEntitlement at the component layer
  • Standalone PWA — renders within the shell; no URL-bar dependency

Interactions#

  • Rights/terms consent (checkbox, data-curated-cards-consent-checkbox)
    • Function: toggles consentAccepted; builds a ConsentConfirmation (disclosureVersion: 1, acknowledgedAtUnixSeconds stamped on accept) that every card's preflight consumes — Generate stays blocked until accepted
    • Keyboard: Space toggles; in tab order before the cards
    • Screen reader: announces the label "I confirm I have rights to the inputs and accept the curated-generation terms."
    • Telemetry: none direct (state feeds preflight canProceed)

Each card embeds the shared CardShell, which owns these controls (one per card; nested under the card wrapper):

  • Kind-specific inputs panel (per card)
    • illustrationsubject (input), stylePreset (select), aspect (select), locale (select)
    • narrationtext (textarea), voiceProfileId (select), locale (select), prosody (select)
    • ambient-audiomood, tempoBpm, durationSeconds, loopable
    • explainersubject, claimId, sophiaEvidencePinId, stylePreset, aspect (Sophia evidence pin carries grounding)
    • caption-dubsourceAssetId, targetLocales (operates on an existing asset, not generate-from-scratch)
    • accessibility-passsourceAssetId, passes (checkbox group)
  • Direction (textarea, direction-<cardKind>) — free-text steer for regenerate; placeholder "e.g. softer palette, slower pace"
  • Generate (button, data-card-action="generate")
    • Function: calls submit(spec, inputs)POST /v1/isis/curated-cards/generate with { cardId, inputs, supportedStylePresets, tenantSupportedLocales }
    • Disabled when: !preflight.canProceed || !inputsValid || !canSubmit || busy → blocking reasons listed above the buttons; label switches to "Generating…" while busy
    • Telemetry: none client-side beyond the status line
  • Regenerate with direction (button, data-card-action="regenerate") — disabled until at least one variant exists
  • Variant compare (checkbox per variant, data-variant-action="toggle-compare") — up to 4; renders a side-by-side compare grid (data-testid="card-variant-compare") once ≥2 selected
  • Send to editorial (button, data-variant-action="send-to-editorial") — per non-taken-down variant; wired to onSendToEditorial (no-op here — this surface passes () => undefined, so the studio editorial routing is not yet connected from this page)
  • Takedown (button, data-variant-action="takedown") — per variant; onTakedown is () => undefined here (same caveat)

Data & contracts#

  • Reads: GET /v1/isis/curated-cards/catalog{ cards: CatalogEntry[], creatorTier } where each entry is { spec: CardSpec, provenance: ProvenancePreview }
  • Writes: POST /v1/isis/curated-cards/generate{ cardId, inputs: CuratedCardInputs, supportedStylePresets, tenantSupportedLocales }; fail-closed 503 until a provider is configured
  • Realtime: None.
  • Caching: client fetch on mount (no SWR); SSR shell only
  • Auth/role check: proxy route gate (creator) + checkCardEntitlement (component layer) + server-side entitlement/leak-audit on generate
  • Contracts: @oshun-web/isis-curated-cardsCardSpec, CuratedCardInputs, ConsentConfirmation, ProvenancePreview, PENDING_COST_COEFFICIENTS, computePreflight, checkCardEntitlement, gracefulStatus

Cross-references#

Open questions / known gaps#

  • onSendToEditorial / onTakedown are wired to () => undefined on this page — the editorial-routing and takedown call sites exist in CardShell but are not connected from CuratedCardsStudio. Confirm whether these are intended to route to /operator/studio once the BFF endpoints land.
  • tenantId is hard-coded to '' in renderCard — confirm the tenant is meant to be resolved from the session/proxy header rather than left blank.
  • Caption-dub and accessibility-pass operate on a pre-existing sourceAssetId; the backend providers are now REAL (2026-06-10): caption-dub resolves ElevenLabs Dubbing from OSHUN_DUBBING_ELEVENLABS_API_KEY/OSHUN_ELEVENLABS_API_KEY + the OSHUN_ASSET_STORE_* object store (dub per target language → persist to the store → signed URL; every dub carries the cloned-voice-used human-review trigger at the Isis gate), and accessibility-pass runs the in-repo WCAG contrast pass + optional OSHUN_A11Y_ML_* vision alt-text/Whisper transcript service (see .env.example + V1_DEPLOYMENT_REQUIREMENTS.md §8/§9). Still open on THIS surface: an asset-picker so the creator can choose a sourceAssetId from their library instead of typing an object key.