Files
tv-control/.github/copilot-instructions.md
space 2f4a238eba
Some checks failed
Build App / build (push) Failing after 3m44s
feat: update image requirements to landscape orientation and add CI/CD workflow
2026-03-01 15:53:36 +01:00

93 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.