Skip to content

iPhone: the widgets and the Live Activity in the app's material (WP14) - #54

Merged
frahlg merged 4 commits into
mainfrom
claude/ios-wp14-widgets
Oct 14, 2026
Merged

frahlg merged 4 commits into
mainfrom
claude/ios-wp14-widgets

Conversation

@frahlg

@frahlg frahlg commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

WP14 of the iPhone polish release (Phosphor): the widgets and the Charge now Live Activity in the app's material, so the home screen, the lock screen and the app read as one. Design: design/ftw-ios-polish-2026-10/ftw-ios-redesign.md §15 WP14, §10, §12; charts-research.md §4.6 and §5.7; Fredrik's note 17.

Paired with the car view PR (WP12, #55): both carry the first commit, the receipt log. Whichever merges first, the other's copy drops out on rebase.

What changes

One palette. The widgets' own Tone table is gone. Every widget, the Live Activity and the Control read the roles in FTWCharts/Theme.xcassets through Theme, as the app does. Backgrounds are the ground, and grey (hairline) when there is nothing for the moment. Figures are SF Pro with even digits. Kickers, AS OF, CHEAPEST and the receipt are the terminal layer in SF Mono.

Grid today, in the app's grammar (research decision D). One signed line with its body, buying above zero and selling below, magenta both ways. It is the app's own ChartSeries.history: one point per slot of at least 6 pt (30 min on the medium widget, 60 on the small), each the exact mean of its quarters, through a Steffen curve that never overshoots. The medium adds the band of the quarters in each slot. A hole the box has no record for breaks the line and shows a dotted baseline; nothing is drawn after the read. The arrow beside net becomes a word in magenta, köpt netto or sålt netto. The chart says ↑ KÖPER and ↓ SÄLJER beside the scale's ends, in the corner the hours still to come leave empty, and only where that side has room. The kWh, the cost from the box and kl 16:55 stay. No bars.

Price. The small widget, also StandBy's, adds the next twelve hours as the block-glyph scale: one bar an hour in eight levels, ink by thirds, the hour that holds now in full ink. The lock screen's rectangle writes the same hours in SF Mono, ▁▂▃▄▅▆▇█, under the price, with CHEAPEST 00–02 · AS OF 17:21 on one mono line. PriceBlocks (FTWShared) works the hours out for both, on the household's clock, and ends the strip where the market has not priced. The medium keeps the app's price chart as still paths.

Plan. The medium draws the next twelve hours as Now's strip does, in still paths in place of Swift Charts: the car and the battery as 6 pt lanes (violet with a tick at the car's goal, cyan storing, gold giving), an idle lane as a faint hairline, the price as bars in eight levels by thirds, mono hours and a now rule. Every plan widget ends on PLANERAD 17:21, or PLAN FRÅN 14:05 · ÖPPNA FTW once old.

Tinted and clear. Every widget reads widgetRenderingMode: lines, bars and runs in .primary, with the line, the now bar or the runs marked .widgetAccentable().

The Live Activity. The three capsules become the receipt log, SKICKAT ▸ MOTTAGET ▸ UPPMÄTT ▸ BEKRÄFTAT, each slot lit on its own evidence (below). It goes stale two minutes after the box last confirmed Charge now, not fifteen: the app confirms it once a minute while someone looks, and nothing does while it sleeps. The Control still trusts its record for fifteen. The card sits on FTW's ground, the time is SF Pro with even digits, and nothing glows.

The receipt log (ReceiptLog in FTWShared, drawn by ReceiptRow in FTWCharts). Four slots, always on the line, so lighting one moves nothing: SENT on the local send, ACCEPTED on cmd.ack, MEASURED when lane 0 shows the car above 42 W while live, once the box has the command, and CONFIRMED (lime) on cmd.result. The box often confirms before the car draws, so the slots light in the order their evidence comes. A refusal takes the last slot as REFUSED in alarm red, padded to the same width. Lane 0 has one car figure for every charger, so LoadpointsModel lights MEASURED on the Live Activity only when the home has one up.

