New modern dashboard at /v2 running alongside the existing UI at /. Same backend, same APIs, same WebSocket — zero backend changes. Stack: React 19 + Vite + TypeScript + Recharts Source: frontend/ — build output: static/v2/ Phase 1 delivers: - Character overview cards in a responsive CSS Grid - Live HP/Stamina/Mana bars via WebSocket vitals - Kills/hr, total kills, deaths, session uptime - VTank state badge (Combat/Nav/Idle) - Location coordinates - Click to expand: combat stats, prismatic count, CPU/RAM - Global stats header: active chars, total kills, total rares, server health - WebSocket hook with auto-reconnect - HTTP poll fallback for initial load + server health - Mobile responsive (single column on narrow screens) - Dark theme matching the MosswartOverlord palette Build: cd frontend && npm run build Access: /v2 (served by existing NoCacheStaticFiles mount) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
89 lines
3.3 KiB
TypeScript
89 lines
3.3 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { VitalBar } from './VitalBar';
|
|
import type { CharacterState } from '../types';
|
|
|
|
interface Props {
|
|
character: CharacterState;
|
|
}
|
|
|
|
const vtankBadge = (state: string) => {
|
|
const s = (state || 'idle').toLowerCase();
|
|
if (s === 'combat') return { label: 'Combat', cls: 'badge-combat' };
|
|
if (s === 'nav' || s === 'navigation') return { label: 'Nav', cls: 'badge-nav' };
|
|
return { label: 'Idle', cls: 'badge-idle' };
|
|
};
|
|
|
|
export const CharacterCard: React.FC<Props> = React.memo(({ character }) => {
|
|
const [expanded, setExpanded] = useState(false);
|
|
const { telemetry: t, vitals: v, combat: c } = character;
|
|
const badge = vtankBadge(t?.vt_state ?? '');
|
|
|
|
return (
|
|
<div className="char-card" onClick={() => setExpanded(!expanded)}>
|
|
<div className="char-header">
|
|
<span className="char-name">{character.name}</span>
|
|
<span className={`char-badge ${badge.cls}`}>{badge.label}</span>
|
|
</div>
|
|
|
|
{v ? (
|
|
<div className="char-vitals">
|
|
<VitalBar label="HP" current={v.health_current} max={v.health_max}
|
|
color="linear-gradient(90deg, #ff4444, #ff6666)" bgColor="#330000" />
|
|
<VitalBar label="ST" current={v.stamina_current} max={v.stamina_max}
|
|
color="linear-gradient(90deg, #ffaa00, #ffcc44)" bgColor="#331a00" />
|
|
<VitalBar label="MN" current={v.mana_current} max={v.mana_max}
|
|
color="linear-gradient(90deg, #4488ff, #66aaff)" bgColor="#001433" />
|
|
</div>
|
|
) : (
|
|
<div className="char-vitals-placeholder">Awaiting vitals...</div>
|
|
)}
|
|
|
|
<div className="char-stats-row">
|
|
<div className="stat">
|
|
<span className="stat-value">{t?.kills_per_hour ?? '--'}</span>
|
|
<span className="stat-label">kills/hr</span>
|
|
</div>
|
|
<div className="stat">
|
|
<span className="stat-value">{t?.kills?.toLocaleString() ?? '--'}</span>
|
|
<span className="stat-label">kills</span>
|
|
</div>
|
|
<div className="stat">
|
|
<span className="stat-value">{t?.deaths ?? '0'}</span>
|
|
<span className="stat-label">deaths</span>
|
|
</div>
|
|
<div className="stat">
|
|
<span className="stat-value">{t?.onlinetime?.replace(/^00\./, '') ?? '--'}</span>
|
|
<span className="stat-label">uptime</span>
|
|
</div>
|
|
</div>
|
|
|
|
{t && (
|
|
<div className="char-location">
|
|
{t.ns?.toFixed(1)}N, {t.ew?.toFixed(1)}E
|
|
</div>
|
|
)}
|
|
|
|
{expanded && (
|
|
<div className="char-expanded">
|
|
{v?.vitae ? <div className="vitae-warn">Vitae: {v.vitae}%</div> : null}
|
|
<div className="expanded-row">
|
|
<span>Prismatics: {t?.prismatic_taper_count ?? '--'}</span>
|
|
<span>Total Deaths: {t?.total_deaths ?? '--'}</span>
|
|
</div>
|
|
{c?.session && (
|
|
<div className="expanded-row">
|
|
<span>Session Dmg: {c.session.total_damage_given?.toLocaleString()}</span>
|
|
<span>Session Kills: {c.session.total_kills}</span>
|
|
</div>
|
|
)}
|
|
<div className="expanded-row">
|
|
<span>RAM: {t?.mem_mb ? (t.mem_mb / 1048576).toFixed(0) + ' MB' : '--'}</span>
|
|
<span>CPU: {t?.cpu_pct?.toFixed(1) ?? '--'}%</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
CharacterCard.displayName = 'CharacterCard';
|