diff options
Diffstat (limited to 'makima/ios/docs/ios-v1-plan.md')
| -rw-r--r-- | makima/ios/docs/ios-v1-plan.md | 222 |
1 files changed, 222 insertions, 0 deletions
diff --git a/makima/ios/docs/ios-v1-plan.md b/makima/ios/docs/ios-v1-plan.md new file mode 100644 index 0000000..876a6ff --- /dev/null +++ b/makima/ios/docs/ios-v1-plan.md @@ -0,0 +1,222 @@ +# Makima iOS — v1 Plan + +**Date:** 2026-04-24 +**Target:** iOS 18.0+ · iPhone only · Pure native SwiftUI · No Android +**Upstream:** `soryu/makima` (Rust + Postgres server, React frontend at `makima.jp`) +**Location:** new folder `apps/ios/` inside the existing `soryu-co/soryu` monorepo (sibling to `frontend/`, `makima/`, `k8s/`). +**License:** MIT (carry repo-level LICENSE; no per-folder LICENSE needed). +**App Store seller:** Soryu LTD. +**Bundle ID:** `co.soryu.makima` (dropped the `.ios` suffix per preference). + +--- + +## 1. Product shape + +**Primary surface: Composite Home.** A glanceable dashboard with section cards: + +- **Active Contracts** — count + top 3 running, tap → Contracts list +- **Active Daemons** — mesh status pill (N online / M offline) +- **Pending Directive Questions** — amber badge, tap → Directives +- **Latest Listen Session** — last transcript preview, tap → Listen detail +- **Recent Tasks** — 3 most recent tasks across contracts, tap → Task detail (live output) + +Secondary surfaces reachable from home: Contracts list, Tasks list, Directives, Daemons, Listen (read-only history in v1), Settings. + +**Not in v1:** Speak (TTS), Files editor, Orders write-path, mesh merge UI, worktree diff viewer, daemon reauth flow, supervisor questions *answering* (read-only badge only — answer on web). + +## 2. Stack + +- **Language:** Swift 5.10+ / SwiftUI +- **Min iOS:** 18.0 (same as Litter/KittyLitter — lets us use latest SwiftUI APIs, Observation, Swift Concurrency) +- **Architecture:** MV (Model + View) with `@Observable` stores — no MVVM ceremony +- **Networking:** `URLSession` + `URLSessionWebSocketTask` (no Alamofire/Starscream) +- **Persistence:** SwiftData for cached contracts/tasks/transcripts; Keychain for API key(s); `UserDefaults` for server-profile list +- **Markdown/code:** `swift-markdown-ui` (MIT) for markdown, `Splash` or `Runestone` for syntax highlighting (evaluate at impl time — Splash is lighter, Runestone handles big files) +- **Push:** APNs via a small `services/push-proxy` later — **not in v1**. v1 uses local notifications triggered by WS events while app is foreground/recent. True background push = v1.1. +- **Build:** XcodeGen (`project.yml` source of truth, matches Litter) +- **No dependencies beyond:** swift-markdown-ui, KeychainAccess (or hand-roll), a syntax highlighter. Everything else stdlib. + +## 3. Auth & server configuration + +### 3.1 Multi-profile server picker *(question 3 = a: single field, but we're building the primitive that generalises)* + +User picked (a) — single "Server URL" field. Implementation: + +- `Settings → Server URL` text field, default `https://makima.jp` +- Stored in Keychain alongside the API key (so swapping server swaps the credential bundle) +- On change: clear cached contracts/tasks/daemons, re-fetch `/health`, prompt for new API key if 401 +- Internally represented as `ServerProfile { url: URL, apiKey: String, label: String }` — list-of-one in v1, lets us upgrade to multi-profile in v1.x without migration + +### 3.2 API key flow *(question 2 = c: both — but v1 = API key only)* + +User said (c) both, but Supabase OAuth needs project keys and deep-link wiring. Ship API key paste in v1, add Supabase in v1.1: + +1. First-launch onboarding: + - Screen 1: Server URL (prefilled `https://makima.jp`) + - Screen 2: "Open web settings → create API key → paste here" with a `Open in Safari` button to `<server>/settings` + - Screen 3: validate via `GET /api/v1/mesh/daemons` (any authenticated endpoint); on success, store and proceed to Home +2. API key stored in Keychain, scoped per-server-URL +3. `Settings → Rotate key` button — calls `POST /api/v1/auth/api-keys/refresh`, replaces stored key + +### 3.3 Auth header *(verified 2026-04-24 against `src/server/auth.rs`)* + +- **API keys → header `x-makima-api-key: <full-key>`** (constant `API_KEY_HEADER` line 404). Keys are prefixed `mk_`. +- **Supabase JWTs → header `Authorization: Bearer <jwt>`** (line 1039–1041). +- Order of precedence in the server: tool key → API key → JWT. +- **v1 sends `x-makima-api-key` only.** `Authorization: Bearer` is reserved for v1.1 Supabase OAuth. Don't mix. +- WebSocket: same header works via `URLSessionWebSocketTask` custom request headers (not via `?token=` query). Open Q #7 resolved — update client code accordingly. + +## 4. Surfaces & endpoints + +| Surface | Endpoints (v1) | Notes | +|---|---|---| +| Home | `GET /mesh/tasks?status=running`, `GET /mesh/daemons`, `GET /directives?pending=true`, `GET /listen/sessions?limit=1` | Parallel fetch, 30s refresh, pull-to-refresh | +| Contracts list | `GET /contracts` | Filter: active / completed / archived | +| Contract detail | `GET /contracts/{id}`, `GET /mesh/tasks?contract_id={id}` | Shows phase, tasks | +| Tasks list | `GET /mesh/tasks` | Filter by status, contract | +| **Task detail (hero)** | `GET /mesh/tasks/{id}`, `GET /mesh/tasks/{id}/output`, `WS /mesh/tasks/subscribe` | Live output stream, markdown+code rendered | +| Directives | `GET /directives` | Read-only; badge for pending questions | +| Daemons | `GET /mesh/daemons` | Status list, read-only | +| Listen (history) | `GET /listen/sessions`, `GET /listen/sessions/{id}` | Transcript viewer, no live mic in v1 | +| Settings | `GET /auth/api-keys`, `POST /auth/api-keys/refresh`, `DELETE /auth/api-keys` | Server URL, key rotate, about | + +### 4.1 WebSocket: Task output livestream *(question 4)* + +Single WS: `wss://<server>/api/v1/mesh/tasks/subscribe` +Headers on the upgrade request: `x-makima-api-key: <full-key>` (same as HTTP — verified above). + +- Subscribes to all tasks the user has access to +- Server pushes task events: `task.output`, `task.status`, `task.completion_gate`, `task.supervisor_question` +- App maintains in-memory buffer per visible task, SwiftData-persists last 2000 lines on background +- Auto-reconnect with exponential backoff (1s → 30s cap), shown as pill in masthead +- When app is foregrounded, send `resync` frame with last-seen event IDs per task to catch up + +## 5. Notifications *(question 5 must-have)* + +**v1 (local only):** +- While app is open or recently backgrounded (iOS background WS grace ~30s), fire `UNUserNotificationCenter` local notifications for: + - Task completed + - Task failed + - Supervisor question arrived +- Notification taps deep-link to the relevant surface (`makima://task/{id}`, `makima://directive/{id}`) + +**v1.1 (push):** +- Small `services/push-proxy` service (own repo or monorepo folder) that holds APNs device tokens and subscribes to the same WS on behalf of users +- Server emits "notify" events; proxy fans out to APNs +- Matches Litter's `services/push-proxy/` pattern + +## 6. Aesthetic port *(question 5 must-have)* + +Goal: feel like a native extension of makima.jp, not a generic iOS app. + +- **Colors:** port exactly — `#0c1729` bg, `#9bc3ff` foreground, `#3f6fb3` accent, amber `#f59e0b` for alerts. Central `Palette.swift`. +- **Typography:** SF Mono for all chrome (nav, labels, ticker), SF Pro for body. Uppercase + tracked for nav labels (matches NavStrip). +- **Chrome elements to port:** + - `NAV//` prefix strip → iOS tab bar replaced with a custom masthead bar (top), tab-bar-less + - Dashed borders (`border-dashed` in Tailwind → `StrokeStyle(dash: [4, 4])` in SwiftUI) + - Masthead ticker → a thin top bar with build version + WS status + server label +- **Logo:** reuse the existing logo from the web frontend — `frontend/public/logo/makima-logo.svg` (concentric rings). Copy into `apps/ios/Sources/Makima/Resources/Logo/` at M1, render as SwiftUI `Image` with SVG support via a tiny helper (SwiftUI 18 has native SVG; fallback = rasterize to PDF asset at build time). Also port `crane-logo-transparent.png` for the Soryu masthead credit if the design calls for it. + - `JapaneseHoverText` → `JapaneseLongPressText` on iOS (long-press reveals English). Same term list: 支配する, 命令, 契約, 史料, 聴取, etc. +- **Motion:** respectful — fades and springs only. No parallax, no bounce. + +## 7. Markdown & code rendering *(question 5 must-have)* + +- `swift-markdown-ui` with custom theme matching web: inline code in `#1a2840` pill, code fences with language label chip, tables scroll horizontally +- Syntax highlighting via Splash (lightweight) or Runestone (full editor; overkill unless we add editing later) +- Task output stream is treated as a sequence of markdown chunks with optional `<COMPLETION_GATE>` blocks rendered as a distinct status card (not raw XML) +- Links are tappable; `file://` or repo-relative links show a "not supported on mobile" toast rather than erroring + +## 8. Data model (client-side SwiftData) + +``` +ServerProfile { url, label, apiKeyRef, lastSeenAt } +Contract { id, name, phase, status, autonomous_loop, updated_at } +Task { id, contract_id?, parent_id?, status, plan, daemon_id?, updated_at } +TaskEvent { id, task_id, kind, payload_json, ts } // bounded by task, prune >2k +Daemon { id, status, last_heartbeat, hostname? } +Directive { id, title, status, has_pending_question, updated_at } +ListenSession { id, title?, started_at, ended_at?, transcript_preview } +``` + +Everything else (file contents, full transcripts, mesh details) = fetch on demand, don't cache. + +## 9. Repo layout — as a folder inside `soryu-co/soryu` + +``` +soryu/ # existing monorepo root + apps/ + ios/ # <— NEW + project.yml # XcodeGen source + Makefile # ios-device-fast, ios-sim-fast, bootstrap, lint + Sources/ + Makima/ + App/ # MakimaApp, entry, scene + Design/ # Palette, Typography, Components (DashedBorder, MastheadBar, JapaneseLongPressText, Logo) + Net/ # APIClient, WebSocketClient, AuthStore, ServerProfileStore + Stores/ # HomeStore, ContractsStore, TasksStore, DirectivesStore, DaemonsStore (all @Observable) + Features/ + Home/ # HomeView + section cards + Contracts/ + Tasks/ # TasksListView, TaskDetailView (hero), OutputStreamView + Directives/ + Daemons/ + Listen/ # history only in v1 + Settings/ + Onboarding/ + Models/ # Contract, Task, TaskEvent, Daemon, Directive, ListenSession + Markdown/ # Theme, code block, completion gate block + Resources/ + Logo/ # copied from ../../../frontend/public/logo/ + Assets.xcassets + Tests/ + MakimaTests/ # AuthStore, APIClient (URLProtocol stubs), WebSocketClient reconnect + docs/ + DEVELOPMENT.md + RELEASING.md + README.md # points back to repo-root LICENSE (MIT) + frontend/ # existing + makima/ # existing + k8s/ # existing + LICENSE # repo-root MIT (confirm exists; add if missing) + README.md # add "apps/ios — Makima iOS client" line +``` + +## 10. Milestones + +| M | Scope | Done-when | +|---|---|---| +| M0 | Repo scaffold, XcodeGen, CI (GitHub Actions `xcodebuild`), empty app launches | App opens to "Hello, Makima" on sim + device | +| M1 | Design system: Palette, Typography, DashedBorder, MastheadBar, Logo (Canvas), JapaneseLongPressText | Standalone previews render; aesthetic matches web side-by-side | +| M2 | Onboarding + ServerProfile + Keychain API key + APIClient + auth probe | Paste key, see 200 from `/mesh/daemons` | +| M3 | Home composite view (static → wired) | 5 cards render from live endpoints, pull-to-refresh | +| M4 | Contracts list + detail, Tasks list + detail (polled output first, no WS yet) | Can drill from Home → Contract → Task and see output | +| M5 | WebSocket livestream + markdown/code rendering + completion gate card | Task detail updates without polling; completion gate shows as status card | +| M6 | Directives read-only, Daemons read-only, Listen history | All nav items reachable | +| M7 | Local notifications + deep links | Foregrounded WS fires notifications that deep-link correctly | +| M8 | Polish pass + TestFlight | Review-ready build, screenshots, privacy copy | + +## 11. Decisions locked (2026-04-24) + +| # | Decision | +|---|---| +| License | **MIT**, carried by repo-root `LICENSE` in `soryu-co/soryu` | +| App Store seller | **Soryu LTD** | +| Bundle ID | **`co.soryu.makima`** (no `.ios` suffix) | +| Supabase OAuth | Deferred to **v1.1** | +| Logo | **Reuse** existing `frontend/public/logo/makima-logo.svg` (no redraw) | +| Privacy policy | Point to repo README (KittyLitter-style) | +| Auth header | `x-makima-api-key: <mk_…>` — **verified** against `src/server/auth.rs` | +| Push-proxy (v1.1) | `soryu-co/soryu` repo, new folder **`services/push-proxy/`** | +| Repo location | Folder **`apps/ios/`** inside existing `soryu-co/soryu` monorepo | + +All blockers resolved. M0 (scaffold + XcodeGen + CI) is unblocked. + +--- + +## Cross-checks against user preferences + +- ✅ Zero-friction auth: single paste, then nothing. Supabase OAuth queued for v1.1. +- ✅ Customizable API URL: single-profile today, multi-profile-ready internally. +- ✅ Aesthetic-forward: dedicated M1 before any feature work. +- ✅ License-clean: concentric-circle logo redrawn in Canvas, no character likeness — matches "symbolic/abstract" preference. No copyrighted assets. +- ✅ Pure native: zero JS, zero RN, zero Expo, zero Rust-UniFFI. |
