Skip to content

feat: reload widgets through Apple when prices or a car change - #12

Closed
frahlg wants to merge 3 commits into
claude/cloud-token-nowfrom
claude/cloud-widget-push
Closed

frahlg wants to merge 3 commits into
claude/cloud-token-nowfrom
claude/cloud-widget-push

Conversation

@frahlg

@frahlg frahlg commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Stacked on #11. Merge #11 first; this PR then shows only its own commit.

What

Widget push from ADR 0008 (C6) and the protocol's Push table. ADR 0009 step 7
needs it first: a watch registers its widget token the same way, with
app: watch.

  • A widget registers its APNs token over the app session with
    push.register {kind: widgets}. It needs ftw.live.read, as the protocol
    says, so a viewer's widgets may register; alerts still need
    ftw.events.read.
  • The service sends the widgets push when a box's priced hours reach further
    than before, as when tomorrow's prices arrive, and when a loadpoint goes
    from unplugged to plugged. The first prices and lp after a start only
    set what the next is compared with. The midnight shift and an unplug send
    nothing.
  • The push is Apple's: apns-push-type: widgets, topic
    energy.ftw.app.push-type.widgets, body {"aps":{"content-changed":true}},
    no data and no priority header, as in Apple's sample. The widgets then read
    /v1/now with their token (feat: widgets read /v1/now with a token bound to a roster entry #11).
  • At most 4 widget pushes an hour per home, apart from the alert cap. Apple
    budgets widget reloads, so a car that flaps cannot spend that budget.
  • A roster prunes each kind by the scope it needs: losing ftw.events.read
    drops a device's alert tokens and keeps its widget tokens.
  • A suspended device's widgets get nothing, as its alerts get nothing.
  • The uplink's OnEvent hook becomes OnHeld, which hands push every held
    envelope with its class, after the box's signature has been checked.
  • The APNs client takes a push type and the app's bundle id. Step 7 uses the
    bundle id for the watch.
  • deploy/apns.md: a topic-specific key needs the widgets topic, and the
    widget extension needs Push Notifications.

cloudwire names only PushKindAlert; this PR spells widgets in
internal/push beside it. The protocol's Push table names the type.

Tests

  • A widget registers with ftw.live.read, and one without it is refused.
  • Tomorrow's prices reach the widgets once, with the widgets push type and
    body, and nothing goes to the alert tokens. Today's prices, a resend and
    midnight send nothing.
  • Plug-in steps: the first lp, staying plugged and unplugging send nothing;
    a new plug-in sends one.
  • The cap holds at 4 an hour and lifts after an hour; the alert cap is
    separate.
  • A roster prunes alerts and widgets each by its own scope.
  • A suspended device's widgets get nothing.
  • The APNs client sends a widgets push to <bundle id>.push-type.widgets
    without a priority, and an alert to the named app with priority 10.
  • The uplink hands push prices and lp with their class.

Local: go vet (with and without ftwclouddev), go test -race ./...,
go test -tags ftwclouddev ./..., scripts/check-imports.sh and the arm64
build all pass.

Not tried

No widget push has gone through Apple yet; the phone's widget does not
register a token. That needs a build of the app with the push handler.

After review (b4da95a)

An independent review found two defects here:

  • Widgets need only ftw.live.read, so a viewer may register, and nothing
    capped a device's tokens. One push then called Apple once per row on the
    single send goroutine, which held up other homes' alerts. A device now
    keeps its newest 4 tokens per kind.
  • PushGone deleted every registration of a token, so Apple refusing the
    widgets topic also deleted the same token's alert registration. It now
    deletes only the refused kind.

Each has a test. The branch also carries #11's review fixes.

🤖 Generated with Claude Code

A widget registers its push token with push.register {kind: widgets},
which needs ftw.live.read. When a box's priced hours reach further than
before, as when tomorrow's prices arrive, or a car is plugged in, the
service sends Apple's widgets push: no data, only content-changed, to
the app's .push-type.widgets topic. The widgets then read /v1/now with
their token. At most 4 an hour per home, since Apple budgets reloads.

The uplink's OnEvent hook becomes OnHeld, which hands push every held
envelope with its class. A roster prunes each kind by the scope it
needs. The APNs client takes the push type and the app's bundle id.

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-14T12:33:53.230773Z 4f35030 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.

frahlg and others added 2 commits October 14, 2026 15:02
…nly its kind

From review:
- Widgets need only ftw.live.read, so a viewer may now register, and
  nothing capped how many tokens a device held. One push then called
  Apple once per row on the single send goroutine, holding up other
  homes' alerts. A device now keeps its newest 4 tokens per kind.
- PushGone deleted every registration of a token, so Apple refusing the
  widgets topic also deleted the same token's alert registration. It now
  deletes only the kind Apple refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg added a commit that referenced this pull request Oct 14, 2026
…ssion gets 403

Merges claude/cloud-widget-push. Register now stores the app and trims
the device's tokens of that kind.

From review: token.get over the one-shot read answered 500 when Issue
found the device shut out after admission; it is a refusal, 403. The
README no longer says the service checks the watch role: it checks none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg added a commit that referenced this pull request Oct 14, 2026
…at /v1/app/once (#11, #12, #13)

One squash for three stacked PRs, ADR 0008 C6 and ADR 0009 step 7:
- #11: the extension token (token.get, hashed, bound to a roster entry) and GET /v1/now.
- #12: widget reloads through Apple when prices or a car change.
- #13: a watch reads its home at /v1/app/once in one HTTPS request, Noise one-shot per the golden vectors.

An independent review found no admission bypass, limit leak or logged secret; its smaller findings are fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg

frahlg commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

Included in #13, merged as 341bd65.

@frahlg frahlg closed this 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