feat(ios): N0 skeleton + N1 session logging, and a written Xcode handover - #30
Merged
Merged
Conversation
N0 groundwork — the Xcode-side setup that only James can do in the UI, committed so agent sessions have a project to build against. - App, unit test, and UI test targets; bundle IDs under com.get-forged - iOS 26.5 deployment target, matching the installed simulator runtimes - File system synchronized groups, so new .swift files on disk are picked up without touching project.pbxproj (per ios/CLAUDE.md) - Stock SwiftUI template ContentView, replaced as soon as N0 screens land Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…system The whole N0 loop runs end to end in the Simulator: sign in, session in the Keychain, authed /api/week rendered in both themes. Design (Forge/Design/) — Void × Volt and Paper × Moss ported verbatim from the --volt token block in styles.css, scoped to a module theme environment so Cook and Shop can each own an accent without ever mixing. DIN Condensed for display and large numerals, SF for body, per the type bake-off. Kit (Forge/Kit/) — APIClient actor as the single server boundary; Codable mirrors of the /api/week payloads; the unit layer and ISO week maths ported from web/src/api.ts. Train — week view Mon–Sun with paging capped one week ahead, today focus, missed-day flags, dangling-session banner. Read path only; logging is N1. Server: GET /auth/login?native=1 makes /auth/callback finish on forge://auth?token=… instead of /. The token is the same signed forge_session value the PWA's cookie carries, so there is no second auth surface — the app just replays it as a Cookie header and stores no cookies itself. Google refuses OAuth inside a WKWebView, so ASWebAuthenticationSession (and therefore a custom scheme callback) is the only workable native path. The browser flow is untouched; tests pin that the flag is per-request and can't leak into it. Two things worth calling out: - The client never computes "today". The server runs on Europe/London and the phone may not; across midnight the app opened on last week. First load now sends no date and lets the server anchor it — caught by looking at the running app, not by a test. - Modules are folders in the app target rather than local SPM packages under Packages/. Linking a package means editing project.pbxproj, which is the Xcode-UI work agents must not do; extraction is a file move later, when a Watch or widget target actually needs to share the code. Tests: 24 Swift Testing cases (units, lipids, plate steppers, week snapping, the BST midnight bug), 65 server tests green including 5 new native-auth ones. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Agents kept having to infer what was configured and what wasn't. XCODE-HANDOVER.md reads the real state out of project.pbxproj (no team, no entitlements, Swift 5.0, deployment target 26.5) and lists every remaining Xcode-UI and developer-portal item with what it unblocks and how to verify it. Also records the two things that look like handover items but aren't: the forge:// scheme needs no Info.plist entry (ASWebAuthenticationSession intercepts it in-process), and ATS doesn't block http://localhost, so the dev loop needs no exception. Both verified in the Simulator. Flags one live disagreement for James to settle: deployment target is 26.5 in the project, iOS 18 in the plan doc. ios/CLAUDE.md and the plan doc now point at it, and the plan doc defers to it on current state. Adds the iOS CI workflow from plan §0.3.3 — macos-26, simulator, no signing — which is agent-side work, not handover. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The make-or-break screen. Day detail (/api/today) with plate math, last-time and trim notes; a logging screen with DIN steppers, rest clock, RPE, and a running tonnage; SwiftData offline queue behind it. Logging is optimistic: a set the user just did is a fact, the network isn't. It lands in local state immediately and either reaches the server or waits in the queue. Flush semantics are ported from web/src/queue.ts — offline or 401 keeps everything, 5xx keeps that set and continues, anything else drops it so a poisoned set can't wedge the queue. /sets has no server-side idempotency key, so the queue dedupes on (sessionId, slug, setNo), the same triple the edit endpoint uses. Three bugs found by driving the running app against a local server, none of which the tests would have caught: - Loads were round-tripping through the display unit. 35 kg renders 77.2 lb and converts back to 35.02, so a set logged without touching the stepper stored 20 g off the prescription — confirmed in the DB, then fixed by holding kg and stepping via Units.stepped. Re-verified end to end: a fresh set now stores 42.5 kg against a 42.5 kg prescription. - Exercise names rendered as raw slugs. fitted.targets are plan entries with no name; they now come from the day payload's library-resolved names, with a title-cased slug as a last resort. - Resuming a session didn't rehydrate. Backgrounding mid-workout restarted set numbering at 1 and wrote over what was already logged. Also diverges one ported string deliberately: the plate line now ends in "kg". The web omits it, but with unit_load defaulting to lb, "Per side: 5 + 2.5" under "77.2 lb" reads as pounds. Tests: 41 Swift cases (plate math incl. the greedy fill and unreachable remainders, queue dedupe, the lossy-round-trip regression), 65 server tests still green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Takes the iOS app from an empty Xcode template to a working Train client, and writes down exactly what's left for James in Xcode.
Everything here was built and visually verified in the Simulator against a local sqlite server — sign in, week, day detail, log sets, resume.
N0 — walking skeleton
Design system (
Forge/Design/) — Void × Volt (dark) and Paper × Moss (light), ported verbatim from the--volttoken block instyles.cssand scoped to a module theme environment, so Cook and Shop can each own an accent later without two ever landing on one screen. DIN Condensed for display type and numerals, SF for body.Kit (
Forge/Kit/) —APIClientactor as the single server boundary, Codable mirrors of the payloads, and the unit layer + ISO week maths ported fromweb/src/api.ts.Train read paths — week view Mon–Sun with paging capped one week ahead, today focus, missed flags, dangling-session banner; day detail with plate math, last-time, and what the time budget trimmed.
Server change:
GET /auth/login?native=1makes/auth/callbackfinish onforge://auth?token=…. Same signedforge_sessionvalue the PWA's cookie carries — the app replays it as an explicitCookie:header and stores no cookies. Needed because Google refuses OAuth in aWKWebView. The custom scheme needs noInfo.plistentry:ASWebAuthenticationSessionintercepts the redirect in-process, which is also why another app registeringforge://can't steal the token. The browser flow is unchanged and tests pin that the flag can't leak into it.N1 — session logging
The make-or-break screen: DIN steppers, rest clock, RPE, running tonnage, and a SwiftData offline queue behind it. Logging is optimistic — a set the user just did is a fact, the network isn't. Flush semantics are ported from
web/src/queue.ts; since/setshas no server-side idempotency key, the queue dedupes on(sessionId, slug, setNo), the same triple the edit endpoint uses.Four bugs found by running the app, not by testing it
Worth reading, because in each case the suite was green:
Units.stepped; re-verified end to end (42.5 kg prescribed → 42.5 kg stored).fitted.targetsare plan entries with noname.The handover
ios/XCODE-HANDOVER.mdis new and is the point of the middle commit. It reads the real state out ofproject.pbxprojand lists every remaining Xcode-UI and portal item with what it unblocks and how to verify. It also records two things that look like handover items but aren't (theforge://scheme, and ATS vslocalhost), and flags one live disagreement: deployment target is 26.5 in the project, iOS 18 in the plan doc — James's call.ios/CLAUDE.mdand the plan doc now point at it. Also adds.github/workflows/ios.yml(macos-26, simulator, no signing) — agent-side work, not handover.Tests
41 Swift Testing cases and 65 server tests, all green. Notable: the greedy plate fill, unreachable plate remainders, queue dedupe, and a regression test pinning why loads are held in kg.
Not done
Signing, capabilities, Swift 6, and the device install — all in the handover file. N0 is not signed off until
/api/weekrenders on both phones. History, coach, and settings are the remaining N1 slices.🤖 Generated with Claude Code