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 setsrobots: noindex/nofollow/noarchive/nosnippetso a crawler that discovers it via an inbound link does not index it) - Studio shell nav —
active="studio"sidebar entry (shortcut6), 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 → insidedata-curated-cards-error; loading → insidedata-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 (viaCardShell) 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#
- Loading —
data-curated-cards-loadingpanel "Loading approved cards…" whileGET /v1/isis/curated-cards/catalogis 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
checkCardEntitlementverdict isdenyrendersdata-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-errorpanel "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
checkCardEntitlementat 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 aConsentConfirmation(disclosureVersion: 1,acknowledgedAtUnixSecondsstamped 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)
- Function: toggles
Each card embeds the shared CardShell, which owns these controls (one per
card; nested under the card wrapper):
- Kind-specific inputs panel (per card)
- illustration —
subject(input),stylePreset(select),aspect(select),locale(select) - narration —
text(textarea),voiceProfileId(select),locale(select),prosody(select) - ambient-audio —
mood,tempoBpm,durationSeconds,loopable - explainer —
subject,claimId,sophiaEvidencePinId,stylePreset,aspect(Sophia evidence pin carries grounding) - caption-dub —
sourceAssetId,targetLocales(operates on an existing asset, not generate-from-scratch) - accessibility-pass —
sourceAssetId,passes(checkbox group)
- illustration —
- 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/generatewith{ cardId, inputs, supportedStylePresets, tenantSupportedLocales } - Disabled when:
!preflight.canProceed || !inputsValid || !canSubmit || busy→ blocking reasons listed above the buttons; label switches to "Generating…" whilebusy - Telemetry: none client-side beyond the status line
- Function: calls
- 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 toonSendToEditorial(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;onTakedownis() => undefinedhere (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-cards—CardSpec,CuratedCardInputs,ConsentConfirmation,ProvenancePreview,PENDING_COST_COEFFICIENTS,computePreflight,checkCardEntitlement,gracefulStatus
Cross-references#
- Parent:
../studio-overview.md - Siblings:
studio-generation-music.md,studio-generation-nyx-3d.md,studio-generation-living-scene.md,studio-generation-gallery.md - Component sources:
apps/oshun/web/src/components/studio/generation/CuratedCardsStudio.tsx,CardShell.tsx,IllustrationCard.tsx,NarrationCard.tsx,AmbientAudioCard.tsx,ExplainerCard.tsx,CaptionDubCard.tsx,AccessibilityPassCard.tsx - Journeys:
journeys/atelier-image-to-library.md(illustration path),journeys/sophia-grounded-answer.md(explainer evidence pin)
Open questions / known gaps#
-
onSendToEditorial/onTakedownare wired to() => undefinedon this page — the editorial-routing and takedown call sites exist inCardShellbut are not connected fromCuratedCardsStudio. Confirm whether these are intended to route to/operator/studioonce the BFF endpoints land. -
tenantIdis hard-coded to''inrenderCard— 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 fromOSHUN_DUBBING_ELEVENLABS_API_KEY/OSHUN_ELEVENLABS_API_KEY+ theOSHUN_ASSET_STORE_*object store (dub per target language → persist to the store → signed URL; every dub carries thecloned-voice-usedhuman-review trigger at the Isis gate), and accessibility-pass runs the in-repo WCAG contrast pass + optionalOSHUN_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 asourceAssetIdfrom their library instead of typing an object key.