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
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,8 +524,7 @@ partial; missing data is unknown. One instance supplies its local shard only;
public multi-shard totals need an explicit safe collector.

The separate anonymous daily-report feed is `GET /v1/fleet/public`. It exposes
only daily counts and dimension labels with at least 5 reports, for at most
90 days. It leaves the health counts out; only `/v1/ops` carries them. It contains imported relay history and new reports received by Cloud;
daily counts and labels from one report up, for at most 90 days. It leaves the health counts out; only `/v1/ops` carries them. It contains imported relay history and new reports received by Cloud;
consumers replace the old archive rather than adding both. It needs no hourly
energy activation. Updated Core sends it only after the current project
statistics answer; legacy versions retain their existing daily-report setting. Averaging seven complete UTC days gives an estimate of active
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/fxamacker/cbor/v2 v2.9.4
github.com/gorilla/websocket v1.5.3
github.com/quic-go/quic-go v0.63.0
github.com/srcfl/ftw/go/protocol v0.0.0-20261108080825-fd4426a107c1
github.com/srcfl/ftw/go/protocol v0.0.0-20261108151528-95f1c85b9c07
golang.org/x/crypto v0.57.0
golang.org/x/net v0.58.0
golang.org/x/sys v0.48.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ github.com/quic-go/quic-go v0.63.0 h1:LIFGHI4PFUhhw2dDD1ARHdCff143ffMHwZtbnbuJ78
github.com/quic-go/quic-go v0.63.0/go.mod h1:RAro2j2yN9a9EiPACLHT9IB2NXCvGQmmo/alT0yYI0w=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/srcfl/ftw/go/protocol v0.0.0-20261108080825-fd4426a107c1 h1:jMTkobIdBd+6uLxjfuWrBXGLOroK7EhubT6g8giZCbg=
github.com/srcfl/ftw/go/protocol v0.0.0-20261108080825-fd4426a107c1/go.mod h1:VEjr/o0yZijJ1nApuJK+nak6FxJ/fubAUBNOunqNrUU=
github.com/srcfl/ftw/go/protocol v0.0.0-20261108151528-95f1c85b9c07 h1:2Nyg0erLi0eClgq73hYmQLH4wRRrhFYvi081rH5/9v0=
github.com/srcfl/ftw/go/protocol v0.0.0-20261108151528-95f1c85b9c07/go.mod h1:VEjr/o0yZijJ1nApuJK+nak6FxJ/fubAUBNOunqNrUU=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
Expand Down
34 changes: 10 additions & 24 deletions internal/fleet/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import (
"github.com/srcfl/ftw/go/protocol/fleetwire"
)

