Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/statistics-48-hours.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ftw": minor
---

Limit project statistics to 48 hours under new Cloud and local policies. Earlier choices do not accept the new policy. Collection still waits for the signed privacy review.
10 changes: 5 additions & 5 deletions docs/adr/0008-ftw-cloud.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ that question and the DPIA have been reviewed and signed.
prices, free text, configuration documents or logs.
- **`statsId`.** 128 random bits drawn by the box at consent, derived from
nothing else. A new consent draws a new one.
- **Retention.** Rows go 13 months after the hour they describe. Leaving Cloud, stopping local sharing or
- **Retention.** Rows go 48 hours after the hour they describe. Leaving Cloud, stopping local sharing or
a purge deletes unpublished rows when Cloud receives the signed request.
An offline box sends it on reconnection. Published aggregates stay, and the
consent screen says so.
Expand Down Expand Up @@ -218,7 +218,7 @@ The privacy notice says so.
| Access logs | 14 days, with a keyed hash instead of the address |
| SSM sessions and deploy log | 24 months |
| Backups | None; boxes refill the service |
| Statistics | 13 months |
| Statistics | 48 hours |

## Operations and cost

Expand Down Expand Up @@ -311,8 +311,8 @@ rules, the app asks again. A public launch needs C2–C4 and the legal work.

### Project statistics policy, October 2026

`ftw-project-stats-2026-10-v2` covers configured capacities, flexible power,
solar rating and closed-hour energy. `ftw-cloud-2026-10-v2` includes that scope
`ftw-project-stats-2026-10-v3` covers configured capacities, flexible power,
solar rating and closed-hour energy. `ftw-cloud-2026-10-v3` includes that scope
for every Cloud home. An owner reads `GET /api/cloud/stats-policy`, then
accepts the Cloud terms through `POST /api/cloud/grant`. Core records the
scope, random stats id and start time in the same grant. An older Cloud
Expand All @@ -336,7 +336,7 @@ The public view counts installations reporting in the last 24 closed hours.
It sums the recorded hours, which may be incomplete; it does not claim all
FTW users, full-day production or measured control capacity. Configured
limits remain distinct from measured command results. Unknown values stay
absent. Cloud keeps the consented hourly rows for 13 months and publishes
absent. Cloud keeps the consented hourly rows for 48 hours and publishes
only the existing group thresholds. Withdrawal deletes unpublished rows
when Cloud receives the signed roster; offline withdrawal takes effect on
reconnect. Published group totals remain. Native consent UI must adopt the
Expand Down
10 changes: 5 additions & 5 deletions docs/protocol-cloud.md
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,7 @@ the service find it offline; it pairs over the relay or the home network.

Signed box to service only. Never forwarded to phones or agents. The box
sends at most one report per closed UTC hour, after fresh consent to
`ftw-project-stats-2026-10-v1`. Older policies send nothing.
`ftw-project-stats-2026-10-v3`. Older policies send nothing.

| Field | Type | Meaning |
|---|---|---|
Expand All @@ -771,7 +771,7 @@ limits, with shared inverter groups counted once; it does not prove physical
control. Energy is recorded ledger energy and may cover only part of an hour.
No names, hardware ids, location, prices or live readings leave this path.

The service upserts by home and hour, keeps these consented rows for 13 months,
The service upserts by home and hour, keeps these consented rows for 48 hours,
and deletes them when it receives statistics withdrawal or a home purge.
Only cells with at least 20 homes and no home above 30 percent of the sum
are public. The public view counts reporting installations, never people.
Expand Down Expand Up @@ -1625,14 +1625,14 @@ constants, so `srcfl/ftw-cloud` need not spell them by hand.
| Sessions closed | When the roster runs out |
| Tokens and APNs dropped after | 30 days |
| Home deleted after | 90 days without a roster; 24 hours for a new home without one |
| Statistics rows kept | 13 months from the hour they describe |
| Statistics rows kept | 48 hours from the hour they describe |
| Rule N clock lead allowed | 2 s |
| Rule U drift margin, lifetime | 0.01 % of the time since learned; 24 hours |


### Cloud terms and local statistics, policy version 2
### Cloud terms and local statistics, policy version 3

`ftw-cloud-2026-10-v2` includes `ftw-project-stats-2026-10-v2` for every Cloud
`ftw-cloud-2026-10-v3` includes `ftw-project-stats-2026-10-v3` for every Cloud
Comment thread
frahlg marked this conversation as resolved.
home. `Grant.termsPolicy` carries the owner-accepted terms. Once the operator
approves collection, Cloud admits only that bundled grant or a statistics-only
roster. Old terms are not silently upgraded; they need an owner answer.
Expand Down
2 changes: 1 addition & 1 deletion go/internal/api/cloud_statistics.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func (s *Server) handleCloudStatsPolicy(w http.ResponseWriter, r *http.Request)
"fields": cloudwire.StatsMetricKeys,
"meaning": "One reporting installation, configured flexible power and solar ratings, battery capacity, and recorded closed-hour energy. Configured limits do not prove physical control. Missing readings are unknown.",
"resolution": "Closed UTC hours, energy rounded to 10 Wh, capacities and limits rounded to 10 Wh or W. No names, addresses, device ids or location.",
"retention": "13 months. Leaving Cloud or stopping local sharing deletes unpublished household rows; published group totals remain.",
"retention": "48 hours. Leaving Cloud or stopping local sharing deletes unpublished household rows; published group totals remain.",
"publication_minimum_homes": cloudwire.StatsMinimumHomes,
"publication_max_home_share": 0.3,
"controller": "sourceful", "local_default": "ask_yes", "local_optional": true, "cloud_required": true,
Expand Down
7 changes: 5 additions & 2 deletions go/protocol/cloudwire/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import "slices"
// StatsPolicy names the scope included in the current Cloud terms, or
// accepted through the separate local statistics question. Earlier choices
// never accept this policy. Collection still requires service approval.
const StatsPolicy = "ftw-project-stats-2026-10-v2"
const CloudTermsPolicy = "ftw-cloud-2026-10-v2"
const StatsPolicy = "ftw-project-stats-2026-10-v3"
const CloudTermsPolicy = "ftw-cloud-2026-10-v3"
const MsgStats = "stats"
const StatsHourMS = int64(3_600_000)

// StatsRetentionMS is the maximum age of a stored household hour.
const StatsRetentionMS = 48 * StatsHourMS
const StatsMinimumHomes = 20

