Skip to content

[minor] Add the live progression site - #3

Merged
matt-edmondson merged 20 commits into
mainfrom
feat/progression-site
Oct 17, 2026
Merged

matt-edmondson merged 20 commits into
mainfrom
feat/progression-site

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Summary

Adds a public, read-only live site for the server's progression, built to run at cs2-progression.ktsu.dev.

  • Core: adds the live wire contracts, change tracking for lifetime rows, a bounded sender that drops the oldest batch and never blocks, and a server rankings query. Stored dates now read back as UTC.
  • Plugin: with Site.Url set, the plugin sends a batch every 2 seconds. A batch holds the server snapshot, new skill shot and milestone feed entries, and the lifetime rows that changed. With no URL set, it behaves exactly as before.
  • Site (CounterStrikeProgression.Site): an ASP.NET Core app.
    • Ingest is on port 8081 and needs a bearer token.
    • The public pages, JSON APIs, and a server-sent events stream are on port 8080.
    • Live rows are laid over read-only MariaDB data. The larger value wins, because counters only grow.
    • Database reads are capped at 4 at once.
    • The feed history of the last 50 entries survives a restart.
    • The pages are in the game HUD style, with no front-end build step.
  • CI: publishes ghcr.io/ktsu-dev/counterstrikeprogression-site:<version> and :latest on release.

The design is in docs/superpowers/specs/2026-10-17-progression-site-design.md, and the plan is in docs/superpowers/plans/2026-10-17-progression-site.md.

Test plan

  • Release build with no warnings, and 236 MSTest tests pass.
  • Local smoke run covering:
    • ingest returns 204 with the token and 401 without it
    • ingest is unreachable on 8080
    • the live API and the stream
    • feed history kept across a restart
  • A hostile player name renders as text on every page (simulated DOM).
  • The container image builds to a local archive with both tags.
  • After merge, the release publishes the plugin zip and the site image.
  • Check in a real browser once deployed, including at phone width.

🤖 Generated with Claude Code

Combine took whole rows in arrival order, so a late stale batch could lower the overlay. It now takes the larger value field by field.

Set PackageProjectUrl on the site so the container image url label builds.
The site serves the public pages, JSON APIs, and an SSE stream on 8080, and
takes plugin batches on 8081 only. A middleware checks the port the
connection arrived on, so the Host header can't reach ingest from 8080.
SiteOptions now binds Core's DatabaseSettings, and an empty host falls back
to the in-memory store. JSON sent to browsers writes 64-bit numbers as
strings. A failed feed history write no longer escapes LiveState.Apply.
LiveState takes an ILogger and warns, with the path and the exception, the
first time a feed history write fails. It logs again only after a write
succeeds and a later one fails. The batch stays applied in memory, so a
disk fault can't block live updates, and the warning shows why the history
won't survive a restart. FeedStore exposes its path for the message.
Add the live, ranks, and player pages in the game HUD style, with one
shared stylesheet and script and no front-end build step. Every value
from the API goes into the page as text, and player links are built only
from well-formed SteamID64s. The site project now copies wwwroot to the
build and publish output, because Microsoft.NET.Sdk doesn't do that on
its own.
The ranks page refetches on any progress event, and on a snapshot that
shows a human player the page doesn't list yet. The live page replaces
the scoreboard with an offline message while the server is offline,
merges the fetched feed with entries already streamed, and leaves the
weapon off weapon level-up rows, whose title already names it.
The MySQL connection string sets DateTimeKind to Utc, so first seen and
last seen load with a UTC kind and serialize with a Z. The plugin writes
DateTime.UtcNow, and the rankings query no longer needs its own
SpecifyKind.
ProgressReader runs at most four store queries at once, and callers that
miss the cache for the same player or for the rankings share one
in-flight query. The ranks and player endpoints pass the request's
cancellation token, so an aborted request stops waiting while a shared
query runs on for the others.
A full resend carries every connected player's rows, which can pass
1 MiB on a full server.
SiteReporter.Collect logs any failure through Log.EventFailed and skips
that batch, so a fault in the optional site never reaches the game
timer. The README lists Site__Database__RequireTls, says the /data
volume must be writable by UID 1654, and says the health probe uses
port 8080.
Comment on lines +60 to +62
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
}
Comment on lines +63 to +65
catch (IOException) when (httpContext.RequestAborted.IsCancellationRequested)
{
}
Comment on lines +45 to +47
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
}
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
46.7% Coverage on New Code (required ≥ 80%)
E Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@matt-edmondson
matt-edmondson merged commit 123ce8d into main Oct 17, 2026
10 of 11 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