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:
parent
b77450b6eb
commit
52e1bcd6b8
12 changed files with 710 additions and 226 deletions
|
|
@ -6,85 +6,167 @@ interface Props { id: string; charName: string; zIndex: number; }
|
|||
|
||||
export const CharacterWindow: React.FC<Props> = ({ id, charName, zIndex }) => {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [tab, setTab] = useState<'attr' | 'skills' | 'titles'>('attr');
|
||||
const [rightTab, setRightTab] = useState<'augs' | 'ratings' | 'other'>('augs');
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch<any>(`/character-stats/${encodeURIComponent(charName)}`)
|
||||
.then(setData).catch(() => {});
|
||||
.then(d => setData(d)).catch(() => {});
|
||||
}, [charName]);
|
||||
|
||||
const sd = data?.stats_data;
|
||||
if (!data) {
|
||||
return (
|
||||
<DraggableWindow id={id} title={`Character: ${charName}`} zIndex={zIndex} width={600} height={500}>
|
||||
<div style={{ padding: 20, color: '#666' }}>Loading character data...</div>
|
||||
</DraggableWindow>
|
||||
);
|
||||
}
|
||||
|
||||
const sd = data.stats_data || data;
|
||||
const attrs = sd.attributes || {};
|
||||
const skills = sd.skills || {};
|
||||
const vitals = sd.vitals || {};
|
||||
const titles = sd.titles || [];
|
||||
const properties = sd.properties || {};
|
||||
|
||||
const specSkills = Object.entries(skills).filter(([, v]: [string, any]) => v?.training === 'Specialized');
|
||||
const trainedSkills = Object.entries(skills).filter(([, v]: [string, any]) => v?.training === 'Trained');
|
||||
|
||||
return (
|
||||
<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>
|
||||
) : (
|
||||
<>
|
||||
{/* 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>}
|
||||
<DraggableWindow id={id} title={`Character: ${charName}`} zIndex={zIndex} width={620} height={520}>
|
||||
<div style={{ flex: 1, overflowY: 'auto', fontSize: '0.75rem', color: '#ccc' }}>
|
||||
{/* Header: Level + Race + XP */}
|
||||
<div style={{ padding: '8px 10px', borderBottom: '1px solid #333', display: 'flex', flexWrap: 'wrap', gap: '12px', fontSize: '0.78rem' }}>
|
||||
{data.level && <span><strong>Lv {data.level}</strong></span>}
|
||||
{data.race && <span>{data.race}</span>}
|
||||
{data.gender && <span>{data.gender}</span>}
|
||||
{data.total_xp != null && <span>XP: {Number(data.total_xp).toLocaleString()}</span>}
|
||||
{data.unassigned_xp != null && <span>Unasgn: {Number(data.unassigned_xp).toLocaleString()}</span>}
|
||||
{data.luminance_earned != null && <span>Lum: {Number(data.luminance_earned).toLocaleString()}</span>}
|
||||
{data.deaths != null && <span>Deaths: {data.deaths}</span>}
|
||||
{sd.skill_credits != null && <span>Skill Credits: {sd.skill_credits}</span>}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', minHeight: 350 }}>
|
||||
{/* Left panel */}
|
||||
<div style={{ flex: 1, borderRight: '1px solid #333' }}>
|
||||
<div style={{ display: 'flex', gap: 2, padding: '4px 6px', borderBottom: '1px solid #333' }}>
|
||||
{(['attr', 'skills', 'titles'] as const).map(t => (
|
||||
<button key={t} className={`ml-stats-range-btn ${tab === t ? 'active' : ''}`}
|
||||
onClick={() => setTab(t)} style={{ flex: 1 }}>
|
||||
{t === 'attr' ? 'Attributes' : t === 'skills' ? 'Skills' : 'Titles'}
|
||||
</button>
|
||||
))}
|
||||
</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 style={{ padding: 8 }}>
|
||||
{tab === 'attr' && (
|
||||
<>
|
||||
{/* Vital bars */}
|
||||
{Object.entries(vitals).map(([k, v]: [string, any]) => (
|
||||
<div key={k} style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3 }}>
|
||||
<span style={{ width: 55, color: '#888', fontSize: '0.7rem' }}>{k}</span>
|
||||
<div style={{ flex: 1, height: 6, background: '#222', borderRadius: 3, overflow: 'hidden' }}>
|
||||
<div style={{ width: '100%', height: '100%', background: k === 'health' ? '#c44' : k === 'stamina' ? '#ca0' : '#48f', borderRadius: 3 }} />
|
||||
</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>
|
||||
<span style={{ fontSize: '0.68rem', color: '#aaa', width: 40, textAlign: 'right' }}>{v?.base ?? v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Attributes table */}
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<div style={{ fontWeight: 600, color: '#6aadff', marginBottom: 4, fontSize: '0.72rem' }}>Attributes</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1px 16px' }}>
|
||||
{Object.entries(attrs).map(([k, v]: [string, any]) => (
|
||||
<div key={k} style={{ display: 'flex', justifyContent: 'space-between', padding: '1px 0' }}>
|
||||
<span style={{ color: '#888', textTransform: 'capitalize' }}>{k}</span>
|
||||
<span>{v?.base ?? v} {v?.creation != null ? `(${v.creation})` : ''}</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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{tab === 'skills' && (
|
||||
<>
|
||||
{specSkills.length > 0 && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div style={{ fontWeight: 600, color: '#6aadff', marginBottom: 3, fontSize: '0.7rem' }}>Specialized</div>
|
||||
{specSkills.sort(([a], [b]) => a.localeCompare(b)).map(([k, v]: [string, any]) => (
|
||||
<div key={k} style={{ display: 'flex', justifyContent: 'space-between', padding: '1px 0' }}>
|
||||
<span style={{ color: '#ccc' }}>{k}</span>
|
||||
<span style={{ color: '#8f8' }}>{v?.base ?? v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{trainedSkills.length > 0 && (
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, color: '#888', marginBottom: 3, fontSize: '0.7rem' }}>Trained</div>
|
||||
{trainedSkills.sort(([a], [b]) => a.localeCompare(b)).map(([k, v]: [string, any]) => (
|
||||
<div key={k} style={{ display: 'flex', justifyContent: 'space-between', padding: '1px 0' }}>
|
||||
<span style={{ color: '#999' }}>{k}</span>
|
||||
<span>{v?.base ?? v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{specSkills.length === 0 && trainedSkills.length === 0 && (
|
||||
<div style={{ color: '#555' }}>No skill data available</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{tab === 'titles' && (
|
||||
titles.length > 0 ? (
|
||||
<ul style={{ paddingLeft: 16, margin: 0 }}>
|
||||
{titles.map((t: string, i: number) => <li key={i} style={{ padding: '1px 0' }}>{t}</li>)}
|
||||
</ul>
|
||||
) : <div style={{ color: '#555' }}>No titles</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right panel */}
|
||||
<div style={{ width: 220 }}>
|
||||
<div style={{ display: 'flex', gap: 2, padding: '4px 4px', borderBottom: '1px solid #333' }}>
|
||||
{(['augs', 'ratings', 'other'] as const).map(t => (
|
||||
<button key={t} className={`ml-stats-range-btn ${rightTab === t ? 'active' : ''}`}
|
||||
onClick={() => setRightTab(t)} style={{ flex: 1, fontSize: '0.6rem' }}>
|
||||
{t === 'augs' ? 'Augs' : t === 'ratings' ? 'Ratings' : 'Other'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ padding: 6, fontSize: '0.7rem' }}>
|
||||
{rightTab === 'augs' && (
|
||||
Object.keys(properties).length > 0 ? (
|
||||
Object.entries(properties).slice(0, 20).map(([k, v]) => (
|
||||
<div key={k} style={{ display: 'flex', justifyContent: 'space-between', padding: '1px 0' }}>
|
||||
<span style={{ color: '#888' }}>{k}</span>
|
||||
<span>{String(v)}</span>
|
||||
</div>
|
||||
))
|
||||
) : <div style={{ color: '#555' }}>No augmentation data</div>
|
||||
)}
|
||||
{rightTab === 'ratings' && (
|
||||
<div style={{ color: '#555' }}>Rating data will appear here from character_stats events</div>
|
||||
)}
|
||||
{rightTab === 'other' && (
|
||||
<>
|
||||
{data.allegiance && (
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, color: '#6aadff', marginBottom: 3 }}>Allegiance</div>
|
||||
{data.allegiance.name && <div>Name: {data.allegiance.name}</div>}
|
||||
{data.allegiance.monarch?.name && <div>Monarch: {data.allegiance.monarch.name}</div>}
|
||||
{data.allegiance.patron?.name && <div>Patron: {data.allegiance.patron.name}</div>}
|
||||
{data.allegiance.rank != null && <div>Rank: {data.allegiance.rank}</div>}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DraggableWindow>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue