Repository navigation
iPhone: open on the last truth, from a sealed home file (WP4) - #35
Conversation
A cold start drew a grey copy of Now and filled it in one part at a time: only the six readings reached the disk. The app now keeps the rest of what a screen shows in a sealed home-<site> file, and every screen opens on it in its final shape, dated, until the box answers. - Stamped<T>: a value with when it was true and where it came from (cache, relay, cloud). The same value again changes nothing. - home-<site>: what hello said, the planner's preferences, the plan, the forecast, the prices, the chargers with their goals and windows, and Now's nodes. Written on the snapshot's triggers, only when it differs. - Session.restore applies the kept hello with the readings in one change, and leaves heardFromBox false. Controls are drawn from the kept caps and role, and stay inert until the box has spoken. - The plan keeps its age until the box answers; "No plan" only after an answer. Before any answer with nothing kept, the line is a placeholder. - plan- and prices- are read back from the App Group when the home file has none. Prices are kept over midnight. - The car keeps its last charger watts, with their age; an old read never changes the live home figure. - FTW Cloud's prices and chargers reach the models as stamped values. - Cached readings age against the uptime they were taken on, so hello no longer moves their age. - The demo plans once a quarter, as Core does, and -FTWDemoCache YES (Debug) lets it keep its cache, to record and time a cold start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 679e4e16d4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if let kept = site.kept?.chargers, kept.asOfMs >= shared?.record.readAtMs ?? 0 { | ||
| stamp = Stamped(kept.value.points, asOfMs: kept.asOfMs, origin: .cache) | ||
| windows = kept.value.windows | ||
| readAtMs = kept.asOfMs | ||
| } else if let read = shared { | ||
| stamp = Stamped(read.value.cars.map { Self.loadpoint($0, readAtMs: read.record.readAtMs) }, asOfMs: read.record.readAtMs, origin: .cache) |
There was a problem hiding this comment.
Merge newer widget readings into the sealed charger cache
When the App Group CarEntry is newer than kept.chargers—for example, after a Cloud loadpoint update lands between sealed-cache writes—this branch discards the sealed Loadpoint objects and their cached schedule, plan windows, and other charger details. CarEntry contains only the reduced widget fields reconstructed by loadpoint(_:readAtMs:), so the next cold launch loses the goal and charging plan until the box API responds. Preserve the sealed details and overlay the newer dynamic widget fields instead of choosing the widget record wholesale.
Useful? React with 👍 / 👎.
| guard let taken = Stamped.taking(Stamped(next, asOfMs: cloud.asOfMs, origin: .cloud), over: stamp) else { return } | ||
| stamp = taken | ||
| readAtMs = taken.asOfMs |
There was a problem hiding this comment.
Carry Cloud freshness into charger stamps
When a Cloud loadpoint message is accepted here, its CloudView grade is discarded, and stale later considers the resulting .cloud stamp live whenever the unrelated relay session is streaming. If the Cloud copy subsequently ages or an heartbeat marks it behind while relay telemetry remains connected, the car can continue showing old power/SOC as “Live” and may use that power to split the live house load until another /api/loadpoints read occurs. Retain the Cloud loadpoint grade or otherwise age .cloud stamps independently of the relay phase.
Useful? React with 👍 / 👎.
Why
On 0.3.0 Fredrik wrote: "It feels too blinky when it loads data from scratch; it needs to be softer." And: "Surely it should already have the data when you go back into the app a second time?"
Only the six Now readings reached the disk. A cold start drew a grey copy of Now, then filled it in one part at a time: "No plan from your box yet", then the plan, then the car's windows, then the price.
This is WP4 of the polish release, the first step of the cache work. WP5 (sealed history tiles and the live ring, asks keyed on the data's age, redial at
.inactive) comes next and is not in here.What changes
Stamped<T>(State/Stamped.swift): a value, when it was true, and where it came from: cache, relay or cloud. One rule intaking(_:): the same value again changes nothing; the box confirming what the cache said moves only the origin and the age; an older copy never replaces what the box said.State/HomeCache.swift,home-<site>): what hello said (caps, modes, the box's id, build and zone, role, scopes), the planner's preferences, the plan, the forecast, the price window, the chargers with goals and windows, and Now's nodes in order. Same key and seal as the readings. Written on the same triggers (every 15 s while streaming, on inactive, on background), and only when it differs. Each model adds its own part withsite.keep. Removing the phone erases it with the rest.Session.restore(_:hello:)applies the kept hello and the readings in one change and leavesheardFromBoxfalse.ModeModel.mayControldraws a control from the kept caps;canControlalso needsheardFromBox. The mode button on Now, Change in a node view, the mode row in the plan view, and the car view's level, goal and Plan · Charge now · Pause bar are drawn in place and do nothing until the box has spoken.PlanShare.moment(nowMs:)decides this for Now and the plan view.plan-andprices-are read back from the App Group, ascar-already was.NowFlow.readings(site:loadpoints:)). The car comes off the house figure only when both are as old, so an old read never changes a live figure.PriceModelandLoadpointsModelasStampedwith origin.cloud. The relay's answer replaces them only if it differs.taking. The plan widget still gets the new time the box confirmed its plan (PlanModel.received).appproto.Plan.UptimeMsis when the plan was made). Debug only:-FTWDemoCache YESlets the demo keep its sealed cache, so a cold start can be recorded and timed.Evidence
Tests.
swift test: FTWShared 105 and FTWKit 286, all green. New inHomeCacheTests.swift:hello_ok;.cloud.I removed the inert gate, the equal-arrival rule, the midnight rule and the cached clock one at a time; each made its test fail.
NowFitsTestson an iPhone SE (3rd generation), iOS 26.5: 8 of 8 pass, so Now still fits with the plan card's two kept lines.Recording. A cold start of the demo in the simulator (iPhone 17 Pro, iOS 26.5). Left: main. Right: this branch with
-FTWDemoCache YES.Full recordings: main, this branch, side by side.
One frame every 0.1 s from the first frame of the app. Top, main: blank, then a grey placeholder for four frames, then the home with "No plan from your box yet" for three, then the plan without the car, then the plan with the car and the price. Bottom, this branch: the whole screen from the cache in the first frame, then one change to live.
The cached first frame beside the live one:
LaunchClock. Debug build, simulator, 10 cold launches of each, run in turns, machine load average 20 to 70. Milliseconds after process start, medians:
Content now comes in the first frame, 370 ms sooner. That frame comes 130 ms later because it draws the whole of Now instead of a placeholder: reading and decoding the home file takes 0.4 ms (Mac test), and the empty-cache row shows the keychain read costs nothing that shows. With its cache the demo goes live about 200 ms later. Its box runs in the same process, so the handshake waits for that first full frame; a real box waits on the network. This needs a measurement on a phone against the home box.
Not in here
CLAUDE.md: the rules this adds (open on the kept home, controls inert until hello) can go in with WP2's update, which rewrites the same section.Overlap
PR #27 (plan dial, draft) touches PlanView, PlanShare, PriceModel and SimulatedHistory. When it rebases: build the plan view's moment with
models.planShare.moment(nowMs:)instead ofPlanGlance.live(entry, nowMs:), and keepPriceModel.windowas the stored value (pricesandfromMsnow read from it).🤖 Generated with Claude Code