Skip to content

Claude/cleanup standalone app hvijkh - #4

Open
Yloft17 wants to merge 895 commits into
muftiarfan:mainfrom
Yloft17:claude/cleanup-standalone-app-hvijkh
Open

Claude/cleanup standalone app hvijkh#4
Yloft17 wants to merge 895 commits into
muftiarfan:mainfrom
Yloft17:claude/cleanup-standalone-app-hvijkh

Conversation

@Yloft17

@Yloft17 Yloft17 commented Aug 6, 2026

Copy link
Copy Markdown

No description provided.

NoopApp added 30 commits June 26, 2026 18:13
…ft pretty-print

Swift JSONEncoder .prettyPrinted emits "key" : value (space-colon-space); the
Kotlin emit() used "key": value. Switch the Kotlin object separator to " : "
so the two platforms' meta.json bytes line up, as asserted by the parity test.
Empty-object {} and array branches are unaffected.
…y pass

The Android Sleep mode emitted no trace: IntelligenceEngine called
AnalyticsEngine.analyzeDay with no traceSink, so detectSleep's gate trace and
the Rest sub-score line never reached the bundle. Mirror the iOS wiring: thread
an optional sleepTraceSink through analyzeRecent/analyzeRecentOnCpu (Context-free,
default null = byte-identical untraced path) and pass it into analyzeDay. The two
Context-aware callers (AppViewModel 15-min loop, WhoopBleClient post-backfill pass)
build the sink ONLY when TestCentre.active(SLEEP), routing each line to the
.sleep-tagged strap log via externalLog/log. Zero-cost when off.
…ding line

emitBatteryTrace (iOS) and estimateTrace (Android) had no caller outside tests, so
the discharge-run / fitted-slope / gate trace never reached the bundle; only the
per-reading soc line emitted. Add a gated caller on each platform, on a natural
throttle, never a tight loop. iOS: call emitBatteryTrace from bankBatterySample,
right after the per-reading bank line, under the same Battery-mode gate (the strap
banks at most one SoC point per ~8 min). Android: emit estimateTrace(...).second
through the .battery-tagged externalLog inside the TodayScreen battery LaunchedEffect
(the Today-badge refresh that already loads samples + rated, so no extra Room read).
Both pure: estimateTrace returns the same Estimate the badge shows, so no displayed
number changes. Zero-cost when the mode is off.
The Test Centre gated the experimental 5/MG block on the pref alone
(selectedModelName == WHOOP5_MG.name), but the Settings screen it re-hosts uses
pref OR live.whoop5Detected. Add the || live.whoop5Detected clause (collect the
live snapshot) so a live-detected 5/MG whose pref is not yet written still sees
the block, matching SettingsScreen.kt showFiveMGControls.
previewText showed only report.txt, but the assembled bundle also carries meta.json
and (Android) last-crash.txt, so the user confirmed a share without seeing those.
Concatenate every text-decodable entry, each under a === <name> === header, in bundle
order, on both platforms. The bounded raw-capture stream is excluded (binary, already
PII-scrubbed by the assembler, not a report surface). report.txt body still renders, so
the existing contains() preview tests stay green.
…t Centre

The iOS Test Centre Advanced section shows the Continuous-HRV toggle; the Android
twin omitted it though Android already stores the key. Add it to the AdvancedCard,
bound to the SAME NoopPrefs key (noop.continuousHrv) and the same write path
(vm.setContinuousHrv, which persists and re-applies keep-stream-for-data) the
Settings card uses, so toggling from either screen is one and the same setting.
Append connection, workouts, display, import, steps, recovery and hrv to
TestModeRegistry.all in screen priority order (after sleep; battery keeps its
slot). All are plain toggles with the spec section-4 captures, one-line
questionnaire and live-readout ids; Display carries includesScreenshot. Update
the registry tests to pin all 9 entries: order/ids/count, per-profile captures,
questionnaire id+kind, live-readout ids, priorities, toggle kind and the
screenshot/5MG flags, plus an em-dash guard on every blurb and prompt.
Mirror the Swift registry entries byte-for-byte (same ids, titles, captures,
questionnaire ids+kinds, live-readout ids, priorities, toggle kind). Android
icons map to ic_* drawable ids; the dataImport entry uses TestDomain.IMPORT
(wire id 'import'). Extend the parity test to all 9 entries with the matching
order/ids/count, capture sets, questionnaire id+kind, readouts, priorities,
toggle and screenshot/5MG assertions, plus the em-dash guard (escape literal).
…lytics)

