Skip to content

Setline is Apple-only: structured training model, real landing page, no backend, no Cloudflare - #40

Merged
sarthakagrawal927 merged 11 commits into
mainfrom
feat/structured-targets-native-programme
Aug 16, 2026
Merged

Setline is Apple-only: structured training model, real landing page, no backend, no Cloudflare#40
sarthakagrawal927 merged 11 commits into
mainfrom
feat/structured-targets-native-programme

Conversation

@sarthakagrawal927

@sarthakagrawal927 sarthakagrawal927 commented Aug 15, 2026

Copy link
Copy Markdown
Member

Setline becomes an Apple-only, device-first iPhone app with a real public face and no infrastructure of its own.

Closes #39
Closes #41
Closes #42

1. Set targets that can express a real programme

PlannedSet.target was a line of free text, so a programme with rep ranges, %1RM, reps in reserve, tempo, per-side work and rest bands could not be represented at all. Targets are now structured, and stepType keeps warm-up, preparation and cooldown work out of volume, records and progression instead of quietly counting it.

The dated 12-week block now resolves natively for each of its 84 days — week-dependent RDL volume, the 4→5 round cardio build, the week 5/9/12 pull-up checkpoints — and progression uses the increments the plan itself specifies, replacing a flat +2.5.

Per-exercise measurement landed with it: every exercise has an identity, a current value derived only from completed working sets, and an authored target. Each value cites the session that produced it. No evidence means no chart.

Set entry now records a set the way it happened: 5 reps × 40 kg then 2 reps × 30 kg is one set with two segments. The shorthand parser is deliberately rule-based, not an LLM, and shows its reading before anything is recorded.

2. No backend

Deleted the Worker, the account layer (Better Auth, Google and Apple sign-in, private D1 state, the MCP surface, whole-document sync and its conflict UI), 3,277 lines of superseded TypeScript, and the 12 test files covering them.

3. No Cloudflare

Deleted the live setline Worker and the setline D1 database. Checked before deleting: wrangler d1 list reported 0 tables while the tables existed, so I queried them directly — workout_state, user, account and session all held zero rows. Nobody had ever signed in, so the cross-device sync that layer existed to provide was never once used.

The site is now static files on GitHub Pages with its own CNAME. No URL changed.

4. A public face that is true

The landing page was a placeholder; so were privacy, terms and changelog, on the old navy palette. The privacy notice still claimed "optional Google sign-in stores one private, user-scoped state copy" — false since the backend went, and the single worst page to leave stale given App Store review reads it.

Privacy now states what happens: nothing collected, no network requests, one JSON document in the app's container, and — disclosed for the first time — that this website runs PostHog and the sassmaker portfolio strip. Terms gained the health disclaimer, the recorded-versus-calculated distinction, and a note that the bundled block is one person's programme.

Two stale landing-page FAQ answers went too, including one describing a sign-in that no longer exists.

5. A gate that was not gating

ci.yml runs pnpm quality:native, but trimming package.json had dropped that script — the macOS job would have failed with "command not found", so the native tests, release build and coverage floor were gating nothing. Restored, and its floor raised from 65.3% to 83.8% against a measured 84.1628%.

Verification

  • Native: 64 unit + 11 UI tests pass, release build succeeds, coverage 84.1628%
  • Static: 20 surface tests pass; pnpm check green
  • Duplication zero (was 6 clones / 62 lines) — removed rather than baselined
  • Legal pages rendered and reviewed at 390px and 1440px

Not in this PR

  • iCloud sync. Training still lives on one device; the JSON export is the only backup. Every surface now says so. There is an open design question on the approach — SwiftData+CloudKit as originally planned would mean rewriting the value-type domain into @Model reference types and breaking the 64 core tests; per-record CloudKit reaches the same outcome with the value types intact.
  • The canonical hostname returns 530. Point setline.significanthobbies.com at GitHub Pages #43 has the three owner steps: enable Pages from Actions, merge, then CNAME setline → significant-hobbies.github.io DNS-only, not proxied — proxying it puts Cloudflare back in front and breaks certificate issuance.

