# OlloVue Scene SDK

A venue's TVs run a playlist of **scenes** — and a scene is just a web
page. If you can build a webpage, you can build a TV app for any venue on OlloVue.
This doc is the contract.

## Two ways to ship a scene

**1. Hosted URL (zero-touch).** Build any page, host it anywhere, and it plays
as a playlist item `{ "type": "url", "src": "https://your.app/scene" }`. The
display client iframes it full-screen. Examples in production: `/trivia-tv`,
`/giftcards-tv`, `/waitlist-tv`, `/weather-tv`.

**2. Managed scene (hub-hosted).** POST your HTML to the hub and it's stored,
versioned with the venue's data, and rendered inside the branded
**scene shell**: full-bleed 16:9, the brand palette as CSS variables
(`--brand-primary`, `--brand-secondary`, `--brand-accent`, …), the Anton
display font, and the `VENUE.*` data API pre-wired (`LAUNICA.*` is kept as an alias).

```
POST /admin/scenes   (x-admin-token)   { id, name, html }
→ play as { "type": "scene", "src": "<id>" }
```

## Design rules

- Target **16:9 landscape**, size in `vh`/`vw` — screens range 720p→4K.
- `overflow:hidden`; nothing interactive (TVs have no touch). Motion yes,
  but subtle — these run for hours.
- A scene shows for 10–600 s per rotation (the playlist decides). Design the
  first second: no loading spinners; fetch fast, render optimistic.
- Support `@media (orientation:portrait)` if you can — some screens hang tall.
- QR codes are the only "input": `/qr?path=/your-path` returns a scannable SVG
  for any hub path.

## Which venue am I standing in? (store apps)

Never ask a venue for its own zip code, timezone, or brand — the hub already
knows, and your app learns it automatically two ways:

1. **`?ov_hub=<url>`** is appended to your scene URL at serve time. That's the
   venue hub your iframe is playing on. Point every data call below at it:
   `fetch(hub + "/tenant")` → name, address, brand colors, locale;
   `fetch(hub + "/weather")` → the forecast for *that* venue's neighborhood;
   `/hours`, `/events/list`, `/menu/live` — all already localized.
2. The lifecycle handshake (below) repeats it: the `shown` message carries
   `hub`, plus `venue` (display name) and `tz`.

Build against `ov_hub` and one deployment of your app serves every city with
zero install-time questions. (For local dev, open your scene as
`https://your.app/scene?ov_hub=https://<any-venue>.ollovue.com`.)

## Start with the starter scene