Add the two Phase-2 Test Centre engine traces, both pure and side-effect-free,
both proven to leave the engine numerics byte-identical:

- RecoveryScorer.recoveryTrace: the Charge term-breakdown. Rebuilds the four-plus-one
  weighted terms (hrv/rhr/resp/sleepPerf + skinTempDev) from the SAME inputs recovery()
  reads, emits each baseline state (mean/spread/nValid/status), each term z*weight, the
  nil terms that forced the renorm, the composite z, and the final logistic score + band.
  The returned score IS RecoveryScorer.recovery verbatim, so the trace cannot diverge.
- HRVAnalyzer.analyzeTrace: the cleaning trace. Recomputes nInput/nClean/rejected
  fraction, the range + Malik ectopic counts, the minBeats gate and the spot
  rejected-fraction gate, with the result read from analyze() verbatim.
- TestReadout: pure parsers for the last Charge breakdown / last HRV computation from
  the tagged log tail, for the iOS live-readout panels.

Tests pin the lines a fixture produces and assert traced == untraced output, the nil
term naming, the cold-start nil reason, and no em-dashes.
- IntelligenceEngine: read TestCentre.active(.recovery) once before the scoring loop;
  when on, emit each night's Charge term-breakdown via the new recoveryTraceLines helper
  (which feeds the SAME inputs recomputeRecovery does into RecoveryScorer.recoveryTrace),
  tagged .recovery. Zero work when off; the Charge score path is byte-identical.
- HRVSnapshotView.finish: when TestCentre.active(.hrv), emit the cleaning trace tagged
  .hrv and take the result from analyzeTrace (which reuses analyze verbatim). When off,
  the plain analyze path is untouched.
- TestCentreView: add RecoveryReadoutPanel (lastChargeBreakdown) and HrvReadoutPanel
  (lastHrvComputation) alongside the Sleep/Battery panels, same ReadoutRow tokens, no
  hardcoded colours.
Kotlin twins of the StrandAnalytics traces, byte-aligned with the Swift line shape:

- RecoveryScorerTrace.recoveryTrace + HrvAnalyzerTrace.analyzeTrace: same term-breakdown
  and cleaning-trace output, each reusing RecoveryScorer.recovery / HrvAnalyzer.analyzeRaw
  verbatim for the returned value so the engine numerics stay byte-identical.
- TestReadout: the lastChargeBreakdown / lastHrvComputation tagged-tail parsers.
- IntelligenceEngine: add a recoveryTraceSink param (default null = byte-identical path);
  when non-null each scored night emits its Charge breakdown via recoveryTraceLines.
- AppViewModel + WhoopBleClient: pass recoveryTraceSink only when TestCentre.active(RECOVERY),
  routing lines to the .recovery-tagged strap log (log()/externalLog PII-scrub).
- HrvSnapshotScreen: when TestCentre.active(HRV), emit the cleaning trace tagged HRV and
  take the result from analyzeTrace; the plain analyzeRaw path is untouched when off.

Tests are pure-JVM (no Robolectric/Mockito) and assert traced == untraced output.
…panel)

The Connection & Sync test mode, wired through the proven Sleep/Battery/Recovery
pattern: pure StrandAnalytics formatters/parsers, BLE/offload emitters gated
zero-cost behind TestCentre.active(.connection), and an in-app readout panel.

- ConnectionReadout.swift (StrandAnalytics): pure ConnectionTrace line formatters
  (the CLOCK-DRIFT summary = strap-reported [oldest, newest] vs wall clock with a
  FUTURE-DATE flag, the firmware-layout line, the trim=0xFFFFFFFF no-cursor sentinel)
  plus the ConnectionReadout tagged-tail parsers for connectionUptime / reconnectCount
  / lastOffloadResult. No clock, no IO, no PII.
- BLEManager: emit connect timing + uptime marker (didConnect), bond-state transitions
  (both genuine-bond sites), reconnect churn count+reason (didDisconnect / didFailToConnect),
  the failed-encrypt / other-central hint (bond-write refusal), and the upfront clock-drift
  summary in the GET_DATA_RANGE handler. Every site reads the .connection gate BEFORE
  building any string; the connect/bond/offload behaviour is unchanged.
- FrameRouter: one frame-timing line per genuine frame-TYPE transition (the existing
  change-guard throttles the raw flood), gated zero-cost.
- Backfiller: a cheap connectionActive() gate + connectionLog sink (default inert) so the
  offload-progress / firmware-layout / no-cursor lines build only when the mode is on.
- TestCentreView: ConnectionReadoutPanel alongside the others, same ReadoutRow tokens,
  no hardcoded colours.
- Tests: pure ConnectionReadoutTests (formatters + parsers) and a StrandTests gating proof
  that a handled frame emits ZERO .connection lines with the mode off and exactly one with
  it on (FrameRouter), plus the type-transition throttle.
NoopApp and others added 30 commits July 3, 2026 21:38
The whole-day HR view (Deep Timeline) still existed but was buried; make the live
HR card a one-tap route into it with a visible 'Full day' affordance. And the new
Today only showed strap battery %, never charging state, so add a bolt on the
battery ring + '% . Charging' in the data-sources row, matching Mac/Android.
Two data-corruption fixes from pipiche38's real Gen 3 captures, reimplemented
clean on both platforms. (1) The 0x6F SpO2 per-sample decoder was adding a scaled
base/status nibble to each sample, producing impossible ~223% readings; the
samples are direct percentages, so discard the base field. (2) unixSeconds() only
guarded ms > 0, so a corrupt ring timestamp on a full history dump could bank a
1970 or far-future sample; gate the result to the same 2020-2035 plausible window
used for anchoring. Thanks pipiche38.
…p (#983)

An HC-only user (no strap) had no sleep at all: the importer collapsed each
SleepSessionRecord to a per-day minute total and never wrote a SleepSession row,
so the Sleep screen (which reads repo.sleepSessions) fell to its empty state.
Now also build SleepSession rows (bounds + per-stage minutes -> stagesJSON, same
shape as the CSV/Xiaomi importers) under WHOOP, only for days the strap does not
already cover so a real night is never shadowed. Thanks Retsim.
resolveDayOwner issued a per-day LIMIT-1 HR presence probe per paired device, even
on the common single-WHOOP install where the owner is a foregone conclusion. Short-
circuit when there is exactly one live device that is the fallback id (byte-identical
in that case), saving ~one tiny read per scanned day per analyzeRecent. Thanks MrL0
for the read-efficiency writeup.
The Deep Timeline was clamped to the shown day; older data was only reachable via
the day-stepper. Widen the pan/zoom clamp to a rolling 3-day window (the shown day
plus the two before it) so a left drag scrolls continuously back, deliberately
bounded so one drag can't fling through weeks. The data reload keys on the visible
window, so panned-to days load. Both platforms. Thanks ryanbr.
11 fixes on top of the Liquid Metal launch — carry-vitals + Rest honesty, the
iOS widget refresh, the Haptic Clock gaps, the Android bond-loop bound, HC-only
sleep sessions, the Oura SpO2/timestamp fixes, the full-day HR tap-through,
charging on Today, the Deep Timeline 3-day scroll-back, and two perf trims.
Version 8.0.1 (iOS build 168 / Android versionCode 257).
Live Sessions: a silence-first strap coach. A recovery-gated HR band
(Charge scales a Karvonen %HRR ceiling), two-signal haptic vocabulary
(2 light taps = push, 3 heavy = ease off), warm-up/climb grace, dwell +
cool-down hysteresis, never-fabricate guards (stale stream = grey ring,
coaching pauses). Pure engine + haptics + store in packages with Kotlin
twins pinned by identical golden vectors; runner/UI on all 3 platforms
behind a beta toggle (default on).

Fixes:
- #991 Android: HR + Last Workouts cards no longer overlay their own
  section headers (Box -> spaced Column at the call sites).
- #992 iOS: Liquid Today battery row shows the ~X-days-left estimate
  again (verbatim TodayView.estimateText).
- #1000: backups carry a settings.json entry (whitelisted profile +
  unit prefs, byte-compatible container, legacy zips unchanged);
  restore re-applies after a successful DB swap only. iOS folder
  picker starts at a sensible directory + alerts on a nil selection.
- Live5RestFrozenTests: banked regression test for the #977 root cause.