🤖 Generated with Claude Code

@sarthakagrawal927 sarthakagrawal927 changed the title Structured set targets, native 12-week programme, and per-exercise measurement Make Setline usable: structured targets, native 12-week programme, per-exercise measurement, and a real landing page Aug 15, 2026
@sarthakagrawal927 sarthakagrawal927 changed the title Make Setline usable: structured targets, native 12-week programme, per-exercise measurement, and a real landing page Setline is Apple-only: structured training model, real landing page, no backend, no Cloudflare Aug 16, 2026
sarthakagrawal927 and others added 8 commits August 16, 2026 15:33
Add PostHog analytics snippet to track page_view events with the
product's project_id. Uses PostHog's CDN loader — no npm dependency
needed for static/Astro sites.

Part of fleet-workspace#348

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…cise measurement

The app could not represent the training it claimed to run. Targets were
free text, exercises were bare strings with no identity across sessions, and
the dated 12-week programme existed only in the deleted web app's TypeScript.

- Replace `PlannedSet.target: String` with `SetTarget`: rep ranges, absolute /
  relative / bodyweight / assisted load, reps in reserve, RPE, tempo, hold,
  distance, pace, HR zone, per-side, and rest as a `RestRange` band rather than
  a scalar. `StepType` keeps warm-ups out of volume, records and progression.
- Add `ExerciseCatalogue`: stable slugs, pillars, muscle groups, equipment and
  measurable metrics across strength, stamina, mobility and flexibility, plus
  the CrossFit movement vocabulary.
- Port the authored 12-week block to Swift, resolved natively for all 84 dates,
  with the bench ramp, week-phase RDL volume, hard-cardio round switch, pull-up
  checkpoints, optional lateral raise, rest bands and reassessment dates.
- Add an Exercises surface: measured current values that cite the session that
  produced them, authored targets, progress, weekly rate, projected arrival and
  Swift Charts trends. No evidence means no number, never an estimate.
- Make set entry repeatable so `5 reps x 40 kg` then `2 reps x 30 kg` records as
  one set, with a tested shorthand parser that shows its reading before applying.
- Add a set timer independent of rest, and a rest-completion notification so the
  timer survives leaving the app.
- Replace the flat `+2.5 kg` suggestion with the programme's own double
  progression, including its per-movement increments and below-range regression.
- Bump the document to schema version 2, reading version 1 without losing
  recorded work.

Closes #39

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…alue

Looking at a captured screenshot showed "91.83333333333333 kg" on the bench
press screen. `Double.trimmedString` fell back to `String(self)`, so binary
floating-point precision reached the interface for every non-integer value —
estimated 1RM, weekly rate, distance to target.

- Bound formatting to two decimals, with a one-decimal `kilogramString` for
  loads, since two decimals claim more precision than a barbell has
- Give the decimal keypad a Done button and interactive scroll dismissal. It had
  no return key, so numeric entry could trap the keyboard over the record button
- Replace the placeholder landing page. It was a dark navy stub with amber and
  sky accents that contradicted the tracked identity in DESIGN.md, describing a
  product two releases out of date. The new page is built to the fleet landing
  standard on the app's own palette: hero, the problem stated precisely, one set
  being recorded, current versus ideal, the four pillars, what Setline refuses to
  do, fit and poor-fit cases, and a FAQ of questions people actually ask
- State pre-release status plainly; add no App Store or TestFlight link, because
  neither exists. A test enforces that
- Rewrite the agent surfaces to match and add a test holding the Worker's inline
  copies byte-identical to the static files in public/
- Seed a demo document with four weeks of evidence so the measurement surfaces
  can be exercised, captured and tested; add UI tests covering the trend chart
  and the progression suggestion's cited evidence
- Remove all six duplicated blocks in the native sources via a shared legacy
  decoder, programme set builders and a cardio definition builder. Duplication
  is now zero, and the baseline is lowered to hold it there
