Skip to main content

Anilist Chart

@magistr/anilist-chartv2026.08.19.1· 23d agoMODELS
01README

Read-only render layer for the AniList chat statistics site: turns the anilist_metadata and user_scores ClickHouse tables (written by the @anilist/api ingest model) into the six static pages — the «Доска почёта» board, the /anime landing, and the four genre charts (chart, current, fresh, bayes). This is a faithful port of the legacy Python (generate_board.py, generate_landing.py, anilist_chart*.py); every ranking rule, formatter, and award is reproduced from the oracle. The render boundary is strictly read-only — the worst failure is a stale page for a week, fixable by re-run.

Methods render the pages from whatever data is present and publish each page that rendered (fan-out; one failing page never suppresses the rest).

02Models1
@magistr/anilist-chartv2026.08.19.1extensions/models/anilist_chart.ts

Global Arguments

ArgumentTypeDescription
clickhouseUrl?stringClickHouse HTTP endpoint for the read-only render user, e.g. https://host:8443.
clickhouseUser?stringRead-only ClickHouse user (sent as the X-ClickHouse-User header).
clickhouseKey?stringPassword for the read-only ClickHouse user (X-ClickHouse-Key header).
clickhouseDatabasestringClickHouse database holding anilist_metadata and user_scores.
userNamesarrayAniList usernames whose scores feed the four genre charts.
topKnumberRows per genre in each chart; also advertised on the /anime landing.
bayesMinVotesnumberBayesian prior weight m (minimum votes for confidence) on the /bayes chart.
penaltyRatenumberPer-season age penalty applied on the /fresh chart.
nodeHost?stringSSH host of the node serving the pages. Required for `publish`.
nodeUserstringSSH user for the serving node.
outputDir?stringAbsolute path of the nginx-served output directory on the node. Required for `publish`.
sshTimeoutMsnumberTimeout for the ssh publish fallback spawn (per page). A hung ssh connection
clickhouseMaxResponseBytesnumberCap on the total bytes read from a single ClickHouse query response body
fn settings()
Echo the resolved render settings from global arguments. Makes no external calls; useful for verifying configuration before a render run.
fn publish()
Write the rendered pages from the renderedPage artifacts onto the serving node over SSH. Each file is written atomically (temp + rename) and one failing page never suppresses the rest, so a broken write never leaves a half-served page.
fn render(topK: number, bayesMinVotes: number)
Render all six pages (seven artifacts) from ClickHouse and publish every one that passes the freshness gate and the publish backstop. One failing page never suppresses the rest; refused/failed pages are reported in the run marker.
ArgumentTypeDescription
topKnumberRows per genre; threaded into BOTH the chart compute and the landing copy.
bayesMinVotesnumberBayesian prior weight m; threaded into the /bayes chart.

Resources

settings(30d)— Resolved render settings (topK, bayes m, penalty rate, user count); no secrets.
renderedPage(45d)— One rendered static artifact (board, landing, a genre chart, or the bayes JSON), keyed by kind.
renderRun(90d)— Per-run summary marker: freshness verdict, published/refused/failed pages, anomalies.
publishRun(90d)— Per-publish marker: which rendered pages were written to the serving node, which failed, which were missing.
03Previous Versions2
2026.08.02.1

2026.08.02.1

