MAJOR UPDATE ALERT!!!!!
This commit is contained in:
76
.github/copilot-instructions.md
vendored
76
.github/copilot-instructions.md
vendored
@@ -5,45 +5,77 @@
|
||||
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, polls backend every 5s when fullscreen.
|
||||
- **`functions/control/`** — Python serverless function deployed on `shsf-api.reversed.dev`. Acts as state store; persists to `/app/state.json` on the container.
|
||||
- **`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 a single endpoint:
|
||||
All communication goes through one base URL:
|
||||
```
|
||||
https://shsf-api.reversed.dev/api/exec/15/1c44d8b5-4065-4a54-b259-748561021329/{route}
|
||||
```
|
||||
|
||||
Routes (all `POST` except pull routes which are `GET` implicitly via `fetch`):
|
||||
- `push_text` — body: `{ title: string }`
|
||||
- `push_dismiss_text`
|
||||
- `push_image` — body: `{ image_b64: string, caption: string }`; uploads to MinIO, stores public URL
|
||||
- `push_dismiss_image`
|
||||
- `pull_full` — returns full state `{ text, image_popup }`
|
||||
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.
|
||||
|
||||
Images are stored in MinIO at `content2.reversed.dev`, bucket `tv-control`.
|
||||
| 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 portrait image (height > width) 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 (portrait 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 shape to `DEFAULT_STATE` in `functions/control/main.py`
|
||||
2. Add `push_*` / `pull_*` handlers in `main(args)` in the same file
|
||||
3. Add a TypeScript interface and `useState` in `tv/src/App.tsx`; render the popup in the fullscreen branch
|
||||
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. Register it in the `TABS` array in `mobile/App.tsx` with `hideInNav: true` (prevents it appearing in `BottomNav`, navigation is triggered programmatically via `navigate()`)
|
||||
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
|
||||
|
||||
- Uses a **custom in-memory router** (`mobile/src/router.tsx`) — no expo-router or react-navigation. `Route` type is a string union: `"home" | "text" | "image"`.
|
||||
- `TABS` in `mobile/App.tsx` is the single source of truth for routes and pages. `BottomNav` imports `TABS` directly from `App.tsx`.
|
||||
- Tabs with `hideInNav: true` are reachable only via `navigate(route)`, not from the bottom bar.
|
||||
- Styling uses React Native `StyleSheet` throughout — no CSS or Tailwind.
|
||||
- **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
|
||||
|
||||
- Uses **Tailwind CSS v4** via `@tailwindcss/vite` (no `tailwind.config.js` — config lives in `vite.config.ts`).
|
||||
- Polling only starts when the browser enters fullscreen (`screenStatus === "fullscreen"`).
|
||||
- State shape mirrors `DEFAULT_STATE` from the Python function exactly.
|
||||
- **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
|
||||
|
||||
@@ -57,4 +89,4 @@ cd mobile && pnpm android # Android emulator
|
||||
cd mobile && pnpm ios # iOS simulator
|
||||
```
|
||||
|
||||
Package manager: **pnpm** for both `tv/` and `mobile/`. Python function has no local runner — deploy changes directly.
|
||||
Package manager: **pnpm** for both `tv/` and `mobile/`. Python functions have no local runner — deploy changes directly.
|
||||
|
||||
Reference in New Issue
Block a user