Skip to content

feat(ios): N0 skeleton + N1 session logging, and a written Xcode handover - #30

Merged
lemmy-winks merged 4 commits into
mainfrom
claude/ios-redesign-xcode-plan-auqj8v
Jul 27, 2026
Merged

feat(ios): N0 skeleton + N1 session logging, and a written Xcode handover#30
lemmy-winks merged 4 commits into
mainfrom
claude/ios-redesign-xcode-plan-auqj8v

Conversation

@lemmy-winks

@lemmy-winks lemmy-winks commented Jul 26, 2026

Copy link
Copy Markdown
Owner

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 --volt token block in styles.css and 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/) — APIClient actor as the single server boundary, Codable mirrors of the payloads, and the unit layer + ISO week maths ported from web/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=1 makes /auth/callback finish on forge://auth?token=…. Same signed forge_session value the PWA's cookie carries — the app replays it as an explicit Cookie: header and stores no cookies. Needed because Google refuses OAuth in a WKWebView. The custom scheme needs no Info.plist entry: ASWebAuthenticationSession intercepts the redirect in-process, which is also why another app registering forge:// 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 /sets has 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:

  1. The app opened on last week. The client computed "today" locally; the server runs Europe/London. Across midnight they disagree. First load now sends no date and lets the server anchor it.
  2. Loads drifted off the prescription. 35 kg renders 77.2 lb and converts back to 35.02, so a set logged without touching the stepper stored 20 g off plan — confirmed in the database. Fixed by holding kg and stepping via Units.stepped; re-verified end to end (42.5 kg prescribed → 42.5 kg stored).
  3. Exercise names rendered as raw slugsfitted.targets are plan entries with no name.
  4. Resuming didn't rehydrate — backgrounding mid-workout restarted set numbering at 1 and wrote over logged sets.

The handover

ios/XCODE-HANDOVER.md is new and is the point of the middle commit. It reads the real state out of project.pbxproj and 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 (the forge:// scheme, and ATS vs localhost), and flags one live disagreement: deployment target is 26.5 in the project, iOS 18 in the plan doc — James's call.

ios/CLAUDE.md and 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/week renders on both phones. History, coach, and settings are the remaining N1 slices.

🤖 Generated with Claude Code

lemmy-winks and others added 2 commits July 26, 2026 16:30
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>
@lemmy-winks lemmy-winks changed the title feat(ios): Xcode project scaffold for the Forge app target feat(ios): N0 walking skeleton — auth, /api/week, Void × Volt design system Jul 26, 2026
lemmy-winks and others added 2 commits July 26, 2026 17:05
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>
@lemmy-winks lemmy-winks changed the title feat(ios): N0 walking skeleton — auth, /api/week, Void × Volt design system feat(ios): N0 skeleton + N1 session logging, and a written Xcode handover Jul 27, 2026
@lemmy-winks
lemmy-winks merged commit 4219819 into main Jul 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant