summaryrefslogtreecommitdiff
path: root/makima/ios/docs/ios-v1-plan.md
blob: 876a6ff439ecec54ca69bdb99b99c0b7b916dc8c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
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.