Per-search filter selecting which crit-damage tiers are allowed on armor. Default (all allowed) is byte-identical to current behavior; "prefer highest allowed tier" falls out of existing scoring. Go-only (live solver); Python copy left frozen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| agent | ||
| alembic | ||
| discord-rare-monitor | ||
| docs | ||
| frontend | ||
| go-services | ||
| grafana | ||
| inventory-service | ||
| nginx | ||
| scripts | ||
| static | ||
| .gitignore | ||
| .mcp.json | ||
| AGENTS.md | ||
| alembic.ini | ||
| CLAUDE.md | ||
| db.py | ||
| db_async.py | ||
| deploy-frontend.sh | ||
| docker-compose.yml | ||
| Dockerfile | ||
| generate_data.py | ||
| main.py | ||
| Makefile | ||
| README.md | ||
Mosswart Overlord (Dereth Tracker)
Real-time telemetry, inventory, and analytics platform for Asheron's Call — driven by a firehose of WebSocket events from the companion MosswartMassacre DECAL plugin running on 60+ characters.
The production backend is written in Go (go-services/). It replaced the
original Python/FastAPI implementation via a strangler-fig migration: the Go
services ran in parallel against live traffic until every endpoint was proven
byte-identical, then production was cut over. The Python implementation is
preserved on the python-legacy branch.
Architecture
MosswartMassacre plugin ──wss──> nginx ──> Go tracker (tracker-go) ──> dereth (TimescaleDB)
(60+ game clients) │ │
│ ├──HTTP──> Go inventory (inventory-go) ──> inventory_db
Browsers ──https──────────────────> nginx │
│ └──/ws/live──> Discord rare bot (relays rares + chat)
└──> Grafana (/grafana/) death/idle alerts → Discord webhook
| Component | Path | Runs as | Notes |
|---|---|---|---|
| Tracker (ingest + website + read API + WS) | go-services/tracker-go/ |
Docker dereth-tracker-go, 127.0.0.1:8770 |
serves the React frontend, login/admin, the plugin /ws/position, browser /ws/live, and the full read API; writes the dereth DB |
| Inventory (search + suitbuilder + ingestion) | go-services/inventory-go/ |
Docker inventory-go, 127.0.0.1:8772 |
normalized item search, the suitbuilder solver (SSE), inventory ingestion; writes inventory_db |
| Telemetry DB | TimescaleDB | Docker dereth-db, 5432 |
hypertables telemetry_events, spawn_events |
| Inventory DB | postgres:14 | Docker inventory-db, 5433 |
7-table normalized item schema |
| React frontend | frontend/ → static/ |
served by tracker-go |
unchanged by the migration — same paths, same API |
| Classic v1 / legacy pages | static/classic/, static/*.html |
served by tracker-go |
/classic, /suitbuilder.html, /inventory.html |
| Grafana | compose dereth-grafana |
127.0.0.1:3000 | anonymous Viewer auth, proxied at /grafana/ |
| Discord rare bot | discord-rare-monitor/ (Python) |
Docker, reads Go /ws/live |
posts rares + relays allegiance chat |
| Overlord Agent (assistant) | agent/ |
host-side systemd overlord-agent, 127.0.0.1:8767 |
shells out to claude -p; outside Docker by design |
Stack: Go 1.25 (stdlib net/http with 1.22 method+path routing, pgx/v5,
coder/websocket, bwmarrin/discordgo, golang.org/x/crypto/bcrypt), distroless
multi-stage images. React 19 + Vite + TypeScript. PostgreSQL/TimescaleDB. nginx
reverse proxy (host-side). Unlike the old single-worker Python service, the Go
tracker uses GOMAXPROCS = all available cores, so traffic bursts parallelize
instead of bottlenecking on one core.
Build & run
Everything builds and runs in Docker — no host Go toolchain needed (the multi-stage images compile from source). The production stack is the base compose (databases, Grafana, Discord bot) plus two override files for the Go services and the cutover wiring.
# --- build the Go service images ---
export BUILD_VERSION="$(date -u +%Y.%-m.%-d.%H%M)-$(git rev-parse --short HEAD)"
docker compose -f docker-compose.yml -f go-services/docker-compose.go.yml \
build dereth-tracker-go inventory-go
# --- production: Go services in write mode, serving the site + ingest ---
docker compose -f docker-compose.yml \
-f go-services/docker-compose.go.yml \
-f go-services/docker-compose.cutover.yml \
up -d --no-deps dereth-tracker-go inventory-go
docker-compose.go.ymldefines the Go services (plus the isolated shadow DBs used during the parallel run).docker-compose.cutover.ymlflips the Go services to write mode against the production DBs (READ_ONLY=false,SKIP_SCHEMA_INIT=trueso they run no DDL and trust the existing schema) and points the Discord bot at the Go/ws/live. Drop this file to return the Go services to read-only parallel mode.BUILD_VERSIONis shown in the frontend sidebar (CalVer:YYYY.M.D.HHMM-gitshorthash).- Required env (server
.env, never committed):SHARED_SECRET,SECRET_KEY,POSTGRES_PASSWORD,INVENTORY_DB_PASSWORD,DISCORD_ACLOG_WEBHOOK,DISCORD_RARE_BOT_TOKEN, the Discord channel IDs, and Grafana admin. See.env.example.
Frontend (unchanged by the migration)
The React app and the legacy static pages call the same absolute paths
(/api/..., /inv/..., /live, …) — the Go tracker answers them, so the
frontend ships as-is.
cd frontend && npm run dev # local dev, port 5173, /api → :8770
bash deploy-frontend.sh # complete build + copy into static/ (runs npm run build itself)
The tracker serves static/ directly (bind-mounted), so static/JS/CSS changes
need no restart. ⚠️ npm run build writes to static/_build/; only
deploy-frontend.sh copies it into the served static/.
nginx
The live config is host-side at /etc/nginx/sites-enabled/overlord (source copy
in nginx/overlord.conf); the tracker_go upstream is in
/etc/nginx/conf.d/tracker_go.conf (server 127.0.0.1:8770;). Production routes
/, /api/, /websocket/ to the Go tracker. Every location that proxies to the
tracker must set X-Forwarded-For — it drives the internal-trust auth rule.
Overlord Agent
Unchanged by the migration — it's a host-side Python systemd service. Code change:
git pull && sudo systemctl restart overlord-agent. Its env lives separately at
/etc/overlord/agent.env. See agent/ and CLAUDE.md.
WebSocket contract
/ws/position— plugin → backend. Telemetry, vitals, inventory, portal, rare, combat, quest, chat, share_*, … Authenticated by theX-Plugin-Secretheader againstSHARED_SECRET(constant-time; fails closed when unset). The tracker forwards inventory toinventory-go, accumulates kill/combat stats, and re-broadcasts to browsers./ws/live— browser ↔ backend. Session-cookie (or internal-trust) authenticated. Acceptssubscribe,request_dungeon_map, and{player_name, command}envelopes routed to the matching plugin socket. Telemetry is broadcast typeless so the browser ignores it and takes player data from the 5 s/livepoll (matching the original design — broadcasting it typed flaps the per-player counters).- Internal-trust rule: a request skips cookie auth only when its source is private/loopback and carries no
X-Forwarded-For. nginx sets XFF on all internet traffic, so only host-side / compose-network callers qualify.
Payload note
Payloads are snake_case JSON; keep field names and shapes stable across plugin +
backend. The plugin sends several numeric telemetry fields as strings
(kills_per_hour, deaths, total_deaths, prismatic_taper_count) — the backend
coerces them (coerceNum in tracker-go/reads.go).
Auth & users
Session cookies are signed with SECRET_KEY via an itsdangerous-compatible
URLSafeTimedSerializer (HMAC-SHA1, 30-day expiry) — cookies interoperate with
the legacy Python service. Login at /login (bcrypt against the users table),
admin user CRUD at /api-admin/users, current user at /me.
Databases
Two separate Postgres databases, both schema-from-code:
dereth(TimescaleDB,dereth-db): hypertablestelemetry_events+spawn_events, pluschar_stats,combat_stats(_sessions),rare_*,portals,character_stats,users. Persisted event types: telemetry, spawn, rare, portal, character_stats, combat_stats. Everything else (vitals, quest, cantrips, nearby_objects, dungeon_map, share_*) is memory-only.inventory_db(postgres:14,inventory-db): 7 normalized tables (items+ combat/requirements/enhancements/ratings/spells/raw_data).
In cutover mode the Go services reuse these production databases directly; the
shadow DBs in docker-compose.go.yml exist only for isolated parallel-run
validation. Backups: pg_dump -Fc of both DBs; TimescaleDB restore needs
timescaledb_pre_restore() / post_restore() around pg_restore.
Route conventions
- nginx strips
/api/before proxying, so backend routes do not start with/api/. - Hyphenated routes (
/api-version,/api-admin/...) deliberately bypass the strip (they fall through nginx'slocation /). - The static SPA is the catch-all (
GET /), registered after the API routes, withindex.htmlfallback for client-side routing. /inv/*reverse-proxies to the inventory service;/api/agent/*is proxied by nginx (not the tracker) to the host-side agent.
Operational notes
- Discord: the rare bot posts rares + relays allegiance chat; death/idle alerts come from the tracker itself via
DISCORD_ACLOG_WEBHOOK. - Issue board persists to the flat file
static/openissues.json(web-served, mounted read-write). - Logs:
docker logs dereth-tracker-go,docker logs inventory-go. Read-only psql:docker exec dereth-db psql -U postgres -d dereth,docker exec inventory-db psql -U inventory_user -d inventory_db. - This repo is PUBLIC on git.snakedesert.se — never commit secrets.
.envis gitignored;.env.exampleis the template.
Branches
master— the Go production backend (this).python-legacy— the original Python/FastAPI implementation, preserved for reference and rollback.
See CLAUDE.md for contributor/agent guidance and deeper internals.