The fastest way in is to copy one working file. **[/starter-scene](/starter-scene?ov_hub=https://ollovue.com)**
is a complete scene in about a hundred lines: it finds the venue from `ov_hub`,
takes the venue's own name and brand colours from `/tenant`, shows the live
weather and today's hours, puts up a QR, and handles the lifecycle below.
[Download the source](/starter-scene.html), change what it shows, host it as
https, and submit the URL at [/developers](/developers). These docs are also at
[/docs](/docs), rendered.

## Lifecycle handshake

The screen talks to your scene with `postMessage`. Listen for messages whose
data carries `ollovue: 1`:

```js
addEventListener("message", (e) => {
  const m = e.data;
  if (!m || !m.ollovue) return;
  if (m.event === "shown")  { /* you are on screen — refresh, start timers */ }
  if (m.event === "hidden") { /* you are leaving — stop work nobody can see */ }
});
```

`shown` arrives the moment your slide is on (and again on each rotation), with:

| Field | Meaning |
|---|---|
| `seconds` | how long this slide is scheduled to stay up |
| `hub` | the venue hub URL — the same value as `?ov_hub` |
| `venue` | the venue's display name |
| `tz` | the venue's IANA timezone, e.g. `America/New_York` |
| `screen` | the id of the TV showing you |
| `type` | the playlist item type that resolved to you (`url`, `app`, `scene`) |

`hidden` arrives as the loop moves on. Treat it as "stop": clear intervals,
pause video, release anything expensive. A scene that keeps fetching while
hidden is doing work nobody can see, on hardware that runs all night.

Also sent: `{ type: "menu" }` when the venue's menu changes — refetch
`/menu/live` if you show prices.

## Public data API (no auth — read-only JSON)

| Endpoint | What you get |
|---|---|
| `/tenant` | Restaurant identity: name, address, brand colors, logo, locale |
| `/menu/live` | Live menu: id → { name, price, available } |
| `/menu/stats` | 28-day best-sellers + item pairings |
| `/events/list` | Upcoming events |
| `/waitlist/public` | Current waitlist (first names + position) |
| `/reviews/list` | Google rating + spotlighted 4–5★ reviews |
| `/trivia/state` | Live trivia game state (poll ~1s) |
| `/weather` | Local forecast |
| `/sports/scoreboard?league=nfl` | Live scores/odds (nfl, nba, mlb…) |
| `/loyalty/program` | Reward ladder + tiers |
| `/shoutouts/list` | Today's approved guest shoutouts |
| `/hours` | Open/closed + today's schedule |
| `/bingo/state`, `/game/state` | Live bingo, and the room games (vote / fastest thumb / raffle / tournament) — poll ~1s |
| `/jukebox/state`, `/board/jukebox` | What the jukebox is playing and what is queued (guest view / screen view) |
| `/board/parlay?id=` | A bet slip's legs grading live off the play |
| `/appstore/catalog` | Every approved store app |

All of these send `Access-Control-Allow-Origin: *`, so a scene hosted on your own
domain can read them directly. Managed scenes additionally get `VENUE.stats()`, `VENUE.menu()`,
`VENUE.events()` helpers injected by the shell (`LAUNICA.*` still works).

## Control API (x-admin-token)

- `/admin/signage` GET/POST — screens + playlists (add your scene to rotation)
- `/admin/takeover` POST — interrupt every TV (or specific ones) for N seconds
- `/admin/scenes` GET/POST/DELETE — managed scene CRUD
- SSE at `/board/events` pushes playlist changes + takeovers to displays live

## Developer accounts, API keys & the app store

`/developers` is the portal: sign up with an email (a magic link signs you
in — no password), and from there you can:

- **Take an API key** (`ov_live_…`, shown once). Send it as `x-api-key` (or
  `?key=`) on any public-read endpoint. The anonymous tier keeps working —
  a key buys *identity*: per-key usage analytics in the portal, and the
  named principal that future write scopes will attach to. A key that is
  sent but invalid fails with a 401 rather than silently degrading.
- **Submit apps to the store.** An app is a name, a tagline, an icon, and an
  https scene URL. Submissions go through human review (the hub renders your
  URL at TV size for the reviewer); approved apps land in the store catalog
  (`/appstore/catalog`) where venues install them into their playlists as
  `{type:"app", src:<app-id>}` — an indirection the hub resolves to your URL
  at serve time. That's what makes the store safe to build on: ship a URL fix
  and it reaches every venue without a re-install, and each showing is
  counted into your per-app play stats in the portal. The hub also probes
  your URL every 10 minutes — if it stops answering, screens skip your slot
  (you get an email) and restore it automatically when it answers again.
  Editing a submitted or approved app sends it back through review: the
  version a venue approved is never silently swapped.

Review decisions arrive by email, with the reviewer's note when changes are
asked for.

## Previewing

Open your scene URL in a browser at 1920×1080 — what you see is what the TV
plays. Managed scenes preview at `/scene?id=<id>`. No approval loop: publish
to a playlist and the TVs pick it up within a minute (SSE) or at next poll.

## Ideas nobody has built yet

Karaoke queue · Lotería caller · UFC/boxing fight cards · lottery numbers ·
neighborhood news · birthday slideshows from the shoutout wall · "guess the
price" of tonight's special · community sports league standings.

---
*Multi-tenant note: scenes reading `/tenant` for name/colors instead of
hardcoding one venue work unchanged on every venue running OlloVue — build
tenant-aware and your scene is a product, not a one-off.*

## Match the house style

Scenes that get featured look like the ones already on the wall. The style is written down as seven checkable rules — **Marquee** — at [`/scene-style`](/scene-style) (`kiosk/SCENE-STYLE.md`). Read it before you design; review checks against it.
