# Copilot Instructions ## Architecture Overview Three-component system for remote-controlling a TV display from a mobile phone: - **`mobile/`** — Expo React Native app (the "remote control"). Sends commands to the backend. - **`tv/`** — Vite + React web app (the TV display). Runs in a browser on the TV, polls backend every 5s when fullscreen. - **`functions/control/`** — Python serverless function on `shsf-api.reversed.dev`. Single state store; persists to `/app/state.json` on the container. - **`functions/data-sources/`** — Second serverless function (stub); currently unused beyond a `test` route. ## API / Backend All communication goes through one base URL: ``` https://shsf-api.reversed.dev/api/exec/15/1c44d8b5-4065-4a54-b259-748561021329/{route} ``` All routes use `POST` (fetch with no body is still POST). `pull_full` returns `{ status, state }` — the state object is under the `state` key, not the root. | Route | Body | Notes | |---|---|---| | `push_text` | `{ title }` | | | `push_dismiss_text` | — | | | `push_image` | `{ image_b64, caption }` | Uploads to MinIO, stores URL | | `push_dismiss_image` | — | | | `push_data_card` | `{ card: DataCard }` | Upserts by `card.id` | | `push_delete_data_card` | `{ id }` | | | `push_upload_images` | `{ images: [{image_b64, ext}] }` | Returns `{ urls[] }`, no state change | | `push_settings` | `{ background_url }` | Persists global TV settings | | `pull_full` | — | Returns `{ state: { text, image_popup, data_cards[], settings } }` | | `pull_data_cards` | — | Returns `{ data_cards[] }` | Images/rotator assets stored in MinIO at `content2.reversed.dev`, bucket `tv-control`. ## Global Settings `settings` is a persistent object with global TV display configuration (typed as `SettingsState` in `tv/src/types.ts`). - **`background_url`** — a landscape image (width > height) rendered as a full-screen `object-cover` background behind all TV content. Empty string = no background. - Set via `push_settings { background_url }`, cleared by passing `""`. - The constraint (landscape orientation) is enforced in the mobile picker (`mobile/src/pages/settings.tsx`) — `asset.width <= asset.height` is rejected. - Images are uploaded first via `push_upload_images` to get a MinIO URL, then saved via `push_settings`. - The Settings tab is visible in the mobile bottom nav (no `hideInNav`). ## Data Cards System `data_cards` is a persistent array of typed card objects displayed on the TV in a **4-column × 4-row CSS grid**. Each card has `{ id, type, name, config, layout? }`. Four card types (defined in `tv/src/types.ts` and mirrored in `mobile/src/pages/datacards.tsx`): - **`custom_json`** — polls an external URL; `config.take` uses `$out.path[0].field` syntax (handled by `tv/src/utils/evaluatePath.ts`) to extract a value - **`static_text`** — fixed text with font size/color - **`clock`** — live clock (`mode: "time"`) or countdown (`mode: "timer"`) with optional `timezone` (IANA) and `target_iso` - **`image_rotator`** — cycles through an array of image URLs `layout` defaults to `{ grid_col: 1, grid_row: 4, col_span: 1, row_span: 1 }` when absent. `assignLayouts()` in `tv/src/App.tsx` resolves collisions deterministically via a string hash of `card.id`. ## Adding a New Content Type 1. Add state to `DEFAULT_STATE` in `functions/control/main.py`; add `push_*`/`pull_*` handlers in `main(args)` 2. Add TypeScript types to `tv/src/types.ts` 3. Add `useState` and rendering in `tv/src/App.tsx` (fullscreen branch) 4. Add a page in `mobile/src/pages/` 5. Add a tab to `TABS` in `mobile/App.tsx` with `hideInNav: true`; update the `Route` union in `mobile/src/router.tsx` ## Mobile App Conventions - **Custom in-memory router** (`mobile/src/router.tsx`) — no expo-router or react-navigation. `Route = "home" | "text" | "image" | "datacards"`. History stack backed by a `useRef`; Android back button wired via `BackHandler`. - `TABS` in `mobile/App.tsx` is the single source of truth for routes/pages. `BottomNav` imports it directly. - `hideInNav: true` tabs are reachable only via `navigate(route)`. - Styling uses React Native `StyleSheet` — no CSS or Tailwind. ## TV App Conventions - **Tailwind CSS v4** via `@tailwindcss/vite` — no `tailwind.config.js`; config lives in `vite.config.ts`. - Polling only starts when fullscreen (`screenStatus === "fullscreen"`). - Data cards grid uses **inline CSS grid styles** (not Tailwind) because `gridColumn`/`gridRow` values are dynamic. All card widgets wrap in the `CardShell` component (`tv/src/components/DataCardWidget.tsx`). - State shape in `tv/src/App.tsx` must exactly mirror `DEFAULT_STATE` in the Python function. ## Dev Workflows ```bash # TV display cd tv && pnpm dev # Mobile app cd mobile && pnpm start # Expo Go / dev server cd mobile && pnpm android # Android emulator cd mobile && pnpm ios # iOS simulator ``` Package manager: **pnpm** for both `tv/` and `mobile/`. Python functions have no local runner — deploy changes directly.