CLAUDE.md gains the widgets' material and the receipt log under "The look".

FTW Cloud's widget push: not wired

WP14 changes how the widgets look, not what they know. The app already writes the App Group cache whenever it reads the box and reloads the timelines, and it refreshes prices in the background. /v1/now carries the live readings, prices and the heartbeat, but not the plan or today's grid totals, so it could feed only the price widget. Wiring it would put a token in the App Group and make the widget extension a network client, against the rule in CLAUDE.md that widgets read the App Group cache and nothing else. The gain would be tomorrow's prices reaching the price widget around 13:00 while the app sleeps. That is worth its own change, with the rule rewritten on purpose.

Proof

  • swift test: FTWShared 190 tests, FTWKit 342 (the live suites skip, as always), all green. New: ReceiptLogTests, the Live Activity's MEASURED and two-minute stale date (ChargeNowTests), lane 0 lighting MEASURED through the demo box (RemoteTests), the grid's signed line, its slots by width, holes and sides (GridGlanceTests, GridTodayShareTests), the block strip and AS OF (PriceGlanceTests).
  • NowFitsTests on an iPhone SE (3rd generation), iOS 26.5: 11 of 11 green.
  • Simulator, iPhone 17 Pro, iOS 26.5, Swedish, the demo's records in the App Group, widgets placed on two home pages.

Home screen, both pages, demo records in the App Group (17:21), light / dark / tinted / clear:

Light Dark
Tinted Clear

Lock screen: the price as block glyphs beside Grid today, then beside the plan; and the whole lock screen with a Live Activity that got no answer.

Live Activity, lock screen: sent, accepted (the car already drawing, so MEASURED is lit before CONFIRMED), on with every slot lit, and stale two minutes after the last confirmation (time stopped, all ink):

Light: on and stale. Dynamic Island: compact, then expanded while accepted, on and stale.

Not proven here: StandBy at night (the simulator cannot enter StandBy) and a run on Fredrik's iPhone. Widgets have no motion, so there is no Animation Hitches run. The inline price widget did not change and is not pictured. The commits carry no DCO sign-off.

🤖 Generated with Claude Code

frahlg and others added 4 commits October 14, 2026 17:34
…e Activity

ReceiptLog (FTWShared) is the car command's receipt in the terminal
layer: SENT ▸ ACCEPTED ▸ MEASURED ▸ CONFIRMED. Every slot is always on
the line and lights only on its own evidence: the local send, the box's
cmd.ack, lane 0 showing the car above 42 W while live once the box has
the command, and cmd.result. The box often confirms before the car
draws, so the slots light in the order their evidence comes. A refusal
takes the last slot in alarm red, padded to the same width, so the line
never moves. ReceiptRow (FTWCharts) draws it for the Live Activity and,
next, the car view.

The Charge now Live Activity shows the receipt in place of its three
capsules. LoadpointsModel lights MEASURED from lane 0 when the home has
one Live Activity up; lane 0 has one car figure for every charger. The
card goes stale two minutes after the box last confirmed it (it was
fifteen): the app confirms it once a minute while someone looks, and
nothing does while it sleeps. The Control still trusts its record for
fifteen. The card sits on FTW's ground with SF Pro figures and never
glows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… its body

Grid today drew the day as bars for what it bought and a thin curve
below zero for what it sold. It now draws what the grid's Today draws
in the app (research decision D): each quarter's net power as one
signed line, buying above zero and selling below, magenta both ways.
The geometry is the app's own ChartSeries.history: one point per slot of
at least 6 pt (30 minutes on the medium widget, 60 on the small), each
the exact mean of its quarters, through a Steffen curve that never
overshoots, with its body, and on the medium the band of the quarters
in each slot. A hole the box has no record for breaks the line and shows
a dotted baseline; nothing is drawn after the read. Still paths, a 2 pt
line, no glow.

The arrow beside net becomes a word in the grid's colour, "köpt netto"
or "sålt netto", next to both sides. The chart says ↑ KÖPER and ↓ SÄLJER
beside the scale's ends, in the corner the hours still to come leave
empty, where that side has room. The kWh, the cost from the box and
"kl hh:mm" stay. Figures are SF Pro with even digits; the kicker and the
time are the terminal layer. Tinted and clear draw the line in primary
at 0.7, marked for the accent, and the body fainter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…OF on every one

One palette: the widgets' own Tone table is gone, and every widget,
the Live Activity and the Control read the roles in
FTWCharts/Theme.xcassets through Theme, as the app does. Backgrounds are
the ground, grey (hairline) when there is nothing for the moment.

The price: the small widget, StandBy's, adds the next twelve hours as
the block-glyph scale, one bar an hour in eight levels by thirds of ink,
the hour that holds now in full ink. The lock screen's rectangle writes
the same hours in SF Mono, ▁▂▃▄▅▆▇█, under the price, with CHEAPEST and
AS OF on one mono line. PriceBlocks (FTWShared) works the hours out for
both, from the household's clock. The medium keeps the app's price
chart as still paths. Figures are SF Pro with even digits; the kicker,
CHEAPEST and AS OF are the terminal layer.

The plan: the medium draws the next twelve hours as Now's strip does, in
still paths in place of Swift Charts: the car and the battery as 6 pt
lanes of runs (violet with a tick at the car's goal, cyan storing, gold
giving), an idle lane as a faint hairline, the price as bars in eight
levels by thirds, mono hours and a now rule. Every plan widget ends on
PLANNED 17:21, or PLAN FROM 14:05 once old.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T17:20:20.232090Z 97639e5 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 97639e5414

ℹ️ 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".

Comment on lines +478 to +480
let ids = host.loadpointIDs(home: activityHome)
guard ids.count == 1 else { return }
showChargeNow(ids[0], .measured(atMs: site.scheduler.nowMs))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Don't attribute aggregate EV power to one charger

When a home has multiple chargers but only one active Live Activity, Contract.FID.evW is the site-wide car load, while ids.count == 1 only proves that one activity is open. If another charger is already drawing under its plan, any subsequent site observation will therefore mark the active command's MEASURED slot even when its own charger has not started; use per-loadpoint power or require a single charger, not merely a single activity.

Useful? React with 👍 / 👎.

current: current,
cheapest: cheapest,
curve: curve,
hours: PriceBlocks.hours(curve, nowMs: t, count: stripHours, clock: ZoneClock.of(calendar.timeZone)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the final available price block

When exactly one price slot remains active, PriceStrip.summary still supplies current but intentionally returns an empty bars array; passing that empty curve here makes both the small widget and lock-screen widget show no block or glyph for a price that is still valid. Build the block input from the remaining slots independently, or ensure the singleton current slot is retained.

Useful? React with 👍 / 👎.

Comment on lines +282 to +283
if e.kind == .car, e.targetPct != nil {
path.addRect(CGRect(x: max(x0, x1 - 2), y: y - 2, width: 2, height: Self.lane + 4))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hide goal ticks beyond the displayed horizon

When a car charge window continues past the 12-hour horizon, x(e.endMs) is clamped to the chart's right edge, but this unconditional target tick is still added there. The widget consequently depicts the goal as occurring at the end of the visible horizon even though PlanGlance deliberately preserves the later real end time; add the tick only when the event end is inside the displayed interval.

Useful? React with 👍 / 👎.

@frahlg
frahlg merged commit 5cff2a7 into main Oct 14, 2026
1 check passed
frahlg added a commit that referenced this pull request Oct 14, 2026
The car view extends WP14's receipt log with the slots a command has and
MEASURED for the car view; inside each conflict this branch's side is the
superset, and everything else comes from main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg frahlg mentioned this pull request Oct 14, 2026
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.

1 participant