Repository navigation
Conversation
The plan as a round dial, from Fredrik's film (srcfl/sourceful-films, product/ftw-confirmed), in the app's own tokens: - FTWShared/PlanDial: the sun's path (midnight at the bottom, noon at the top, up on the left), a quarter per bar from today's midnight to the end of tomorrow, the battery's action per quarter, the car's quarters, the sky from the box's own solar forecast, the finger's turn across midnight and VoiceOver's hour stops. Worked out once per plan; tested. - FTWCharts/PlanDialView: the drawing, shared by the app and the widgets (later the Watch). Canvas layers with a stable id per quarter: the bars redraw when the plan or the quarter changes, the hand by the minute, the mark as the finger moves. New bars grow in with the charts' spring; still under Reduce Motion and in Low Power Mode. Its own dark glass. - Plan view: the dial at the top in place of the 12-hour lanes. A finger dragged round it scrubs the plan, .selection per quarter, .impact(light) at now and the cheapest window's edges; the line under it says what is planned there. VoiceOver: one adjustable element, an hour a step. - Widget energy.ftw.plan-dial: systemSmall (home screen and StandBy) and accessoryCircular (lock screen: today's ticks, the hand, the battery). - PlanEntry carries the forecast's daylight and a live battery level, both optional (old records still read); the widget's record now holds today and tomorrow. PriceModel says when it is fetching. - The demo plans as far as its prices reach, so tomorrow fills at 13:00. - CLAUDE.md: "Plan dial" under Design references. 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: d5033d69d4
ℹ️ 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".
| guard !face.tomorrowPriced, face.quarters.contains(where: { $0.turn == 0 && $0.priceMinor != nil }) else { return nil } | ||
| var calendar = Calendar(identifier: .gregorian) | ||
| calendar.timeZone = face.timeZone | ||
| let early = calendar.component(.hour, from: Date(timeIntervalSince1970: nowMs / 1000)) < 13 |
There was a problem hiding this comment.
Align the price refresh with the 13:00 message
When a price-consuming view remains active across 13:00, this switches to “Tomorrow's prices are not in yet,” but PriceModel.wanted does not change from pending to published until its existing publishHour = 14. Since LiveAsk does not repeat an unchanged request, no new price request is made during that hour, so the dial can claim prices are late while it has not attempted to fetch them. Use the same publication boundary for both the note and the refresh key.
Useful? React with 👍 / 👎.
| let middle = calendar.date(byAdding: .day, value: 1, to: today).map(ms) ?? start + 86_400_000 | ||
| let end = calendar.date(byAdding: .day, value: 2, to: today).map(ms) ?? middle + 86_400_000 |
There was a problem hiding this comment.
Fetch the full calendar span on fall-back days
When today or tomorrow contains a fall-back DST transition, this calendar-derived end is 49 actual hours after start, but PriceModel.load() still requests only its fixed 48-hour horizon from midnight. The resulting face therefore has no price bars for the final local hour even though tomorrowPriced is true, so it suppresses the missing-price note as well. The price request needs to extend through this same calendar boundary rather than assuming two days always equal 48 hours.
Useful? React with 👍 / 👎.
The runner could not type-check a nested ternary in the test's price fixture in time. A small function and written-out types take it well under 80 ms here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mark Fredrik's answers on #27: - The plan view opens on the dial; a segmented switch at the top (Tavla | Banor) brings back the 12-hour lanes, and the phone keeps the viewer's choice (@AppStorage "FTWPlanStyle"). With the lanes, the event rows use the lanes' colours again. - On the dial gold stays the sun's; Now's lanes keep gold for discharge. The scrub, profiled in Instruments: - A drag event is arithmetic and one lookup in a table of the two days' 192 quarters (Face.index(atProgress:), was a scan), and writes nothing a view reads. - A change of quarter moves one @observable PlanDialSelection, read only by the mark, the middle, the line under the dial and a small view that holds the haptics. The card, the face, the bars and the hand stay put. With .equatable() alone SwiftUI still re-ran the bars and the hand. - The sky and the hand are worked out in their own layers, so a redraw of the face does not pay for them. - Two Points of Interest signposts stay: "dial drag" and "dial quarter". Tests: the finger's lookup on the nights the clocks change; a UI test that the lanes are one tap away and stay after a new launch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Replaced by #53 (WP11): the plan dial as one turn for the next 24 hours, on the new palette and the same face as the Apple Watch dial from #43, with its own widget. #53 keeps this PR's scrub path, widget kind, |
FTW's plan as a round dial, from Fredrik's film (srcfl/sourceful-films,
product/ftw-confirmed), in the app's own tokens. It is the plan view's default, where a finger dragged round it scrubs the plan, with the 12-hour lanes one tap away. It is also a new widget for the home screen, StandBy and the lock screen. Now keeps its straight plan and never scrolls.Based on main after #17, #20 (with #18, #19 and #21), #25 (0.3.0, build 100 uploaded) and #26. For the release after 0.3.0; a draft until then.
The rules
Fredrik's, from 13 Oct 2026, now under "Plan dial" in CLAUDE.md (Design references):
Theme.storage) stores, white (Theme.fg) uses,Theme.fgMutedwaits. ATheme.mobilitydot marks a quarter when the car charges. No gold for the battery.What changed
FTWShared/PlanDial.swiftPlanEntryalone, so app, widget and later the Watch agree.FTWCharts/PlanDialView.swiftPlanDialSelection(a finger, VoiceOver, later the Crown) moves. New bars grow in with the charts' spring (response 0.55, damping 0.72, capped stagger); nothing moves under Reduce Motion or in Low Power Mode.ChartMotion's spring moved toFTWCharts/ChartSpring.swiftso both use it.FTW/UI/PlanDialCard.swift,PlanView.swift.selectionper quarter (at most 25 a second),.impact(weight: .light)at now and at the cheapest window's edges. The line under it says what is planned there ("i morgon 02:30–02:45 batteriet väntar · bilen laddar · 30,0 öre/kWh") and springs back on release. VoiceOver: one adjustable element, "Plan dial", an hour a step, reading time, plan, price and, at now, the battery.FTWWidgets/Plan/PlanDialWidget.swiftenergy.ftw.plan-dial: systemSmall (home screen, StandBy) and accessoryCircular (today's quarters as ticks, the hand, the battery as a ring round its level). Entries every quarter hour for 12 hours. Grey with "Plan från 14:42" once the plan is an hour old; monochrome when tinted or at night.PlanEntry,PlanSharedaylight(the forecast's sunny runs and its reach) andbatteryLevel(read live, rewritten at most every 5 minutes once it moved). Old records still read (tested). The widget's record now holds today and tomorrow instead of an hour back to a day ahead.PriceModelfetching, so a price gap on its way is told from a missing one.Fredrik's answers
@AppStorage("FTWPlanStyle")). With the lanes, the event rows go back to the lanes' colours.energy.ftw.plan-dial, beside the Flighty-style plan widget.GET /api/mpc/plan) as the sun's and the home's views do: when it opens, then every quarter hour while open.Also as built: the past has prices but no plan (the box's plan starts now), so today's earlier quarters are grey price bars, dimmed. The glass is a dark disc of plain paint, not Liquid Glass.
Scrub cost
On Fredrik's iPhone the plan's scrub lagged in 0.3.0, so the dial's path was profiled and rebuilt:
Face.index(atProgress:)). It writes nothing a view reads: the finger's place sits in an object no view observes.@Observablevalue (PlanDialSelection). Only the views that read it redraw: the mark (a small Canvas), the middle's figures, the line under the dial, and a tiny view that holds the haptics. The card, the face, the bars and the hand stay as they are..selectionat most every 40 ms,.impact(weight: .light)at now and at the cheapest window's edges.Measured with Instruments (Time Profiler + Points of Interest) on the iOS 26.5 simulator on this Mac, Debug build (the demo's launch flags are Debug only).
PlanDialTourdrove four slow drags round the dial with a real XCTest touch. Temporary signposts in each layer counted redraws; they are not in the code. The twodial draganddial quartersignposts stay (Points of Interest), so anyone can repeat this.An in-between version showed the trap: with
.equatable()on the bars and the hand, SwiftUI still re-ran both on every change of quarter, without asking==, and the bars' Canvas took 0.5 ms (p95 1.1 ms) each time. The selection object removes the dependence on.equatable()altogether.Not measured here: Animation Hitches (Instruments has no Hitches on the simulator) and the haptic engine. Both need a phone. The same tools can measure the 0.3.0 chart scrub; that is not in this PR.
Evidence
swift testinappleApp/FTWKit: 103 tests in FTWShared and 267 in FTWKit pass. New:PlanDialTests(14: the sun's path, quarters, DST, actions, car, empty outer turn, the finger past midnight, reach, hour stops and steps, cheapest edges, sky, centre, old records, the finger's lookup on the nights the clocks change), a readout test in English and Swedish, the widget record test, andPlanDialCard.swiftin the Swedish-words test.NowFitsTests: 8 of 8 on iPhone SE (3rd generation) and on iPhone 17 Pro Max, iOS 26.5, on this commit.PlanDialTests(UI), 2 of 2: the dial is one element named "Plan dial" that starts at now with price and battery (iOS XCTest cannot swipe an adjustable element, so the hour steps are tested in FTWShared,Face.step); and the lanes are one tap away, stay after a new launch, and the dial comes back with one tap.Plan view and scrub (iPhone 17 Pro, iOS 26.5, Swedish demo)
Recordings of the scrub, one touch per drag (
FTWUITests/PlanDialTour): light, dark.Widget on the home screen
Light, dark, without tomorrow's prices, and an hour-old plan:
Lock screen circle
StandBy: rendered, not captured
The iOS Simulator cannot enter StandBy. These are the widget's own view drawn on the Mac as StandBy shows it: on black, without the widget background, scaled up; at night in one red.
Not proven yet
Retake the pictures
TEST_RUNNER_FTW_DIAL=1 xcodebuild test … -only-testing:FTWUITests/PlanDialTour, withxcrun simctl io <sim> recordVideo, or withxcrun xctrace record --template 'Time Profiler' --instrument 'Points of Interest' --attach <pid>for the scrub cost.-FTWPlanStyle lanes.energy.ftw.plan-dialinIconState.plistbefore boot, write aplan-<site>record into the App Group, thenTEST_RUNNER_FTW_WIDGETS=home … WidgetTour.TEST_RUNNER_FTW_LOCK=1 TEST_RUNNER_FTW_LOCK_PAGE=2 … LockScreenTourplaces the circle;TEST_RUNNER_FTW_LOCK=shotphotographs it.🤖 Generated with Claude Code