summaryrefslogtreecommitdiff
path: root/makima/ios/docs/ios-v1-plan.md
diff options
context:
space:
mode:
Diffstat (limited to 'makima/ios/docs/ios-v1-plan.md')
-rw-r--r--makima/ios/docs/ios-v1-plan.md222
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.