Skip to content

iPhone and watch: the car first, from the box's car list (car step 4) - #85

Merged
frahlg merged 3 commits into
mainfrom
claude/ios-cars
Oct 17, 2026
Merged

frahlg merged 3 commits into
mainfrom
claude/ios-cars

Conversation

@frahlg

@frahlg frahlg commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Why

Fredrik, 17 October 2026: "jag ser ju inte min bil någonstans". His box (Core v0.147) knows the car. GET /api/cars lists "Car 1", a Tesla at 72 % from tesla-ha, an hour old, with a goal of 80 % by 07:00 and 76 kWh. The app drew its car row from the charger, so with the cable out it showed nothing. This is step 4 of the car design (FTW-stack/design/vehicles/car-entity-2026-10-15.md, sections 3, 4, 8 and 9; srcfl/ftw docs/cars.md).

What changes

A box that lists der.car keeps cars apart from chargers. A box without it keeps today's charger view, unchanged.

  • The car comes first. The car view shows the car's name, then its level in large type with its source and age ("72 % · from the car · 1 h ago", "≈ 22 % · from the car 16:31 + 6 kWh", "≈ 20 % · assumed at plug-in"). Under that come the limit the car reported, its goal and its plan. The charger is one line ("On Easee · 11 kW max"), shown only while the car is on it. An old reading says how old it is, and the level never glows.
  • A car with the cable out still shows its last level and age, in the car view, the car node's row and hero, and Now's car node.
  • "Which car?" With two cars and no answer, a chip in the car view offers one button per car and "None of these". One tap sends session.car.set. The receipt (SENT ▸ ACCEPTED ▸ CONFIRMED) stays under the charger line once the view turns into the named car. The car node's row says "Which car?" while a charger asks.
  • The notification for car.which gets the same buttons. The app registers one category for each set of the owner's cars, so the buttons carry the names from the phone's own table. The extension picks the set the event offers, from the event's opaque car ids and the table the box gave over Noise (hello_ok.cloud.cars). A button acts only for a verified car.which that offered that car, on the plug-in the event named, behind unlock.
  • Settings → Cars: the list, and on each car its name (the box calls Fredrik's car "Car 1"), battery size (1 to 300 kWh, one decimal), level sources (car.update), add and remove. Only the fields that changed go to the box. The phone mints a new car's id and keeps it when it tries again after an answer it did not get.
  • The goal and the level are the car's own (car.goal.set, car.soc.set), for a car on a charger or away. A goal "to the car's own limit" stays one when it is saved again.
  • A visitor's car (a plug-in the box cannot name) shows no level anywhere and never borrows another car's. It keeps the charger's controls, which act on that session alone (docs/cars.md, one release of overlap), and its own row in the car node.
  • The watch shows the car plugged in or not, with its name, level and age ("from the car · 1 h ago", "≈" for an estimate). It keeps the chargers until the list is read, and a visitor's charger with Charge now. A charger that asks takes the car page with one button per car, one tap each (WatchCommand.Op.chooseCar, run on the phone through CarsModel).
  • Widgets and the Live Activity. The App Group car record now carries the car on each charger (level, name) and the owner's cars with their last levels and the car's own level source (CarEntry.owned, CarEntry.asking, optional fields, so older records still read). The watch and Siri's "Is the car ready?" read it. The Live Activity shows Charge now's state and no level, so nothing there read the charger's level before; it is unchanged.

How it reads without polling

The list is read after each hello, when the home comes back on screen and the last read is over a minute old, and after each car command. It is also read when a read of the chargers or a pushed plan shows a car that moved: plugged in, named, asked about, or its level changed. FTW Cloud's car class moves each car's level as the box sends it, mapped through hello_ok.cloud.cars. A cmd.result carries the car or the charger's car, so the screen changes at once before the next read. No timer.

FTW Cloud names a car by an opaque id from hello_ok.cloud.cars. When the list has a car that table lacks (one added here or on the box page), the app sends hello again without a sub. The box answers with hello_ok alone, and the stream runs on. The notification's buttons start from the App Group row at launch and keep the box's order.

Names

Every wire name comes from the registry: CarField, ChargerCarField, CarSocSource and CarIdentifiedBy are held to car_fields, charger_car_fields, car_soc_sources and car_identified_by by a test. HTTP names are their snake_case, as Core spells them. Contract.scopeCarsWrite names ftw.cars.write.

Demo

The demo box is now a Core with cars, with one car on the charger (-FTWDemoCars one). -FTWDemoCars away | two | asking | old opens each case. old is a box from before cars. Unit tests build the box without cars unless they ask, so the existing tests run against an old box as before.

Tests

  • swift test in appleApp/FTWKit: 266 FTWShared tests and 517 FTWKit tests pass, among them the new CarWireTests, CarsFlowTests and CarShareTests.
    • The car list from Fredrik's box's own JSON (VIN left out).
    • The level sentences.
    • The old-box fallback: no der.car, no /api/cars read, the charger view as before.
    • A car with the cable out, read once and never again while nothing moves.
    • The car on the charger showing its own level.
    • Two cars asking, then one tap, then "None of these", and a tap for a plug-in that ended.
    • car.goal.set and car.soc.set for a car on the charger and one away.
    • Add, rename, battery size, sources and remove.
    • The notification's car buttons and their order, the buttons at launch, the watch's car page (an estimate from the car, a visitor, an empty list, an old question), and a car record from before cars.
    • FTW Cloud's car class moving a level, a visitor on Now, the cloud table asked for again once without a sub, a goal to the car's limit, and a retried add.
  • xcodebuild builds the app, its widgets and the watch app for the simulator.
  • FTWUITests/CarsTour: testOneTapNamesTheCar taps Zoe on the demo and waits for the box's answer. testShots (with `TEST_RUNNER_FTW_| One car, cable out (Fredrik's case) | The car node, cable out | One car, plugged in |
    |---|---|---|
    | | | |
Two cars, no answer After one tap on Zoe The car node asks
Two cars in the car node An old box: today's view Settings → Cars
One car's settings Dark mode Largest text

Swedish, two cars and no answer:

=1`) took the screenshots below.

Screenshots (iPhone 17 Pro simulator, demo box)

One car, cable out (Fredrik's case) The car node, cable out One car, plugged in
Two cars, no answer After one tap on Zoe The car node asks
Two cars in the car node An old box: today's view Settings → Cars
One car's settings Dark mode Largest text

Swedish, two cars and no answer:

Not in this PR

  • The box page (step 5) is feat(web): the box page shows the owner's cars ftw#1694.
  • The app does not adopt a box-minted car with new_id, and does not copy the owner's list between boxes. Both matter only with more than one box.
  • Now's front page shows the car's level on its car node but not the "Which car?" chip. Now is one fixed page, and the chip lives in the car view and the car node's row.
  • I have not tried this against the home box. I read its GET /api/cars and GET /api/loadpoints read-only and used the car list as a test fixture.

🤖 Generated with Claude Code

frahlg and others added 2 commits October 17, 2026 11:39
On a box that lists der.car the app reads GET /api/cars after each hello
and draws the car before the charger: its name and level with source and
age, its limit, goal and plan, and the charger as one line while the car
is on it. A car with the cable out still shows its last level. Two cars
and no answer ask "Which car?" in the car view, on the watch and on the
notification, one tap through session.car.set. Settings list the cars:
rename, battery size, level sources, add and remove. A box without der.car
keeps today's charger view.

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

The choice's receipt stays under the charger line once the view turns into
the named car. The list is read once after hello, not again on the first
read of the chargers. "Is the car ready?" gives an unplugged car's last
level. Level sources leave the chargers out. CarsTour taps Zoe on the demo
and photographs each case.

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

chatgpt-codex-connector Bot commented Oct 17, 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-17T09:54:20.769280Z 6c98b2a 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: 6c98b2aa08

ℹ️ 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 +95 to +97
actions: set.cars.map { car in
UNNotificationAction(identifier: PushActions.carAction(car.id), title: car.name, options: [.authenticationRequired], icon: UNNotificationActionIcon(systemImageName: "car.side"))
} + [UNNotificationAction(identifier: PushActions.carAction(nil), title: PushActions.noneTitle(language), options: [.authenticationRequired])],

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 Limit notification categories to four actions

When a car.which event offers four cars, this constructs five custom actions because “None of these” is appended after the four car actions. iOS notification categories support at most four custom actions, so the final action can be omitted and the user cannot identify the vehicle as a visitor from that notification. Reserve one of the four slots for “None of these,” or decline to create a category when there are too many car choices.

Useful? React with 👍 / 👎.

- The watch keeps the car's own source (vehicle, manual, assumed) in
  CarEntry.Owned, so an estimate from the car never reads "set by hand".
- The watch keeps the chargers until the list is read, and a plugged-in
  charger whose car the box cannot name, so a visitor still gets Charge now.
- A visitor never shows another car's level on Now or in the car's node,
  and the node lists its charger.
- "Which car?" buttons survive a launch: the categories start from the
  App Group row.
- A car FTW Cloud's table lacks asks for hello again, without a sub, so the
  stream runs on; the table keeps the box's order.
- Settings send only what changed, with a battery from 1 to 300 kWh in
  0.1 kWh steps.
- A stale or cached list says how old it is; a goal to the car's limit
  stays one; a retried add keeps its id; no Int() on a NaN from the wire.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg merged commit 8e19079 into main Oct 17, 2026
1 check passed
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