diff --git a/specs/done_022-default-telemetry-dashboard/OPEN-DECISIONS.md b/specs/done_022-default-telemetry-dashboard/OPEN-DECISIONS.md index 49381ba3a..d4ea95b08 100644 --- a/specs/done_022-default-telemetry-dashboard/OPEN-DECISIONS.md +++ b/specs/done_022-default-telemetry-dashboard/OPEN-DECISIONS.md @@ -1,6 +1,6 @@ # Open Decisions -**Status**: 0 open; 5 ruled (OD-2, OD-3 and OD-4 on 2026-10-06, OD-1 and OD-5 on 2026-10-07). +**Status**: 0 open; 6 ruled (OD-2, OD-3 and OD-4 on 2026-10-06, OD-1 and OD-5 on 2026-10-07, OD-6 on 2026-10-08). Per CLAUDE.md rule 10, an open value MUST NOT be committed as a settled contract in code, chart defaults or docs until it is ruled here. @@ -102,3 +102,11 @@ Per CLAUDE.md rule 10, an open value MUST NOT be committed as a settled contract | Challenge lifetime | 15 minutes | Longer than a Raspberry Pi 4 needs at the 26-bit cap. | | Challenge requests per source | 10 per minute, burst 5 | One source can't raise the difficulty for everyone. | | Used-challenge memory | 1,000,000 entries | Filling it needs that many solved challenges within 15 minutes. | + +### OD-6: Dashboard as-of day + +**Status**: RULED (2026-10-08, user) + +**Question**: The receiver dashboard's ranges ended on the latest complete UTC day (yesterday), so a provider saw "No reports yet" for up to a day after its first accepted report, and today's reports never showed until the day closed. + +**Ruling**: the dashboard shows all data. Every range (7, 30, 90 or 365 days) ends on today, the current UTC day, which is still in progress and labelled so. The empty state appears only when the range holds no report at all. Today's lapsed-installs value stays `0` until the lifecycle job finalises the day. The public `GET /v1/summary` is unchanged and keeps the latest complete day. diff --git a/specs/done_022-default-telemetry-dashboard/contracts/receiver-http.md b/specs/done_022-default-telemetry-dashboard/contracts/receiver-http.md index cfa48148b..00e3a4691 100644 --- a/specs/done_022-default-telemetry-dashboard/contracts/receiver-http.md +++ b/specs/done_022-default-telemetry-dashboard/contracts/receiver-http.md @@ -125,6 +125,7 @@ An E2E check (R17) compares the unauthenticated response against the login page } ``` +- `asOf` is today, the current UTC day, which is still in progress (OD-6). Every range ends on it, its figures change until the day closes, and its lapsed-installs value stays `0` until the lifecycle job finalises the day. `/v1/summary` keeps the latest complete day. - When the range has no data: `empty: true`, with `basic` and `extended` set to `null`. The HTML renders the empty state (FR-027). - When there is basic data but no extended reports: `extended: null`, and the HTML explains that no reports in the range included extended data. diff --git a/specs/done_022-default-telemetry-dashboard/data-model.md b/specs/done_022-default-telemetry-dashboard/data-model.md index 2de557289..4947f4c9b 100644 --- a/specs/done_022-default-telemetry-dashboard/data-model.md +++ b/specs/done_022-default-telemetry-dashboard/data-model.md @@ -224,14 +224,14 @@ Every `value` is a `telemetryschema` enumeration member or `other`, so the row c ### Derived views (computed at read time, [contracts/receiver-http.md](contracts/receiver-http.md)) -The range R is 7, 30, 90 or 365 days and ends on the latest complete day. +The range R is 7, 30, 90 or 365 days and ends on today, the current UTC day, which is still in progress (OD-6). Today's figures change until the day closes, and its lapsed-installs value stays 0 until the lifecycle job finalises the day. | View | Computation | |---|---| | Reports per day | `daily_basic.reports` series over R | | Version adoption (reports) | `daily_version` summed over R. Top 10 by count, then `Other` (the rest) and `Invalid`. | | Fleet distribution and median | `daily_fleet` summed over R. Bands `0,1,2,5,10,25,50,100,250,250+` are applied at read. The exact median comes from the cumulative counts. | -| Latest-day totals | `daily_basic.servers_sum`, `templates_sum` for the latest complete day | +| Latest-day totals | `daily_basic.servers_sum`, `templates_sum` for today (the as-of day, in progress) | | Unique active installs | `count(activity where last_seen ≥ asOf − {0, 6, 29})` | | New and lapsed installs | `daily_ext.new_installs` and `lapsed_installs` series over R | | Version adoption (installs) | `activity.last_version` for records with `last_seen` inside `min(R, ACTIVITY_EXPIRY_DAYS)`. The window used is shown. | @@ -239,4 +239,4 @@ The range R is 7, 30, 90 or 365 days and ends on the latest complete day. | Extended coverage (FR-026) | Σ `ext_reports` ÷ Σ `reports` over R | | Public summary (FR-029) | `reports` for asOf; Σ `reports` over the 30 days ending asOf; `meta.reports_total`; `count(activity where last_seen ≥ asOf − 29)`; `asOf` | -`asOf` is the latest complete UTC day, which is yesterday. When no `daily_basic` row exists in R, every view returns its empty state instead of zeros (FR-027). +For the dashboard, `asOf` is today (the current UTC day, in progress); the public summary keeps the latest complete day. When no `daily_basic` row exists in R, every view returns its empty state instead of zeros (FR-027). diff --git a/specs/done_022-default-telemetry-dashboard/spec.md b/specs/done_022-default-telemetry-dashboard/spec.md index 71aa9bbb3..73baa628d 100644 --- a/specs/done_022-default-telemetry-dashboard/spec.md +++ b/specs/done_022-default-telemetry-dashboard/spec.md @@ -164,7 +164,7 @@ These answer questions that basic counts can't, such as "how many installs are s **Acceptance Scenarios**: 1. **Given** extended reports over a range, **When** an authorized viewer looks at installs, **Then** the dashboard shows: - - unique active installs for the latest day and the trailing 7 and 30 days + - unique active installs for today (in progress) and the trailing 7 and 30 days - new installs (first seen) per period - lapsed installs (not seen for 30 days) per period 2. **Given** extended reports, **When** an authorized viewer looks at environment, **Then** it shows each install's share by Kubernetes minor version, distribution, CPU architecture, and node-count band. @@ -330,10 +330,10 @@ Someone learns an install's ID, for example from a screenshot of Admin Settings, - reports per day as a trend - version adoption, with the top 10 versions plus "Other" and "Invalid" - fleet-size distributions and medians for servers and templates - - total servers and templates on the latest day + - total servers and templates today (in progress) - a "data as of" timestamp - **FR-025**: The dashboard MUST show these **extended** views: - - unique active installs (latest day, trailing 7 and 30 days) + - unique active installs (today in progress, trailing 7 and 30 days) - new and lapsed installs per period - version adoption by install - environment breakdowns diff --git a/specs/done_022-default-telemetry-dashboard/tasks.md b/specs/done_022-default-telemetry-dashboard/tasks.md index b20a398d8..2199fec94 100644 --- a/specs/done_022-default-telemetry-dashboard/tasks.md +++ b/specs/done_022-default-telemetry-dashboard/tasks.md @@ -361,7 +361,7 @@ description: "Task list for feature 022: default telemetry destination, extended - fleet bands and the exact median - `latestDay` totals - `empty` - - `asOf` = yesterday (UTC) + - `asOf` = yesterday (UTC). **Superseded 2026-10-08 (OD-6):** the dashboard ends on today (UTC, in progress); `/v1/summary` keeps yesterday. Invalid ranges fall back to 30. - [X] T066 [US4] Load the `dataviz` skill, then design the receiver pages in `telemetry-receiver/telemetry-dashboard.pen` (seeded with a copy of the HeroUI design, spec Q10) through Pencil MCP: the login page (also used for the refusal, with "Invalid credentials"), the overview's basic section with the range selector (7, 30, 90, 365), the empty state, and the "approximate, self-reported" labelling (FR-026). Ask the user to open that file, then save it, run `design-export` into `telemetry-receiver/design-export/`, and commit. @@ -397,7 +397,7 @@ description: "Task list for feature 022: default telemetry destination, extended - `GET /metrics` returns `404` on the public listener, `401` on the dashboard listener without the Bearer token, and `200` with it - [X] T071 [US4] Add these subtests to `test/e2e/telemetry_e2e_test.go`: - `dashboard_refuses_unauthenticated`, where `/` returns 303 to a login page that contains no digits from the data and `/api/v1/views` returns 401 - - `dashboard_shows_reports`, which signs in with the Secret's token through `Env.PortForward` on 8081 (setting the `Cookie` header by hand, since the session cookie is `Secure` and the port-forward is plain HTTP). Views end at yesterday (UTC), so same-day reports from earlier subtests are not in them: it asserts that `/api/v1/views` returns `200` with `asOf` = yesterday and a well-formed body, and that `/metrics` (Bearer) counts the earlier reports. The exact figures are covered by `views_test.go`. + - `dashboard_shows_reports`, which signs in with the Secret's token through `Env.PortForward` on 8081 (setting the `Cookie` header by hand, since the session cookie is `Secure` and the port-forward is plain HTTP). Views end at yesterday (UTC), so same-day reports from earlier subtests are not in them: it asserts that `/api/v1/views` returns `200` with `asOf` = yesterday (**superseded 2026-10-08, OD-6:** views end on today and the check asserts `asOf` = today) and a well-formed body, and that `/metrics` (Bearer) counts the earlier reports. The exact figures are covered by `views_test.go`. **Checkpoint**: the provider keeps history and shows basic views privately. diff --git a/telemetry-receiver/README.md b/telemetry-receiver/README.md index 5f0d3a4f9..fb04d4171 100644 --- a/telemetry-receiver/README.md +++ b/telemetry-receiver/README.md @@ -67,7 +67,8 @@ public provider turns it on. See ### Dashboard Server-rendered HTML with inline SVG charts, no JavaScript, for the last 7, -30, 90 or 365 days (`?range=`). It needs the dashboard token: a browser +30, 90 or 365 days (`?range=`), ending on today (UTC), which is still in +progress. It needs the dashboard token: a browser logs in at `/login` and gets a 12-hour session cookie; scripts can send `Authorization: Bearer ` to `/api/v1/views`. `/metrics` accepts only the Bearer token. An unauthenticated request never sees a figure: the HTML diff --git a/telemetry-receiver/charts_test.go b/telemetry-receiver/charts_test.go index 0f882cbfe..aff652c9f 100644 --- a/telemetry-receiver/charts_test.go +++ b/telemetry-receiver/charts_test.go @@ -109,7 +109,7 @@ func TestDayLabelEvery(t *testing.T) { } // seedExtendedDash stores one basic report and two extended reports on the -// latest complete day (2026-10-06) of a dashboard test server. +// day before today (2026-10-05) of a dashboard test server, inside every range. func seedExtendedDash(t *testing.T, s *server) { t.Helper() s.now = func() time.Time { return dashNow.Add(-24 * time.Hour) } diff --git a/telemetry-receiver/dashboard_test.go b/telemetry-receiver/dashboard_test.go index 3ea62484b..b6a6c2765 100644 --- a/telemetry-receiver/dashboard_test.go +++ b/telemetry-receiver/dashboard_test.go @@ -17,7 +17,7 @@ const ( dashOrigin = "http://example.com" // httptest.NewRequest uses Host example.com ) -var dashNow = time.Date(2026, 10, 7, 12, 0, 0, 0, time.UTC) +var dashNow = time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC) // newDash builds a receiver with a fixed clock and returns it with its // dashboard handler. @@ -31,8 +31,8 @@ func newDash(t *testing.T, cfg config) (*server, http.Handler) { return s, s.dashboardRoutes() } -// seedDash stores reports on the latest complete day (2026-10-06) and the -// day before it. +// seedDash stores reports on today (2026-10-06, the as-of day) and the day +// before it. func seedDash(t *testing.T, s *server) { t.Helper() for _, r := range []struct { diff --git a/telemetry-receiver/design-export/MANIFEST.md b/telemetry-receiver/design-export/MANIFEST.md index fa3e482b1..11573bacb 100644 --- a/telemetry-receiver/design-export/MANIFEST.md +++ b/telemetry-receiver/design-export/MANIFEST.md @@ -37,3 +37,13 @@ This is a snapshot of `telemetry-receiver/telemetry-dashboard.pen` for review in - "Lapsed installs per day" → `i0xK1E` - "No reports in this range included extended data" → `jy9Rn` - "DASHBOARD_TOKEN value" → `HBYBE`, `a7wUpP` + +## Incremental export (2026-10-08): dashboard ends on today (OD-6) + +| Node | Change | +|---|---| +| `i0xK1E` | "Data through 2026-10-06 (UTC, today in progress)"; tiles "Reports, today" and "Active installs, today" with "2026-10-06 (UTC, in progress)"; "Median servers per install" sub "Last 30 days"; captions "…, ending today (in progress)", "Reports in range by reported version…", "Reports by game-server count. Median 3." and the lapsed note "Today's bar is final after the UTC day closes." | +| `Z079vK` | Empty-state text: "Figures appear as soon as this receiver accepts a report; today's figures are in progress and update as reports arrive. …" | +| `y49Ie` | `sDDfo` Stat tile sample: "Reports, today", "2026-10-06 (UTC, in progress)" | + +Method: 15 `Update` calls in one `execute`. The JSON files were produced by applying the same edits to the previous export, then checked equal to Pencil (`JSON.stringify(Get(id, {depth: 40, includePathGeometry: true}))` length and FNV-1a hash per frame: `i0xK1E` 69071, `Z079vK` 4684, `y49Ie` 1854). PNGs: `Export(ids, "png", …, {scale: 2})`, same sizes as before. Content check: "today in progress" → `i0xK1E`; "accepts a report" → `Z079vK`. diff --git a/telemetry-receiver/design-export/json/Z079vK.json b/telemetry-receiver/design-export/json/Z079vK.json index 40af934c9..7b3741df5 100644 --- a/telemetry-receiver/design-export/json/Z079vK.json +++ b/telemetry-receiver/design-export/json/Z079vK.json @@ -306,7 +306,7 @@ "fill": "$foreground/muted", "textGrowth": "fixed-width", "width": 520, - "content": "Figures appear once this receiver has accepted reports for a full UTC day. Installs send to POST /ingest on the public port; nothing is shown until there is data, so no chart is drawn from zeros.", + "content": "Figures appear as soon as this receiver accepts a report; today's figures are in progress and update as reports arrive. Installs send to POST /ingest on the public port; nothing is shown until there is data, so no chart is drawn from zeros.", "lineHeight": 1.5, "textAlign": "center", "fontFamily": "$typography/font-sans", diff --git a/telemetry-receiver/design-export/json/i0xK1E.json b/telemetry-receiver/design-export/json/i0xK1E.json index 681aa21b7..948ad89a0 100644 --- a/telemetry-receiver/design-export/json/i0xK1E.json +++ b/telemetry-receiver/design-export/json/i0xK1E.json @@ -105,7 +105,7 @@ "id": "W416z", "name": "As of", "fill": "$foreground/muted", - "content": "Data through 2026-10-06 (UTC)", + "content": "Data through 2026-10-06 (UTC, today in progress)", "fontFamily": "$typography/font-sans", "fontSize": 13, "fontWeight": "normal" @@ -289,17 +289,17 @@ "id": "WySx8", "type": "ref", "ref": "sDDfo", - "name": "Reports, latest day", + "name": "Reports, today", "width": "fill_container", "descendants": { "yia1E": { - "content": "Reports, latest day" + "content": "Reports, today" }, "tZRdh": { "content": "1,284" }, "oPQEN": { - "content": "2026-10-06 (UTC)" + "content": "2026-10-06 (UTC, in progress)" } } }, @@ -353,7 +353,7 @@ "content": "3" }, "oPQEN": { - "content": "Latest day; mean 4.6" + "content": "Last 30 days" } } } @@ -402,7 +402,7 @@ "id": "rZQiH", "name": "Sub", "fill": "$foreground/muted", - "content": "Accepted reports per UTC day, last 30 days", + "content": "Accepted reports per UTC day, last 30 days, ending today (in progress)", "fontFamily": "$typography/font-sans", "fontSize": 13, "fontWeight": "normal" @@ -1665,7 +1665,7 @@ "fill": "$foreground/muted", "textGrowth": "fixed-width", "width": 600, - "content": "Reports on the latest day by reported version. Top 10, then Other and Invalid.", + "content": "Reports in range by reported version. Top 10, then Other and Invalid.", "fontFamily": "$typography/font-sans", "fontSize": 13, "fontWeight": "normal" @@ -1893,7 +1893,7 @@ "fill": "$foreground/muted", "textGrowth": "fixed-width", "width": 600, - "content": "Installs on the latest day by game-server count. Median 3, mean 4.6.", + "content": "Reports by game-server count. Median 3.", "fontFamily": "$typography/font-sans", "fontSize": 13, "fontWeight": "normal" @@ -2592,17 +2592,17 @@ "id": "aRoss", "type": "ref", "ref": "sDDfo", - "name": "Active installs, latest day", + "name": "Active installs, today", "width": "fill_container", "descendants": { "yia1E": { - "content": "Active installs, latest day" + "content": "Active installs, today" }, "tZRdh": { "content": "912" }, "oPQEN": { - "content": "2026-10-06 (UTC)" + "content": "2026-10-06 (UTC, in progress)" } } }, @@ -3696,7 +3696,7 @@ "id": "NMwMD", "name": "Sub", "fill": "$foreground/muted", - "content": "Installs whose last report was 30 days earlier", + "content": "Installs whose last report was 30 days earlier. Today's bar is final after the UTC day closes.", "fontFamily": "$typography/font-sans", "fontSize": 13, "fontWeight": "normal" diff --git a/telemetry-receiver/design-export/json/y49Ie.json b/telemetry-receiver/design-export/json/y49Ie.json index c6dae311f..6ad38d10c 100644 --- a/telemetry-receiver/design-export/json/y49Ie.json +++ b/telemetry-receiver/design-export/json/y49Ie.json @@ -41,7 +41,7 @@ "id": "yia1E", "name": "Label", "fill": "$foreground/secondary", - "content": "Reports, latest day", + "content": "Reports, today", "fontFamily": "$typography/font-sans", "fontSize": 13, "fontWeight": "normal" @@ -61,7 +61,7 @@ "id": "oPQEN", "name": "Note", "fill": "$foreground/muted", - "content": "2026-10-06 (UTC)", + "content": "2026-10-06 (UTC, in progress)", "fontFamily": "$typography/font-sans", "fontSize": 12, "fontWeight": "normal" diff --git a/telemetry-receiver/design-export/screenshots/Z079vK.png b/telemetry-receiver/design-export/screenshots/Z079vK.png index 43319786c..eed683f90 100644 Binary files a/telemetry-receiver/design-export/screenshots/Z079vK.png and b/telemetry-receiver/design-export/screenshots/Z079vK.png differ diff --git a/telemetry-receiver/design-export/screenshots/i0xK1E.png b/telemetry-receiver/design-export/screenshots/i0xK1E.png index 14954dd89..15993afd4 100644 Binary files a/telemetry-receiver/design-export/screenshots/i0xK1E.png and b/telemetry-receiver/design-export/screenshots/i0xK1E.png differ diff --git a/telemetry-receiver/design-export/screenshots/y49Ie.png b/telemetry-receiver/design-export/screenshots/y49Ie.png index 7e4581348..b8823a5f4 100644 Binary files a/telemetry-receiver/design-export/screenshots/y49Ie.png and b/telemetry-receiver/design-export/screenshots/y49Ie.png differ diff --git a/telemetry-receiver/population_test.go b/telemetry-receiver/population_test.go index ae2a2ec74..d63a2390c 100644 --- a/telemetry-receiver/population_test.go +++ b/telemetry-receiver/population_test.go @@ -178,7 +178,7 @@ func TestPopulationProducesExactNewLapsedAndActiveCounts(t *testing.T) { // The views at the end of day 59: active counts and the 30-day unique // count (SC-008 allows 1%; the distinct count is exact). - *clock = popDay(popDays).Add(12 * time.Hour) + *clock = popDay(popDays - 1).Add(12 * time.Hour) // today (asOf) is day 59 v, err := BuildViews(ctx, st, 30, *clock) if err != nil { t.Fatalf("BuildViews: %v", err) @@ -341,7 +341,7 @@ func TestBuildViewsExtendedSharesGamesAndFeatures(t *testing.T) { t.Fatalf("status = %d (%s)", w.Code, w.Body) } } - *clock = extStart.Add(24 * time.Hour) // asOf is 2026-10-07 + *clock = extStart.Add(24 * time.Hour) // asOf is 2026-10-08 (today); the reports are on 2026-10-07 v, err := BuildViews(ctx, s.store, 7, *clock) if err != nil { t.Fatal(err) diff --git a/telemetry-receiver/specs.md b/telemetry-receiver/specs.md index e70d5c764..e0ef41821 100644 --- a/telemetry-receiver/specs.md +++ b/telemetry-receiver/specs.md @@ -18,7 +18,7 @@ Optional collector for the usage telemetry that Gameplane installs report daily. - **Proof-of-work (optional)**: with `INGEST_POW=true`, `/ingest` requires a solved challenge from `/v1/challenge` before the body is read; difficulty adapts to the challenge rate - **Aggregation and storage**: daily aggregates and expiring per-install activity records in SQLite (`$DATA_DIR/telemetry.db`); an install ID is stored only as `HMAC-SHA256(pepper, installID)` - **Per-source limits**: accepted reports per source per UTC day (`INGEST_SOURCE_DAILY_LIMIT`), login attempts, summary reads and challenge requests, all held in memory; source addresses come from the TCP peer, or from `X-Forwarded-For` only when the peer is inside `TRUSTED_PROXY_CIDRS` -- **Dashboard**: server-rendered HTML with inline SVG charts and no JavaScript, behind one operator token; the same view model is served as JSON at `/api/v1/views` +- **Dashboard**: server-rendered HTML with inline SVG charts and no JavaScript, behind one operator token; the same view model is served as JSON at `/api/v1/views`. Every range ends on today (UTC), which is still in progress - **Public summary (optional)**: `GET /v1/summary` returns five counts when `PUBLIC_SUMMARY=true` - **Version sanitization**: invalid syntax is bucketed as `"invalid"`. Retain at most 128 distinct strings matching `^[A-Za-z0-9][A-Za-z0-9._+-]{0,31}$` per process for the Prometheus label; additional versions share `"other"`. The two bucket names are reserved, for at most 130 series. Retained labels are never evicted; restart resets the budget with the counters. - **Metrics**: Prometheus series on the dashboard listener only, behind the token diff --git a/telemetry-receiver/telemetry-dashboard.pen b/telemetry-receiver/telemetry-dashboard.pen index 68ec79346..dc8e0da68 100644 --- a/telemetry-receiver/telemetry-dashboard.pen +++ b/telemetry-receiver/telemetry-dashboard.pen @@ -12521,7 +12521,7 @@ "id": "yia1E", "name": "Label", "fill": "$foreground/secondary", - "content": "Reports, latest day", + "content": "Reports, today", "fontFamily": "$typography/font-sans", "fontSize": 13, "fontWeight": "normal" @@ -12541,7 +12541,7 @@ "id": "oPQEN", "name": "Note", "fill": "$foreground/muted", - "content": "2026-10-06 (UTC)", + "content": "2026-10-06 (UTC, in progress)", "fontFamily": "$typography/font-sans", "fontSize": 12, "fontWeight": "normal" @@ -12716,7 +12716,7 @@ "id": "W416z", "name": "As of", "fill": "$foreground/muted", - "content": "Data through 2026-10-06 (UTC)", + "content": "Data through 2026-10-06 (UTC, today in progress)", "fontFamily": "$typography/font-sans", "fontSize": 13, "fontWeight": "normal" @@ -12900,17 +12900,17 @@ "id": "WySx8", "type": "ref", "ref": "sDDfo", - "name": "Reports, latest day", + "name": "Reports, today", "width": "fill_container", "descendants": { "yia1E": { - "content": "Reports, latest day" + "content": "Reports, today" }, "tZRdh": { "content": "1,284" }, "oPQEN": { - "content": "2026-10-06 (UTC)" + "content": "2026-10-06 (UTC, in progress)" } } }, @@ -12964,7 +12964,7 @@ "content": "3" }, "oPQEN": { - "content": "Latest day; mean 4.6" + "content": "Last 30 days" } } } @@ -13013,7 +13013,7 @@ "id": "rZQiH", "name": "Sub", "fill": "$foreground/muted", - "content": "Accepted reports per UTC day, last 30 days", + "content": "Accepted reports per UTC day, last 30 days, ending today (in progress)", "fontFamily": "$typography/font-sans", "fontSize": 13, "fontWeight": "normal" @@ -14276,7 +14276,7 @@ "fill": "$foreground/muted", "textGrowth": "fixed-width", "width": 600, - "content": "Reports on the latest day by reported version. Top 10, then Other and Invalid.", + "content": "Reports in range by reported version. Top 10, then Other and Invalid.", "fontFamily": "$typography/font-sans", "fontSize": 13, "fontWeight": "normal" @@ -14504,7 +14504,7 @@ "fill": "$foreground/muted", "textGrowth": "fixed-width", "width": 600, - "content": "Installs on the latest day by game-server count. Median 3, mean 4.6.", + "content": "Reports by game-server count. Median 3.", "fontFamily": "$typography/font-sans", "fontSize": 13, "fontWeight": "normal" @@ -15203,17 +15203,17 @@ "id": "aRoss", "type": "ref", "ref": "sDDfo", - "name": "Active installs, latest day", + "name": "Active installs, today", "width": "fill_container", "descendants": { "yia1E": { - "content": "Active installs, latest day" + "content": "Active installs, today" }, "tZRdh": { "content": "912" }, "oPQEN": { - "content": "2026-10-06 (UTC)" + "content": "2026-10-06 (UTC, in progress)" } } }, @@ -16307,7 +16307,7 @@ "id": "NMwMD", "name": "Sub", "fill": "$foreground/muted", - "content": "Installs whose last report was 30 days earlier", + "content": "Installs whose last report was 30 days earlier. Today's bar is final after the UTC day closes.", "fontFamily": "$typography/font-sans", "fontSize": 13, "fontWeight": "normal" @@ -18922,7 +18922,7 @@ "fill": "$foreground/muted", "textGrowth": "fixed-width", "width": 520, - "content": "Figures appear once this receiver has accepted reports for a full UTC day. Installs send to POST /ingest on the public port; nothing is shown until there is data, so no chart is drawn from zeros.", + "content": "Figures appear as soon as this receiver accepts a report; today's figures are in progress and update as reports arrive. Installs send to POST /ingest on the public port; nothing is shown until there is data, so no chart is drawn from zeros.", "lineHeight": 1.5, "textAlign": "center", "fontFamily": "$typography/font-sans", diff --git a/telemetry-receiver/views.go b/telemetry-receiver/views.go index 36441eae7..767efc831 100644 --- a/telemetry-receiver/views.go +++ b/telemetry-receiver/views.go @@ -175,12 +175,12 @@ func normalizeRange(days int) int { } // BuildViews computes the basic dashboard views over the rangeDays ending -// on the latest complete UTC day, which is yesterday relative to now. An +// on today, the current UTC day, which is still in progress (OD-6). An // invalid rangeDays falls back to 30. When no daily_basic row exists in the // range the result is the empty state: Empty is true and Basic is nil. func BuildViews(ctx context.Context, st *store, rangeDays int, now time.Time) (Views, error) { rangeDays = normalizeRange(rangeDays) - asOf := now.UTC().Truncate(24*time.Hour).AddDate(0, 0, -1) + asOf := now.UTC().Truncate(24 * time.Hour) from := asOf.AddDate(0, 0, 1-rangeDays) v := Views{Range: rangeDays, AsOf: dayString(asOf)} fromDay, toDay := dayString(from), dayString(asOf) @@ -383,7 +383,7 @@ func fleetMedian(hist []fleetPoint) float64 { } // buildExtended computes the extended block over [from, to], the rangeDays -// ending on the latest complete day. basicReports is the sum of daily_basic +// ending on today (in progress). basicReports is the sum of daily_basic // reports over the same range. It returns nil when the range has no extended // reports. func buildExtended(ctx context.Context, st *store, rangeDays int, from, to string, basicReports int64) (*ExtendedViews, error) { diff --git a/telemetry-receiver/views_test.go b/telemetry-receiver/views_test.go index aac24ad3c..5012bee96 100644 --- a/telemetry-receiver/views_test.go +++ b/telemetry-receiver/views_test.go @@ -10,13 +10,12 @@ import ( "time" ) -// viewsNow is the clock of the views tests: the latest complete day (asOf) -// is 2026-10-06. -var viewsNow = time.Date(2026, 10, 7, 12, 0, 0, 0, time.UTC) +// viewsNow is the clock of the views tests: today (asOf) is 2026-10-06. +var viewsNow = time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC) -// viewsDay returns the day offset days before asOf (2026-10-06). +// viewsDay returns the day offset days before asOf (2026-10-06, today). func viewsDay(offset int) string { - return dayString(viewsNow.AddDate(0, 0, -1-offset)) + return dayString(viewsNow.AddDate(0, 0, -offset)) } // seedViews writes a synthetic multi-day dataset. Reports sit at 0, 3, 10, @@ -156,6 +155,37 @@ func TestBuildViewsReportsPerDayIsDense(t *testing.T) { } } +// TestBuildViewsIncludesTodayInProgress proves that the range ends on today +// (UTC, in progress): a report accepted today is the last reportsPerDay +// entry, leaves the empty state, and its totals are the latest-day row. +func TestBuildViewsIncludesTodayInProgress(t *testing.T) { + st := openTestStore(t.Context(), t, config{}) + ctx := context.Background() + today := dayString(viewsNow) + for range 2 { + if err := st.recordBasic(ctx, today, "1.0.0", 4, 9); err != nil { + t.Fatal(err) + } + } + v, err := BuildViews(ctx, st, 7, viewsNow) + if err != nil { + t.Fatalf("BuildViews: %v", err) + } + if v.Empty || v.Basic == nil { + t.Fatalf("empty = %v with reports accepted today, want the populated view", v.Empty) + } + if v.AsOf != today { + t.Fatalf("asOf = %s, want today %s", v.AsOf, today) + } + days := v.Basic.ReportsPerDay + if last := days[len(days)-1]; last.Day != today || last.Reports != 2 { + t.Fatalf("last reportsPerDay entry = %+v, want %s with 2 reports", last, today) + } + if want := (LatestDayRow{ServersTotal: 8, TemplatesTotal: 18}); v.Basic.LatestDay != want { + t.Fatalf("latestDay = %+v, want %+v (today's totals)", v.Basic.LatestDay, want) + } +} + func TestBuildViewsInvalidRangeFallsBackTo30(t *testing.T) { st := openTestStore(t.Context(), t, config{}) seedViews(t, st) @@ -185,8 +215,8 @@ func TestBuildViewsEmptyState(t *testing.T) { } } check("empty store") - // Reports from today (not yet a complete day) and from beyond the - // widest range do not leave the empty state. + // Reports from tomorrow (after asOf) and from beyond the widest range do + // not leave the empty state; today's do (TestBuildViewsIncludesTodayInProgress). if err := st.recordBasic(context.Background(), "2026-10-07", "1.0.0", 1, 1); err != nil { t.Fatal(err) } diff --git a/telemetry-receiver/web/layout.html b/telemetry-receiver/web/layout.html index 3155cfa1f..fef5426fb 100644 --- a/telemetry-receiver/web/layout.html +++ b/telemetry-receiver/web/layout.html @@ -24,7 +24,7 @@ {{if .Empty}}No data yet{{else}}Approximate · self-reported{{end}}
-{{if not .Empty}}Data through {{.AsOf}} (UTC){{end}} +{{if not .Empty}}Data through {{.AsOf}} (UTC, today in progress){{end}}
diff --git a/telemetry-receiver/web/overview.html b/telemetry-receiver/web/overview.html index 70024e007..e4e89b58d 100644 --- a/telemetry-receiver/web/overview.html +++ b/telemetry-receiver/web/overview.html @@ -14,7 +14,7 @@

