Skip to content

feat(ev): the charger behind its bubble, and a token gate both surfaces share - #19

Merged
frahlg merged 1 commit into
mainfrom
the-charger-behind-its-bubble
Aug 9, 2026
Merged

feat(ev): the charger behind its bubble, and a token gate both surfaces share#19
frahlg merged 1 commit into
mainfrom
the-charger-behind-its-bubble

Conversation

@frahlg

@frahlg frahlg commented Aug 9, 2026

Copy link
Copy Markdown
Member

Round one of the reuse programme: the flagship EV surface, read-only, app-only. No box changes — both routes it reads (GET /api/loadpoints, GET /api/mpc/plan) are already priced Read on the box's master, verified there and against a live box.

What a thumb gets

Tap the EV bubble on Now and a sheet opens with what the box knows: charging now and at what power, what this session has delivered, the schedule on the kitchen clock ("Ready by 08:00 · every day" — the wire's UTC minutes converted at the last moment, nowhere else), and when the optimiser intends to charge next, folded from the MPC plan's quarter-hour slots into windows a person reads ("19:00–21:30 · up to 7.2 kW").

Honesty rules carried through:

  • A charger that cannot measure the car's charge shows no percentage anywhere — the schedule sentence drops its target rather than dress a goal as a measurement.
  • The plan is decoration on the panel: a box too old to serve /api/mpc/plan still shows its charger, and the missing windows are said plainly ("Charging times aren't readable right now") rather than drawn as an idle week.
  • The panel re-asks each minute while open (the History/Energy epoch rule) and heals through drops via askWhenLive. The wire's own state stays the freshness band's sentence; the sheet neither repeats it nor clears what it read.
  • Opening is gated on api.passthrough; the bubble itself already appears only when the box reports the field.

The simulator grows the charger the real box has

Same generator as the 1 Hz stream — the panel's "charging at 7.2 kW" and the hero's bubble read one household. Snake_case as the wire marshals it, tiers exactly as the box prices them (reads Read, every write Actuate and refused over the passthrough — the refusal round 2 exists to lift), and a car that honestly does not know its own charge.

The token gate

src/styles/tokens.css has claimed since it was written that it is kept in step with the box's theme.css, and nothing held that claim. Now scripts/check-token-drift.mjs does, in CI beside the registry check: every custom property a vendored component reads must resolve to the same value in both files, both themes. Proven to fail on a genuinely drifted value; found three missing aliases on its first run (--card-pad, --card-pad-tight, --stat-warn — the history and savings cards would have run on fallbacks), now pinned.

Coordination note

src/lib/state/flow.ts gets a one-line change here (the EV planet becomes clickable). An in-flight session is re-vendoring ftw-energy-flow and touching the same file's grid/battery lines — distinct hunks, merges cleanly whichever lands first.

Verified: 745 tests green (30 new), types, production build, token check against the box checkout, and the panel exercised in a browser at desktop and phone widths against the simulator.

🤖 Generated with Claude Code

…es share

Round one of bringing the on-box experience into the app: read-only, no
box changes, everything a fact the box served.

- Tap the EV bubble on Now and a sheet answers the question a thumb
  brings: what flows now, what this session delivered, what the schedule
  says (on the kitchen clock, never UTC), and when the optimiser intends
  to charge next — folded from /api/mpc/plan into windows a person reads.
- GET /api/loadpoints and GET /api/mpc/plan ride the passthrough at Read,
  exactly as the box prices them. The plan is decoration on the panel: a
  box too old to serve it still shows its charger, with the missing
  windows said plainly rather than drawn empty.
- The panel re-asks each minute while open, the same epoch rule History
  and Energy follow, and heals through drops with askWhenLive.
- The simulator grows the charger the real box has — same generator as
  the stream, snake_case as the wire, tiers as the box prices them, and
  a car that honestly does not know its own charge.
- scripts/check-token-drift.mjs holds the "kept in step" promise the
  tokens file has made since it was written: every custom property a
  vendored component reads must resolve identically in this repo's
  tokens.css and the box's theme.css, both themes, in CI beside the
  registry check. It found three missing aliases on its first run —
  --card-pad, --card-pad-tight, --stat-warn — now pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
ftw-webapp db0a3f9 Aug 09 2026, 03:21 PM

@frahlg
frahlg merged commit bfb1497 into main Aug 9, 2026
3 checks passed
@frahlg
frahlg deleted the the-charger-behind-its-bubble branch August 9, 2026 15:23

@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: db0a3f9786

ℹ️ 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 thread src/lib/state/flow.ts
// The one bubble that opens something: the charger's sheet. The hero
// makes a clickable planet a button with a name, so the tap target
// and its announcement come from the component, not from here.
clickable: true,

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 Disable the EV button until passthrough is available

When the app is showing cached fields, or the connected box lacks api.passthrough, an EV_W field still makes this planet a focusable button announced as “Open EV charger,” while Now.svelte refuses to open the panel unless that capability is present. Cached snapshots deliberately do not restore capabilities, so this produces a dead control during cold starts and indefinitely while offline; derive clickable from the capability gate rather than setting it unconditionally.

Useful? React with 👍 / 👎.

Comment thread src/views/EvPanel.svelte
Comment on lines +46 to +48
function clock(ms: number): string {
return new Date(ms).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
}

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 Include the day in planned charging windows

When a plan contains windows on multiple dates, this formatter discards the date and makes later-day windows appear to occur earlier on the same day. The checked simulator's 24-hour plan demonstrates this at 18:30: the panel can list today's 18:30–19:30 followed by tomorrow's 17:00–18:30 with no indication that the second window is tomorrow, so the displayed charging plan is ambiguous and potentially misleading.

Useful? React with 👍 / 👎.

Comment thread src/views/EvPanel.svelte
is a dialog, so what is behind it is inert to a screen reader. -->
<div class="backdrop" onclick={onclose} aria-hidden="true"></div>

<section class="sheet" role="dialog" aria-modal="true" aria-label="EV charger">

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 Move keyboard focus into the modal dialog

When the sheet is opened with a keyboard, focus remains on the EV button behind it because nothing focuses the dialog or its Close button; aria-modal alone neither moves focus nor makes sibling DOM inert. Keyboard users therefore start outside the advertised modal and can navigate or activate controls behind the backdrop, so opening should move focus into a trapped/inert modal and closing should restore it.

Useful? React with 👍 / 👎.

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