# Mosswart Overlord (Dereth Tracker) Real-time telemetry, inventory, and analytics platform for Asheron's Call. FastAPI backend + React frontend + PostgreSQL (TimescaleDB) + Discord integrations, all driven by WebSocket events from the companion [MosswartMassacre](https://github.com/SawatoMosswartsEnjoyersClub/MosswartMassacre) DECAL plugin. --- ## Table of Contents - [Overview](#overview) - [Architecture](#architecture) - [Features](#features) - [Requirements](#requirements) - [Installation](#installation) - [Configuration](#configuration) - [Deploying Changes](#deploying-changes) - [WebSocket Contract](#websocket-contract) - [HTTP API Reference](#http-api-reference) - [Frontend](#frontend) - [Database Schema](#database-schema) - [Operations & Health](#operations--health) - [Contributing](#contributing) --- ## Overview Mosswart Overlord is the backend that consumes a firehose of telemetry, vitals, inventory, combat, and chat events from 60+ characters running the `MosswartMassacre` plugin. It stores selected data in TimescaleDB, runs analytics (combat stats, idle/death detection), and broadcasts live updates to connected browser clients. The frontend is a React + Vite app served at `/` with a live map, draggable windows (inventory, chat, combat, radar, etc.), and a server uptime sidebar. The previous vanilla JS frontend is preserved at `/classic`. ## Architecture ``` ┌─────────────────────────┐ │ MosswartMassacre (C#) │ ← plugin per game client └────────────┬────────────┘ │ WebSocket /ws/position (authenticated) ▼ ┌────────────────────────────────────────────────────────┐ │ dereth-tracker (FastAPI) │ │ • main.py — WS routing, analytics, broadcasts │ │ • idle/death detection → Discord webhook │ │ • combat stats delta/lifetime accumulation │ │ • vital sharing relay (cross-machine) │ └──┬──────────────────┬────────────────────┬────────────┘ │ │ │ │ WS /ws/live │ HTTP │ HTTP ▼ ▼ ▼ ┌──────────┐ ┌──────────────────┐ ┌──────────────────┐ │ Browsers │ │ inventory-svc │ │ Discord bot │ │ (React) │ │ (FastAPI) │ │ (rare monitor) │ └──────────┘ └────────┬─────────┘ └──────────────────┘ ▼ ┌──────────────┐ │ inventory-db │ └──────────────┘ ┌──────────────┐ │ dereth-db │ ← TimescaleDB (telemetry, spawns, rares, portals) └──────────────┘ ``` All services run via Docker Compose. ## Features ### Live Data - **Live Map** — real-time player positions, dots, trails, portals, heatmap - **WebSocket firehose** (`/ws/live`) — broadcasts every incoming event to browsers - **Per-client subscriptions** — clients can send `{"type":"subscribe","message_types":[...]}` to receive only specific event types (the Discord rare monitor bot uses this to filter the 82GB/day firehose down to just `rare` and `chat`) ### Inventory - Full inventory snapshot on login + incremental `inventory_delta` updates (add/update/remove) - Per-character live refresh in the browser (debounced 2s) - Advanced search with filters: material, set, armor level, spells, tinks, workmanship, etc. - **Suitbuilder** at `/suitbuilder.html` — constraint-based armor optimization across multiple mule inventories with primary/secondary set support, cantrip overlap detection, and real-time SSE streaming ### Combat Stats (Mag-Tools style) - Plugin parses combat chat into session deltas - Backend accumulates lifetime totals from per-session snapshots - Offense/defense broken out per damage element - Browser combat window shows monster-by-monster damage ### Cross-Machine Vital Sharing - WebSocket relay replaces UtilityBelt's localhost-only `VTankFellowHeals` - Plugin broadcasts its own vitals and consumes peer vitals - In-game `DxHud` overlay shows peer health/stamina/mana bars with direction arrows ### Discord Integration - **Rare Monitor Bot** — posts rares (split by common/great) to configured channels - **Death Alerts** — webhook to `#alerts` when a character's vitae goes from 0 → >0 (rate-limited to one per character per 5 min) - **Idle Alerts** — webhook after 5 minutes of continuous idle state (caught portals, stuck nav, etc.). The grace period prevents false positives on brief idle blips. - **Vortex Warning** — bot watches for "whirlwind of vortexes" chat and posts a warning embed ### Portals - Automatic discovery + 1-hour retention - Coordinate-deduplicated (rounded to 0.1 precision) ### Stats - Per-character lifetime kills, deaths, rares, taper counts - Grafana dashboards (2x2 iframe grid in the stats window) ### Health & Monitoring - Server uptime + latency + player count from TreeStats.net (checked every 30s) - Only current state is kept — no historical `server_health_checks` table (removed April 2026 as write-only bloat) ## Requirements - Docker & Docker Compose (recommended) - OR: Python 3.11+, Node.js 20+, and a PostgreSQL 14+ with TimescaleDB ## Installation ```bash git clone git@git.snakedesert.se:SawatoMosswartsEnjoyersClub/MosswartOverlord.git cd MosswartOverlord cp .env.example .env # fill in secrets (see Configuration below) docker compose up -d ``` ### Frontend development loop ```bash cd frontend npm install npm run dev # local Vite server # ...edit files, hot reload... cd .. bash deploy-frontend.sh # builds + copies to static/ for production serving ``` ⚠️ **`npm run build` writes to `static/_build/` but the web server serves from `static/`.** You must run `deploy-frontend.sh` to copy `_build/ → static/`. Otherwise the browser keeps loading the previous bundle. ## Configuration All secrets go in `.env`: | Variable | Purpose | |---|---| | `POSTGRES_PASSWORD` | Telemetry DB password | | `INVENTORY_DB_PASSWORD` | Inventory DB password | | `SHARED_SECRET` | Plugin auth for `/ws/position` | | `SECRET_KEY` | Session cookie signing | | `DISCORD_RARE_BOT_TOKEN` | Bot token for rare monitor | | `DISCORD_ACLOG_WEBHOOK` | Webhook URL for death/idle alerts | | `GF_SECURITY_ADMIN_PASSWORD` | Grafana admin | | `COMMON_RARE_CHANNEL_ID` | Discord channel ID for common rares | | `GREAT_RARE_CHANNEL_ID` | Discord channel ID for great rares | | `ACLOG_CHANNEL_ID` | Discord channel ID for the rare bot's status/vortex messages | | `MONITOR_CHARACTER` | Which character's chat the bot monitors | ## Deploying Changes Live backend host: `overlord.snakedesert.se` (SSH user `erik`, key-based auth). ### Quick deploy — Python / static file changes ```bash ssh erik@overlord.snakedesert.se \ "cd /home/erik/MosswartOverlord && git pull --ff-only origin master" # Python changes require a restart: ssh erik@overlord.snakedesert.se "docker compose restart dereth-tracker" # Static files (JS/CSS/HTML) are served from the bind-mounted static/ — no restart. ``` ⚠️ Uvicorn runs **without** `--reload` in production. Do not add it back — without the `watchfiles` package it falls back to a polling reloader that busy-loops at 100% CPU and eats a whole core. ### React frontend deploy ```bash cd frontend && npm run build && cd .. bash deploy-frontend.sh git add static/ && git commit -m "deploy frontend" && git push ssh erik@overlord.snakedesert.se "cd /home/erik/MosswartOverlord && git pull" # No container restart needed. ``` ### Full rebuild — Dockerfile / pip package / version stamp changes ```bash ssh erik@overlord.snakedesert.se "cd /home/erik/MosswartOverlord && \ git pull --ff-only origin master && \ export BUILD_VERSION=\"\$(date -u +%Y.%-m.%-d.%H%M)-\$(git rev-parse --short HEAD)\" && \ docker compose build --no-cache --build-arg BUILD_VERSION=\$BUILD_VERSION dereth-tracker && \ docker compose up -d dereth-tracker" ``` `BUILD_VERSION` is displayed in the sidebar of the live frontend. Format is CalVer: `YYYY.M.D.HHMM-gitshorthash`. ## WebSocket Contract ### `/ws/position` (plugin → backend) Authenticated via `?secret=` or `X-Plugin-Secret` header. Accepts JSON frames with a `type` discriminator: | `type` | Purpose | |---|---| | `telemetry` | Position, kills, session metrics (every 2s per character) | | `vitals` | Health/stamina/mana/vitae percentages | | `character_stats` | Full attributes/skills/allegiance (every 10 min) | | `inventory` / `full_inventory` | Complete inventory dump on login | | `inventory_delta` | Incremental add/update/remove of a single item | | `equipment_cantrip_state` | Equipped spell effects | | `portal` | Discovered portal with coordinates | | `spawn` | Monster spawn observation | | `chat` | In-game chat line (any channel) | | `quest` | Quest timer / progress | | `rare` | Rare item find notification | | `nearby_objects` | On-demand radar data (nearby entities) | | `combat_stats` | Session combat snapshot (Mag-Tools parser output) | | `share_*` | Cross-machine vital/debuff sharing envelopes | | `dungeon_map` | Dungeon floor tile data for radar overlay | See `EVENT_FORMATS.json` for exact per-type schemas. ### `/ws/live` (browser → backend) Session-cookie authenticated (except for internal Docker network clients, which are trusted by IP). Clients can: - Send `{"type":"subscribe","message_types":["rare","chat"]}` to filter which events they receive. Without subscribing, all types are forwarded (browser default). - Send `{"player_name":"Larsson","command":"/radar start"}` to route a command to that character's plugin client. - Send `{"type":"request_dungeon_map","landblock":"..."}` to pull cached dungeon tile data. Backend pushes the same firehose (subject to subscription filter) to every browser client. ## HTTP API Reference See `EVENT_FORMATS.json` for event schemas. Major HTTP endpoints: - `GET /live` — active players seen in the last 30s - `GET /history?from=…&to=…` — historical telemetry snapshots - `GET /trails` — recent player trails for the map - `GET /spawns/heatmap?hours=N` — aggregated spawn density - `GET /portals` — discovered portals within retention window - `GET /inventory/{character}` — current inventory (proxied to inventory-service) - `GET /character-stats/{character}` — full character attributes/skills - `GET /combat-stats/{character}` — session + lifetime combat stats - `GET /vital-sharing/peers` — currently-registered vital sharing peers - `GET /api-version` — build version stamp - `GET /server-health` — current Coldeve server status + player count ## Frontend ### React v2 (primary, at `/`) - Map-first layout with draggable/resizable windows - Code-split bundles: one chunk per window type, lazy-loaded on open - Window types: Chat, Stats, Inventory, Character, Radar, CombatStats, CombatPicker, Issues, VitalSharing, QuestStatus, PlayerDashboard - Per-character inventory version counter — an open inventory window refreshes 2s after its own character's last `inventory_delta`, ignoring unrelated traffic - Direct DOM pan/zoom on the map (no React state per frame) - Service worker caches a small whitelist of static assets - Version badge in the sidebar confirms which build is loaded ### Classic v1 (preserved at `/classic`) The original vanilla JS frontend with element-pooling optimization is kept for fallback and reference. ## Database Schema ### Telemetry DB (`dereth`, TimescaleDB) | Table | Type | Retention | Purpose | |---|---|---|---| | `telemetry_events` | hypertable | 30 days | Position/stats snapshots | | `spawn_events` | hypertable | 7 days | Monster spawn observations (heatmap source) | | `rare_events` | regular | forever | Rare find history | | `portals` | regular | 1 hour | Discovered portals, dedup by rounded coords | | `char_stats` | regular | forever | Per-character lifetime kill total | | `rare_stats` | regular | forever | Per-character lifetime rare total | | `rare_stats_sessions` | regular | forever | Per-session rare count | | `combat_stats` | regular | forever | Lifetime combat accumulator | | `combat_stats_sessions` | regular | forever | Per-session combat snapshots | | `character_stats` | regular | forever | Latest full stats JSON per character | | `server_status` | regular | forever | Current Coldeve server state (single row) | ### Inventory DB (`inventory_db`, PostgreSQL) Normalized schema: `items`, `item_combat_stats`, `item_requirements`, `item_enhancements`, `item_ratings`, `item_spells`, `item_raw_data`. `items.container_id` stores the in-game ID of the container holding the item (0 = character body). The frontend groups items into packs by this ID. ## Operations & Health ### PostgreSQL tuning `dereth-db` runs with explicit memory overrides in `docker-compose.yml`: - `shared_buffers=8GB` (was 96GB via auto-tune on a 32GB host — caused thrashing) - `effective_cache_size=16GB` - `work_mem=16MB`, `maintenance_work_mem=1GB` - `max_wal_size=4GB` ### Retention policies - `telemetry_events`: 30-day drop, daily - `spawn_events`: 7-day drop, daily - `portals`: 1-hour cleanup (background task in `main.py`) - `server_health_checks`: **removed** — was write-only, 850K rows of nothing ### Log levels Both `dereth-tracker` and `inventory-service` run at `LOG_LEVEL=INFO`. Do not set to `DEBUG` in production — it dumps full inventory_delta payloads for every item update (hundreds of KB/sec). ### Host (Proxmox VM) - 6 vCPU, 32 GiB RAM (of which ~30 GiB is normally free under current load) - Live host: `overlord.snakedesert.se` - Reverse proxy: Nginx on the host terminates TLS and strips the `/api/` prefix before forwarding to port 8765 ### Debug commands ```bash docker ps docker logs mosswartoverlord-dereth-tracker-1 --tail 100 docker logs mosswartoverlord-inventory-service-1 --tail 100 docker logs mosswartoverlord-discord-rare-monitor-1 --tail 100 docker exec dereth-db psql -U postgres -d dereth ``` ## Contributing Contributions welcome. Please: - Keep cross-repo protocol changes additive (new optional fields > renames/removes) - Update both this README and `CLAUDE.md` when workflows change - Test end-to-end: plugin → backend → browser for any new event type For detailed architecture notes and ongoing investigations, see `CLAUDE.md` and `docs/plans/`.