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

4.9 KiB
Raw Permalink Blame History

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

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