feat: major cleanup + death alerts + idle detection + Discord webhooks
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>
This commit is contained in:
parent
d2c30b610b
commit
adb9d5feab
163 changed files with 2756 additions and 2910 deletions
|
|
@ -17,6 +17,7 @@ export interface DashboardState {
|
|||
inventoryVersion: number;
|
||||
equipmentCantrips: Map<string, any>;
|
||||
characterStats: Map<string, any>;
|
||||
deathAlerts: Array<{ character_name: string; vitae: number; timestamp: string }>;
|
||||
socketRef: React.RefObject<WebSocket | null>;
|
||||
}
|
||||
|
||||
|
|
@ -33,6 +34,7 @@ export function useLiveData(): DashboardState {
|
|||
const [equipCantripVersion, setEquipCantripVersion] = useState(0);
|
||||
const characterStatsRef = useRef(new Map<string, any>());
|
||||
const [charStatsVersion, setCharStatsVersion] = useState(0);
|
||||
const [deathAlerts, setDeathAlerts] = useState<Array<{ character_name: string; vitae: number; timestamp: string }>>([]);
|
||||
const [nearbyObjects, setNearbyObjects] = useState<Map<string, any>>(new Map());
|
||||
const charsRef = useRef(characters);
|
||||
charsRef.current = characters;
|
||||
|
|
@ -56,6 +58,11 @@ export function useLiveData(): DashboardState {
|
|||
updateChar(t.character_name, s => ({ ...s, telemetry: t, lastUpdate: Date.now() }));
|
||||
} else if (msg.type === 'vitals') {
|
||||
const v = msg as VitalsMessage;
|
||||
// Detect death: vitae went from 0 to > 0
|
||||
const prev = charsRef.current.get(v.character_name)?.vitals;
|
||||
if (prev && (prev.vitae ?? 0) === 0 && (v.vitae ?? 0) > 0) {
|
||||
setDeathAlerts(a => [...a, { character_name: v.character_name, vitae: v.vitae, timestamp: new Date().toISOString() }].slice(-50));
|
||||
}
|
||||
updateChar(v.character_name, s => ({ ...s, vitals: v, lastUpdate: Date.now() }));
|
||||
} else if (msg.type === 'combat_stats') {
|
||||
const c = msg as CombatStatsMessage;
|
||||
|
|
@ -193,5 +200,5 @@ export function useLiveData(): DashboardState {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const characterStats = useMemo(() => characterStatsRef.current, [charStatsVersion]);
|
||||
|
||||
return { characters, serverHealth, totalRares, totalKills, recentRares, chatMessages, nearbyObjects, inventoryVersion, equipmentCantrips, characterStats, socketRef };
|
||||
return { characters, serverHealth, totalRares, totalKills, recentRares, chatMessages, nearbyObjects, inventoryVersion, equipmentCantrips, characterStats, deathAlerts, socketRef };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue