# Zacus Hub — Native macOS/iOS App + Gateway

**Status:** scaffolded 2026-05-24 — foundation only, modes need implementation.
**Owner:** L'électron rare
**Repos:** monorepo (`apps/zacus-hub/` SwiftUI, `tools/zacus-gateway/` FastAPI).

## Goal

A single SwiftUI multiplatform app (macOS 14+, iOS 17+) acting as a unified
hub for the Zacus escape-room ecosystem, with three modes:

| Mode | Audience | Backend surfaces |
|------|----------|------------------|
| **Game-master** | operator in the room | gateway `/state`, `/hint`, `/scene/*`, `/esp32/*` |
| **Companion** | players (outside-room extension) | gateway `/voice/*`, `/qr`, `/hint/auto` |
| **Studio** | author | gateway `/scenario/*`, file sync of `game/scenarios/*.yaml` |

Switching modes is a top-level tab (iPad/macOS) or sheet (iPhone).

## Why a new gateway

The 3 modes need an aggregated read-model (current scene + ESP32 state + last
hint + voice session id) and a uniform auth surface. Calling 4 backends
(`voice-bridge :8200`, `hints :8311`, ESP32 REST, scenario files) directly
from the app would duplicate logic and leak Tailscale addresses into Swift.

The gateway is thin (FastAPI, ~300 LOC target): token auth, request
fan-out, WebSocket multiplex for live state, no business logic.

## Architecture

```
iPhone / iPad / Mac (SwiftUI)
        │  bearer token (Keychain)
        ▼
tools/zacus-gateway  (FastAPI :8400)
   ├── /v1/auth/ping          token check
   ├── /v1/state              SSE/WS live game state
   ├── /v1/gm/*               game-master actions
   ├── /v1/companion/voice/*  proxies voice-bridge :8200
   ├── /v1/companion/hint     proxies hints :8311
   ├── /v1/studio/scenario    YAML read/write
   └── /v1/esp32/*            proxies master REST (when on LAN)
        │
        ▼
   voice-bridge :8200  |  hints :8311  |  ESP32 master  |  file system
```

## Auth (MVP)

- Single shared bearer token, generated by `tools/zacus-gateway/gen_token.py`.
- Stored in iOS/macOS Keychain (via `KeychainAccess` or raw Security.framework).
- Gateway validates `Authorization: Bearer <token>` against env var `ZACUS_HUB_TOKEN`.
- Transport: Tailscale tailnet `electron-rare` — no TLS termination needed
  for v1 (Tailscale provides WireGuard).

## Sprints

| # | Scope | Done when |
|---|-------|-----------|
| 0 | **Scaffold (this commit)** | Gateway serves `/v1/auth/ping`, app builds on both targets, 3 empty tabs render |
| 1 | Game-master read-only | Live state WS, scene list, ESP32 health visible |
| 2 | Game-master actions | Trigger hint, advance scene, replay NPC line |
| 3 | Companion voice | Push-to-talk → voice-bridge → response audio + transcript |
| 4 | Companion hints | Request hint, anti-cheat respected, adaptive level |
| 5 | Studio read | List scenarios, view YAML, syntax-highlight |
| 6 | Studio edit | Edit YAML in editor, validate via `compile_runtime3.py` |
| 7 | Polish | TestFlight, icons, App Store Connect (internal distribution) |

## Build

```bash
# Gateway
cd tools/zacus-gateway
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
ZACUS_HUB_TOKEN=$(python gen_token.py) uvicorn main:app --port 8400

# App (requires Xcode 16+ and XcodeGen)
brew install xcodegen
cd apps/zacus-hub
xcodegen generate
open ZacusHub.xcodeproj
```

## Open decisions

- **Distribution**: TestFlight internal vs ad-hoc IPA? Defer to sprint 7.
- **Live state transport**: SSE (simpler) vs WebSocket (bidirectional).
  Default to WS so companion voice can share the same channel.
- **Studio offline edit**: sync via gateway file-sync vs git checkout on
  device? Sprint 6 decision.
