Cleanup:
- Removed 109 stale asset files from static/assets/ (was 122, now 13)
- Removed static/v2/ entirely (was duplicate of root assets)
- Removed dead dashboard code: DashboardView, Layout, GlobalStats,
CharacterCard, CharacterGrid, VitalBar, TabContainer, CombatTab,
RaresTab, MapTab, InventoryTab, global.css, MapTransformContext
- Removed recharts dependency (425KB chunk eliminated)
- CSS reduced from 17KB to 10KB
- Added deploy-frontend.sh script for one-command build+deploy
- Updated CLAUDE.md with combat_stats, share_*, dungeon_map events
and React frontend architecture
Death alerts (frontend + backend):
- Frontend: DeathNotification component with red banner + sawtooth
sound when vitae goes from 0 to >0
- Backend: detects vitae transition in vitals handler, sends Discord
webhook to #aclog with "☠️ CHARACTER died! (vitae: X%)"
- Rate-limited: max 1 Discord alert per character per 5 minutes
Idle detection (backend):
- Background task runs every 60 seconds
- Detects: vt_state "default"/"idle" OR kph=0 while in combat/hunt
- Sends Discord webhook: "⚠️ CHARACTER appears idle (state: X, KPH: 0)"
- Auto-clears alert when character becomes active again
- No duplicate alerts for same idle period
Discord integration:
- DISCORD_ACLOG_WEBHOOK env var for webhook URL
- Used by both death alerts and idle detection
- Graceful fallback when not configured
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Functional:
1. Chat: "▼ New messages below" indicator when scrolled up, click to jump
2. Combat stats: "Clear Session" button (red, with confirm dialog)
3. Inventory: live updates via inventory_delta WS (re-fetches on change)
4. Inventory: real mana time from equipment_cantrip_state WS (live
countdown with state dot: green=active, red=inactive, yellow=unknown)
Visual:
5. Thin separator line between tool links and sort buttons
6. Selected player row highlighted with darker background (#2a3344)
7. Scroll-to-top button (▲) appears when scrolled past 200px in player list
UX:
8. Double-click player dot on map opens their chat window
9. Right-click player dot shows context menu (Chat/Stats/Inv/Char/Combat/Radar)
10. Ctrl+D keyboard shortcut toggles between map and dashboard views
11. Sound notification on rare drops (880Hz sine beep via Web Audio API)
Backend:
12. Deep-merge lifetime offense/defense per element — accumulates
total_attacks, failed_attacks, crits, damage per AttackType×Element
instead of overwriting with latest session data
13. Startup cleanup: deletes stale combat_stats records from before
the lifetime fix (pre-2026-04-14T09:00Z)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The combat_stats DB column stores stats_data as JSON, but SQLAlchemy
returns it as a string (not a parsed dict). This caused:
- _combat_lifetime_cache loaded a string, merge failed silently
- API endpoints returned string instead of object for lifetime
- Frontend saw lifetime as a string, couldn't read .monsters
Fix: parse JSON string with json.loads() wherever stats_data is read
from DB — in the lifetime cache loader, single-character endpoint,
and all-characters endpoint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Plugin now sends only session data (lifetime=null). Backend computes
the delta between consecutive session snapshots (new - previous) and
merges it into a persisted lifetime record in the combat_stats table.
- _combat_last_session: tracks last-seen session per char:session_id
- _combat_lifetime_cache: in-memory lifetime per character
- _combat_session_delta(): computes diff between two cumulative snapshots
- _combat_merge_into_lifetime(): adds delta totals into lifetime record
- First snapshot for a new session = entire session is the delta
- Lifetime loaded from DB on first message, cached in memory after
- Broadcast enriched with computed lifetime so frontend gets both
This means session resets on login but lifetime persists in DB across
all sessions. The two will now show different data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Peers who unsubscribed or disconnected from vital sharing were lingering
forever in the Vital Sharing browser window because nothing ever deleted
them from the server-side state or told the browser to drop them.
Backend:
- share_unsubscribe now pops the character from _vital_sharing_peer_state
(not just flips connected=false) and broadcasts a share_peer_removed
envelope to browser clients.
- On real plugin disconnect, do the same: pop the state entry and
broadcast share_peer_removed so the NetworkUI updates immediately.
Frontend:
- New removeVitalSharingPeer(name) deletes from the local
vitalSharingPeers dict and re-renders.
- socket.onmessage now routes share_peer_removed to it.
- refreshVitalSharingPeers() reconciles against the server's list and
prunes any local entries the server no longer knows about, catching
any race where the broadcast was missed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_broadcast_share_to_plugin_clients was discarding a character from
_vital_sharing_subscribers whenever a single send_json hit the 1-second
timeout or raised any exception. Under heavy load this permanently
dropped clients from the subscriber set even though their WebSocket was
still fully connected — the user had to toggle vital sharing off and on
to get peer updates flowing again.
Now we log the send failure but leave the subscriber intact. Actual
eviction still happens on real WebSocket disconnect via the finally
block in the plugin receive loop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Accepts new share_subscribe / share_unsubscribe / share_* WebSocket
messages from MM plugin clients and fans them out to other opted-in
plugin clients (excluding origin) and to browser clients for the
NetworkUI window.
- main.py: _vital_sharing_subscribers set, _vital_sharing_peer_state
snapshot, _broadcast_share_to_plugin_clients relay, disconnect
cleanup, GET /vital-sharing/peers endpoint.
- static/index.html: new sidebar link for Vital Sharing window.
- static/script.js: showVitalSharingWindow with live HP/STA/MANA bars,
per-peer status dot/tags/position, 5s /vital-sharing/peers poll, and
share_* routing through the existing browser WebSocket handler.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace Nginx basic auth with proper user accounts:
- Session cookies via itsdangerous (30-day expiry, httponly, secure)
- Password hashing with bcrypt via passlib
- Login page with AC-themed UI
- Admin page for user management (CRUD)
- AuthMiddleware exempts plugin WS and browser WS endpoints
- Issues/comments author auto-populated from session
- Sidebar shows logged-in username, admin link, and logout
- Seed users: erik (admin), alex, lundberg
- SECRET_KEY env var for cookie signing
Root "/" path doesn't match .html ending. Checking the response's
content-type header catches index.html served via html=True and is
more robust for any URL that returns HTML/JS/CSS/JSON.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The StaticFiles path is empty for root "/" which served index.html via
html=True. Include root/directory paths in the no-cache match.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FastAPI StaticFiles sets Last-Modified/ETag but no Cache-Control, so
browsers use heuristic caching and can serve stale code after git pull.
Subclass StaticFiles to add 'Cache-Control: no-cache, must-revalidate'
on .html/.js/.css/.json files. ETag/Last-Modified still work, so
revalidation returns efficient 304 Not Modified when unchanged.
Other assets (images, fonts, tile textures) are unaffected and cache
normally.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New PATCH /issues/{id} endpoint to toggle resolved flag
- Add resolved:false to new issues
- Frontend: click "✓ Resolve" marks issue green with strikethrough
- Resolved issues show "↺ Reopen" and "🗑 Delete" buttons
- Delete requires confirmation
- Sort: unresolved first, then resolved
- Issues persist until explicitly deleted
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Was stored at /app/openissues.json (ephemeral container filesystem).
Moved to /app/static/openissues.json which is bind-mounted to the
host at ./static/, persisting across container rebuilds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Version: CalVer + git hash shown in top-right corner of main page.
Built via Docker ARG BUILD_VERSION at build time. Served via /api-version.
Issues Board: shared notepad window for tracking issues with plugin,
overlord, nav files, macros. Stored in openissues.json on server.
- GET/POST/DELETE /issues endpoints
- Draggable window matching Chat/Radar pattern
- Category tags (plugin, overlord, nav, macro, other) with colors
- Add/resolve issues through the UI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Backend: dungeon_map event handler with permanent in-memory cache
by landblock ID, request_dungeon_map for late-joining browsers
- Frontend: render dungeon cells as colored rectangles when in dungeon,
multi-level Z support (current floor bright, others dimmed),
automatic overworld/dungeon switching based on is_dungeon flag,
raw physics coordinate positioning for dungeon objects
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Browser can open a radar window per character that streams nearby
monsters, players, NPCs, portals, and other objects in real-time.
On-demand activation via start_radar/stop_radar commands through
the existing WebSocket command channel.
- Backend: nearby_objects event handler with in-memory cache and broadcast
- Frontend: canvas mini-map + entity list table in draggable window
- Radar button added to player list alongside Chat/Stats/Inventory/Char
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
🧝♂️ Fix 1: The Mailman Story
Imagine you're a mailman delivering letters. Before, you had to
knock on every door and wait for someone to answer before you could
go back to your truck and get more letters.
But your truck keeps getting MORE letters dumped in it! And if
you're stuck waiting at grandma's door... your truck overflows
and letters fall everywhere! 📬💥
The fix: Now you have a magic helper elf! You hand the letters to
the elf and say "you go deliver these!" while you run back to the
truck to grab more. The elf handles the doors, you handle the truck.
Nobody waits! 🧝♂️✨🍕 Fix 2: The Pizza Party Story
Now let's talk about that elf. The elf had a problem too!
Imagine you have 5 friends at a pizza party and you're handing out
slices. Before, the elf would:
1. Give pizza to Tommy → wait for him to take a bite 🍕
2. Give pizza to Sally → wait for her to take a bite 🍕
3. Give pizza to Bobby → wait for him to take a bite 🍕
So boring! Everyone's just sitting there hungry!
The fix: Now the elf throws ALL the pizza slices at the same time!
🍕🍕🍕🍕🍕 Everyone gets their pizza at once and nobody has to
wait for Tommy to finish chewing!
Yay! 🎉
Technical details:
- Use asyncio.create_task() to fire-and-forget broadcasts
- Use asyncio.gather() to send to all browsers concurrently
- Plugin receive loop no longer blocks on slow browser clients
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix remaining f-string SQL injection in process_inventory (same pattern
as single-item endpoints: parameterized ANY(:ids) queries)
- Add null guard for item_id in backend delta remove handler
- Add response status logging for inventory service HTTP calls
- Fix frontend ID fallback consistency in updateInventoryLive
- Replace debug print() with logger.debug()
- Add comment for Decal Slot_Decal magic number
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the AC stone-themed single-scroll character window with a TreeStats-
style tabbed interface. Two side-by-side tab containers: left (Attributes,
Skills, Titles) and right (Augmentations, Ratings, Other), plus an Allegiance
section below. Exact TreeStats color palette (#000022 bg, #af7a30 gold
borders, purple specialized, teal trained). Backend accepts new properties
and titles fields in character_stats message for JSONB storage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Implement client-side sorting for all columns including spell_names
- Add computed_spell_names CTE for server-side sort fallback
- Add resizable columns with localStorage persistence
- Add Cloak slot detection by name pattern
- Increase items limit to 50000 for full inventory loading
- Increase proxy timeout to 60s for large queries
- Remove pagination (all items loaded at once for sorting)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>