Skip to content

Publish presence and reachability over time - #39

Merged
HarryCordewener merged 1 commit into
mainfrom
feat/time-series-api
Aug 15, 2026
Merged

HarryCordewener merged 1 commit into
mainfrom
feat/time-series-api

Conversation

@HarryCordewener

@HarryCordewener HarryCordewener commented Aug 15, 2026

Copy link
Copy Markdown
Member

§10 asks for "time-series endpoints for presence and availability" and there were none. Two routes, each hung off the game it describes:

GET /api/games/{id-or-slug}/presence?grain=hour|day&from=&to=
GET /api/games/{id-or-slug}/availability?from=&to=

Presence comes from the rollup, not the raw table

§5.2 lets retention drop raw partitions once they've been aggregated, so a series read off presence_sample would quietly shorten as a deployment aged. The rollup is the copy that outlives them, and its daily grain is kept for ever.

The silence is the hard part

A JSON array is the format most likely to flatten §5.4's three states into one — whatever a consumer is handed, it will draw a line through. So they go out as three distinguishable things:

State On the wire
counted bucket present; min/max/mean over the counted probes
probed, uncountable bucket present, tallied separately, stats null
never measured bucket absent — never a zero row

countedSamples and uncountableSamples count probes, not players, and are kept separate so nothing downstream can add them together. Every response carries the sentence that stops a consumer inferring the rest.

Two decisions worth flagging

  • Both routes resolve through IGameQueries.FindAsync rather than reading a store by id. That's load-bearing, not tidiness: FindAsync carries the rule that keeps an unclaimed submission off every public surface, and a series route that went to the store directly would be a way to read presence for a game the listing refuses to show.
  • A window wider than we serve is a 400, never a silent clamp. Truncating answers a different question and says so nowhere — a consumer paging through history would read short pages as the end of the record. Caps are 90 days of hours (2,160 buckets) and five years of days (1,826).

Availability spans are filtered by overlap, not containment, so the span running when the window opened is in the answer though it began before it — otherwise a game dark for three months answers "no spans", which reads as we never measured for exactly the game whose availability is most worth knowing.

IPresenceSeries is a read port beside the store: publishing a series needs one method, and the store also writes, rolls up, sets watermarks and drops partitions. On the demo fixture it answers empty — nothing there was measured, and unlike a page a JSON body carries no demo banner to say so.

One thing this PR found about itself

The API surface guard (NothingOnThisSurfaceSaysTheWordWeDoNotUse) swept a hard-coded route list that these two routes were not on — which is precisely how the first draft of this branch published the word §5.8 forbids, in copy explaining that the word doesn't apply. Both routes are on the guard's list now, and the copy states the limitation without the word.

Tests

8 new tests covering absent-vs-null, grain parsing and refusal, the window cap, a backwards window, the §5.7 redirect preserving sub-route and querystring, 404, and span overlap.

dotnet build MUIndex.slnx -c Release      → 0 warnings, 0 errors
MUI.Catalog.Tests    300 passed  (real Postgres, 0 skipped)
MUI.Web.Tests        356 passed
MUI.Crawl.Tests      142 passed
MUI.Crawler.Tests    156 passed
MUI.Discovery.Tests  264 passed
                    ---- 1218 total, 0 failed, 0 skipped

Not here

PeakDistinctEstimate stays unpublished. It's computed, stored and rolled up, but publishing a per-game unique-player estimate is a new claim about a game rather than a reformatting of one we already make, and the rollup's own docs are explicit that it is a peak and deliberately not a union. Worth a decision of its own rather than riding in on a plumbing PR.

Independent of #38 — this reads the rollup store directly rather than the heatmap path, so the two don't conflict.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added presence and availability time-series API endpoints for games.
    • Added configurable time grains, time-window validation, bucketed statistics, and availability spans.
    • Added API metadata, discoverable routes, slug redirects, cancellation support, and clear responses for invalid or missing data.
  • Tests
    • Added coverage for grains, windows, redirects, missing games, bucket semantics, and overlapping availability spans.

§10 asked for time-series endpoints and there were none. Two routes, both
hung off the game they describe:

    GET /api/games/{id-or-slug}/presence?grain=hour|day&from=&to=
    GET /api/games/{id-or-slug}/availability?from=&to=

Presence is served from the rollup rather than the raw table. §5.2 lets
retention drop raw partitions once they have been aggregated, so a series
read off presence_sample would quietly shorten as a deployment aged; the
rollup is the copy that outlives them, and the daily grain is kept for ever.

The silence is the hard part of publishing a series, and a JSON array is the
format most likely to flatten it. Three states go out as three things:

- counted           bucket present, min/max/mean over the counted probes
- probed, no count  bucket present, tallied separately, stats null
- never measured    bucket ABSENT — never a zero row

countedSamples and uncountableSamples count probes and not players, and are
separate so that nothing downstream can add them together. Each response
carries the sentence that stops a consumer inferring the rest.

