Repository navigation
Conversation
FTW Cloud C4 carries notifications. cloudwire gains the event class body (eventId, ts, kind, severity, args, src, lp), the phone's push.register and push.unregister, and the APNs payload the service builds: the same aps for every alert, and the box's envelope unchanged under "ftw". ReadAlert is the reference for what a phone's notification extension makes of a payload: a fresh, anchored, box-signed event of a known kind, the service's own box.unreachable, or the generic line. The catalogue gains charging.goal_at_risk. cloud-vectors.json gains three signed events; push-vectors.json holds fourteen payloads, the phone's clock and the verdict each must get, for the service and the app to replay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
K5 and the Core half of FTW Cloud C4.
- A catalogue message carries its kind, its arguments, the device it
concerns and when it happened (dispatchCatalogue set no kind before).
- Every transport publishes side by side under its own 10 s limit, so a
gateway that hangs costs only itself.
- notifications.CloudPublisher only queues. cloudlink.QueueEvent keeps up
to 32 events; the writer signs each as an event with eventId, ts and
seq, cuts opaque ids for the driver and loadpoint, and drops name
arguments. An event older than ten minutes is dropped and counted in
GET /api/cloud (eventsDropped). The grant's events scope gates it all.
- Link.Bye sends bye{why:"update"} for a restart the box chose, and
bye{why:"shutdown"} on SIGTERM, then stops dialling, so FTW Cloud holds
back box.unreachable through an update.
- charging.goal_at_risk: once per plug-in, from a fresh plan's shortfall
(at least 1 kWh), only on a level that is not assumed and a ready time
still ahead. Opt-in like the other charging rules.
- charging.session_complete for a car that reports nothing: when the
box's estimate from a level someone set reaches the target, in kWh,
once, and not after a restart beside a car that is already done.
- charging.connected waits 90 s and goes only when the level is assumed
or no goal is set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /charger/{id}/stall makes the simulated charger deliver nothing
while it still reports Charging, as a tripped relay does; /resume lets it
go on. It is what charging.interrupted is for, and the FTW Cloud C4
end-to-end test raises one with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 13, 2026
Member
Author
|
Included in #1607 (FTW Cloud C2, C3 and C4 joined), squash-merged to master. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
FTW Cloud step C4 on the box: notifications go to FTW Cloud as signed events, so a home's phones get them through APNs. The service wraps them unchanged; the phone checks the box's signature, writes its own words and picks the buttons from the verified kind. FTW Cloud never chooses what a user can do. Builds on #1595 (C2); based on its branch.
Contract-pair: srcfl/ftw-webapp@claude/cloud-c4-catalogue
Paired pull requests
go/protocolat8cca4203.charging.goal_at_risk, stacked on fix(ci): gate release workflow on successful test run #81).claude/cloud-c3-core: srcfl/ftw#1603. Itshello_ok.cloud(K11) is where the phone gets the box key the extension checks with.What changes
go/protocol/cloudwire/push.go(Apache-2.0):Event: theeventclass witheventId,ts,kind,severity,args,srcandlp, plus the header'sseq,boot,uptimeMsandwallMs. The field allowlist gains its row.PushRegister/PushUnregister, which a phone sends over its app session.AlertPayload: the APNs payload. It has the sameapsfor every alert (localised generic line,mutable-content: 1) and the box's envelope underftw.event.box.unreachableis the onlyftw.kinda phone takes from the service.ReadAlertis the reference for the phone's extension: a box-signed event, anchored bywallMs(at most 2 s ahead of the phone), at most 10 minutes old, of a kind in the catalogue. Anything else gets the generic line.cloud-vectors.jsongains three signed events. The existing signatures are unchanged.push-vectors.jsonholds 14 payloads, the phone's clock and the verdict each must get; the app replays them byte for byte.K5 in
notifications:Kind(dispatchCatalogueset none),Args, the device it concerns and when it happened.CloudPublisheronly queues.cloudlink:QueueEventkeeps up to 32 events and never blocks.driver.offlinegoes withsrc, nevername.GET /api/cloud→eventsDropped). The grant'sftw.events.readgates it all.Link.Byesendsbye{why:"update"}for a restart the box chose (the API restart, which a native update also uses) andbye{why:"shutdown"}on SIGTERM, then stops dialling. FTW Cloud holds back "box out of reach" for an hour after the first.New and changed kinds (B3 in the iPhone plan):
charging.goal_at_risk(new, catalogue paired): once per plug-in, from a plan that is neither pending nor outdated, when the shortfall is at least 1 kWh, the ready time is ahead and the level is not assumed. Opt-in like the other charging rules.charging.session_completefor a car that reports nothing itself: when the box's estimate from a level someone set reaches the target. Said in kWh, once, and never after a restart beside a car that is already done (it must have drawn power in this process).charging.connectedwaits 90 s, then goes only when the level is assumed or no goal is set. A car that reports its own level within that time and has a goal needs no question.sim-ocppgainsPOST /charger/{id}/stalland/resume, a charger that stops delivering while it still reports Charging, which the end-to-end test uses forcharging.interrupted.For #1589's protocol text (not edited here):
eventgainsts: the moment of the event, which can be earlier thanwallMswhen it waited for the uplink.AlertMaxAgeMsabove.Tests
End-to-end, on this laptop
Before the APNs key existed
Setup
claude/cloud-c4-core(19ea338c,fe359327), with the endpoint, a local service key and a dev root set by-ldflags -X.ftwclouddfrom srcfl/ftw-cloudclaude/cloud-c4-eb03d7, built with-tags ftwclouddevand-dev-apns-capture. That flag writes each alert to a file instead of sending it to Apple.sim-ferroamp,sim-sungrow, andsim-ocppas an Easee charger.claude/ios-m3-notify(Debug), on the simulator "FTW M3" (iPhone 17, iOS 26.5, Swedish).-FTWPairLink).-FTWCloudHome …, standing in for C3's K11.xcrun simctl push.POST /api/cloud/grant {scopes: [ftw.live.read, ftw.events.read]}on loopback. The uplink connected; homefid4fo4j….charging.connectedat 08:37:09: level assumed, no goal, after the 90 s settle. The service sent the alert to 1 phone 11 ms later.cloudwire.ReadAlertwith the box's key:verdict=event kind=charging.connected lp=7edYjyHw-MQ seq=108. Theapsis the generic one, and the event is the box's envelope unchanged.7edYjyHw-MQ→garageand sentloadpoint.soc.set. The box's result came in 1,002 ms:setLevel(pct: 80.0) answered in 1002 ms,push: ftw.level.80: done: FTW planerar nu från 80 %.The box then readsoc_source: inferred, current_soc: 0.80.charging.connected→charging.bonnected).ReadAlertgaveverdict=generic. In the app, a tap on 20 % loggedpush: ftw.level.20 not sent: generic, and the box stayed at 80 %.charging.interruptedReadAlertgaveverdict=event kind=charging.interrupted.POST /api/restartat 08:54:00. Core sentbye{why:"update"}and exited; it has no supervisor here. 11.5 minutes later no alert had left the service.kill -9at 09:06:18. The service loggeda box went away bye="" out_of_reach_after=10m0s, and at 09:16:18a box is out of reach; telling its phones, with the alert to 1 phone. Its payload is{"ftw":{"v":1,"home":"fid4fo4j…","kind":"box.unreachable"}}, andReadAlertgivesverdict=unreachable.Through Apple's sandbox, with the real key
The same house, with
ftwclouddsending toapi.sandbox.push.apple.com. KeyK9299SY46H, teamL66M368PZ6, read from SSM Parameter Store into a temporary folder and deleted afterwards; nothing printed it. The extension (FTWNotify) now ran for every push.box.unreachable(sent with/dev/push/raw)200. The extension loggedpush: unreachable. The lock screen showed TIDSKÄNSLIG · Din box går inte att nå – "Den har slutat svara. Strömavbrott, eller så är internet hemma nere."notifications: sent event=charging.connectedat 09:32:59.870. The service sent to Apple at 09:33:00.032, and the extension loggedpush: event charging.connectedat 09:33:00.138, 268 ms after Core. On screen: Bilen är inkopplad – "Öppna för att se laddningen, kolla bilens nivå och välja klartid."setLevel(pct: 80.0) answered in 990 ms,push: ftw.level.80: done: FTW planerar nu från 80 %.The box went fromsoc_source: assumed, current_soc: 0.20toinferred, 0.80.charging.bonnected), sent with/dev/push/raw. Apple200; the extension loggedpush: generic. The lock screen showed "Något hände hemma / Öppna FTW för att se vad.", and "Visa" opened it with no buttons.charging.interruptedpush: event charging.interruptedat 09:47:02.076. On screen: TIDSKÄNSLIG · Laddningen avbröts – "Bilen slutade ladda innan den var klar." Buttons: Ladda nu, Öppna.chargeNow answered in 9433 ms,push: ftw.chargeNow: done: FTW har startat laddningen. Väntar på att bilen tar ström.The planned update and the crash ran in the first part. They never reach Apple: no alert left the service for the update, and the crash's
box.unreachableis the payload shown in the first row here. Screenshots are kept with the test, outside the repository.Not done
expect.sessiononloadpoint.soc.set(B4), so a notification button cannot correct a later plug-in. The 10-minute window for buttons covers most of it.update.installedand the other kinds go as before. Only the charging kinds changed.🤖 Generated with Claude Code