Usage across reporting installs

{{if .Empty}}

No reports yet

-

Figures appear once this receiver has accepted reports for a full UTC day. Installs send to POST /ingest on the public port; nothing is shown until there is data, so no chart is drawn from zeros.

+

Figures appear as soon as this receiver accepts a report; today's figures are in progress and update as reports arrive. Installs send to POST /ingest on the public port; nothing is shown until there is data, so no chart is drawn from zeros.

{{else}} {{with .Basic}} @@ -22,13 +22,13 @@

No reports yet

Basic reports

Version, server count and template count from every report, basic or extended.

-

Reports, latest day

{{num $.LatestReports}}

{{$.AsOf}} (UTC)

+

Reports, today

{{num $.LatestReports}}

{{$.AsOf}} (UTC, in progress)

Reports, last {{$.Range}} days

{{num $.RangeReports}}

Accepted reports

Reports since collection began

{{num $.ReportsTotal}}

{{if $.Since}}Since {{$.Since}}{{end}}

Median servers per install

{{.Fleet.Servers.Median}}

Last {{$.Range}} days

-
Reports per dayAccepted reports per UTC day, last {{$.Range}} days +
Reports per dayAccepted reports per UTC day, last {{$.Range}} days, ending today (in progress) {{if $.Table}}View as chart{{else}}View as table{{end}}
{{template "columns" $.PerDay}}
diff --git a/telemetry-receiver/web/overview_extended.html b/telemetry-receiver/web/overview_extended.html index f592012f3..a55e0190f 100644 --- a/telemetry-receiver/web/overview_extended.html +++ b/telemetry-receiver/web/overview_extended.html @@ -17,7 +17,7 @@