Both routes resolve through IGameQueries.FindAsync rather than reading a
store by id, which is load-bearing rather than tidy: FindAsync carries the
rule that keeps an unclaimed submission off every public surface, and a
series route that went to the store directly would be a way to read presence
for a game the listing refuses to show. Both honour §5.7 forever-redirects
and keep the sub-route and the querystring across them, because a series URL
is likelier to be in somebody's scheduled job than in a browser.

A window wider than we serve is a 400 rather than a silent clamp — truncating
would answer a different question and say so nowhere, and a consumer paging
through history would read short pages as the end of the record. Availability
spans are filtered by overlap and not containment, so the span running when
the window opened is in the answer though it began before it.

IPresenceSeries is a read port beside the store: publishing a series needs one
method, and the store also writes, rolls up, sets watermarks and drops
partitions. On the demo fixture it answers empty, because nothing there was
measured and a JSON body carries no demo banner to say so.

The API surface guard swept a hard-coded route list that these two were not
on, which is how the first draft published the word §5.8 forbids. Both routes
are on the list now and the copy says the limitation without the word.

1218 tests across five suites, Postgres exercised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 707b417b-5ec7-4c44-a7df-5fe41df1526e

📥 Commits

Reviewing files that changed from the base of the PR and between 67cf28e and efc99af.

📒 Files selected for processing (12)
  • src/MUI.Catalog/Persistence/NpgsqlPresenceRollupStore.cs
  • src/MUI.Catalog/PresenceRollup.cs
  • src/MUI.Web/Api/ApiModels.cs
  • src/MUI.Web/Api/ApiRoutes.cs
  • src/MUI.Web/Api/MuiApi.cs
  • src/MUI.Web/Api/SeriesEndpoints.cs
  • src/MUI.Web/Data/PostgresData.cs
  • src/MUI.Web/Fixtures/FixturePresenceSeries.cs
  • src/MUI.Web/SiteComposition.cs
  • tests/MUI.Web.Tests/Api/ApiHost.cs
  • tests/MUI.Web.Tests/Api/ApiSurfaceGuardTests.cs
  • tests/MUI.Web.Tests/Api/SeriesApiTests.cs

Walkthrough

The PR adds presence and availability time-series API endpoints. It defines the presence read contract, response models, query validation, game resolution, dependency injection, fixture support, route-index entries, and API tests.

Changes

Series API

Layer / File(s) Summary
Series contracts and response shapes
src/MUI.Catalog/PresenceRollup.cs, src/MUI.Catalog/Persistence/NpgsqlPresenceRollupStore.cs, src/MUI.Web/Api/ApiModels.cs, src/MUI.Web/Api/ApiRoutes.cs
IPresenceSeries defines asynchronous rollup retrieval. NpgsqlPresenceRollupStore implements the interface. API models represent presence buckets, presence series, and availability series. Route helpers build game-specific series URLs.
Series endpoint execution
src/MUI.Web/Api/SeriesEndpoints.cs, src/MUI.Web/Api/MuiApi.cs
The API validates grains and UTC time windows, resolves game IDs and historical slugs, retrieves series data, maps rollups and availability spans, and registers and documents both routes.
Production and fixture wiring
src/MUI.Web/Data/PostgresData.cs, src/MUI.Web/Fixtures/FixturePresenceSeries.cs, src/MUI.Web/SiteComposition.cs, tests/MUI.Web.Tests/Api/ApiHost.cs
PostgreSQL exposes the presence store through IPresenceSeries. Fixture and test hosts register an empty default implementation that caller registrations can override.
API behavior coverage
tests/MUI.Web.Tests/Api/SeriesApiTests.cs, tests/MUI.Web.Tests/Api/ApiSurfaceGuardTests.cs
Tests cover bucket semantics, grain selection, invalid and oversized windows, redirects, missing games, availability overlap, route discoverability, and configurable presence-series data.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to efc99

This PR adds the requested presence and availability time-series endpoints with documented behavior and reported passing build and test checks; no actionable merge-blocking risk remains beyond normal review.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SeriesEndpoints
  participant IGameQueries
  participant IPresenceSeries
  Client->>SeriesEndpoints: Request presence series
  SeriesEndpoints->>IGameQueries: Resolve game key
  SeriesEndpoints->>IPresenceSeries: ForGameAsync(gameId, grain, from, to)
  IPresenceSeries-->>SeriesEndpoints: Presence rollups
  SeriesEndpoints-->>Client: PresenceSeriesView JSON
Loading

Possibly related PRs

  • SharpMUSH/MUIndex#9: Introduced PostgreSQL presence-store wiring that this PR exposes through IPresenceSeries.
  • SharpMUSH/MUIndex#11: Reorganized presence-data importing and classifications used by the new series API.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: publishing presence and reachability time-series data through API endpoints.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@HarryCordewener
HarryCordewener merged commit d0360a3 into main Aug 15, 2026
3 checks passed
@HarryCordewener
HarryCordewener deleted the feat/time-series-api branch August 15, 2026 22:04
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