# 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.
