Repository navigation
Apple Watch: Now, the plan dial on the Crown, the car, the price and complications (W1) - #43
Conversation
…complications (W1) A watch app and its complications, reading and commanding through the iPhone. watchOS gives an ordinary app no WebSocket (TN3135), so the phone hands the watch the records its widgets read after each cache write, sends Now's frames while the watch looks, and runs each command over its own session. FTW Cloud never controls. - FTWWatch: Now (lit only while a live frame is fresh, ink with its time otherwise), the plan dial with the Crown stepping by quarter hour, the car with Charge now, Pause and Back to the plan, the price, and the mode. Each command shows SENT ▸ ACCEPTED ▸ CONFIRMED from the box's answers. - FTWWatchWidgets: the dial as accessoryCircular, plus rectangular, corner and inline, and a RelevanceConfiguration card for the cheapest hours. - Built to stand alone: its own cache, its own key, its own cmdId, and two seams (ReadPath, CommandPath). PhonePath is the only path today. - Phone side: WatchBridge in the app, WatchShare in FTWKit, the wire in FTWShared/Watch. Session.command takes the watch's cmdId. - Each colour set gains a watch variant equal to its dark one, since watchOS compiles no dark appearance; ThemeTests holds them equal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, the chart small and growing (WP7) (#44) * iPhone: history and ahead on the engine, the chart small and growing (WP7) Today and 48 h draw from HistoryPicture: a fixed day with 30-minute slots, 48 h with 60-minute slots, each slot the mean of the box's five-minute means through Steffen, with its band. One stable scale for the chosen series. Holes on their way are hatched with a dashed estimate; holes with no record are a dotted baseline. AS OF marks old data. A tile that lands moves each slot to its new mean. Ahead draws from AheadPicture: plan quarters as ghost bars, forecasts as a dashed line, the price strip under the same slots. Fredrik's note 16: the chart is compact and a tap zooms it to the whole screen. Chips add or take away series, kept per viewer per node; the last one stays and a hidden series changes nothing the others say. The scale re-fits as one spring. Size caps for the largest text move to where each view is placed, so @ScaledMetric sees them; this fixes the price and node heroes too. NodeCharts, ChartMotion and ChartScrub are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * iPhone: every power trace in the live trace's material (WP7) Fredrik on 0.3.5: the live view feels right, so every other chart should feel and look the same. Live, Today and 48 h, small and enlarged, now draw through one place, TraceInk: the same band, body, 2.5 pt line, and glow only while live (on Today, only the slot still filling). Live uses ChartAxes and NodeChartLook like the rest. SlidingSurface gives history and Ahead Live's motion: a change for the same slots slides over 0.25 s, a new scale or a series coming or going springs over 0.35 s, with the same ease. AheadPicture.blended moves a new plan or forecast slot by slot. Under a finger the hero follows on Today and 48 h, as on Live, and the line under the chart says the energy so far or between, from the same functions. The dot takes its side's colour, the span tag gives the average, and nothing dims behind the finger. History's landmarks are the high, the low, now and each end of a run of data, as on Live. The debug finger reads the newest track; HistoryTour starts on Live. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- 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: fa9a2330d4
ℹ️ 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 nowArrivedAtMs == nil || (ctx.now?.readAtMs ?? 0) > (now?.readAtMs ?? 0) { | ||
| now = ctx.now | ||
| nowArrivedAtMs = nil | ||
| relight() |
There was a problem hiding this comment.
Reset live readings when the home changes
When a context for a different home arrives while nowArrivedAtMs is non-nil, this comparison can retain the previous home's now record whenever the new home has no reading yet or an older timestamp. Because nowArrivedAtMs is not cleared when the glow expires, the Now page can display another home's readings indefinitely after switching homes; detect a home change and replace the transient reading regardless of timestamps.
Useful? React with 👍 / 👎.
| let site = reader.latestSiteID(PlanEntry.self) ?? reader.latestSiteID(PriceEntry.self) | ||
| guard let site else { return (nil, nil, nil) } | ||
| return (reader.read(PlanEntry.self, siteId: site), reader.read(PriceEntry.self, siteId: site), reader.read(WatchNow.self, siteId: site)) |
There was a problem hiding this comment.
Scope complication reads to the active home
After switching homes, WatchCache.save leaves the previous home's per-site files in the App Group. If the new context has prices or Now but no plan yet, this unconditional preference for the latest PlanEntry selects the old home's site and all three complication records are then read from that site, potentially showing the previous household indefinitely. Persist or read the active context's home identifier instead of inferring it from whichever plan file exists.
Useful? React with 👍 / 👎.
| private func receipt(_ r: WatchReceipt) { | ||
| let target = Self.target(r.op) | ||
| guard let mine = receipts[target], let next = mine.taking(r) else { return } | ||
| receipts[target] = next |
There was a problem hiding this comment.
Restore the cached receipt without requiring in-memory state
On a cold launch, receipts is empty, so the final receipt carried in WatchContext reaches this guard without a matching in-memory tapped receipt and is discarded. This defeats the context's stated purpose of showing how a command ended when the watch slept or was terminated before the result arrived; a trusted context receipt needs to seed the target when no local receipt exists.
Useful? React with 👍 / 👎.
| let prices = Kept.read().prices | ||
| let nowMs = Date.now.timeIntervalSince1970 * 1000 | ||
| let window = WatchGlance.cheapWindows(prices?.value, nowMs: nowMs).first { $0.startMs == configuration.startMs } | ||
| let fetched = prices.map { PriceGlance.moment($0, atMs: nowMs).fetchedText } |
There was a problem hiding this comment.
Keep the configured cheap window for its full interval
During an active cheap window, recomputing cheapWindows with the current time removes elapsed slots, so after the first slot ends the best block starts later and no longer matches configuration.startMs. The entry then becomes nil and the Smart Stack card is redacted for the remainder of the relevance interval even though the configuration also carries the original end time; reconstruct or look up the configured interval without filtering away its start.
Useful? React with 👍 / 👎.
| private func keep(_ ctx: WatchContext) { | ||
| guard let cache else { return } | ||
| Task { await cache.save(ctx) } |
There was a problem hiding this comment.
Serialize context cache writes
Every received context starts an independent task, while WatchCache.save suspends between several record writes. Rapid contexts can therefore interleave, allowing an older task to write its records or final watch-context file after a newer task, so complications or the next launch regress to stale state. Queue these saves or discard writes older than the latest accepted context.
Useful? React with 👍 / 👎.
The first FTW app for Apple Watch (W1 in the watch plan). Fredrik asked on 14 Oct 2026 for "the plan, control and so on" on the wrist.
What the watch does
○ PER 11:13). The status line uses the phone's words (● LIVE · FTW STYR)..selectiontick per quarter and a light tap at now, at the cheapest hours' edges and at the car's goal. The middle shows that quarter's time and price, and the line under it says what is planned. A tap brings the hand back to now. Quarters with no price yet are stubs, never a level.accessoryCircular(the price ring, the battery's action, the hand at now, and the battery's level in the middle while it is under 15 min old),accessoryRectangular(what happens next, block bars,PLANERAD 11:00),accessoryCorner(price now, "billigast nu") andaccessoryInline(price now). ARelevanceConfigurationcard, "Billigt nu", rises in the Smart Stack 15 min before each day's cheapest two hours left and stays until they end.Each command asks once more (the yes has
handGestureShortcut(.primaryAction)), then showsSKICKAT ▸ MOTTAGET ▸ BEKRÄFTAT. Every mark after "sending" comes from the box's own answers as the phone saw them. The watch can only say that it is sending, that the phone was out of reach, or that no answer came within 45 s. It can never mark CONFIRMED itself, and a test holds that.How it reaches the box
watchOS gives an ordinary app no WebSocket and no Network connection (TN3135), so in W1 the watch goes through the iPhone:
WatchBridgehands the watch the same records withupdateApplicationContext: prices, cars, plan, Now, the modes and the last receipt. Each keeps where it came from and when. They travel as JSON packed with LZFSE; the demo's context is about 2 KB.transferCurrentComplicationUserInfo.sendMessageData. The phone checks the home and the time, then runs it exactly as the car view or the mode sheet would, and sends each step back.Built to stand alone
Fredrik wants the watch to work fully on its own on LTE later. So W1 already:
WKRunsIndependentlyOfCompanionApp;kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly); nothing uses it yet;cmdId(UUIDv7). The phone sends the box that id, so when the relay exchange (ADR 0009) exists, a command tried both ways runs once;ReadPathandCommandPath.PhonePathis the only path now.CloudReadPathandDirectPathcome after the design docs for standalone reads; none of their network code is here.Outside the watch targets
FTWKit/Package.swift: FTWShared builds for watchOS 26.FTWShared/Watch/: the wire (WatchContext,WatchAsk,WatchNews,WatchReceipt), the dial's face on the WP1 engine (SlotGrid,ZoneClock), the watch's words and their Swedish.FTWKit/State/WatchShare.swift: the records from the models, and the command runner with its receipts.Session.command,LoadpointsModel(Charge now, Pause, Back to the plan,perform) andModeModel.settake an optionalcmdId. Every other caller is unchanged.FTW/Watch/WatchBridge.swift, two lines inFTWApp.swift, andIntentRunner.hold, so the watch and the intents share one count before the session closes.Theme.xcassets: watchOS compiles no dark appearance, and a watch is always dark. Each colour set gains awatchvariant equal to its dark one.ThemeTestsholds them equal and now also scans the watch folders for colours in code.LockOn.bracketsfrom iPhone: the node view streams, the live trace as its hero (WP6) #41.LiveSeriesdoes not fit Now's one figure per node, so the watch does not use it.RELEASING.mdlists the new bundle IDs.CLAUDE.md: the watch's scope, where its code lives, and the colour rule.Proof
swift test: FTWShared 172 and FTWKit 304 pass on the Mac. In the watchOS 27 simulator, FTWShared's 171 pass; the one that draws with AppKit builds only on the Mac.cmdIdand confirm in order.xcodebuildbuilds the iPhone app with the watch app and its complications inside, and theFTWWatchscheme alone.Recordings, from the same pair:
The Crown recording predates the switch to
LockOn's brackets; the still at 20:15 shows them.W0 in the simulator
.smallview in the watch's Smart StackNeeds a real Apple Watch
.requiresAuthenticationon the phone's Control blocks a tap on the watch while the phone is locked.Choices to look at
watchvariant sits in the same colour sets as the dark one, rather than a second palette. The alternative is to resolve colours by hand on the watch.PlanEntryonmainhas no forecast; iPhone plan dial: today and tomorrow round the sun's path, in the plan view and as a widget #27'sdaylightis not merged. When WP11's dial lands in FTWShared, the watch should draw that face and drop its own.energy.ftw.app.watchkitappandenergy.ftw.app.watchkitapp.widgetsneed the App Group on team L66M368PZ6. The release archive uses-allowProvisioningUpdates, which should register them. A local archive with the same flag does too.Not merged and not for merge yet.
🤖 Generated with Claude Code