# TCN API-driven machine ads RFC ## Goal Move Ballbox machine ad/media control from hardcoded repo mappings to API-managed desired state so future changes do not require rebuilding or reinstalling the Android sidecar. ## Reality confirmed on device - Installed sidecar can sync successfully on Android 7.1.2 once `minSdk` and storage behavior are fixed. - Correct target root on the machine is `/sdcard/TcnFolder`. - `ImageScreen/` is the fullscreen standby slot. - `VideoAndImageAd/` is the smaller top video slot. - Both folders accept looped video files. - Current sidecar prune behavior can delete stale files inside managed folders. - Freshly synced media may require restarting the main vending app or rebooting Android before visible playback updates reliably apply. - The implementation plan should assume both actions are useful: restart vending app and reboot Android. - High-resolution/high-fps fullscreen video can decode poorly on the machine; a 2160x3840 60fps test clip played in slow motion for the first seconds. - A safer fullscreen profile worked better operationally: 30fps with lower decode load. Current compromise test moved from 720x1280 30fps to a higher-quality 1080x1920 30fps version to preserve visual quality while staying below the failing 60fps profile. ## Problem with current MVP Current backend manifest generation is hardcoded in `lib/machine-ads-manifest.ts`. That means every content change currently needs a code edit and deploy. This is too slow for operator use and does not scale to arbitrary file replacement/prune. ## Target state Ballbox should own a per-machine desired state model. The sidecar should: 1. wait ~10 seconds after boot before first sync 2. fetch manifest 3. download listed files 4. verify hashes 5. write exact target paths 6. prune stale files inside managed directories 7. continue periodic sync afterward 8. keep a manual operator menu accessible at all times from some discoverable place Changing content should mean changing Ballbox backend state, not Android code. ## Proposed manifest shape ```json { "machineId": "2601070188", "generatedAt": "2026-06-08T00:00:00.000Z", "version": "...", "restartPolicy": "app", "targetRootHint": "/sdcard/TcnFolder", "managedDirs": ["ImageScreen", "VideoAndImageAd"], "files": [ { "target": "ImageScreen/foo.mp4", "url": "https://ballbox.app/machine-assets/.../foo.mp4", "sha256": "...", "bytes": 123, "restartRequired": "app" } ] } ``` ## Desired backend model Short term: - file-backed machine config in repo - one JSON document per machine - assets still served from `public/machine-assets/...` Medium term: - DB-backed machine asset entries - admin-authenticated upload and assignment UI/API - no code edits required for normal operator changes ## Minimal data model Per machine config: - `machineId` - `targetRootHint` - `managedDirs[]` - `restartPolicy` - `entries[]` Per entry: - `target` - `publicPath` - `restartRequired` - optional future metadata: `label`, `active`, `slot`, `contentType` ## Rollout plan ### Phase 1 - move hardcoded manifest config into file-backed JSON - expose `managedDirs` and `targetRootHint` in manifest - update sidecar default target root to `/sdcard/TcnFolder` - keep sidecar reinstalls rare ### Phase 2 - sidecar runs automatically after boot with ~10 second delay - sidecar repeats sync periodically in background - sidecar keeps a manual operator menu reachable from an accessible entrypoint instead of hiding sync behind install-time flows - operator menu includes at least: - sync now - open logs/status - restart vending app - reboot Android system ### Phase 3 - add admin mutation path in Ballbox backend - validate target paths against allowed machine directories - allow add/replace/remove entries per machine - add upload flow and stored asset references - optionally track asset history and rollback ## Safety constraints - prune only inside explicitly managed directories - never allow arbitrary absolute target paths from user input - validate machine target prefixes against allowlist - keep manifest truthful: only list assets that exist and hash correctly - operator UX must make restart/reboot requirements explicit when runtime cache prevents hot swap - reboot action is irreversible enough operationally that the UI should make intent obvious and avoid accidental taps ## Current implementation decision Start with a repo-backed config file because it is the cheapest path from hardcoded code to API-driven desired state. That gives immediate leverage while keeping migration path open to DB/admin UI. ## Immediate implementation steps 1. add machine config JSON files under repo data directory 2. refactor manifest builder to read config instead of hardcoded map 3. include `managedDirs` and `targetRootHint` in manifest response 4. update Android sidecar default root to `/sdcard/TcnFolder` 5. constrain fullscreen transcode defaults to machine-safe profiles like 24/30fps and lower resolution instead of assuming source originals are decodable in real time 6. add boot receiver / startup path that waits ~10 seconds before first sync 7. add periodic background sync scheduling 8. add persistent operator-accessible menu entry with restart-app and reboot-system actions 9. later add admin write path