4.9 KiB
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 onshsf-api.reversed.dev. Single state store; persists to/app/state.jsonon the container.functions/data-sources/— Second serverless function (stub); currently unused beyond atestroute.
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-screenobject-coverbackground 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.heightis rejected. - Images are uploaded first via
push_upload_imagesto get a MinIO URL, then saved viapush_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.takeuses$out.path[0].fieldsyntax (handled bytv/src/utils/evaluatePath.ts) to extract a valuestatic_text— fixed text with font size/colorclock— live clock (mode: "time") or countdown (mode: "timer") with optionaltimezone(IANA) andtarget_isoimage_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
- Add state to
DEFAULT_STATEinfunctions/control/main.py; addpush_*/pull_*handlers inmain(args) - Add TypeScript types to
tv/src/types.ts - Add
useStateand rendering intv/src/App.tsx(fullscreen branch) - Add a page in
mobile/src/pages/ - Add a tab to
TABSinmobile/App.tsxwithhideInNav: true; update theRouteunion inmobile/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 auseRef; Android back button wired viaBackHandler. TABSinmobile/App.tsxis the single source of truth for routes/pages.BottomNavimports it directly.hideInNav: truetabs are reachable only vianavigate(route).- Styling uses React Native
StyleSheet— no CSS or Tailwind.
TV App Conventions
- Tailwind CSS v4 via
@tailwindcss/vite— notailwind.config.js; config lives invite.config.ts. - Polling only starts when fullscreen (
screenStatus === "fullscreen"). - Data cards grid uses inline CSS grid styles (not Tailwind) because
gridColumn/gridRowvalues are dynamic. All card widgets wrap in theCardShellcomponent (tv/src/components/DataCardWidget.tsx). - State shape in
tv/src/App.tsxmust exactly mirrorDEFAULT_STATEin the Python function.
Dev Workflows
# 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.