fix(v2): comprehensive bug fix round — all reported issues

1. Server stats: now shows player count, latency (rounded), uptime hours
2. Rares/Kills counters: fixed API response fields (all_time/total)
3. Chat send: wired socket.send with v1 envelope { player_name, command }
4. Stats button: opens Grafana iframe grid (4 panels, time range selector)
5. Char button: opens character window with attributes/skills/vitals from
   /character-stats/{name} API, structured display with sections
6. Inventory button: full inventory window with equipment table (material,
   set, imbue, AL, dmg, work, tink) + pack contents pill grid + filter
7. Radar button: opens radar window, sends start/stop commands via socket
8. Sidebar links: added Inventory Search, Suitbuilder, Player Debug
9. Color palette: expanded from 30 to 60 distinct colors matching v1
10. Window types properly routed: stats- prefix → Grafana, char- → character
    data, inv- → inventory, radar- → radar with socket commands

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-12 18:31:06 +02:00
parent de7b547349
commit b77450b6eb
19 changed files with 529 additions and 223 deletions

View file

@ -2,58 +2,88 @@ import React, { useEffect, useState } from 'react';
import { DraggableWindow } from './DraggableWindow';
import { apiFetch } from '../../api/client';
interface Props {
id: string;
charName: string;
zIndex: number;
}
interface Props { id: string; charName: string; zIndex: number; }
export const CharacterWindow: React.FC<Props> = ({ id, charName, zIndex }) => {
const [stats, setStats] = useState<Record<string, unknown> | null>(null);
const [data, setData] = useState<any>(null);
useEffect(() => {
apiFetch<Record<string, unknown>>(`/combat-stats/${encodeURIComponent(charName)}`)
.then(setStats).catch(() => {});
apiFetch<any>(`/character-stats/${encodeURIComponent(charName)}`)
.then(setData).catch(() => {});
}, [charName]);
const session = (stats as any)?.session;
const sd = data?.stats_data;
return (
<DraggableWindow id={id} title={`Character: ${charName}`} zIndex={zIndex} width={500} height={400}>
<div style={{ padding: 8, fontSize: '0.8rem', color: '#ccc', overflowY: 'auto', flex: 1 }}>
{session ? (
<>
<div style={{ marginBottom: 8 }}>
<strong>Session</strong>: {session.total_kills ?? 0} kills, {(session.total_damage_given ?? 0).toLocaleString()} dmg given
</div>
<div style={{ marginBottom: 8 }}>
<strong>Monsters fought</strong>: {Object.keys(session.monsters ?? {}).filter((k: string) => k !== '__cloak_surges__').length}
</div>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.75rem' }}>
<thead>
<tr style={{ borderBottom: '1px solid #444', color: '#888' }}>
<th style={{ textAlign: 'left', padding: '2px 4px' }}>Monster</th>
<th style={{ textAlign: 'right', padding: '2px 4px' }}>Kills</th>
<th style={{ textAlign: 'right', padding: '2px 4px' }}>Dmg Given</th>
</tr>
</thead>
<tbody>
{Object.values(session.monsters ?? {})
.filter((m: any) => m.name !== '__cloak_surges__')
.sort((a: any, b: any) => (b.damage_given ?? 0) - (a.damage_given ?? 0))
.slice(0, 30)
.map((m: any) => (
<tr key={m.name} style={{ borderBottom: '1px solid #222' }}>
<td style={{ padding: '2px 4px' }}>{m.name}</td>
<td style={{ textAlign: 'right', padding: '2px 4px' }}>{m.kill_count}</td>
<td style={{ textAlign: 'right', padding: '2px 4px' }}>{(m.damage_given ?? 0).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</>
<DraggableWindow id={id} title={`Character: ${charName}`} zIndex={zIndex} width={550} height={500}>
<div style={{ padding: 10, fontSize: '0.78rem', color: '#ccc', overflowY: 'auto', flex: 1 }}>
{!sd ? (
<div style={{ color: '#666' }}>Loading character data...</div>
) : (
<div style={{ color: '#666' }}>Loading combat data...</div>
<>
{/* Level + XP header */}
<div style={{ display: 'flex', gap: 16, marginBottom: 10, flexWrap: 'wrap' }}>
{data?.level && <span><strong>Level:</strong> {data.level}</span>}
{data?.total_xp != null && <span><strong>Total XP:</strong> {Number(data.total_xp).toLocaleString()}</span>}
{data?.unassigned_xp != null && <span><strong>Unassigned:</strong> {Number(data.unassigned_xp).toLocaleString()}</span>}
{data?.luminance_earned != null && <span><strong>Luminance:</strong> {Number(data.luminance_earned).toLocaleString()}</span>}
{data?.deaths != null && <span><strong>Deaths:</strong> {data.deaths}</span>}
</div>
{/* Attributes */}
{sd.attributes && (
<div style={{ marginBottom: 10 }}>
<div style={{ fontWeight: 600, color: '#88f', marginBottom: 4 }}>Attributes</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '2px 12px' }}>
{Object.entries(sd.attributes).map(([k, v]: [string, any]) => (
<div key={k} style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#888' }}>{k}</span>
<span>{typeof v === 'object' ? (v.buffed ?? v.base ?? JSON.stringify(v)) : v}</span>
</div>
))}
</div>
</div>
)}
{/* Skills */}
{sd.skills && (
<div style={{ marginBottom: 10 }}>
<div style={{ fontWeight: 600, color: '#88f', marginBottom: 4 }}>Skills</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1px 12px', fontSize: '0.72rem' }}>
{Object.entries(sd.skills)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]: [string, any]) => (
<div key={k} style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#888' }}>{k}</span>
<span>{typeof v === 'object' ? (v.buffed ?? v.base ?? JSON.stringify(v)) : v}</span>
</div>
))}
</div>
</div>
)}
{/* Vitals */}
{sd.vitals && (
<div>
<div style={{ fontWeight: 600, color: '#88f', marginBottom: 4 }}>Vitals</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '2px 12px' }}>
{Object.entries(sd.vitals).map(([k, v]: [string, any]) => (
<div key={k} style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#888' }}>{k}</span>
<span>{typeof v === 'object' ? (v.current ?? JSON.stringify(v)) : v}</span>
</div>
))}
</div>
</div>
)}
{/* Raw data fallback if no structured sections */}
{!sd.attributes && !sd.skills && !sd.vitals && (
<pre style={{ fontSize: '0.68rem', color: '#888', whiteSpace: 'pre-wrap' }}>
{JSON.stringify(sd, null, 2)}
</pre>
)}
</>
)}
</div>
</DraggableWindow>