Real-fix (not byte-frozen) for all 7 latent bugs tracked in the LOCAL anilist-chart-latent-bugs issue-lifecycle model (3 MED, 4 LOW; 0 CRITICAL/HIGH — see the 2026.08.01.1 entry below for the original per-bug writeup, written when all 7 were accepted-but-unfixed). model.version / manifest.yaml bump 2026.08.01.1 -> 2026.08.02.1, with a single upgrades[] entry (upgradeAttributes: (old) => old — the two new global args are DEFAULTED, so an old attribute record lacking both keys still reads identically once the zod defaults apply).

  • LB1 (MED) The 11 ClickHouse reads (board, chartScores, distinctIds, chartMeta, six landing aggregates) plus the freshness read ran with no try/catch, so any read throw escaped execute() with NO diagnostic marker left behind. They are now wrapped in one try/catch: on any throw, a renderRun marker is written (ok:false, refuseReason: "read failed: <message>", anomalies: [<message>]) THEN the error is re-thrown (write-then-rethrow — the same fail-loud shape as publish()'s existing guard), so the workflow step still fails AND the marker now exists for swamp report get / swamp data get to inspect.
  • LB2 (MED) The ssh publish Deno.Command spawn carried no AbortSignal/timeout, so a hung ssh connection blocked publish() forever. A new DEFAULTED global arg sshTimeoutMs (default 30000) now bounds the whole spawn/write/output round-trip via AbortController + setTimeout + clearTimeout (NOT AbortSignal.timeout, so a fast success cancels the pending timer instead of leaving it to fire after the fact). On abort the child is killed, output() rejects, and the existing per-page try/catch in publishPages marks just that page failed — the rest still publish.
  • LB3 (MED) ClickHouseClient.query() buffered the entire response body via res.text() with no cap. It now streams the body through res.body.getReader(), counting bytes as chunks accumulate, and throws ClickHouse response exceeds N bytes the moment the running total passes the cap — freeing the partial buffer early rather than fully buffering an unbounded/misbehaving upstream first. The cap comes from a new DEFAULTED global arg clickhouseMaxResponseBytes (default 67108864 = 64MiB), threaded through configFrom into ClickHouseConfig.maxResponseBytes; a body under the cap still returns every row (a ceiling, not a silent truncator).
  • LB4 (LOW) A malformed freshness timestamp (Date.parse -> NaN) coerced newestDataAgeMs to null, which silently disabled the staleness anomaly (evaluateFreshness only fires staleness when newestDataAgeMs !== null). render() now distinguishes "raw present but unparseable" from "genuinely absent" and passes a new optional newestTimestampMalformed flag into FreshnessInput; when true, evaluateFreshness pushes an explicit unparseable-timestamp anomaly (staleness check skipped). Still ok:true, still no false "publishing last-known-good" — the gap is now SIGNALLED, not silent.
  • LB5 (LOW) A non-numeric media_id from distinctMediaIdsQuery became NaN, which arrayIntParam truncated to the literal string "NaN" — an invalid Array(Int64) wire value that a real ClickHouse rejects, aborting the whole render. render() now filters ids through .filter(Number.isFinite) before ever building the array param, so a corrupt id is skipped, never sent (empty ids still routes through the existing [] branch). arrayIntParam also now throws loud on a non-finite input as defense-in-depth, so the poisoned literal can never be constructed even by a future caller — though filtering means that throw never fires in practice.
  • LB6 (LOW) A ClickHouse error response's body was echoed verbatim (up to 500 chars, including newlines) into the thrown error. The body is now trimmed to 200 chars, whitespace runs collapsed to a single space, and defensively .replaceAll'd for the configured key (belt-and-braces — a CH error body never legitimately contains it). Still no credential leak in any thrown error or written resource; confirmed by a new pure adversarial test that stubs a body literally containing a sentinel "key" and asserts it comes back [redacted].
  • LB7 (LOW) arrayStringParam's hand-rolled escaping left an embedded NUL byte completely unescaped. A third .replace() pass now encodes it as the two-character \0 escape, running AFTER the backslash-doubling and quote passes (order matters: the new escape's own backslash must not be doubled by

Notes trimmed at a line boundary to fit the registry's 4900-byte per-version cap. Full section: https://github.com/umag/swamp-workspace/blob/d850d8e8372d4c0008c9245959a090b37095de7a/anilist-chart/CHANGELOG.md

2026.07.21.1

Merge pull request #60 from umag/feat/anilist-chart-extension

feat(anilist-chart): add @magistr/anilist-chart render+publish extension

04Stats
A
100 / 100
Downloads
0
Archive size
98.6 KB
  • Has README or module doc2/2earned
  • README has a code example1/1earned
  • README is substantive1/1earned
  • Most symbols documented1/1earned
  • No slow types (deprecated)1/1earned
  • Dependencies pass trust audit2/2earned
  • Has description1/1earned
  • Platform support declared (or universal)2/2earned
  • License declared1/1earned
  • Verified public repository2/2earned
05Platforms
06Labels