Skip to content

feat(fleet): take ftw.fleet/2 and its health counts - #52

Merged
frahlg merged 16 commits into
mainfrom
feat/fleet-report-health
Oct 24, 2026
Merged

frahlg merged 16 commits into
mainfrom
feat/fleet-report-health

Conversation

@frahlg

@frahlg frahlg commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

What changes

srcfl/ftw#1821 adds four coarse health buckets to the anonymous daily fleet report (ftw.fleet/2): the slowest plan of the box's last day, how many plans fell back, the share of driver polls that failed, and the newest update of its last week. This moves the protocol pin to that branch, so the service:

The privacy argument for the four fields is in srcfl/ftw#1821.

Order

Checks

go vet ./..., go test -race ./..., bash scripts/check-imports.sh pass locally. New tests: TestHealthIsCountedButNotPublished (fleet), TestTheFeedCarriesHealthCounts (ops), and an ftw.fleet/2 report through the gateway. TestBothReportsReachTheirTotalsThroughARelay now names ftw.fleet/1 for its seven-field report. TestHbAndLiveTravelAsDatagrams failed once on a loaded laptop and passed three runs in a row after, here and on #51.

Nothing is deployed.

🤖 Generated with Claude Code

ftwcloudd is the gateway (RFC 9458) for the box's daily energy report,
which reaches it through an independent relay, so the service sees the
report and never the box's address.

- POST /v1/ohttp opens a request with the gateway key, takes only the one
  padded inner request with one valid report (energywire), adds its six
  sums to the date's row and answers a sealed 204 or 400. Nothing is kept
  per report: no row, logged body or address. One shared rate budget, and
  at most 100,000 reports a date.
- GET /.well-known/ohttp-gateway serves the key configuration.
- GET /v1/energy/public lists the closed dates of the last 30 days; a date
  with fewer than 5 reports shows null.
- The key comes from Parameter Store into tmpfs at boot, as the Noise key
  does (deploy/ohttp.md, deploy/ohttp.sh); keygen -ohttp prints the pin
  Core needs. Without a key the gateway routes answer 501.

Pairs with srcfl/ftw feat/anonymous-energy-ohttp; go/protocol is pinned at
its commit 28fddd802242.

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

