Repository navigation
feat(cloud): draw FTW Cloud's history tiles while the box is away (C6) - #98
Merged
Merged
Conversation
FTW Cloud keeps the box's signed 5-minute tiles of the last 48 hours
(srcfl/ftw docs/protocol-cloud.md, tiles). The phone checks each with the
key the box gave it over Noise, lays the pieces back together as Go's
cloudwire.Lay does, and asks with tiles.get once per cloud session while
the box session cannot answer. Closed tiles are the box's bytes, so they
share the tiles file. The line under the chart dates the copy ("From FTW
Cloud · up to 17:15"); the box's own answer replaces it the moment it
comes. cloud-tile-vectors.json from srcfl/ftw replays here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-FTWDemoBoxAway YES (Debug only): the demo box names its cloud key in hello, streams for three seconds without answering hist.query, then goes. SimulatedCloud runs the app session over loopback with the real Noise handshake and answers tiles.get with tiles the demo box signed, so the picture shows the real path: verify, lay, draw, dated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A long sum under the chart wraps into the second reserved line. With the date first, that cuts the sum, never the line that says the history is a copy and how old it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sum under the chart counted every step after the copy's last bucket as "not recorded": at 16:25 with a copy up to 16:20 it said "10 min not recorded", though the box may well have recorded them. The copy says nothing about that time, so the history leaves it out; holes inside the copy still count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 23, 2026
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: 42488d9c94
ℹ️ 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".
… the first After a relaunch with the box still away, the tiles file already held the copy's tiles, FTW Cloud's answer brought nothing new, and the date line went. Until the box answers, an answer that holds the tiles shown now dates them as the copy. lay and the tile store kept the first closed tile they saw. The box sends a closed tile again when its ledger changed under it, so the later one now replaces it, as srcfl/ftw#1817's Lay does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg
added a commit
that referenced
this pull request
Oct 23, 2026
FTWKit's tiles (#98) read their body with Fields, texts, Header and WireError beside the app's models. With CloudWire in FTWWire those need package access, whichever of the two merges first. The two merged build and pass 861 tests; FTWApp.swift then keeps both debug lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e box wins - check takes the four series and no other, as srcfl/ftw#1817's Check. - The tile store keeps a closed tile the box sent later over one it sent before, whatever order they come in. - A closed tile from FTW Cloud never replaces one the box sent itself, only an earlier one from the copy. - After a relaunch, what is shown is dated as the copy only when the copy reaches as far; the box's own cached history is not labelled as it. - A test ties HistoryModel.series to CloudWire.tileSeries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg
added a commit
that referenced
this pull request
Oct 23, 2026
FTWApp.swift keeps both debug lines: the watch row's `-FTWWatchKeyOffered` and the demo's `-FTWDemoBoxAway`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg
added a commit
that referenced
this pull request
Oct 23, 2026
…Wire #98's CloudTiles.swift read its bodies with CloudWire's helpers, which #99 moved into FTWWire. The wire part follows them: the tile messages, `CloudWire.Tiles`, `lay`, and the history types they build on (`Resolution`, `missingSample`, `HistQuery`, `HistChunk`, `HistEnd`, `HistoryGeometry`). `CloudTileStore` stays in FTWKit, and so does the plan, whose charger names FTWKit's `Loadpoint`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg
added a commit
that referenced
this pull request
Oct 23, 2026
* Split the wire into FTWWire, which the watch can link The watch must soon speak the box's wire itself, but the wire lived in FTWKit, which the watch may not link. FTWWire is a new library target that holds the wire and nothing else. It depends on FTWShared alone and builds for iOS, macOS and watchOS 26. Moved with git mv into Sources/FTWWire: - Crypto: Noise, Primitives, Transport - Protocol: CBOR, Frame, Messages, and Contract and Cars, which Messages needs - Carrier: CloudWire, CloudView - Identity: CloudKeys (CloudIdentity is part of hello_ok) - Support: Bytes, Base64url, and JSON, which Cars needs Cars.swift is split. The car and charger values the box sends stay in FTWWire; the car's words, CarLevel, Car.level and Car.shownName stay in FTWKit as Format/CarText.swift. Messages no longer reads the main-actor Session: the 8 MiB answer cap is now APIRequest.maxBytes, and Session.apiMaxBytes reads it. FTWKit depends on FTWWire and re-exports it with @_exported import, so no app, widget or test source needs a new import. Internals FTWKit uses are now package. Tests that reach wire internals add @testable import FTWWire. The FTWWatch target links the FTWWire product. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(watch): the watch on its own through FTW Cloud With the phone away, the Apple Watch reads its home from FTW Cloud and charges the car through the box's exchange, each one HTTPS request (srcfl/ftw ADR 0009, "Through FTW Cloud"). The relay that was to carry the exchange stopped on 17 October 2026. - FTWWire: CloudOnce (the one-shot request), ExchangeSession (the box's exchange, the cmd the phone's car view sends, the box's answer) and WatchCloud (one read as the watch's records, never newer or more live than the box graded it). Swift replays srcfl/ftw's once, once-x and exchange vectors byte for byte. - The watch: CloudPath and PathChooser behind ReadPath and CommandPath. The phone when it answers; FTW Cloud once on each appearance when it does not, and for a command the phone has not taken within 3 s, under the same cmdId. WatchReach says whether commands can leave only when that changes, so the buttons hold still while the paths change places. A receipt the box confirmed stands against the other path's failure. The mode and "Which car?" stay with the phone. One background read per complication refresh, no timer while on screen. - The phone: the watch offers its public key over WatchConnectivity; an owner adds it with a passkey step-up (Settings, Apple Watch), and the context carries the watch's link. - Tests: CloudOnceTests, ExchangeTests, WatchCloudTests, WatchReachTests; watch UI test testChargeNowThroughFTWCloud against a local FTW Cloud and box (skipped without FTW_RIG_ARGS); phone UI tour of the watch row. Pairs with srcfl/ftw (wire and box) and srcfl/ftw-cloud. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Open CloudWire's body helpers to the package FTWKit's tiles (#98) read their body with Fields, texts, Header and WireError beside the app's models. With CloudWire in FTWWire those need package access, whichever of the two merges first. The two merged build and pass 861 tests; FTWApp.swift then keeps both debug lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(watch): a command keeps its path until the box acks, and never runs twice after a restart The review of #99 found the watch's command rules spread through two views of the paths, where no test could reach them. They now live in the package: - WatchRoute (FTWShared) picks the path. A command stays the phone's until the box's ack comes back through it. When the phone says it could not send it, FTW Cloud carries it under the same cmdId (S1). - WatchCommander (FTWWire) runs one command through the box's exchange. Once a command has left, it ends in the box's answer or "no answer"; "too late" is the box's E_CMD_EXPIRED or a command that never left (S3). A new session whose hello_ok clock is behind the first one's comes from a box that restarted, and the command is not sent again (S5). 403 before sending asks for the watch to be added again; 429 is asked again a second later (N4). - An answer the build cannot read has its own step, and "no answer" keeps the box's ack (N3). - A command's life and every wait run on a monotonic clock (N5). - A tapped command cancels a read under way (N6), and a first refused read waits 30 s, not 15 min (N4). Tests run both against a fake box and service: silence, a lost answer, a restart, 403 before and after sending, 429, expiry before and after sending, an unsettled command, and the phone's failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(watch): watchOS accepts the complication's background read watchOS rejected the `energy.ftw.cloud-read` app refresh because the watch app's Info.plist did not list it, so a complication never read FTW Cloud in the background. The simulator log now shows the task registered instead of "Registration rejected". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(wire): FTW Cloud's tiles and the history chunk move into FTWWire #98's CloudTiles.swift read its bodies with CloudWire's helpers, which #99 moved into FTWWire. The wire part follows them: the tile messages, `CloudWire.Tiles`, `lay`, and the history types they build on (`Resolution`, `missingSample`, `HistQuery`, `HistChunk`, `HistEnd`, `HistoryGeometry`). `CloudTileStore` stays in FTWKit, and so does the plan, whose charger names FTWKit's `Loadpoint`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.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.
Step C6 of ADR 0008, the app's part. While the box cannot answer, a node's history comes from FTW Cloud's copy of the box's signed 5-minute tiles. It is dated, never shown as live.
Paired with srcfl/ftw#1817 (wire format and box) and srcfl/ftw-cloud#48 (the service). The vectors in
Resources/cloud-tile-vectors.jsoncome from srcfl/ftw7dedcbf4.What changes
Carrier/CloudTiles.swift:CloudWire.Tiles(decode,check()),tilesGet(have:),tiles.end, andlay(_:), a port of Go'scloudwire.Lay. It also addsCloudTileStore, which keeps the checked tiles for 48 hours in the order the box sent them.CloudCarrier.tilesGet(have:)sendstiles.get.SiteModel:tilesandtiles.endskipCloudView.accept's seq rule, since tiles are history, not the newest of a class.check()before it is kept.HistoryModel: when the box session does not stream and the cloud session with the copy is open, it asks FTW Cloud once per cloud session. It sendstiles.getwith the closed tiles it holds ashave.tiles-<site>file. Cache first still draws frame one.-FTWDemo YES -FTWDemoBoxAway YESruns a demo box that streams for 3 s and then goes away. Its history then comes through a simulated FTW Cloud, over the real Noise, signature,checkandlaypath. I used it for the screenshots.Widgets: the grid widget does not use cloud tiles. It shows the box's own daily totals and cost from
/api/savings/daily, which nothing can provide while the box is away. Mixing a cloud chart with no totals would break the widget's rule that every number is the box's.NodeView.swift, which draft #93 also touches, gets three added lines inPeriodWords.summaryand no other change.Merge order with #99: #99 moves
CloudWire.swiftinto a newFTWWiremodule.Carrier/CloudTiles.swifthere extendsCloudWireand uses its internalFieldsandtexts. Whichever merges second movesCloudTiles.swift's wire part (Tiles,check,lay,tilesGet) intoFTWWirebesideCloudWire.swift, or makes those two members public.Tests
swift testinappleApp/FTWKit: 578 tests in 86 suites pass.CloudTilesTests:check;laygives Go's exact tile id, etag and data in all five cases;tiles.getandtiles.endbytes match Go's;checkrefuses 11 wrong shapes.CloudHistoryTests:layand in the store.Screenshots, box away
Taken in a simulator (iPhone 17 Pro, iOS 26.5, Debug) with
-FTWDemo YES -FTWDemoBoxAway YES. They are not attached here; they are in the FTW-stack shared notes folder,cloud-c6-shots/:solar-48h-light.png: "From FTW Cloud · up to 16:25" above "48 h: made 20 kWh". The header says "AS OF 16:27" and the footer "NOT CONNECTED".grid-today-dark.png: dark mode, the Today view.battery-48h-sv-default.png: Swedish at the default text size. Both lines fit.battery-48h-sv-xxxl.png: Swedish at the largest text size. The date line comes first and stays whole.The sum leaves out the time after the copy ends (42488d9). It used to count those minutes as "not recorded", although the copy says nothing about them.
After review
checktakes the four series and no other. A test tiesHistoryModel.seriestoCloudWire.tileSeries.🤖 Generated with Claude Code