var StatsMetricKeys = []string{
Expand Down
2 changes: 1 addition & 1 deletion go/protocol/cloudwire/stats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestStatisticsOnlyClosedAllowlistedQuantizedHours(t *testing.T) {
if !valid().Valid(now) {
t.Fatal("valid closed hour rejected")
}
cases := map[string]func(*Stats){"open hour": func(s *Stats) { s.HourMS += StatsHourMS }, "before window": func(s *Stats) { s.HourMS -= 24 * StatsHourMS }, "fine time": func(s *Stats) { s.HourMS++ }, "new field": func(s *Stats) { s.Metrics["address"] = 0 }, "fine energy": func(s *Stats) { s.Metrics["pv_generation_wh"] = 121 }, "too large": func(s *Stats) { s.Metrics["pv_generation_wh"] = 1000010 }, "old policy": func(s *Stats) { s.Policy = "old" }, "no id": func(s *Stats) { s.StatsID = nil }}
cases := map[string]func(*Stats){"open hour": func(s *Stats) { s.HourMS += StatsHourMS }, "before window": func(s *Stats) { s.HourMS -= 24 * StatsHourMS }, "fine time": func(s *Stats) { s.HourMS++ }, "new field": func(s *Stats) { s.Metrics["address"] = 0 }, "fine energy": func(s *Stats) { s.Metrics["pv_generation_wh"] = 121 }, "too large": func(s *Stats) { s.Metrics["pv_generation_wh"] = 1000010 }, "previous policy": func(s *Stats) { s.Policy = "ftw-project-stats-2026-10-v2" }, "no id": func(s *Stats) { s.StatsID = nil }}
for name, change := range cases {
t.Run(name, func(t *testing.T) {
s := valid()
Expand Down
8 changes: 4 additions & 4 deletions go/protocol/cloudwire/testdata/cloud-quic-vectors.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,22 +250,22 @@
},
{
"name": "statistics service ready",
"envelope": "a36162588da563656b6d582022222222222222222222222222222222222222222222222222222222222222226468617665a068626f784e6f6e63655820333333333333333333333333333333333333333333333333333333333333333369726f73746572536571046b7374617473506f6c696379781c6674772d70726f6a6563742d73746174732d323032362d31302d763261746868656c6c6f5f6f6b637369675840a838159a22c63e291c08d701b0ce0f7c0fd155c45fdcfa9b50e1253bf2fa312917ecd76379072c9bbaf3d62d1bbf95b93879177765f4b0adb73dd7ef0e2dd91e",
"envelope": "a36162588da563656b6d582022222222222222222222222222222222222222222222222222222222222222226468617665a068626f784e6f6e63655820333333333333333333333333333333333333333333333333333333333333333369726f73746572536571046b7374617473506f6c696379781c6674772d70726f6a6563742d73746174732d323032362d31302d763361746868656c6c6f5f6f6b637369675840790390a99204558bb3d80cbbc6a436d40548f3d3e865cb4b71329c2f09b2d3abb5f1814526280ba8aac35113d21ae1dd715950bf2ccf7a66b30484cfc0d1b406",
"datagram": false
},
{
"name": "statistics closed hour",
"envelope": "a3616258b8a8637365711464626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b066686f75724d731b000001a130a0b18066706f6c696379781c6674772d70726f6a6563742d73746174732d323032362d31302d76326677616c6c4d731b000001a130d7f9d8676d657472696373a270666c657869626c655f706f7765725f77192af87070765f67656e65726174696f6e5f776818786773746174734964508888888888888888888888888888888868757074696d654d731a0001443861746573746174736373696758406147f18ed9f5927d3abf1599168322a021194369ad42d8b199abbd894040cc7e87c4fcff89e551c210863d2ed4f2bdeaddd6304f5fdf00188a5232b3246c23d6",
"envelope": "a3616258b8a8637365711464626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b066686f75724d731b000001a130a0b18066706f6c696379781c6674772d70726f6a6563742d73746174732d323032362d31302d76336677616c6c4d731b000001a130d7f9d8676d657472696373a270666c657869626c655f706f7765725f77192af87070765f67656e65726174696f6e5f776818786773746174734964508888888888888888888888888888888868757074696d654d731a000144386174657374617473637369675840134ad340a1190a23d332c1cd39aabafb2a8e88a6393870feb391a7178b39302ff8a38ec7fc1ba9e4a658b8d8d470176f2b979df63dbc258f31450268eb33721f",
"datagram": false
},
{
"name": "statistics only roster",
"envelope": "a36162590133aa637365711764626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a862696450444444444444444444444444444444446673636f706573806773746174734964508888888888888888888888888888888868707572706f736573816573746174736a636f6e74726f6c6c657269736f7572636566756c6b7374617473506f6c696379781c6674772d70726f6a6563742d73746174732d323032362d31302d76326b7465726d73506f6c696379781c6674772d70726f6a6563742d73746174732d323032362d31302d76326c737461747353696e63654d731b000001a13069c300656e6f697365406677616c6c4d731b000001a130d805906764657669636573806770656e64696e678068757074696d654d731a00014ff069726f7374657253657107617466726f7374657263736967584018851c1cb544043d4e18b2609e5b2a3f6c321bd1ba04a136b2af103992876c90bc33c8d584840917a2d7a1fb0427b22df6deae7fad456c664e8e33f95654b4f7",
"envelope": "a36162590133aa637365711764626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a862696450444444444444444444444444444444446673636f706573806773746174734964508888888888888888888888888888888868707572706f736573816573746174736a636f6e74726f6c6c657269736f7572636566756c6b7374617473506f6c696379781c6674772d70726f6a6563742d73746174732d323032362d31302d76336b7465726d73506f6c696379781c6674772d70726f6a6563742d73746174732d323032362d31302d76336c737461747353696e63654d731b000001a13069c300656e6f697365406677616c6c4d731b000001a130d805906764657669636573806770656e64696e678068757074696d654d731a00014ff069726f7374657253657107617466726f73746572637369675840002b962e9bdd240655dffcc799cacb23414e2f7fd7398209bb9d78b18020a32457bbe8efee41cd35d02ce021940309891e4b0aeacaa6bdcadacd6397b1c4006b",
"datagram": false
},
{
"name": "roster statistics consent",
"envelope": "a36162590130aa637365711564626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a862696450444444444444444444444444444444446673636f706573806773746174734964508888888888888888888888888888888868707572706f736573826573657276656573746174736a636f6e74726f6c6c657269736f7572636566756c6b7374617473506f6c696379781c6674772d70726f6a6563742d73746174732d323032362d31302d76326b7465726d73506f6c696379746674772d636c6f75642d323032362d31302d76326c737461747353696e63654d731b000001a13069c300656e6f697365406677616c6c4d731b000001a130d7fdc06764657669636573806770656e64696e678068757074696d654d731a0001482069726f7374657253657106617466726f73746572637369675840a043311a41972afcec17e4aa0eae3251e985b503677b2802e5b520cd73acd472050cfe6f660ae85058d0dea532ff832759929c048f3738e4588794447552ded7",
"envelope": "a36162590130aa637365711564626f6f7450b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b06474746c531a00015180656772616e74a862696450444444444444444444444444444444446673636f706573806773746174734964508888888888888888888888888888888868707572706f736573826573657276656573746174736a636f6e74726f6c6c657269736f7572636566756c6b7374617473506f6c696379781c6674772d70726f6a6563742d73746174732d323032362d31302d76336b7465726d73506f6c696379746674772d636c6f75642d323032362d31302d76336c737461747353696e63654d731b000001a13069c300656e6f697365406677616c6c4d731b000001a130d7fdc06764657669636573806770656e64696e678068757074696d654d731a0001482069726f7374657253657106617466726f7374657263736967584059638b7a4468f8a7ae3210d8b83bf5f03adbdfec68c46bf4a8f56e0c7b99918927d12e9c6eea548e3f3d3f1bfd615371cd5d7bbb00c27278ef08a3abdca482d9",
"datagram": false
}
],
Expand Down
Loading
Loading