Verified: 1,382 Swift package tests + 2,258 Android JVM tests green;
Release builds green for Strand (macOS), NOOPiOS, and Android.
- Android resolver + habitual-midsleep + Sleep screen join the union
  model: imported history no longer freezes Sleep tile / Vitality /
  Fitness age / Rhythm / coach on the import's last day (#1008 ryanbr,
  #1009, #1014-secondary; + tombstone-first delete parity)
- #993: bare sleep aggregates (HC bedtime spans) no longer override a
  scored night; reconciled with the #547 imports-correct guarantee
  (bare loses only to a genuinely scored night)
- CPS wheel event time 1/2048s + kind-flip baseline reset, both
  platforms (#1007 ryanbr)
- Backup integrity: pre-swap quick_check, post-swap open-verify with
  auto-rollback, non-deleting Android DatabaseErrorHandler (#1014)
- Backfiller: insert-then-archive order parity (#1006 ryanbr) +
  future-dated ranges stop auto-continue (#1015 ryanbr)
- Observability: real Test Centre storage numbers + inactive-mode
  warning (#1002), cumulative drained-rows counter + clock-drift fix
  (#990), clock-latched readout + ~1970 RTC warning (#987), SpO2
  research dump in Connection mode (#945 ryanbr)
- Deep Timeline: sleep band + workout markers + iPhone touch scrub
  (#979 follow-ups)
- Liquid tilt settles level when lying down (#1004); Today HR window
  selector on Android (#985 ryanbr); hydration card live-updates (#989)
- Full German incl. Terms gate (#984 eltomato89); Oura activity/MET
  decode (#960 pipiche38); perf batch byte-identical w/ pins (#995
  #996 #997 #999 ryanbr)
- 'reopen Strand' codename leak fixed + codename guard in the
  artifact script

Verified: 1,529 Swift package tests + 2,337 Android JVM tests green;
Release builds green for Strand (macOS), NOOPiOS, and Android.
…d history

WHOOP exports are onset-to-onset (cycle_start = the sleep onset evening), so
keying imported daily metrics / series / stress off cycle_start filed each night
under the day before. For an import-only user with no live strap, Today had no
row under the current logical day and blanked every score except Stress once the
day rolled. Key cycles off the wake (wake_onset, then cycle_end, then start),
matching the live engine / mergeSleep wake-day convention. Both platforms; the
Android sleep-fold already keyed wake-day so this reunites the two on one row.

Also: the timestamp heal purged any computed/imported row below the 2023-11
strap floor, silently deleting multi-year WHOOP import history on any heal.
Scope the far-past floor to computed (-noop) rows only; imports (bare my-whoop)
keep their real dates. Future-dated rows still purged for all sources.

Extract the keying rule into StrandImport.WhoopDayKeying for an executable pin.
Tests: WhoopDayKeyingTests, importer-preservation heal test, real onset-to-onset
Android regression; repinned fixtures that used the old same-day shortcut.

Verified: 1529+ Swift tests, Android unit suite, macOS + iOS Release builds all green.
…crash-loop

#1014 (wanxorg): 8.2.0's CorruptionPreservingOpenHelperFactory stopped Android
silently deleting a corrupt database (which was wiping non-resendable strap
history), but it copied the file to *.corrupt and LEFT the original in place, so
the next open re-hit the same corruption and the app crash-looped on startup
until a reinstall. onCorruption now MOVES the original aside (rename, or copy+
delete across a storage boundary) and drops the WAL/SHM sidecars, so the next
open finds no file and Room rebuilds a clean store: the app always opens, and the
corrupt copy is preserved for recovery. iOS unaffected (GRDB has no equivalent
preserve-in-place handler; DatabaseIntegrity is a quick_check diagnostic only).
…ecord

Replace the single blanket acknowledgment checkbox with FOUR separate, conspicuous
attestations that must each be ticked before Accept enables: (1) not a WHOOP
employee/contractor/affiliate and not acting on WHOOP's behalf, (2) own device,
own data, at own risk, and may breach WHOOP's ToS, (3) unofficial/experimental/
as-is/no-warranty/not-medical, (4) liability waiver to the fullest extent the law
allows. Separate knowing consents read stronger than one blanket box.

Record the accepted version AND a local timestamp (the on-device consent record).
Bump Terms version 1.1 -> 2.0 so every existing user re-accepts. TERMS.md §7
rewritten to the itemised attestations, §8 reconciled. Both platforms in parity
(TermsGateView.swift / TermsGate.kt), informational points kept as context above.

Non-English locales show the attestations in English (the binding wording);
translations are a courtesy follow-up. NOTE: the exact clause phrasing should get
a solicitor pass before the next release ships (flagged in code).
…ecord

Replace the single blanket acknowledgment checkbox with FOUR separate, conspicuous
attestations that must each be ticked before Accept enables: (1) not a WHOOP
employee/contractor/affiliate and not acting on WHOOP's behalf, (2) own device,
own data, at own risk, and may breach WHOOP's ToS, (3) unofficial/experimental/
as-is/no-warranty/not-medical, (4) liability waiver to the fullest extent the law
allows. Separate knowing consents read stronger than one blanket box.

Record the accepted version AND a local timestamp (the on-device consent record).
Bump Terms version 1.1 -> 2.0 so every existing user re-accepts. TERMS.md §7
rewritten to the itemised attestations, §8 reconciled. Both platforms in parity
(TermsGateView.swift / TermsGate.kt), informational points kept as context above.

Non-English locales show the attestations in English (the binding wording);
translations are a courtesy follow-up. NOTE: the exact clause phrasing should get
a solicitor pass before the next release ships (flagged in code).
Removes everything that only existed for public distribution to many
users (update checker, donation/crypto UI, in-app changelog history,
distribution tooling, community/support docs) and all support for
devices other than WHOOP and the Apple Watch (Oura, Huami/Amazfit/
Xiaomi, generic BLE HR straps, FTMS gym equipment), including the
Android app entirely. macOS, iOS, and watchOS apps are kept, along
with full WHOOP 4.0/5.0 and Apple Watch/HealthKit support.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fn4rJ9z1dn5afNREphUFF
The gravity-magnitude expression's nested optional arithmetic was too
complex for the Swift type-checker to resolve in reasonable time,
failing CI. Pre-existing on main, unrelated to the device-cleanup
work in this PR — breaking it into named sub-expressions fixes it
without changing behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fn4rJ9z1dn5afNREphUFF
…ice support

BLEManager's own bandwidth-fallback path (standardHRMode/standardHRFallback)
decodes the strap's standard BLE Heart Rate characteristic (0x2A37) via
StandardHeartRate.parse — this was wrongly deleted alongside the generic
third-party HR-strap support because of the name overlap. Confirmed via
`git grep` that WhoopStore's StandardHRMapping (which *was* third-party-only,
used solely by the deleted HuamiHRSource/StandardHRSource) is unrelated and
stays deleted. Fixes the "cannot find 'StandardHeartRate' in scope" macOS
build failure in CI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fn4rJ9z1dn5afNREphUFF
connectedPeripheralChanged still calls isWhoop(activeId) with a String,
but I'd deleted the instance overload when simplifying
activeDeviceChanged, leaving only the static isWhoop(_ device:
PairedDevice) — which doesn't type-check against a String and can't be
called on an instance either way. Restores the instance method exactly
as before. Fixes both macOS and iOS app-build failures in CI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fn4rJ9z1dn5afNREphUFF
Strip app down to a personal WHOOP + Apple Watch companion
…noop

All six Xcode targets (Strand, StrandTests, NOOPiOS, NOOPiOSWidgets,
NOOPWatch, NOOPWatchComplications) previously used the com.noopapp.*
identifier family and group.com.noopapp.noop app group inherited from
the project's public-distribution days. Xcode's automatic signing kept
reconciling these against the wrong values locally.

Updates project.yml's bundleIdPrefix, APP_GROUP_ID, every target's
PRODUCT_BUNDLE_IDENTIFIER, WKCompanionAppBundleIdentifier, and the
BGTaskSchedulerPermittedIdentifiers entry, plus every hardcoded Swift
fallback/constant that mirrored the old values (WidgetSnapshot,
WatchScoreSnapshot, ScheduledDebugExport, StorePaths,
RawHistoryArchive, NoopLocalAccessCore) and their tests, the two
xcodegen-managed Info.plist files, and doc references.

Per-target suffixes (.widgets, .watch, .watch.complications,
.strandtests) are preserved since iOS requires each embedded
extension/companion app to have a distinct bundle id from its parent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fn4rJ9z1dn5afNREphUFF
Personal-only app, so drop identity artifacts left over from when this
was a public, community-run project:

- Delete LICENSE and NOTICE (a distribution license/copyright notice
  serves no purpose for a personal, non-distributed build; both also
  linked to DISCLAIMER.md/ATTRIBUTION.md, already deleted).
- Remove ProjectInfo.contactEmail (the old support inbox) and the
  Support screen's "Get in touch" contact card.
- Repoint the in-app bug-report deep link from the old NoopApp/noop
  GitHub org to this repo.
- Delete docs/SAFEGUARDS.md (entirely about the old public repo's
  GitHub-suspension/donation/mirror concerns; every tool/doc it
  references was already removed).
- Trim docs/IOS.md: drop the AltStore/SideStore sideload+distribution
  section and the "fold-in" section's community-PR/anonymization
  history, which no longer apply.
- Strip individual former-contributor name credits from code comments
  (PR# references kept as plain internal history markers) and drop
  dead org/issue links in docs/WHOOP5_DEEP_DATA.md and
  tools/linux-capture/README.md.

Left untouched: the WHOOP-protocol reverse-engineering attributions
(johnmiddleton12/my-whoop, b-nnett/goose) — unrelated third-party
research this app's protocol code depends on, not "the previous team."

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fn4rJ9z1dn5afNREphUFF
…l edits)

Companion to 245f9f8 (which deleted LICENSE/NOTICE/docs/SAFEGUARDS.md):

- Remove ProjectInfo.contactEmail (the old support inbox) and the
  Support screen's "Get in touch" contact card.
- Repoint the in-app bug-report deep link from the old NoopApp/noop
  GitHub org to this repo, and update its test.
- Trim docs/IOS.md: drop the AltStore/SideStore sideload+distribution
  section and the "fold-in" section's community-PR/anonymization
  history, which no longer apply.
- Strip individual former-contributor name credits from code comments
  (PR# references kept as plain internal history markers) and drop
  dead org/issue links in docs/WHOOP5_DEEP_DATA.md and
  tools/linux-capture/README.md.
- README: drop the License section and ATTRIBUTION.md link (both
  pointed at now-deleted files).

Left untouched: the WHOOP-protocol reverse-engineering attributions
(johnmiddleton12/my-whoop, b-nnett/goose) — unrelated third-party
research this app's protocol code depends on, not "the previous team."

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fn4rJ9z1dn5afNREphUFF
Every token that carried functional color now sits on one continuous
violet -> purple -> fuchsia -> pink hue path (cooler/violet reads
calm/good, warmer/pink reads hot/urgent), with lightness shifting in
step with hue on every ramp since hue alone is unreliable at this end
of the wheel for red-green colorblind viewers. Structural chrome
(surfaces, text, the titanium ramp) stays strictly black/white/gray
with a whisper of cool violet in the undertone.

Public API is unchanged (property names frozen per the file's own
convention) - only the hex literals and their describing comments
moved. Classic mode now mirrors Titanium's values 1:1, since the old
red->green throwback scale can't exist inside a pink/purple-only
system.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fn4rJ9z1dn5afNREphUFF
The app had four separate clock-driven decorative backgrounds layered
on top of each other: LiquidSky (a twinkling starfield + hour-cycled
sky gradient behind ~20 screens and Liquid Today), SceneScreenBackground
(hand-picked day-cycle illustrations behind classic Today), TimeOfDayBackground
(a night sky with stars/moon behind the Sleep hero card), plus the
already-flat ScenicHeroBackground. All of them repainted themselves as
the clock advanced.

Replaces all of it with one AppBackground: a static pink/purple/baby-blue
gradient mesh, full-bleed edge-to-edge, for light mode only (dark mode
keeps the existing flat surfaceBase canvas, untouched). No clock, no
TimelineView, no per-frame cost — same look every render, morning or
night. ScreenScaffold now paints this as the single background for every
screen instead of a 240pt top band that faded into the flat canvas below.

Deletes LiquidSky.swift, SceneHeroBackground.swift, and
TimeOfDayBackground.swift outright, and the now-meaningless "Day-cycle
background" Settings toggle (SceneBackgroundPrefs) along with it, since
there's no more day-cycle to toggle.

Left untouched: ScenicHeroBackground (DomainTheme.swift) — a local,
non-time-based, domain-tinted backdrop behind individual hero cards on
8 detail screens, not the page-wide background this task targeted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fn4rJ9z1dn5afNREphUFF
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.

3 participants