The exact series is already deduped on record, but the Discord import (src=3)
and the hourly/daily snapshot backfill were NOT — so flat stretches showed many
identical consecutive points on the graph (live: oettaM 83, ZywOo 172 consecutive
duplicates, mostly src=3). Apply _sm_pseries_collapse_identical to the final
merged list: it drops only strictly-inner identical points and keeps each flat
run's first+last (so a flat segment still renders as a line, not a diagonal).
Declutters the graph and shrinks the response (now that the endpoint is public).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5 days ago
Power-series history: make public (un-gate from MOD-only)
Storage verified sustainable: per-account points hard-capped at 20,000 with
dedup (<=1/15min, 24h heartbeat); on-disk store is gzipped (live: 12,347
accounts / 149k points / 3.2 MB). Going public does not change what is stored
(same data collected) and the read path is cheap (in-memory series + O(snapshot
files) backfill lookups, no file parse per request).
- /power-series: remove the mod 404 gate -> public (Power/Level/Gold are public
leaderboard values already shown in the lookup). mod_key kept as no-op param.
- index.html: move the power-series pieces out of the MOD:START/END regions so
they are served to everyone (ai-power-panel container, loadPowerSeries/
powerSeriesHide calls, power_series.js include). Other mod features untouched.
Verified as a non-mod locally: script loads, endpoint 200, graph renders.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VIP now resolves correctly (comma-separated VIPLeaderboard state, 364/365 names).
Keep the concise "[vip] ok: stored N" line; only dump the raw sample when the
parsed count is far below VIPCount, so steady-state logs don't carry the name list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VIP: retry in ~10min on refresh failure (self-heal transient join errors)
Join can transiently ServerError (4500); previously the next try was 6h away.
Now a failed/empty refresh retries after SM_VIP_RETRY_SECONDS (10min); a success
or held-good-state waits the full 6h.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VIP: broaden name delimiters + log raw sample to diagnose parse
stored=1 vs count=361 means the VIPLeaderboard value is not newline-separated.
Split on newlines/commas/semicolons/pipes/tabs, and log a 600-char raw sample
(+ type) so the actual delimiter/structure is visible if still off.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VIP: log stored-names vs count on successful refresh
Surfaces whether the VIPLeaderboard state field carries all names or a top-N
display, and gives ongoing monitoring of the 6h refresh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VIP: read VIPLeaderboard/VIPCount from world_1 state
world_1's room state exposes VIPLeaderboard + VIPCount as schema fields (world_10
does not). The sidecar now reads those state values (message capture kept as a
fallback for worlds that only broadcast), and the listen window drops back to ~12s
since state syncs within seconds of join.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
world_10 (a gameplay shard) never broadcasts VIPLeaderboard; the VIP statue/
leaderboard lives in the main world. Point the VIP lookup at world_1 (still
SM_VIP_WORLD-overridable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VIP sidecar: widen listen window to catch the broadcast
The VIPLeaderboard/VIPCount broadcast does not arrive in the join burst (only the
friend-list/daily-quest/rivalBoss messages do) — it comes on a longer periodic/
on-change cadence. Listen 75s (SM_VIP_WAIT_MS) with its own subprocess timeout
(SM_VIP_TIMEOUT) instead of the 8s rival window.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VIP sidecar: read server-pushed room messages, not state
The in-game VIP list is delivered as room messages (onMessage 'VIPLeaderboard'
+ 'VIPCount' in NetworkManager, server-broadcast, no client request), not as
state fields — so the state-reading sidecar found nothing ("no VIPLeaderboard/
names in state"). Rewrite to listen on a wildcard message handler, match the VIP
type case-insensitively (the client assembles the name from obfuscated fragments),
and extract names/count from the payload. Listen ~22s to catch periodic broadcasts.
On miss, report the world's actual message types + state keys for diagnosis.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
forge_api.py:
- Split Nightmare Arena into its own action (separate legendary counts, no rate)
- Dwarf-Multi: /player & /player-detail accept names= to union multiple accounts
- Arena/Nightmare leaderboard: "Most legendarys" sort; luckiest/unluckiest now
legendary-per-attempt ratio (no floor, >=1 always beats 0)
forge_stats.js:
- Sub-tabs restructured to Leaderboard (default) + Totals; Item Tables moved under
Totals as "Dwarf" (first); Combine Accounts moved into the lookup
- Combined-account dwarf items are clickable for level distribution
- Nightmare shows legendarys like arena; sort options gated per drop action
app.py:
- VIP backend + Space-side sidecar (vip_lookup.js reads world_10 VIPLeaderboard via
authenticated lookup account), /vip-status + /ingest-vip
index.html:
- VIP badge (no star), Thorns/Freeze formula cards, forge_stats.js cache bump
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Client (static/analytics.js): smTrack('cat:name') sammelt Events und schickt
sie gebuendelt per navigator.sendBeacon('/track') -> feuern-und-vergessen,
blockiert nie den Render-/Interaktionspfad. Nichts Personenbezogenes (kein
Name/IP/UA). Hooks: Tab-Wechsel, Optimize (market/nomarket), Leaderboard-Suche,
Account-/Room-/Clan-/Forge-Lookup, welche Forge-Stat-Details geoeffnet werden.
Server (app.py): /track zaehlt In-Memory pro UTC-Tag + flusht gedrosselt
(<=1x/60s) als winziges JSON nach /data/usage_stats.json; Kategorie-Allowlist
+ Zeichensatz/Laenge + Tages-Kardinalitaetsdeckel + 4KB-Body-Cap als
Flood-Schutz. /usage-admin (MOD-only, wie power-series-admin): stats/raw/csv/
clear. Heisse Pfade (Leaderboard/Stream/Lookups) unberuehrt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
forge_api: periodic forge-DB hot-reload (no restart needed)
The DB connection was cached for the whole process, so a freshly uploaded
forge_stats.db only showed up after a Space restart -> forge/actions could
look stale for a long time. Now _conn() checks every ~30min (FORGE_DB_RELOAD_TTL)
whether the dataset has a new revision and, if so, a background thread downloads
it and atomically swaps the connection + invalidates the DB-derived caches.
No request blocks; the old DB serves until the swap and closes by refcount.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lookup: cut live-lookup timeout 5s->3s (kill the ~12s hang)
Measured the lookup latency bimodal: reachable accounts answer GetPlayerInfo
in <0.8s (slowest real responder 2.3s), unreachable ones never answer. With
the old 5s per-phase timeout and 2 room attempts, a dead lookup hung ~12s and
tied up a worker thread the whole time -> also made the leaderboard sluggish.
3s cleanly separates responders; non-responders fall back fast to the cached
leaderboard row, and the 2nd room attempt still covers other world instances.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Badge reads plain "BANNED"; the reason stays available via hover tooltip
and click popover (cursor stays a pointer).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Shows WHY a banned account was banned, on top of our own ban detection
(is_banned stays authoritative; the reason only supplements it).
- app.py: /ban-reason endpoint loads ban_reasons.json from the forge HF
dataset (cached, TTL) and matches username + aliases (id-bound, like the
forge/power-series technique), since the public ban-logs channel has no
account ids.
- static/index.html: BANNED badge now shows the reason as a hover tooltip
and a click popover (XSS-escaped), loaded once per username.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 days ago
forge stats: bind to account id across renames (aggregate over username+aliases)
Forge/elf/enhance/etc. stats were keyed by player NAME (the compact DB's player_id
is just a name index), so a rename split a player's history. Now the player view
resolves the looked-up name -> the account's full name set (current username +
leaderboard aliases, same resolver the power-series uses) and aggregates
player_action / player_detail / luck across all those local ids. Rename-proof and
id-bound, matching the power/level/gold history. Unknown/not-on-leaderboard names
fall back to single-name (unchanged). app.py injects the resolver into forge_api
(no circular import). Verified on the live compact DB: single lookup unchanged,
two-name aggregation sums exactly, luck+detail OK.
Known follow-up: the /forge/leaderboard ranking tab still ranks per name (a renamed
player can appear twice); grouping the whole board by id is a separate change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The backfill warmer parsed the FULL ~1.6M-account leaderboard snapshot files
(hourly + daily) just to keep the ~10k >=1Qnt accounts, holding the GIL for
seconds each -> the residual hourly/boot freezes. Now each snapshot write also
emits a slim >=1Qnt-only companion (same filename, lb_history_q1 /
lb_history_hourly_q1 dirs); the warmer prefers it and falls back to the full
file only for pre-existing snapshots. ~160x less parse work, full snapshots
untouched for the d1/d7 delta feature.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
power-series: stop timer-rebuilding the name->id index (fixes periodic lookup freezes)
On the 2-vCPU Space, rebuilding the ~1.6M-row name->id index every ~5 min in the
flusher holds the GIL for seconds and froze ALL requests (observed: /health
spiking 0.3s->6-11s, /live-rank timing out at 60s during a rebuild window).
Remove the proactive timer rebuild; the index is now built only lazily on demand
and cached far longer (TTL 5min -> 30min), since name->id changes rarely. Idle
periods no longer trigger freezes and rebuilds during active use are infrequent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Returns exact+discord point counts, power/level span, timespan and sub-1-Qnt
count for one account (by name or player_id). Used to verify the full-history
whitelist landed; mod-key or ingest-key gated like the rest of the endpoint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
power-series: y-axis floor at 0 + full history (incl. <1 Qnt) for whitelisted names
- Chart: clamp the y-domain minimum to 0 (power/level/gold can never be negative;
the 8% bottom padding produced labels like "-612qnt").
- Backend: SM_POWER_SERIES_FULL_NAMES (default "oettaM") bypasses the 1e18 floor
in the recorder and the Discord import, so those accounts keep ALL points incl.
sub-1-Qnt history. On name-normalization collision (a tiny name-twin resolving
to the same id as the real account) the richer series (more points) wins instead
of last-write, so a small twin can't overwrite the real history.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
16 days ago
power-series: keep name->id index warm in the flusher (no request pays the rebuild)
The shared index rebuilds on its 5min TTL; previously whichever request first
found it expired paid the multi-second O(N) build synchronously (the remaining
source of inconsistent load times). Refresh it proactively in the flusher
(runs every 120s < 300s TTL), but only once it has been built at least once, so
an idle Space burns no CPU.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
power-series: periodic (daily) dedup in the flusher to bound idle-account growth
The heartbeat appends 1 identical point/day per idle account; without periodic
cleanup the series would grow unbounded. Collapsing past identical consecutive
points once a day (also once ~25s after each boot) keeps idle accounts at ~2
points, so storage stays flat over years. One-time cleanup already ran live:
141,855 -> 52,951 points (-63%).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
power-series: resolve name->id via shared cached index (kills per-lookup O(N) scan)
_sm_pseries_resolve_key did a full ~1.7M-row leaderboard scan on every id-cache
miss (10min TTL), which is the main cause of the inconsistent/slow history loads.
Route it through the shared name->id index (_sm_pseries_lvlidx_cached, built once
per 5min) so every resolution is an O(1) dict lookup after one build. The hot
recorder path benefits too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
power-series-admin: also accept the ingest key (operator tooling), not only the mod key
Lets the local importer/diagnostics query stats and trigger dedup/clear_discord
with the ingest key it already holds, without exposing the mod key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
power-series: Discord bot-stats history import (src=3), past-dedup + admin/stats endpoint
- /power-series-import-discord (POST, ingest-key gated): bulk-imports historical
player stats parsed from the Discord #bot-commands channel as a NEW uncertain
point source (src=3), bound to account id (name->id via leaderboard). Strict
validation: if ANY historical level for an account exceeds its current level
(name maps to a different/reset account) the ENTIRE account's messages are
discarded. Stored in a separate /data/power_series_discord.json (own lock),
so the hot exact recorder is never touched and re-imports are idempotent.
- /power-series read now merges these (precedence: exact > discord > backfill)
and reports discord_count.
- /power-series-admin (mod-gated): action=stats (sizes/distribution), dedup
(collapse past identical consecutive points in the exact series, keeping run
endpoints), clear_discord (clean re-import).
- Cached the O(N) leaderboard level-index across import chunks (5min TTL).
- Frontend: src=3 rendered as a distinct amber diamond with legen…
Power-series: skip unchanged points, 15-min min gap
- Min gap per account 1 min -> 15 min (SM_POWER_SERIES_MIN_GAP 60 -> 900).
- Skip a point whose level/power/gold/sword/shield are identical to the
account's last point, unless a heartbeat interval (default 1 day,
SM_POWER_SERIES_HEARTBEAT) has passed so flat stretches still extend the
line to ~now. Removes the redundant dense clusters from repeated lookups
of an unchanged account; the series now stores real changes.
Verified: a duplicate push is skipped; power/level/gear changes are kept.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Power-series: keep tooltip on-screen for top/edge points
The hover tooltip is anchored above the point, so for a point near the
top of the chart it ran off the top edge and clipped the date line. Now
it flips below the point when the point sits in the upper part of the
chart, and its horizontal center is clamped so the wide date line never
runs off the left/right edge of the panel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Power-series: fix level dips, gear in snapshots, smaller points
- Level dips: level can never decrease, so a point with a lower level is
stale/wrong (e.g. an hourly snapshot taken right after a restart, before
ingest refreshes A's leaderboard -> old values). The recorder now skips a
point whose level is below the account's last point, and the endpoint
filters any level regression out of the merged series. Removes the dips.
- Snapshots now carry the active sword/shield (type+level): the leaderboard
row stores gear from the scan/lookup, and hourly/daily snapshots append it
as [lvl,pow,gold,sword_type,sword_level,shield_type,shield_level]. Accounts
without gear stay the old 3-element shape (no file bloat); the d1/d7 delta
readers only use indices 0-2, so they're unaffected. Backfill reads the
gear -> coarse snapshot points now show the weapon too.
- Chart data points rendered smaller (r 2.8 -> 1.7).
Verified: stale low-level point dropped (recorder + endpoint); a 7-element
snapshot yields gear in the backfill, a 3-element one yields…
/power-series was unzipping+parsing every leaderboard snapshot file
(hourly ~28, daily ~9; up to ~1.7M keys each on the Space) on EVERY
request, plus doing an O(N) username->id scan over the whole leaderboard
each time. That made the chart load take tens of seconds.
- Backfill now reads a cached snapshot index instead of parsing files on
the request path. A background warmer (in the existing flusher thread,
mtime-gated, after a 25s boot delay) parses each file once, filtered to
>=1 Qnt (~10k accounts -> little RAM), and drops entries for rotated-out
files. Request path is now O(#files) dict lookups.
- Cache the username->id resolution (10 min TTL); the async recorder warms
it on lookup, so repeat chart loads skip the leaderboard scan.
Local load dropped to ~25 ms.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Power-series: store + show active sword/shield level
Each exact point now stores the active sword and shield as type-code +
item level (was type only): stored format
[t, level, power, gold, sword_type, sword_level, shield_type, shield_level].
A normalizer keeps old 6-element points readable (level -> unknown), and
backfill/snapshot points still carry no gear (the leaderboard snapshots
only store level/power/gold — unavoidable). The chart tooltip shows e.g.
"Last Horizon Lv5" / "Final Bastion Lv5".
/ingest-leaderboard now accepts optional sword/shield type+level so the
scanner push can include gear (it already has it from the inventory
lookups it does for power/gold); without them the point stores -1.
Verified end-to-end: lookup and gear-carrying ingest both record type+
level; tooltip renders the level badge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Power-series: bind history to account id, not name
The live lookup only populates player_id when ranks are attached
(_sm_attach_level_ranks), which normal lookups skip — so recorded
points were key'd by name (or dropped), and the chart (which queries
by id) found nothing. Now the recorder resolves the account id from
the scanner-maintained leaderboard by name when the result lacks a
player_id, and stores strictly under id:<player_id> (no name fallback),
so history survives renames and never fragments by name. Harden the
name->id leaderboard scan against concurrent-mutation RuntimeErrors.
Verified end-to-end: a normal lookup (player_id=None) and a scanner
ingest push both now record an exact point under the account id;
sub-1-Qnt accounts are still skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Power/Level/Gold history chart in player lookup (mod-only)
Frontend for the per-scan power/level/gold time-series: a collapsible
SVG line chart in the player-lookup tab, above the forge/action stats.
Metric toggle (power/level/gold), range filter (day/week/month/all-time),
mouse-drag zoom, hover tooltip with the active sword/shield, exact points
filled vs coarse leaderboard-snapshot backfill hollow. All UI text in
English; all timestamps rendered in UTC and labelled.
- static/power_series.js: self-contained chart module (loads on lookup).
- /power-series now merges the leaderboard snapshots (hourly ~28h +
daily ~9d) as coarse backfill with the exact series, dedup per hour,
exact wins; adds sword_names/shield_names + src marker per point.
Mod-only: the panel, its <script> and the lookup hook are wrapped in
MOD:START/END so the public page is built without any trace, and
/power-series requires the sm_mod cookie (or ?mod_key=), else 404.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
18 days ago
Backend: per-scan power/level/gold time-series for >=1 Qnt accounts
Collect a precise (second-granular) time-series point every time an
account with >= 1 Qnt power (1e18) is scanned via the normal lookup /
leaderboard ingest, so the player-lookup tab can later draw power/level/
gold over time. Stored in a SEPARATE persistent store (/data/power_series
.json, own lock + own 120s save interval) so the 600 MB leaderboard
write path is untouched.
- Point format: [t_sec, level, power, gold, sword_type, shield_type].
Active sword/shield captured as the small game type-code (translatable
back via SWORD_TYPE_NAMES / SHIELD_TYPE_NAMES); -1 when unknown.
- Add sword_type/shield_type to the live lookup result.
- Throttle to <=1 point/min/account (SM_POWER_SERIES_MIN_GAP) and cap
per account (SM_POWER_SERIES_MAX_POINTS=20000); env-tunable.
- GET /power-series?username=|player_id= returns the series (or
under_1qnt=true) for the frontend.
Accounts under 1 Qnt store nothing (frontend shows "unter 1qnt").
Frontend graph to follow.
Co-Authored-By: Claude Opus…
Lookup head portrait: in-game-accurate helmets/hair, fixed & centered
Head portrait now reuses the full-body placement pipeline (RivalBoss
skeleton + _rvDress equip-template helmet + _m3dApplyHair incl. VIP),
so helmets and hair sit exactly as in-game, then clips the body away
below the chin so only the head shows, with the old soft orthographic
lighting.
- Frame on the head itself (chin -> skull top), references fixed and
independent of hair/helmet -> head always same size & centred;
hair/helmet render on top without moving or resizing it.
- Disable the character anim -> deterministic bind pose (fixes the
frame-0 vs bind-pose drift that shifted the head/clip line).
- Helmet dropdown now re-renders the head preview live when "show
helmet" is on.
Cosmetics tab: integrate None (clear-x) and the sword level into each
gear field instead of separate boxes; stop marking "Lucifer Skin
Color" as VIP (only the three Transparent skins are VIP).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
22 days ago
Mob preview cards: show the sorted stat as an extra bar when it's not already shown
When the mob gallery is sorted by a bar-able stat other than health/damage/respawn
(Speed, Body radius, Attack range, Attack delay, Spawn count), that stat now appears
as a small bar under Respawn on each preview card; no extra bar otherwise.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
23 days ago
Mob images: per-mob cache-bust so a single re-baked mob updates without a global version bump
_aiMobImage now uses an optional image-config.json mob_versions[name] stamp (the tuner bumps it on re-bake) instead of only the global SM_IMG_VER, so re-baking one mob shows up on the next page reload. Falls back to SM_IMG_VER when no per-mob version is present.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
25 days ago
Game Info: mob/armor art, live Rival render, spawn counts, revamped gallery UI
- Bake 28 mob + 96 armor models to PNG and load the images instead of 3D (image-config.json + SM_IMG_VER cache-bust); images stored via Git LFS
- The Rival (daily Asgard boss): rendered live in-browser with its real skin (whole body), face, armor, sword & shield via the game's setRivalBossSkin loadout, framed with tuned orientation, labeled "The Rival (username)". Hourly server-side Colyseus join (rival_lookup.js) + /rival-info endpoint; falls back to "?" when no live data.
- Mob detail stats: add spawn-count bar
- Gallery controls: large icon category tiles with item counts + custom styled sort dropdown (native select kept as source of truth)
- Catalog: carry speed / enemyBodyRadius / attackDistance so mob stats are available
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the 5th Gear-Gallery sub-tab "Ores" (raw -> smelted -> enchant flow
per ore) and states the confirmed mechanics:
- Raw ore: ore damage = your normal damage / 10; drop chance per hit ~=
(damage/10) / block HP (share of HP that hit removes), drops on hits not
only on kill; looting/spawn/sell context.
- Freeze: 0.25x move speed (75% slower), +1s per level, no damage/power.
- Fortune: +5% of base per level (not +5% flat), all chance-based NPCs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lb-autoheal: take the daily history snapshot under the leaderboard lock
_sm_lb_keep_history_fresh() iterated the 1.6M-row players dict via
_sm_leaderboard_snapshot_daily WITHOUT the lock, so a concurrent ingest
could raise "dict changed size during iteration" and skip that day's
snapshot. Hold _SM_LEADERBOARD_LOCK across the daily snapshot (the hourly
one still runs outside it -- it takes the lock itself). Cheap: the daily
does real work only once/day, a file-exists check otherwise.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lb-autoheal: keep A's history fresh + cheap history-only re-seed for deltas
Proxy mode had disabled A's maintenance worker AND its 600MB save, which
is also where the hourly/daily history snapshots were taken -- so A stopped
producing history and could no longer seed the mirror's d1/d7 deltas after
a mirror restart. Two fixes in the auto-heal loop:
- _sm_lb_keep_history_fresh(): re-take the cheap, index-independent hourly
+ daily snapshots each tick so A stays the durable bootstrap source.
- history-only re-seed: when the mirror has rows but no d1 baseline (fresh
restart), push just the history files -- no 1.6M row re-push, no
starvation risk. The full mirror sync now also pushes history FIRST, so a
heal interrupted mid-rows still lands the deltas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lb-autoheal: key the heal on the mirror's STORED row count, not public total
The public (>=1000) total lags a re-sync by up to one index rebuild
(~30 min), so keying the heal on it would re-push 1.6M rows every 180s
while the mirror merely reindexes. The stored-row count updates the
instant the mirror ingests, so compare that to A's stored count and only
heal a REACHABLE mirror that is genuinely thin (< 0.9x, i.e. wiped by a
restart or a pusher-only refill still missing the dead/inactive accounts).
Added a cooldown as a flap guard. The serving gate still keys on the
public total (users must not see a mirror that hasn't finished indexing).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lb-proxy: self-serve locally when the mirror is degraded + auto-heal it
Fixes the "totals bei 0" regression: the prices mirror runs on ephemeral
storage, so an HF restart wipes it and it re-fills from the pusher
low-level-first -- its public (>=1000) total sits near 0 for a while.
A was proxying that empty answer verbatim.
- _sm_lb_proxy_healthy(): cheap query-independent gate on the mirror's
UNFILTERED public total (cached ~45s; also keeps the free Space awake).
Below a plausibility floor (50k vs the real ~371k) the mirror counts as
degraded/unreachable and _sm_lb_proxy_fetch returns None, so the read
endpoints self-serve A's warm local state instead of forwarding zeros.
- _sm_lb_autoheal_worker(): every ~180s, if the mirror is empty or
materially thinner than A's own qualified population, re-push A's full
players + history so parity and d1/d7 deltas come back on their own.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Leaderboard: add /lb-mirror-sync to push A's full state + history to the mirror
One-time bootstrap: streams A's entire accumulated players set (incl. the dead/
inactive accounts a fresh scan on the mirror can't see) to <target>/ingest-leaderboard
in chunks, then pushes A's daily/hourly history snapshots to <target>/lb-history-import
for immediate d1/d7 deltas. Runs in a background thread, uses A's own ingest key for the
outbound push, guarded by X-Ingest-Key. /lb-mirror-status reports progress.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Leaderboard: optional proxy/offload to the prices Space (env-gated, off by default)
When SM_LEADERBOARD_PROXY_URL is set, the data-heavy leaderboard read endpoints
(/leaderboard, /leaderboard/by-names, /leaderboard-summary, /mob-leaderboard) are
served by that URL (the prices Space, which now holds the 1.6M rows + runs the
expensive numpy index/serve), and A's own periodic index rebuild + ~600 MB cache
save are switched off -- the actual CPU offload. /leaderboard-summary re-injects the
global counts (ccu/total_accounts/qualified) from A's own cached helpers, since the
prices backend makes no game calls. Any proxy failure falls back to A's local
self-serve. A keeps ingesting (pusher dual-writes) so SSE/refresh/fallback still work.
Unset (default) = A behaves exactly as before; 37 leaderboard tests pass unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a "Power/hit" row to the account-lookup Player Details grid,
positioned second-to-last (right before Hits/sec). It shows the same
personal-boss-power-per-hit value already computed for the grey Boss
Power sub-text (bpPerSec / accountHps), just made prominent as
requested. Frontend-only, additive; hidden when no power is loaded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(export): include all gear enchants (sword freeze, shield thorns + fortune)
The account export only reported the optimizer-relevant enchants (sword
flame/looting, shield looting). Mods asked for the full set. The server lookup
(/live-lookup) already returns sword_freeze, shield_thorns and shield_fortune —
only the frontend opt-list account model dropped them.
Thread them through as hidden, optimizer-irrelevant pass-through fields:
accHTML hidden inputs (iswfr/ishth/ishfo), applyDiscordAccountData fills them
from the lookup response, getAccountsSnapshot + removeAcc snapshot read them,
doLoad restores them on save/load, and buildAccountsExport now emits
sword.freeze and shield.thorns/shield.fortune. Purely additive — no change to
the optimizer (it ignores these) or to existing export consumers. Verified in
the browser: export now yields sword{flame,looting,freeze} and
shield{looting,thorns,fortune}. Export contract test extended (177 passed; the
1 failure is the pre-existing unrelated frontend tabs test).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
perf(leaderboard): save the cache every 30min instead of 3min (stop lock-blocking stalls)
Found the main remaining stall source: the full leaderboard cache (~1.6M rows,
~600 MB JSON) is serialized + gzipped + written UNDER _SM_LEADERBOARD_LOCK, so
for the few seconds that takes, every request that needs the lock (ingest,
reads, /health) blocks. It ran every 180s -> matches the observed 3-15s stall
spikes on a ~3min cadence.
Raise SM_LEADERBOARD_SAVE_INTERVAL 180s -> 1800s (30 min). The persisted cache
is only a warm-start convenience (data is continuously re-scanned by the
pusher), and the leaderboard is not required to be near-real-time, so writing it
every 30 min removes ~90% of these blocking saves. Trade-off (accepted): a
restart loses up to ~30 min of ingest from the persisted cache, refilled on the
next sweep. Overridable via SM_LEADERBOARD_SAVE_INTERVAL. No change to displayed
data or to lookups (separate live path). Suite unchanged (177 passed; the 1
failure is the pre-existing unrelated frontend tabs test).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>