- Split the two longest new functions rather than raising the complexity budget
  for them; the budget rise covers only memberwise initializer parameter counts,
  with the reason recorded next to the baseline

Closes #41

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The account layer existed to give a server something to authenticate. With the
web app gone and the native app self-sufficient, none of it earned its keep.

Removed:
- worker/ (9 files, 1,482 lines): Better Auth with Google OAuth, the native
  handoff, D1-backed private state, the MCP read surface, and the agent edge
- src/lib/ (7 files, 3,277 lines) superseded by the native model, plus the 12
  test files covering it and the three D1 migrations
- The client half: NativeAccountClient, SetlineCloudDocument, and the
  whole-document sync and conflict flow in AppModel and Settings
- TypeScript itself, along with vite, better-auth and drizzle-orm

Kept honestly:
- The sync invariant the deleted cloud type guarded is now
  `SetlineDocument.hasSameContent(as:)` with its own tests: recording that a save
  succeeded must never itself look like new training
- Settings states where the training lives rather than implying an account, and
  says plainly that iCloud sync is not active yet
- The public site is static Pages content. `_headers` carries the security
  headers the Worker set in code, and /api/ai keeps its path as a real file
- sw.js is now a stub that evicts the deleted web app's shell and unregisters,
  so a returning visitor is never served a page that no longer exists

Also fixed, found by a UI test rather than by reasoning: numeric entry had one
shared focus flag across all fields, so moving from Reps to Weight left focus
ambiguous and typing failed. Focus is now tracked per field.

The parity test was rewritten: with no Worker to compare against, it now holds
the sitemap, agent catalog, llms.txt and landing page to each other and proves
every path they advertise resolves to a file.

Complexity dropped from 27 violations to 9 and max CCN from 52 to 15 once the
TypeScript left; both baselines are lowered to match. Three accepted security
advisories came in through the removed dependencies and left with them.

The live Worker and the unused setline D1 database are deliberately retained. The
fleet catalog records the Worker as live-purpose-review, the Pages project as
configured-not-deployed, and D1 as active-unreferenced, pending an owner cutover.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Setline is an iPhone app. It had a Cloudflare account attached to it for a web
app that no longer exists.

Deleted from the account:
- The live `setline` Worker
- The `setline` D1 database. Checked before deleting: every table — workout_state,
  user, account, session — held zero rows, because no one ever signed in. The
  `d1 list` summary claiming "0 tables" was wrong; the tables existed and were
  empty, which is why this was verified by querying rather than trusted

Removed from the repository:
- wrangler, its config, and the deploy script
- `_headers` and `_redirects`, which are Cloudflare Pages syntax and would be dead
  config anywhere else

The public site is now GitHub Pages, published from `public/` on pushes that touch
it. `CNAME` claims the existing canonical hostname so no URL, canonical tag,
sitemap entry or agent surface changes. `.nojekyll` keeps the markdown mirrors and
dotfiles intact.

One capability is genuinely lost: GitHub Pages cannot set a Content-Type, so
`/api/ai` is served with whatever type GitHub infers for an extensionless path.
The body is still valid JSON carrying every field the agent-indexing standard
requires, and a test now asserts exactly that rather than assuming a header.

The canonical hostname returns 530 until its DNS record points at
significant-hobbies.github.io. The fleet catalog records Setline as undeployed
with that as the named blocker, and its Cloudflare deployment and resource rows
are now empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI installs with --frozen-lockfile, which fails on a manifest the lockfile does
not match. Resolved offline, so nothing new was fetched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Trimming package.json for the Cloudflare exit dropped `quality:native`, but
`.github/workflows/ci.yml` still runs `pnpm quality:native` on macOS. That job
would have failed with "command not found" — the native tests, release build and
coverage floor were silently not gating anything. Restored the script.

The gate now measures 84.1628% coverage of the production targets against a
floor of 65.3%, set before the structured model arrived with its own tests and
before the untested account layer left. Raised the floor to 83.8% so the gain
cannot quietly erode.