// Public exports only cells already large enough for the project dashboard.
// A day below the minimum has a null count and no dimension labels. Only
// closed UTC days are listed: today's running counts would let whoever just
// forwarded a report, such as the relay, see which count it moved.
// Public exports each day's totals, from one report up (Fredrik, 8 November
// 2026: a version, a price zone or a battery to the nearest kWh is not
// sensitive). Health counts stay out. Only closed UTC days are listed: today's
// running counts would let whoever just forwarded a report, such as the relay,
// see which count it moved.
func (s *Service) Public(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=60")
if r.Method != http.MethodGet {
Expand All @@ -23,32 +24,17 @@ func (s *Service) Public(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
// Five since 23 October 2026 (Fredrik): the fleet is small and the
// reports carry no home id. A day and each label below it stay hidden.
const minimum = 5
today := s.now().UTC().Format(time.DateOnly)
days := make([]map[string]any, 0, len(view.Days))
for _, d := range view.Days {
if d.Date >= today {
continue
}
row := map[string]any{"date": d.Date, "reports": nil}
for key, values := range map[string]map[string]int{"ftw_versions": d.Versions, "channels": d.Channels, "drivers": d.Drivers, "battery_kwh": d.Battery, "price_zones": d.Zones, "install_age": d.Age} {
safe := map[string]int{}
if d.Reports >= minimum {
for label, n := range values {
if n >= minimum {
safe[label] = n
}
}
}
row[key] = safe
}
if d.Reports >= minimum {
row["reports"] = d.Reports
}
days = append(days, row)
days = append(days, map[string]any{
"date": d.Date, "reports": d.Reports, "ftw_versions": d.Versions, "channels": d.Channels, "drivers": d.Drivers,
"battery_kwh": d.Battery, "price_zones": d.Zones, "install_age": d.Age,
})
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"schema": "ftw.fleet-public/1", "meaning": fleetwire.Meaning, "observed_at": s.now().UTC().Format(time.RFC3339), "minimum": minimum, "days": days})
_ = json.NewEncoder(w).Encode(map[string]any{"schema": "ftw.fleet-public/1", "meaning": fleetwire.Meaning, "observed_at": s.now().UTC().Format(time.RFC3339), "minimum": 1, "days": days})
}
91 changes: 40 additions & 51 deletions internal/fleet/public_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fleet

import (
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
Expand All @@ -12,57 +13,6 @@ import (
"github.com/srcfl/ftw/go/protocol/fleetwire"
)

func TestPublicFleetHidesSmallDaysAndEachSmallLabel(t *testing.T) {
reg, err := registry.Open(filepath.Join(t.TempDir(), "db"))
if err != nil {
t.Fatal(err)
}
defer reg.Close()
svc, err := New(reg)
if err != nil {
t.Fatal(err)
}
now := time.Now()
svc.now = func() time.Time { return now }
report := fleetwire.Report{Schema: fleetwire.SchemaV1, Version: "v0.147.0", Channel: "stable", Drivers: []string{}, Battery: "none", Zone: "SE4", Age: "unknown"}
for range 4 {
_, err = reg.FleetPut(report, now)
if err != nil {
t.Fatal(err)
}
}
get := func() map[string]any {
w := httptest.NewRecorder()
svc.Public(w, httptest.NewRequest("GET", "/v1/fleet/public", nil))
var v map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &v); err != nil {
t.Fatal(err)
}
return v
}
// Today is still open, so its running counts are not listed: whoever
// forwarded a report could see which count it moved.
if days := get()["days"].([]any); len(days) != 0 {
t.Fatalf("today listed: %v", days)
}
svc.now = func() time.Time { return now.Add(24 * time.Hour) }
v := get()
d := v["days"].([]any)[0].(map[string]any)
if d["reports"] != nil || len(d["ftw_versions"].(map[string]any)) != 0 {
t.Fatal("small day exposed")
}
report.Version = "v0.147.1"
_, _ = reg.FleetPut(report, now)
v = get()
d = v["days"].([]any)[0].(map[string]any)
if d["reports"] != float64(5) || len(d["ftw_versions"].(map[string]any)) != 0 {
t.Fatal("small version exposed")
}
if d["channels"].(map[string]any)["stable"] != float64(5) {
t.Fatal("safe cell hidden")
}
}

// A report that says how the box ran is taken beside the older shape. Its
// health counts stay out of the public view, which lists its fields by name.
func TestHealthIsCountedButNotPublished(t *testing.T) {
Expand Down Expand Up @@ -101,3 +51,42 @@ func TestHealthIsCountedButNotPublished(t *testing.T) {
}
}
}

// Every field counts from one, however small the day, and today stays out
// until it is closed.
func TestTheDaysTotalsCountFromOne(t *testing.T) {
reg, err := registry.Open(filepath.Join(t.TempDir(), "db"))
if err != nil {
t.Fatal(err)
}
defer reg.Close()
svc, err := New(reg)
if err != nil {
t.Fatal(err)
}
now := time.Date(2026, 11, 8, 12, 0, 0, 0, time.UTC)
report := fleetwire.Report{Schema: fleetwire.SchemaV1, Version: "v0.162.0-beta.1", Channel: "beta", Drivers: []string{"sma_pv@1.1.1"},
Battery: "12", Zone: "SE3", Age: "0-1m"}
if _, err := reg.FleetPut(report, now); err != nil {
t.Fatal(err)
}
read := func() map[string]any {
rr := httptest.NewRecorder()
svc.Public(rr, httptest.NewRequest(http.MethodGet, "/v1/fleet/public", nil))
var v map[string]any
if err := json.Unmarshal(rr.Body.Bytes(), &v); err != nil {
t.Fatal(err)
}
return v
}
svc.now = func() time.Time { return now }
if days := read()["days"].([]any); len(days) != 0 {
t.Fatalf("today listed: %v", days)
}
svc.now = func() time.Time { return now.Add(24 * time.Hour) }
d := read()["days"].([]any)[0].(map[string]any)
if d["reports"] != float64(1) || d["drivers"].(map[string]any)["sma_pv@1.1.1"] != float64(1) ||
d["battery_kwh"].(map[string]any)["12"] != float64(1) || d["price_zones"].(map[string]any)["SE3"] != float64(1) {
t.Fatalf("a day of one report: %v", d)
}
}
7 changes: 3 additions & 4 deletions internal/ops/ops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,10 +291,9 @@ func TestTheDaysAreExactOnceClosed(t *testing.T) {
w = httptest.NewRecorder()
fl.Public(w, httptest.NewRequest(http.MethodGet, "/v1/fleet/public", nil))
_ = json.Unmarshal(w.Body.Bytes(), &public)
for _, d := range public.Days {
if d["reports"] != nil {
t.Fatalf("the public view should hide days below five: %s", w.Body.String())
}
// The public view counts from one report, and health stays out of it.
if len(public.Days) != 1 || public.Days[0]["reports"] != float64(2) || public.Days[0]["plan_solve"] != nil {
t.Fatalf("public view: %s", w.Body.String())
}
}

Expand Down
Loading