Extended reports

{{if .Extended}}{{with .Ext}}
-

Active installs, latest day

{{num $.Extended.Installs.Active1d}}

{{$.AsOf}} (UTC)

+

Active installs, today

{{num $.Extended.Installs.Active1d}}

{{$.AsOf}} (UTC, in progress)

Active, last 7 days

{{num $.Extended.Installs.Active7d}}

Unique installs

Active, last 30 days

{{num $.Extended.Installs.Active30d}}

Unique installs

New installs, last {{$.Range}} days

{{num .NewTotal}}

First report in range

@@ -29,7 +29,7 @@

Extended reports

{{template "columns" .New}}
-
Lapsed installs per dayInstalls whose last report was 30 days earlier
+
Lapsed installs per dayInstalls whose last report was 30 days earlier. Today's bar is final after the UTC day closes.
{{template "columns" .Lapsed}}
diff --git a/test/e2e/telemetry_e2e_test.go b/test/e2e/telemetry_e2e_test.go index a0957d2cf..4e019de42 100644 --- a/test/e2e/telemetry_e2e_test.go +++ b/test/e2e/telemetry_e2e_test.go @@ -245,9 +245,9 @@ func TestTelemetryLifecycle(t *testing.T) { if v.Preview == nil { t.Fatal("no preview while basic is on") } - // Views end at yesterday, so the receiver's same-day record is read - // from its fleet histograms and version label: the next report's - // servers and templates must equal the preview's. + // The receiver's record of the next report is read from its fleet + // histograms and version label: the report's servers and templates + // must equal the preview's. serversCount0 := rx.get(ctx, t, "gameplane_telemetry_servers_count", "") serversSum0 := rx.get(ctx, t, "gameplane_telemetry_servers_sum", "") templatesSum0 := rx.get(ctx, t, "gameplane_telemetry_templates_sum", "") @@ -393,9 +393,9 @@ func TestTelemetryLifecycle(t *testing.T) { t.Run("dashboard_shows_reports", func(t *testing.T) { cookie := dashboardLogin(ctx, t, metricsPort, telemetryDashToken) - before := time.Now().UTC().AddDate(0, 0, -1).Format("2006-01-02") + before := time.Now().UTC().Format("2006-01-02") resp, body := dashboardRequest(ctx, t, metricsPort, http.MethodGet, "/api/v1/views", map[string]string{"Cookie": cookie}, nil) - after := time.Now().UTC().AddDate(0, 0, -1).Format("2006-01-02") + after := time.Now().UTC().Format("2006-01-02") if resp.StatusCode != http.StatusOK { t.Fatalf("GET /api/v1/views with the session = %d: %s", resp.StatusCode, body) } @@ -411,7 +411,7 @@ func TestTelemetryLifecycle(t *testing.T) { var asOf string var empty bool if json.Unmarshal(views["asOf"], &asOf) != nil || (asOf != before && asOf != after) { - t.Fatalf("asOf = %s, want yesterday (UTC), %s: views end at the last complete day", views["asOf"], after) + t.Fatalf("asOf = %s, want today (UTC), %s: views end on the current day (OD-6)", views["asOf"], after) } if json.Unmarshal(views["empty"], &empty) != nil || (empty != (string(views["basic"]) == "null")) { t.Fatalf("empty = %s with basic = %s, want basic null exactly when empty", views["empty"], views["basic"])