fix(v2): all reported issues — missing windows, broken features, layouts

Missing features (now added):
1. Vital Sharing window — polls /vital-sharing/peers, shows peer vitals
   with HP/STA/MANA bars, connection status, position, tags
2. Combat Stats window — full Mag-Tools style with monster list (left),
   damage breakdown grid (right), session/lifetime toggle, element matrix
3. Issues Board window — CRUD with categories, resolve/reopen, comments
4. Quest Status — links to /quest-status.html (separate page like v1)
5. Sidebar: added Issues + Vitals buttons, Quest link, Combat button
   per player row (6 buttons now: Chat/Stats/Inv/Char/Combat/Radar)

Fixed functionality:
6. Radar — fixed command to "start_radar"/"stop_radar" (was wrong path)
7. Character window — redesigned with v1-style tabbed layout:
   Left tabs: Attributes (vitals bars + attribute grid) | Skills
   (specialized/trained grouped) | Titles
   Right tabs: Augs | Ratings | Other (allegiance)
   Header: level, race, gender, XP, luminance, deaths, skill credits
8. Stats window — proper Grafana iframe grid (4 panels 2x2) with
   time range selector (1H/6H/24H/7D)

Color palette: expanded to 60 distinct colors (was 30)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-12 18:49:16 +02:00
parent b77450b6eb
commit 52e1bcd6b8
12 changed files with 710 additions and 226 deletions

View file

@ -0,0 +1,94 @@
import React, { useEffect, useState, useCallback } from 'react';
import { DraggableWindow } from './DraggableWindow';
import { apiFetch } from '../../api/client';
interface Issue {
id: number; title: string; description: string; category: string;
created: string; resolved: boolean; author: string;
comments?: Array<{ id: number; text: string; author: string; created: string }>;
}
interface Props { id: string; zIndex: number; }
const CAT_COLORS: Record<string, string> = {
plugin: '#4488ff', overlord: '#44cc44', nav: '#ffaa00', macro: '#cc44cc', other: '#888',
};
export const IssuesWindow: React.FC<Props> = ({ id, zIndex }) => {
const [issues, setIssues] = useState<Issue[]>([]);
const [title, setTitle] = useState('');
const [desc, setDesc] = useState('');
const [category, setCategory] = useState('plugin');
const refresh = useCallback(async () => {
try {
const data = await apiFetch<{ issues: Issue[] }>('/issues');
setIssues((data.issues ?? []).sort((a, b) => (a.resolved ? 1 : 0) - (b.resolved ? 1 : 0)));
} catch { /* ignore */ }
}, []);
useEffect(() => { refresh(); }, [refresh]);
const addIssue = async () => {
if (!title.trim()) return;
await fetch('/api/issues', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
body: JSON.stringify({ title: title.trim(), description: desc.trim(), category }) });
setTitle(''); setDesc('');
refresh();
};
const toggleResolve = async (issue: Issue) => {
await fetch(`/api/issues/${issue.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
body: JSON.stringify({ resolved: !issue.resolved }) });
refresh();
};
return (
<DraggableWindow id={id} title="Issues Board" zIndex={zIndex} width={540} height={520}>
{/* Issue list */}
<div style={{ flex: 1, overflowY: 'auto', padding: 6, fontSize: '0.75rem' }}>
{issues.length === 0 ? (
<div style={{ padding: 12, color: '#666', textAlign: 'center' }}>No issues</div>
) : issues.map(issue => (
<div key={issue.id} style={{ padding: '6px 8px', marginBottom: 4, background: '#1f1f1f', borderRadius: 3,
border: '1px solid #333', opacity: issue.resolved ? 0.5 : 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: '0.6rem', padding: '1px 6px', borderRadius: 3,
background: CAT_COLORS[issue.category] ?? '#888', color: '#111', fontWeight: 600 }}>
{issue.category}
</span>
<span style={{ flex: 1, fontWeight: 500 }}>{issue.title}</span>
<button onClick={() => toggleResolve(issue)} style={{ fontSize: '0.65rem', padding: '1px 6px',
background: issue.resolved ? '#333' : 'rgba(68,204,68,0.15)', color: issue.resolved ? '#888' : '#4c4',
border: '1px solid #444', borderRadius: 3, cursor: 'pointer' }}>
{issue.resolved ? '↻ Reopen' : '✓ Resolve'}
</button>
</div>
{issue.description && <div style={{ color: '#888', marginTop: 3, fontSize: '0.7rem' }}>{issue.description}</div>}
<div style={{ color: '#555', fontSize: '0.6rem', marginTop: 2 }}>
by {issue.author} &middot; {new Date(issue.created).toLocaleDateString()}
</div>
</div>
))}
</div>
{/* Add issue form */}
<div style={{ padding: 6, borderTop: '1px solid #333', display: 'flex', flexDirection: 'column', gap: 3 }}>
<div style={{ display: 'flex', gap: 4 }}>
<input value={title} onChange={e => setTitle(e.target.value)} placeholder="Issue title..."
style={{ flex: 1, padding: '3px 6px', fontSize: '0.75rem', background: '#222', color: '#eee', border: '1px solid #444', borderRadius: 3 }} />
<select value={category} onChange={e => setCategory(e.target.value)}
style={{ padding: '3px 4px', fontSize: '0.7rem', background: '#222', color: '#eee', border: '1px solid #444', borderRadius: 3 }}>
<option value="plugin">Plugin</option><option value="overlord">Overlord</option>
<option value="nav">Nav</option><option value="macro">Macro</option><option value="other">Other</option>
</select>
</div>
<div style={{ display: 'flex', gap: 4 }}>
<textarea value={desc} onChange={e => setDesc(e.target.value)} placeholder="Description..."
rows={2} style={{ flex: 1, padding: '3px 6px', fontSize: '0.7rem', background: '#222', color: '#eee', border: '1px solid #444', borderRadius: 3, resize: 'vertical' }} />
<button onClick={addIssue} style={{ padding: '4px 12px', background: 'rgba(68,136,255,0.15)', color: '#6aadff',
border: '1px solid rgba(68,136,255,0.3)', borderRadius: 3, cursor: 'pointer', alignSelf: 'flex-end', fontSize: '0.7rem' }}>Add</button>
</div>
</div>
</DraggableWindow>
);
};