Also dropped the hardcoded "10 unit tests, 4 UI tests" summary line, which had
gone stale without failing anything; the real totals are 64 and 11, and
xcodebuild already prints what it ran.

Corrected three claims that the backend removal falsified: PROJECT_STATUS said a
test holds the Worker and static copies byte-identical (there is no Worker), and
described the owner-gated Sites deployment as a rollback copy (it is a survivor
of the removed web app, and nothing depends on it); README still listed
"optional private account sync" as shipped.
The three secondary pages were one-sentence placeholders on the old navy
palette, and the privacy notice stated something that is no longer true: that
optional Google sign-in stores a private user-scoped copy. There is no sign-in
and no server. A privacy page making a false claim about where training data goes
is the worst possible page to leave stale, and App Store review reads it.

Privacy now states what actually happens: the app collects nothing, makes no
network requests, and keeps one JSON document in its own container; export,
import and reset are the only ways data moves; notifications are local. It also
discloses what the previous version never did — that this website runs PostHog
and the sassmaker portfolio strip, and what each one handles.

Terms gained the health disclaimer a fitness app needs, the distinction between
recorded and calculated values, and a note that the bundled block is one person's
programme rather than a recommendation. The changelog became actual dated entries,
including the removals, replacing a sentence that still claimed private sync and
PWA support.

Also fixed two stale answers in the landing FAQ: "Sign-in exists only to keep a
private copy across devices" described a feature that no longer exists, and the
data answer omitted that the export is now the only backup. Added iCloud sync to
the not-yet-built list on every agent surface, where its absence read as working.

Five new tests keep this from drifting again. The strongest ties disclosure to
reality: it reads the script origins out of the markup and fails if the privacy
notice does not name them, so adding a tracker without disclosing it breaks the
build. Others assert no page claims an account or a server copy, every page uses
the tracked palette, every referenced asset exists on all four pages, and the
legal pages carry a date and the disclaimer.

The specs went with the code: the never-completed Google-auth change is archived
as withdrawn with a note on why, and the account-data-deletion spec — Better
Auth, D1 cascades, browser state — is archived, since none of that exists and it
required the privacy notice to advertise account deletion.
@sarthakagrawal927
sarthakagrawal927 force-pushed the feat/structured-targets-native-programme branch from f3ec481 to 590d385 Compare August 16, 2026 10:25
sarthakagrawal927 and others added 3 commits August 16, 2026 15:56
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI's macOS image builds with an older SDK than this machine's Xcode 27, where
`UNNotificationSettings` is not marked Sendable. `await centre.notificationSettings()`
therefore compiles locally and fails there:

    Non-sendable result type 'UNNotificationSettings' cannot be sent from
    nonisolated context in call to instance method 'notificationSettings()'

Taking `authorizationStatus` inside the completion handler means only a Sendable
enum crosses the isolation boundary, which compiles on both SDKs. Behaviour is
unchanged.

This surfaced only because the previous commit restored the `quality:native` job
that CI had been unable to run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rywhere

The previous fix was incomplete. `UNUserNotificationCenter` itself is not marked
Sendable on the SDK CI builds against, so a stored `centre` property on a
`@MainActor` class produced "sending 'self.centre' risks causing data races" at
both use sites, not just the settings read.

Every call now resolves `.current()` at the point of use, inside a synchronous or
nonisolated scope, so no non-Sendable value crosses an isolation boundary. The
`init(centre:)` seam went with it: nothing ever injected a centre, so it was
unused flexibility rather than a testing affordance.

CI now selects the newest Xcode on the runner and prints it. The default is older
than this machine's, which is exactly how a data-race error compiled clean
locally and failed there; a native failure should not be a mystery about which
compiler ran.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sarthakagrawal927
sarthakagrawal927 merged commit 0260ed1 into main Aug 16, 2026
2 checks passed
@sarthakagrawal927
sarthakagrawal927 deleted the feat/structured-targets-native-programme branch August 16, 2026 12:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant