Repository navigation
Conversation
The car ops were cmd frames only, so the box page, which speaks HTTP,
could list cars but not keep them or answer which car is on a charger.
Each op now has a route that hands the same arguments to the same
runtime and answers with the same code and reason:
- POST /api/cars is car.add, PATCH /api/cars/{id} car.update and
DELETE /api/cars/{id} car.remove. Configure, no step-up, and only a
caller with ftw.cars.write.
- POST /api/loadpoints/{id}/car is session.car.set, the tap, with
{session, car_id}. Actuate, Via the op, so a phone is told to send it.
appproto.CarRefusal is the refusal table both use.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The overview gets a card per car, plugged in or not: its level with where it came from and how old it is, an estimate marked with ≈, its charge limit, its goal and the charger it is on, with how the box knows. The level ages between reads, and a list that stopped updating says since when. While a charger asks which car it is, its notice offers one button per car and None of these, and the cars it may be say "On garage?" rather than "Not plugged in". A car the box named itself can be corrected behind "Not this car?". The buttons are rebuilt only when what they show changes, so a poll never takes one from under a tap. Settings gets a Cars tab: each car with its VIN, level sources and charger tags; rename, battery size, add and remove through the car routes. Adding a VIN a car already has is stopped on the page: the box would take that as the app adopting the car it made, and quietly rename it. The EV modal stays as it was; a car on a charger opens it with "Check charging". 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: ccdc7403e8
ℹ️ 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".
| function update(lp, cars, loadpoints, fresh) { | ||
| last = [lp, cars, loadpoints, fresh]; | ||
| const list = fresh ? choices(lp, cars, loadpoints) : []; |
There was a problem hiding this comment.
Clear tap feedback when the charging session changes
If a tap is pending while the car is unplugged and quickly replugged, this row can be updated to the new session before the old request completes; the old response then writes “That plug-in has ended” into the note and renders it beside the new session. The same stale refusal persists across later polls because update never clears the note when lp.session changes. Reset feedback when a different session ID arrives so failures remain associated with the request that produced them.
AGENTS.md reference: AGENTS.md:L22-L23
Useful? React with 👍 / 👎.
…home From the review of #1694: - GET /api/cars gives VINs, charge tags and sources only to a request that has proved itself as a protected one must. The LAN listener names every caller an owner while the house password is off, so a box published under a public name without FTW_API_TOKEN handed them to anyone. Others get the viewer shape and redacted: true. - POST/PATCH/DELETE /api/cars are Local: a phone keeps sending the ops as cmd frames, with their id and expiry. - The routes read their bodies with the ops' own argument checks (appproto.CarAddArgs, CarUpdateArgs): a negative size is refused, and a null identifiers clears the list as car.update does. - The card dates the chargers' read apart from the cars': an old read, or a charger gone quiet, says "Charger status unknown" rather than the last place. The charge limit shows only beside the car's own reading. - A tap gives up after 30 s, and a 401 says the house password is needed. - The Cars tab says hidden, not "Not set", when the box left a field out. - docs/cars.md names the routes, the page and who sees the VIN. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Checked the current head The session-feedback finding still reproduces independently: start a car-choice tap for session A, update the row to session B before the request returns, then finish A with Reset the note/busy/expanded state when the session changes, and compare the captured session after |
Step 5 of the car design (
design/vehicles/car-entity-2026-10-15.md): the box's own page shows the owner's cars.Fredrik, 17 October 2026: "jag ser ju inte min bil någonstans". Core has known the car since #1647, #1650 and #1660, and
GET /api/carson his box lists the Tesla. But no file inweb/read/api/cars. The dashboard's car parts read the loadpoint, which knows no car while the cable is out.What the owner sees
Overview, a card per car, plugged in or not.
Name, then the level with its source and age: "72% · From the car · 1 h ago". An estimate gets ≈ and the energy since the reading ("≈ 75% · From the car · 1 h ago + 2.4 kWh"). A level set by hand says "Set by hand", and an assumed one says "Assumed at plug-in".
The age counts on between reads, from
soc_age_splus the time since the read. If the list stops updating, the card says "Not updated since 11:08". Nothing is shown as live.Charge limit, goal ("Goal 80% by 07:00, every day") and "Solar only".
The charger the car is on, and how the box knows ("The car said so", "The only car", "You chose it", plus "not XC40" after a correction). "Check charging" opens the existing EV modal for that charger.
While a charger asks, a car it may be shows "On garage?" rather than "Not plugged in".
The place comes from the chargers' own read. If that read is older than 15 s while the cars still answer, or the charger has gone quiet, the card says "Charger status unknown" instead of the last place it knew.
The charge limit shows only beside a level that came from the car, whose age it shares.
Which car. When a charger asks (
car_identified_by: asking), its notice reads "Which car is on garage?" with one button per car inchooseand "None of these". A tap sendsPOST /api/loadpoints/{id}/carwith the plug-in'ssessionand gives up after 30 s, as the EV modal's writes do. A refusal shows in words ("That plug-in has ended."). On a LAN device that has not signed in, the page's own fetch asks for the house password and sends the tap again. If the person cancels, the note says choosing the car needs the house password. Once the box has named a car, the notice says "XC40 connected", and "Not this car?" opens the same choice, because a tap may correct the box (docs/cars.md). The buttons are rebuilt only when what they show changes, so the 2 s poll never pulls one away mid-tap.Settings → Cars. Each car shows its VIN, make, level sources ("Level from tesla-ha") and charger tags (or "Hidden until this device signs in" when the box left them out), and has a "Made by this box" note for a box-minted car. You can rename a car, set its battery size, add one (the page mints the 128-bit id, as the app does) and remove one, with a confirm step. The tab writes through the car routes, never
/api/config, and the Save button hides on it.The EV plan, goal and charge-now controls stay in the EV modal, as they were. Moving them under the car would have meant splitting the modal, which is not simple, so the car opens it instead. No new settings.
Core: the smallest routes the page needs
The car ops were cmd frames only. Each op now has a route that reads its body with the op's own argument checks (
appproto.CarAddArgs,CarUpdateArgs) and passes it to the same runtime. A refusal comes back with the op's code and reason (appproto.CarRefusal, now shared):POST /api/carscar.addftw.cars.writePATCH /api/cars/{id}car.updateftw.cars.writeDELETE /api/cars/{id}car.removeftw.cars.writePOST /api/loadpoints/{id}/carsession.car.setVia(session.car.set)The routes that keep the list are Local: a phone keeps sending the ops as cmd frames, with their id and expiry, and the session refuses these routes. The tap is priced like
POST /api/loadpoints/{id}/soc: the session tells a phone to send the op.GET /api/carsno longer hands out VINs to anyone who asks. The LAN listener makes every caller an owner while the house password is off, so a box published under a public name withoutFTW_API_TOKENused to return VINs and charge tags.handleCarsnow gives every field only to a request that has proved itself as a protected one must: a local address, loopback, the house password, the API token on a public name, or an owner's app session. Everyone else gets the viewer shape withredacted: true. Reading the list itself stays open, so a viewer still sees each car's level.Found on the way
Adding a car whose VIN a box-minted car already has is how the owner's app takes that car over. Core then gives it the new id, name and size. On the box page that quietly turned "Model Y" into "Twin, 60 kWh" while saying "Added Twin." The tab now stops that and names the car that has the VIN. Core's behaviour is unchanged.
Evidence
go test ./internal/api ./internal/appproto:api_cars_test.goruns the routes against a realcars.Runtimeand loadpoint manager. It covers add, a duplicate VIN, a bad id, update, a refused size, an unknown car, remove twice, the tombstone, a tap with no car, a tap for an ended plug-in, a tap, None of these, an unknown charger and the route tiers. The argument checks: a negative size, a body that is not an object, a non-text identifier, a null identifiers clearing the list as the op does, and the path naming the car. Who may write: a viewer phone, a watch and an agent get 403, and the owner's phone gets 200. With the house password on, a LAN write without it gets 401, from another site 403, and with it 200. Who sees the VIN: a public name without the token, a LAN device without the house password, a watch, an agent and a viewer get the short list; the token, the LAN, loopback, the house password and the owner's phone get every field. The VIN test fails without the fix.npm test: 798 pass. New:web/cars.test.mjs(28) andweb/settings/tabs/cars.test.mjs(11).dashboard-poll-visibility.test.mjsnow counts the fourth GET in the status poll.make verify: clean.sim-ocpp),sim-sungrowas the site meter, and a rig-only standalone car source that reports 72% read an hour before start, with an 80% limit. Through the page I added a car, renamed one, tried a duplicate VIN, removed one, tapped a car on each charger, and opened the EV modal from a car. The modal showed the tapped car's 76 kWh and its goal. The home box was not touched.Screenshots: desktop from headless Chrome at 1280 px; phone from the app's browser at 375 px (2×), where the page has no sideways scroll. The app's browser pane was hidden, so for those shots
document.hiddenwas overridden to false. Otherwise the page marks its data out of date, as it should when hidden.1. No car plugged in. Model Y has its own reading from an hour ago; the other two have no level.
2. One car plugged in (XC40 on carport, tapped), and a charger asking between two cars (garage: Model Y or EV6).
3. Settings → Cars.
For the owner
minor: a visible new capability. It needs Fredrik's yes here.web/index.html(Access tab script). The hunks do not overlap.🤖 Generated with Claude Code