V1 Web PWA · Surface walkthrough

Scene · Embed (iframe-safe)

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 scene · route /scene/[id]/embed · auth anon · source apps/oshun/web/src/app/scene/[id]/embed/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; content re-verified 2026-06-03 against current source

Purpose#

Iframe-safe minimal embed of a public living offering. Designed to be mounted inside third-party pages as a single full-bleed preview tile. The playable tile carries less chrome than the full viewer (no report link, no download grant, no accessibility panel). It is not fully chrome-free, though: the password-required and tenant-auth-required branches each render an interactive "Open offering" link back to /scene/<id> (see Layout regions).

/scene is in apps/oshun/web/src/proxy.ts PUBLIC_PATHS so the embed loads anonymously.

Entry points#

  • <iframe src="https://oshun.app/scene/<id>/embed"> mounted in a partner page / blog / CMS
  • Direct URL — opens the embed full-window (not typical use)
  • Sharing tools or platforms that consume the OSHUN embed URL

Layout regions#

page.tsx resolves fetchPublicLivingScene(params.id) with no unlock token. It branches in three ways (page.tsx ~15-45): a password/tenant gate branch, the generic non-playable fallthrough, and the playable preview overlay.

kind === 'password-required' || kind === 'tenant-auth-required'#

A centred card (page.tsx ~15-37) explaining the embed cannot show this offering:

  • Heading: Password required or Tenant access required (chosen by kind)
  • Body: "This offering is not embeddable. Open it on OSHUN to view."
  • "Open offering" link (<a href="/scene/<id>" target="_top" rel="noopener">, teal button) — the one interactive element on the embed surface; breaks out of the iframe (target="_top") to the full viewer where the gate can be satisfied

kind !== 'playable' (generic fallthrough, e.g. tombstone)#

  • Main: full-screen grid centred message — "This offering has been retired." This bare tile is reached only after the password/tenant branch above; it has no interactive elements.

kind === 'playable'#

  • Main (relative, overflow-hidden):
    • Background layer: absolute-positioned linear-gradient(120deg, rgba(3,7,18,0.18), rgba(3,7,18,0.66)), url(<openGraph.imageUrl>) cover-centred
    • Bottom gradient panel: absolute bottom strip from black/80 to transparent, padding 4
      • Title (openGraph.title, 2xl serif)
      • Description (openGraph.description, smaller slate text)
      • Visible-mark short code (mono, smaller, near-bottom)

States#

  • Password requiredkind === 'password-required' → "Password required" card with the "Open offering" break-out link
  • Tenant-auth requiredkind === 'tenant-auth-required' → "Tenant access required" card with the same "Open offering" link
  • Retired / other non-playable — generic fallthrough → minimal "This offering has been retired." tile (no interactive elements)
  • Playable (default) — image background + title + description + short code
  • No tenant branding — the playable embed does not surface tenant logo or attribution (unlike the main viewer)
  • Inside an iframe — verify the page sets headers (e.g., X-Frame-Options, CSP frame-ancestors) consistent with embed use; not configured in page.tsx itself
  • Standalone visit — embed loads but lacks navigation; intended to be embedded, not navigated to directly
  • Reduced motion — no animations on this surface; no special handling needed
  • Offline — SW-cache may serve a previously fetched embed

Interactions#

The playable and the bare-retired tiles are read-only (no links, buttons, or forms). The two gate branches are the exception:

  • "Open offering" link (password-required / tenant-auth-required branches only) — <a href="/scene/<id>" target="_top" rel="noopener">; breaks out of the iframe to the full viewer. This is the embed's only interactive element.
  • Verify the playable and generic-retired branches contain no anchor / button / form descendants (only the gate branches render a link)

Data & contracts#

  • Reads: fetchPublicLivingScene(params.id) from @/lib/living-scenes-public (no unlock token)
  • Writes: none
  • Realtime: none
  • Caching: request-time server fetch; verify whether the embed is separately cached (it should be more aggressively cacheable than the full viewer)
  • Auth/role check: anon (/scene is in src/proxy.ts PUBLIC_PATHS)
  • Metadata: title: 'Embedded Living Offering' (bare; the | OSHUN suffix comes from the root layout's title.template = '%s | OSHUN'), robots: { index: false, follow: false }

Cross-references#

  • Sibling scene routes:
  • Component sources:
    • apps/oshun/web/src/lib/living-scenes-public.ts

Open questions / known gaps#

  • Password-protected scenes through the embedfetchPublicLivingScene is called without an unlock token, so password-required (and tenant-auth-required) scenes render the "not embeddable" card with an "Open offering" break-out link rather than the preview. Confirm this break-out-to-full-viewer behaviour is the intended design for private scenes (the embed itself never accepts a password). Verified in scene-password-and-grants.spec.ts: password-protected embed renders target="_top" "Open offering" and no full viewer chrome / Report / Download-grant controls.
  • No CSP frame-ancestors header or X-Frame-Options configuration visible at the page level; check the Next config / middleware / response headers for iframe-allow rules
  • No analytics / view telemetry from this embed surface — verify whether view tracking happens server-side
  • No accessibility panel; embed does not advertise caption / transcript availability — confirm whether the embedding partner is responsible for that