chatgpt-codex-connector Bot commented Oct 24, 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-24T06:35:25.879104Z 4d850f7 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 added a commit to srcfl/ftw-webapp that referenced this pull request Oct 24, 2026
FTW Cloud's /v1/ops carries the health counts of ftw.fleet/2 reports in
its closed fleet days (srcfl/ftw-cloud#52, srcfl/ftw#1821): the slowest
plan and fallback plans of each box's last day, the share of failed
driver reads, and the newest update of its last week. The parser takes a
day with all four, each from its fixed list and none above the day's
reports, or a day with none, and refuses anything between. The private
view shows them beside the other labels. The fixture is ftwcloudd's own
answer with an ftw.fleet/2 report in it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fredrik's change of scope, 24 October 2026: the fleet report moves onto
the same relay and gateway as the energy report, and the two stay apart.

- internal/energy becomes internal/gateway. POST /v1/ohttp reads the one
  padded inner request (go/protocol/reportwire), picks the decoder by
  schema, and adds a fleet report to the existing fleet_days totals that
  /v1/fleet/public reads and an energy report to energy_days. Nothing is
  kept per report.
- POST /v1/fleet stays for boxes on a Core release without a relay; it can
  go one stable Core release after the first that pins the relay.
- The request cap fits the 1,591-byte sealed request; README and
  deploy/ohttp.md describe both reports and the two-per-day traffic.
- go/protocol pinned at srcfl/ftw 4cfd65f7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg force-pushed the feat/internal-ops-stats branch from 4f2c862 to 999854e Compare October 24, 2026 06:43
@frahlg
frahlg force-pushed the feat/fleet-report-health branch from 58b909d to a2f6328 Compare October 24, 2026 06:46
frahlg added a commit to srcfl/ftw-webapp that referenced this pull request Oct 24, 2026
FTW Cloud's /v1/ops carries the health counts of ftw.fleet/2 reports in
its closed fleet days (srcfl/ftw-cloud#52, srcfl/ftw#1821): the slowest
plan and fallback plans of each box's last day, the share of failed
driver reads, and the newest update of its last week. The parser takes a
day with all four, each from its fixed list and none above the day's
reports, or a day with none, and refuses anything between. The private
view shows them beside the other labels. The fixture is ftwcloudd's own
answer with an ftw.fleet/2 report in it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A date with five reports and one car published that car's whole charge in
vehicle_charge_kwh, because the sum counted the other four reports' zeros.
energy_days now counts, for each sum, the reports that had any of it, and
/v1/energy/public shows a sum only when none or at least five had some.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg force-pushed the feat/internal-ops-stats branch from 999854e to d0fe105 Compare October 24, 2026 07:04
@frahlg
frahlg force-pushed the feat/fleet-report-health branch from a2f6328 to 214223e Compare October 24, 2026 07:04
frahlg added a commit to srcfl/ftw-webapp that referenced this pull request Oct 24, 2026
FTW Cloud's /v1/ops carries the health counts of ftw.fleet/2 reports in
its closed fleet days (srcfl/ftw-cloud#52, srcfl/ftw#1821): the slowest
plan and fallback plans of each box's last day, the share of failed
driver reads, and the newest update of its last week. The parser takes a
day with all four, each from its fixed list and none above the day's
reports, or a day with none, and refuses anything between. The private
view shows them beside the other labels. The fixture is ftwcloudd's own
answer with an ftw.fleet/2 report in it.

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

From review of #50:
- Replays: each inner request now carries a random report id and its send
  day (srcfl/ftw a7c025e8). The gateway refuses a request sent before
  yesterday and keeps a hash of each id per day for three days, in a WITHOUT
  ROWID table with no time, so a replay or a retry adds nothing and is still
  answered 204. Seven posts of one ciphertext now count once.
- /v1/fleet/public lists closed UTC days only, so the relay cannot poll it
  and match a new count to the report it just forwarded.
- Each report kind has its own budget, spent only by a report that opened
  and decoded; junk no longer silences real reports.
- A 10-second deadline on reading the body of /v1/ohttp and /v1/fleet; a
  failed read closes the connection.
- ohttp keys: data after the last whole PEM block, such as a second key cut
  short in a rotation, is refused instead of silently ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg force-pushed the feat/internal-ops-stats branch from e755d0f to f675fdc Compare October 24, 2026 07:50
@frahlg
frahlg force-pushed the feat/fleet-report-health branch from 8ad4e57 to f3bb991 Compare October 24, 2026 07:51
frahlg and others added 6 commits October 24, 2026 10:10
The gateway now looks a report id up, read-only, before it spends the
kind's budget. A repeat gets a sealed 204 and costs nothing, so one
captured request sent many times cannot crowd out real reports.

The OHTTP key file must hold whole PKCS #8 blocks and nothing else. A
block cut short at either end, or text between blocks, stops the start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves the protocol pin to srcfl/ftw#1821, whose fleet report adds four
health buckets: the slowest plan of the box's last day, how many plans
fell back, the share of driver polls that failed, and the newest update
of its last week. Both routes, /v1/fleet and the relay's /v1/ohttp,
take ftw.fleet/1 and ftw.fleet/2; the gateway dispatches both schemas
to the fleet totals, so a box released between #1820 and #1821 still
counts. One daily count per field, never their combination.
/v1/fleet/public leaves the health counts out; /v1/ops carries them,
exact, for closed days.

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

ftwcloudd now keeps an hourly summary of every series it counts in the
registry (ops_hours), for 90 days: each counter's rise, each gauge's low,
mean and high, each latency's buckets, how much of the hour a process ran
and how many started. A deploy hands over its part of the hour after the
drain; the next process adds the rest.

GET /v1/ops serves a week of settled hours at a time, with the exact
totals of the anonymous fleet and energy reports for closed days, only to
the key the project stats Worker holds. The service holds the key's
SHA-256 (-ops-key-sha256); without it the route answers 404 to everyone.

New series: sockets by transport (QUIC or WebSocket), bytes written to
boxes and phones, MCP requests and tool calls by outcome and their time,
web push answers, and the drain's sockets. Every label is a fixed word
(TestCountsNameNobody), and traffic makes no new series and leaves no home,
device, key or address in an hour (TestAnHourOfCountsNamesNobody).

Stacked on #50, whose energy day totals it reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A malformed FTW_CLOUD_OPS_KEY_SHA256, such as a stray quote or line end
in compose.env, made ops.New fail, and with it the whole service. The
hash is now read by UseKey after New: quotes and white space around it
are dropped, and a hash that is still not 64 hex digits is logged and
leaves /v1/ops answering 404, while boxes and phones are served as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg force-pushed the feat/internal-ops-stats branch from f675fdc to fa879b0 Compare October 24, 2026 08:22
@frahlg
frahlg force-pushed the feat/fleet-report-health branch from f3bb991 to 82d08a0 Compare October 24, 2026 08:22
frahlg and others added 2 commits October 24, 2026 10:25
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	CLAUDE.md
#	README.md
#	cmd/ftwcloudd/main.go
#	go.mod
#	go.sum
#	internal/server/server.go
#	internal/server/server_test.go
frahlg and others added 2 commits October 24, 2026 11:40
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg added a commit to srcfl/ftw-webapp that referenced this pull request Oct 24, 2026
* feat(stats): show FTW Cloud's anonymous daily energy reports

The "Energy via FTW Cloud" section shows the newest public day of the
anonymous daily energy reports: its report count and its solar, grid,
battery and vehicle sums, and the public days of the last 30. Labelled as
reports, not homes.

The Worker reads https://cloud.ftw.energy/v1/energy/public hourly with the
other Cloud feeds, parses it as strictly as the fleet feed (exact fields,
closed dates only, at least five reports or all null, sums in range) and
keeps only the latest copy in D1 (migration 0004). A Cloud that does not
serve the route yet is recorded as awaiting source, not as a failure.

Pairs with srcfl/ftw and srcfl/ftw-cloud feat/anonymous-energy-ohttp.

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

* docs(stats): the fleet report takes the same relay, with its feed unchanged

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

* fix(stats): read the energy feed on the earlier clock and say when it is stale

From review of #104:
- A feed stamped up to 2 h ahead of the Worker could pass a day as closed
  one day early. The closed-day bound now uses the earlier of the feed's
  stamp and the Worker's clock.
- observed_at and each date must be strings; an array no longer passes
  through String().
- The daily energy feed has its own freshness entry ("Daily energy", stale
  after 48 hours, since Cloud adds a day once a day); the hourly usage dot
  is now "Hourly energy". A stale feed says so in the note and on the tiles,
  also when every day is withheld.
- Cloud now withholds a sum that one to four of a day's reports had any of
  (srcfl/ftw-cloud#50). The parser takes null for such a sum, and the 30-day
  window sums each field over the days that show it.

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

* feat(stats): show how FTW Cloud runs in the private view

The Worker reads FTW Cloud's internal feed (GET /v1/ops, with the
CLOUD_OPS_KEY secret) once an hour at minute 23 and keeps it in D1:
the service's hourly sums for 90 days, and the latest exact totals of
closed fleet and energy report days. The parser takes only the fields
ftw.cloud-ops/1 names, at every level, and refuses the whole answer
otherwise; a test holds it to a feed ftwcloudd wrote.

The private view shows boxes and phones connected, the share on QUIC,
data written this month against AWS's 100 GB of free data out, drops
by reason, push results, assistant calls by outcome, the slowest p99 of
the day, each deploy's drain, and the exact daily reports. Nothing in it
names a home, a device or an address. The public view is unchanged.

Stacked on #104, whose energy fields it shares.

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

* feat(stats): show the fleet report's health counts in the private view

FTW Cloud's /v1/ops carries the health counts of ftw.fleet/2 reports in
its closed fleet days (srcfl/ftw-cloud#52, srcfl/ftw#1821): the slowest
plan and fallback plans of each box's last day, the share of failed
driver reads, and the newest update of its last week. The parser takes a
day with all four, each from its fixed list and none above the day's
reports, or a day with none, and refuses anything between. The private
view shows them beside the other labels. The fixture is ftwcloudd's own
answer with an ftw.fleet/2 report in it.

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

* fix(stats): read the slowest quantile right and keep the panel private

Review of #105:
- A quantile past ftwcloudd's last bucket comes as null beside a count.
  It was dropped or read as 0; it now reads as "over 10 s" (over_10s in
  the API), the slowest case, in every list and in forward_p99_ms.
- The slowest assistant call keeps to the 7 days the panel names.
- The public page no longer carries the panel's markup, copy or script.
  A test reads the public HTML.

Also: a week without hours says so instead of "Nothing dropped"; old
daily totals read stale; missing hours are gaps in the chart, with their
own tooltip; the shards of an hour are added together; data out says
how many of the month's hours it covers.

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

* test(stats): the exact feed has no withheld energy sum

The public energy feed may now show null for a sum few reports had
(#104). The internal feed is exact and never does; its parser refuses a
null sum.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg changed the base branch from feat/internal-ops-stats to main October 24, 2026 09:44
# Conflicts:
#	internal/ops/ops_test.go
#	privacy/internal-operations.md
@frahlg
frahlg merged commit 95386f1 into main Oct 24, 2026
2 checks 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