feat(v2): remove old dashboard, add vitae + resizable windows
- Removed old Recharts dashboard view entirely (no more viewMode toggle, DashboardView lazy import, Ctrl+D shortcut) - Recharts chunk eliminated from build — bundle size reduced - Player Dashboard window: added Vitae column (red when > 0%) - ALL windows now resizable: drag bottom-right corner handle (min 300×200px). Subtle diagonal line grip indicator. - Sidebar: removed 📊 Dashboard toggle link, removed broken /quest-status.html external link (replaced by 📜 Quests window) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
938421999a
commit
a5bd659876
37 changed files with 168 additions and 178 deletions
|
|
@ -1,45 +1,10 @@
|
|||
import { useState, lazy, Suspense, useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { MapLayout } from './components/map/MapLayout';
|
||||
import { useLiveData } from './hooks/useLiveData';
|
||||
import './styles/global.css';
|
||||
import './styles/map-layout.css';
|
||||
|
||||
// Lazy-load dashboard view (contains Recharts ~400KB) — only loaded when user switches to dashboard
|
||||
const DashboardView = lazy(() => import('./DashboardView'));
|
||||
|
||||
type ViewMode = 'map' | 'dashboard';
|
||||
|
||||
export default function App() {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>(
|
||||
() => (localStorage.getItem('v2-view') as ViewMode) || 'map'
|
||||
);
|
||||
const data = useLiveData();
|
||||
|
||||
// Ctrl+D toggles map/dashboard
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.ctrlKey && e.key === 'd') {
|
||||
e.preventDefault();
|
||||
toggleView();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
});
|
||||
|
||||
const toggleView = () => {
|
||||
const next = viewMode === 'map' ? 'dashboard' : 'map';
|
||||
setViewMode(next);
|
||||
localStorage.setItem('v2-view', next);
|
||||
};
|
||||
|
||||
if (viewMode === 'map') {
|
||||
return <MapLayout data={data} onViewToggle={toggleView} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div style={{ background: '#0d0d0d', color: '#888', padding: 40, textAlign: 'center' }}>Loading dashboard...</div>}>
|
||||
<DashboardView data={data} onViewToggle={toggleView} />
|
||||
</Suspense>
|
||||
);
|
||||
return <MapLayout data={data} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,9 @@ import type { DashboardState } from '../../hooks/useLiveData';
|
|||
|
||||
interface Props {
|
||||
data: DashboardState;
|
||||
onViewToggle: () => void;
|
||||
}
|
||||
|
||||
export const MapLayout: React.FC<Props> = ({ data, onViewToggle }) => {
|
||||
export const MapLayout: React.FC<Props> = ({ data }) => {
|
||||
const getColor = usePlayerColors();
|
||||
const [showHeatmap, setShowHeatmap] = useState(false);
|
||||
const [showPortals, setShowPortals] = useState(false);
|
||||
|
|
@ -49,7 +48,6 @@ export const MapLayout: React.FC<Props> = ({ data, onViewToggle }) => {
|
|||
totalKills={data.totalKills}
|
||||
getColor={getColor}
|
||||
onSelectPlayer={handleSelectPlayer}
|
||||
onViewToggle={onViewToggle}
|
||||
showHeatmap={showHeatmap}
|
||||
showPortals={showPortals}
|
||||
onToggleHeatmap={setShowHeatmap}
|
||||
|
|
@ -57,6 +55,7 @@ export const MapLayout: React.FC<Props> = ({ data, onViewToggle }) => {
|
|||
version={version}
|
||||
selectedPlayer={selectedPlayer}
|
||||
/>
|
||||
|
||||
<MapView
|
||||
players={players}
|
||||
getColor={getColor}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ interface Props {
|
|||
totalKills: number;
|
||||
getColor: (name: string) => string;
|
||||
onSelectPlayer: (name: string) => void;
|
||||
onViewToggle: () => void;
|
||||
showHeatmap: boolean;
|
||||
showPortals: boolean;
|
||||
onToggleHeatmap: (v: boolean) => void;
|
||||
|
|
@ -22,9 +21,9 @@ interface Props {
|
|||
}
|
||||
|
||||
export const Sidebar: React.FC<Props> = ({
|
||||
players, vitals, serverHealth, totalRares, totalKills, getColor, onSelectPlayer, onViewToggle,
|
||||
players, vitals, serverHealth, totalRares, totalKills, getColor, onSelectPlayer,
|
||||
showHeatmap, showPortals, onToggleHeatmap, onTogglePortals, version, selectedPlayer,
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const [sortKey, setSortKey] = useState<SortKey>('name');
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
|
|
@ -81,7 +80,6 @@ export const Sidebar: React.FC<Props> = ({
|
|||
|
||||
{/* Tool links */}
|
||||
<div className="ml-tool-links">
|
||||
<span className="ml-tool-link" style={{ cursor: 'pointer' }} onClick={onViewToggle}>📊 Dashboard</span>
|
||||
<a href="/inventory.html" target="_blank" className="ml-tool-link">🔍 Inv Search</a>
|
||||
<a href="/suitbuilder.html" target="_blank" className="ml-tool-link">🛡️ Suitbuilder</a>
|
||||
<a href="/debug.html" target="_blank" className="ml-tool-link">🐛 Debug</a>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useRef, useCallback, useEffect } from 'react';
|
||||
import React, { useRef, useCallback, useEffect, useState } from 'react';
|
||||
import { useWindowManager } from '../../contexts/WindowManagerContext';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -14,7 +14,9 @@ export const DraggableWindow: React.FC<Props> = ({ id, title, zIndex, width = 70
|
|||
const { closeWindow, bringToFront } = useWindowManager();
|
||||
const winRef = useRef<HTMLDivElement>(null);
|
||||
const dragRef = useRef({ dragging: false, sx: 0, sy: 0, ox: 0, oy: 0 });
|
||||
const resizeRef = useRef({ resizing: false, sx: 0, sy: 0, sw: 0, sh: 0 });
|
||||
const posRef = useRef({ x: 420, y: 10 + Math.random() * 40 });
|
||||
const [size, setSize] = useState({ w: width, h: height });
|
||||
|
||||
const onHeaderDown = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -24,16 +26,34 @@ export const DraggableWindow: React.FC<Props> = ({ id, title, zIndex, width = 70
|
|||
dragRef.current = { dragging: true, sx: e.clientX, sy: e.clientY, ox: rect.left, oy: rect.top };
|
||||
}, [id, bringToFront]);
|
||||
|
||||
const onResizeDown = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
resizeRef.current = { resizing: true, sx: e.clientX, sy: e.clientY, sw: size.w, sh: size.h };
|
||||
}, [size.w, size.h]);
|
||||
|
||||
useEffect(() => {
|
||||
const onMove = (e: MouseEvent) => {
|
||||
// Drag
|
||||
const d = dragRef.current;
|
||||
if (!d.dragging || !winRef.current) return;
|
||||
if (d.dragging && winRef.current) {
|
||||
posRef.current.x = d.ox + (e.clientX - d.sx);
|
||||
posRef.current.y = d.oy + (e.clientY - d.sy);
|
||||
winRef.current.style.left = `${posRef.current.x}px`;
|
||||
winRef.current.style.top = `${posRef.current.y}px`;
|
||||
}
|
||||
// Resize
|
||||
const r = resizeRef.current;
|
||||
if (r.resizing) {
|
||||
const newW = Math.max(300, r.sw + (e.clientX - r.sx));
|
||||
const newH = Math.max(200, r.sh + (e.clientY - r.sy));
|
||||
setSize({ w: newW, h: newH });
|
||||
}
|
||||
};
|
||||
const onUp = () => {
|
||||
dragRef.current.dragging = false;
|
||||
resizeRef.current.resizing = false;
|
||||
};
|
||||
const onUp = () => { dragRef.current.dragging = false; };
|
||||
window.addEventListener('mousemove', onMove);
|
||||
window.addEventListener('mouseup', onUp);
|
||||
return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
|
||||
|
|
@ -43,7 +63,7 @@ export const DraggableWindow: React.FC<Props> = ({ id, title, zIndex, width = 70
|
|||
<div
|
||||
ref={winRef}
|
||||
className="ml-window"
|
||||
style={{ zIndex, width, height, left: posRef.current.x, top: posRef.current.y }}
|
||||
style={{ zIndex, width: size.w, height: size.h, left: posRef.current.x, top: posRef.current.y }}
|
||||
onMouseDown={() => bringToFront(id)}
|
||||
>
|
||||
<div className="ml-window-header" onMouseDown={onHeaderDown}>
|
||||
|
|
@ -53,6 +73,8 @@ export const DraggableWindow: React.FC<Props> = ({ id, title, zIndex, width = 70
|
|||
<div className="ml-window-content">
|
||||
{children}
|
||||
</div>
|
||||
{/* Resize handle */}
|
||||
<div className="ml-window-resize" onMouseDown={onResizeDown} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export const PlayerDashboardWindow: React.FC<Props> = ({ id, zIndex, characters
|
|||
state: t.vt_state ?? 'idle',
|
||||
tapers: parseInt(t.prismatic_taper_count as string) || 0,
|
||||
hp: c.vitals?.health_percentage ?? 0,
|
||||
vitae: c.vitals?.vitae ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -74,6 +75,7 @@ export const PlayerDashboardWindow: React.FC<Props> = ({ id, zIndex, characters
|
|||
<th style={{ ...thStyle('deaths'), textAlign: 'right' }} onClick={() => toggleSort('deaths')}>Deaths{arrow('deaths')}</th>
|
||||
<th style={{ ...thStyle('uptime'), textAlign: 'right' }} onClick={() => toggleSort('uptime')}>Uptime{arrow('uptime')}</th>
|
||||
<th style={{ textAlign: 'right', padding: '4px 6px', color: '#888', fontSize: '0.65rem', fontWeight: 600, borderBottom: '1px solid #444' }}>HP%</th>
|
||||
<th style={{ textAlign: 'right', padding: '4px 6px', color: '#888', fontSize: '0.65rem', fontWeight: 600, borderBottom: '1px solid #444' }}>Vitae</th>
|
||||
<th style={{ textAlign: 'right', padding: '4px 6px', color: '#888', fontSize: '0.65rem', fontWeight: 600, borderBottom: '1px solid #444' }}>Tapers</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -98,6 +100,8 @@ export const PlayerDashboardWindow: React.FC<Props> = ({ id, zIndex, characters
|
|||
<td style={{ textAlign: 'right', padding: '3px 6px', color: '#888', fontVariantNumeric: 'tabular-nums' }}>{p.uptime}</td>
|
||||
<td style={{ textAlign: 'right', padding: '3px 6px', fontVariantNumeric: 'tabular-nums',
|
||||
color: p.hp > 80 ? '#4c4' : p.hp > 40 ? '#ca0' : '#c44' }}>{p.hp.toFixed(0)}%</td>
|
||||
<td style={{ textAlign: 'right', padding: '3px 6px', fontVariantNumeric: 'tabular-nums',
|
||||
color: p.vitae > 0 ? '#f66' : '#333' }}>{p.vitae > 0 ? `${p.vitae}%` : ''}</td>
|
||||
<td style={{ textAlign: 'right', padding: '3px 6px', color: '#888', fontVariantNumeric: 'tabular-nums' }}>{p.tapers.toLocaleString()}</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -528,6 +528,18 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ml-window-resize {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: nwse-resize;
|
||||
opacity: 0.3;
|
||||
background: linear-gradient(135deg, transparent 50%, #888 50%, transparent 52%, #888 65%, transparent 67%, #888 80%);
|
||||
}
|
||||
.ml-window-resize:hover { opacity: 0.6; }
|
||||
|
||||
/* ── Stats window (Grafana iframes) ───────────────────── */
|
||||
.ml-stats-controls {
|
||||
display: flex;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{u as c,j as r,D as d}from"./index-D3oSGfL8.js";import"./react-DlyoauG8.js";const p=({id:n,zIndex:i,characters:a})=>{const{openWindow:s}=c(),e=Array.from(a.keys()).sort();return r.jsx(d,{id:n,title:"Combat Stats — Select Character",zIndex:i,width:300,height:400,children:r.jsx("div",{style:{flex:1,overflowY:"auto",padding:6},children:e.length===0?r.jsx("div",{style:{padding:12,color:"#666",textAlign:"center",fontSize:"0.8rem"},children:"No characters online"}):e.map(o=>r.jsx("div",{style:{padding:"5px 8px",cursor:"pointer",borderBottom:"1px solid #222",color:"#ccc",fontSize:"0.82rem"},onMouseEnter:t=>t.currentTarget.style.background="#2a2a2a",onMouseLeave:t=>t.currentTarget.style.background="",onClick:()=>s(`combat-${o}`,`Combat: ${o}`,o),children:o},o))})})};export{p as CombatPickerWindow};
|
||||
import{u as c,j as r,D as d}from"./index-BiGwMY76.js";import"./react-yfL0ty4i.js";const p=({id:n,zIndex:i,characters:a})=>{const{openWindow:s}=c(),e=Array.from(a.keys()).sort();return r.jsx(d,{id:n,title:"Combat Stats — Select Character",zIndex:i,width:300,height:400,children:r.jsx("div",{style:{flex:1,overflowY:"auto",padding:6},children:e.length===0?r.jsx("div",{style:{padding:12,color:"#666",textAlign:"center",fontSize:"0.8rem"},children:"No characters online"}):e.map(o=>r.jsx("div",{style:{padding:"5px 8px",cursor:"pointer",borderBottom:"1px solid #222",color:"#ccc",fontSize:"0.82rem"},onMouseEnter:t=>t.currentTarget.style.background="#2a2a2a",onMouseLeave:t=>t.currentTarget.style.background="",onClick:()=>s(`combat-${o}`,`Combat: ${o}`,o),children:o},o))})})};export{p as CombatPickerWindow};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
static/assets/PlayerDashboardWindow-BfZp59uQ.js
Normal file
1
static/assets/PlayerDashboardWindow-BfZp59uQ.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{r as c,j as t,D as u,a as f}from"./index-D3oSGfL8.js";import"./react-DlyoauG8.js";const j=({id:p,zIndex:x})=>{const[s,h]=c.useState(null);c.useEffect(()=>{const e=async()=>{try{h(await f("/quest-status"))}catch{}};e();const o=setInterval(e,3e4);return()=>clearInterval(o)},[]);const i=s?Object.keys(s.quest_data).sort():[],l=new Set;if(s)for(const e of Object.values(s.quest_data))for(const o of Object.keys(e))l.add(o);const n=Array.from(l).sort();return t.jsx(u,{id:p,title:"Quest Status",zIndex:x,width:780,height:500,children:t.jsx("div",{style:{flex:1,overflow:"auto",fontSize:"0.72rem"},children:s?i.length===0?t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"No quest data available"}):t.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[t.jsx("thead",{children:t.jsxs("tr",{style:{position:"sticky",top:0,background:"#1a1a1a",zIndex:1},children:[t.jsx("th",{style:{textAlign:"left",padding:"4px 8px",borderBottom:"1px solid #444",color:"#888",fontSize:"0.65rem",fontWeight:600,minWidth:140},children:"Character"}),n.map(e=>t.jsx("th",{style:{textAlign:"center",padding:"4px 6px",borderBottom:"1px solid #444",color:"#888",fontSize:"0.6rem",fontWeight:600,maxWidth:120,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},title:e,children:e.replace(" Timer","").replace(" Pickup","")},e))]})}),t.jsx("tbody",{children:i.map(e=>{const o=s.quest_data[e]||{};return t.jsxs("tr",{style:{borderBottom:"1px solid #222"},children:[t.jsx("td",{style:{padding:"3px 8px",color:"#ccc",fontWeight:500,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",maxWidth:160},children:e}),n.map(d=>{const r=o[d],a=r==="READY";return t.jsx("td",{style:{textAlign:"center",padding:"3px 6px",color:a?"#4c4":r?"#ca0":"#333",fontWeight:a?600:400,fontSize:a?"0.7rem":"0.68rem"},children:r||"—"},d)})]},e)})})]}):t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"Loading quest data..."})})})};export{j as QuestStatusWindow};
|
||||
import{r as c,j as t,D as u,a as f}from"./index-BiGwMY76.js";import"./react-yfL0ty4i.js";const j=({id:p,zIndex:x})=>{const[s,h]=c.useState(null);c.useEffect(()=>{const e=async()=>{try{h(await f("/quest-status"))}catch{}};e();const o=setInterval(e,3e4);return()=>clearInterval(o)},[]);const i=s?Object.keys(s.quest_data).sort():[],l=new Set;if(s)for(const e of Object.values(s.quest_data))for(const o of Object.keys(e))l.add(o);const n=Array.from(l).sort();return t.jsx(u,{id:p,title:"Quest Status",zIndex:x,width:780,height:500,children:t.jsx("div",{style:{flex:1,overflow:"auto",fontSize:"0.72rem"},children:s?i.length===0?t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"No quest data available"}):t.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[t.jsx("thead",{children:t.jsxs("tr",{style:{position:"sticky",top:0,background:"#1a1a1a",zIndex:1},children:[t.jsx("th",{style:{textAlign:"left",padding:"4px 8px",borderBottom:"1px solid #444",color:"#888",fontSize:"0.65rem",fontWeight:600,minWidth:140},children:"Character"}),n.map(e=>t.jsx("th",{style:{textAlign:"center",padding:"4px 6px",borderBottom:"1px solid #444",color:"#888",fontSize:"0.6rem",fontWeight:600,maxWidth:120,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},title:e,children:e.replace(" Timer","").replace(" Pickup","")},e))]})}),t.jsx("tbody",{children:i.map(e=>{const o=s.quest_data[e]||{};return t.jsxs("tr",{style:{borderBottom:"1px solid #222"},children:[t.jsx("td",{style:{padding:"3px 8px",color:"#ccc",fontWeight:500,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",maxWidth:160},children:e}),n.map(d=>{const r=o[d],a=r==="READY";return t.jsx("td",{style:{textAlign:"center",padding:"3px 6px",color:a?"#4c4":r?"#ca0":"#333",fontWeight:a?600:400,fontSize:a?"0.7rem":"0.68rem"},children:r||"—"},d)})]},e)})})]}):t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"Loading quest data..."})})})};export{j as QuestStatusWindow};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{r as o,j as t,D as d}from"./index-D3oSGfL8.js";import"./react-DlyoauG8.js";const c=[{title:"Kills per Hour",id:1},{title:"Memory (MB)",id:2},{title:"CPU (%)",id:3},{title:"Mem Handles",id:4}],m=[{label:"1H",value:"now-1h"},{label:"6H",value:"now-6h"},{label:"24H",value:"now-24h"},{label:"7D",value:"now-7d"}],v=({id:s,charName:a,zIndex:i})=>{const[l,r]=o.useState("now-24h"),n=e=>`/grafana/d-solo/dereth-tracker/dereth-tracker-dashboard?panelId=${e}&var-character=${encodeURIComponent(a)}&from=${l}&to=now&theme=light`;return t.jsxs(d,{id:s,title:`Stats: ${a}`,zIndex:i,width:750,height:480,children:[t.jsx("div",{className:"ml-stats-controls",children:m.map(e=>t.jsx("button",{className:`ml-stats-range-btn ${l===e.value?"active":""}`,onClick:()=>r(e.value),children:e.label},e.value))}),t.jsx("div",{className:"ml-stats-grid",children:c.map(e=>t.jsx("div",{className:"ml-stats-panel",children:t.jsx("iframe",{src:n(e.id),width:"100%",height:"100%",frameBorder:"0",title:e.title})},e.id))})]})};export{v as StatsWindow};
|
||||
import{r as o,j as t,D as d}from"./index-BiGwMY76.js";import"./react-yfL0ty4i.js";const c=[{title:"Kills per Hour",id:1},{title:"Memory (MB)",id:2},{title:"CPU (%)",id:3},{title:"Mem Handles",id:4}],m=[{label:"1H",value:"now-1h"},{label:"6H",value:"now-6h"},{label:"24H",value:"now-24h"},{label:"7D",value:"now-7d"}],v=({id:s,charName:a,zIndex:i})=>{const[l,r]=o.useState("now-24h"),n=e=>`/grafana/d-solo/dereth-tracker/dereth-tracker-dashboard?panelId=${e}&var-character=${encodeURIComponent(a)}&from=${l}&to=now&theme=light`;return t.jsxs(d,{id:s,title:`Stats: ${a}`,zIndex:i,width:750,height:480,children:[t.jsx("div",{className:"ml-stats-controls",children:m.map(e=>t.jsx("button",{className:`ml-stats-range-btn ${l===e.value?"active":""}`,onClick:()=>r(e.value),children:e.label},e.value))}),t.jsx("div",{className:"ml-stats-grid",children:c.map(e=>t.jsx("div",{className:"ml-stats-panel",children:t.jsx("iframe",{src:n(e.id),width:"100%",height:"100%",frameBorder:"0",title:e.title})},e.id))})]})};export{v as StatsWindow};
|
||||
|
|
@ -1 +1 @@
|
|||
import{r as n,j as t,D as x,a as m}from"./index-D3oSGfL8.js";import"./react-DlyoauG8.js";const v=({id:o,zIndex:c})=>{const[a,d]=n.useState([]);n.useEffect(()=>{const e=async()=>{try{const s=await m("/vital-sharing/peers");d(s.peers??[])}catch{}};e();const l=setInterval(e,5e3);return()=>clearInterval(l)},[]);const h=(e,l)=>l>0?Math.min(100,e/l*100):0;return t.jsx(x,{id:o,title:"Vital Sharing Network",zIndex:c,width:520,height:450,children:t.jsx("div",{style:{flex:1,overflowY:"auto",padding:6,fontSize:"0.75rem"},children:a.length===0?t.jsx("div",{style:{padding:16,color:"#666",textAlign:"center"},children:"No vital-sharing peers connected"}):a.map(e=>{var l,s,r;return t.jsxs("div",{style:{padding:"6px 8px",marginBottom:4,background:"#1f1f1f",borderRadius:3,border:"1px solid #333"},children:[t.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6,marginBottom:3},children:[t.jsx("span",{style:{color:e.plugin_connected?"#4c4":"#a33",fontSize:"0.8rem"},children:"●"}),t.jsx("strong",{style:{flex:1},children:e.character_name}),e.subscribed&&t.jsx("span",{style:{color:"#6bf",fontSize:"0.65rem"},children:"[subscribed]"})]}),t.jsxs("div",{style:{color:"#666",fontSize:"0.68rem",marginBottom:3},children:["tags: ",((l=e.tags)==null?void 0:l.join(", "))||"none"]}),e.vitals&&e.vitals.max_health>0&&t.jsx("div",{style:{display:"flex",flexDirection:"column",gap:2},children:[{label:"HP",cur:e.vitals.current_health,max:e.vitals.max_health,bg:"#330000",fill:"#c44"},{label:"STA",cur:e.vitals.current_stamina,max:e.vitals.max_stamina,bg:"#331a00",fill:"#ca0"},{label:"MANA",cur:e.vitals.current_mana,max:e.vitals.max_mana,bg:"#001433",fill:"#48f"}].map(i=>t.jsxs("div",{style:{display:"flex",alignItems:"center",gap:4},children:[t.jsx("span",{style:{width:32,color:"#888",fontSize:"0.65rem"},children:i.label}),t.jsx("div",{style:{flex:1,height:6,background:i.bg,borderRadius:3,overflow:"hidden"},children:t.jsx("div",{style:{width:`${h(i.cur,i.max)}%`,height:"100%",background:i.fill,borderRadius:3}})}),t.jsxs("span",{style:{width:60,textAlign:"right",fontSize:"0.65rem",color:"#888"},children:[i.cur,"/",i.max]})]},i.label))}),e.position&&t.jsxs("div",{style:{color:"#555",fontSize:"0.65rem",marginTop:2},children:[(s=e.position.ns)==null?void 0:s.toFixed(1),"N, ",(r=e.position.ew)==null?void 0:r.toFixed(1),"E"]})]},e.character_name)})})})};export{v as VitalSharingWindow};
|
||||
import{r as n,j as t,D as x,a as m}from"./index-BiGwMY76.js";import"./react-yfL0ty4i.js";const v=({id:o,zIndex:c})=>{const[a,d]=n.useState([]);n.useEffect(()=>{const e=async()=>{try{const s=await m("/vital-sharing/peers");d(s.peers??[])}catch{}};e();const l=setInterval(e,5e3);return()=>clearInterval(l)},[]);const h=(e,l)=>l>0?Math.min(100,e/l*100):0;return t.jsx(x,{id:o,title:"Vital Sharing Network",zIndex:c,width:520,height:450,children:t.jsx("div",{style:{flex:1,overflowY:"auto",padding:6,fontSize:"0.75rem"},children:a.length===0?t.jsx("div",{style:{padding:16,color:"#666",textAlign:"center"},children:"No vital-sharing peers connected"}):a.map(e=>{var l,s,r;return t.jsxs("div",{style:{padding:"6px 8px",marginBottom:4,background:"#1f1f1f",borderRadius:3,border:"1px solid #333"},children:[t.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6,marginBottom:3},children:[t.jsx("span",{style:{color:e.plugin_connected?"#4c4":"#a33",fontSize:"0.8rem"},children:"●"}),t.jsx("strong",{style:{flex:1},children:e.character_name}),e.subscribed&&t.jsx("span",{style:{color:"#6bf",fontSize:"0.65rem"},children:"[subscribed]"})]}),t.jsxs("div",{style:{color:"#666",fontSize:"0.68rem",marginBottom:3},children:["tags: ",((l=e.tags)==null?void 0:l.join(", "))||"none"]}),e.vitals&&e.vitals.max_health>0&&t.jsx("div",{style:{display:"flex",flexDirection:"column",gap:2},children:[{label:"HP",cur:e.vitals.current_health,max:e.vitals.max_health,bg:"#330000",fill:"#c44"},{label:"STA",cur:e.vitals.current_stamina,max:e.vitals.max_stamina,bg:"#331a00",fill:"#ca0"},{label:"MANA",cur:e.vitals.current_mana,max:e.vitals.max_mana,bg:"#001433",fill:"#48f"}].map(i=>t.jsxs("div",{style:{display:"flex",alignItems:"center",gap:4},children:[t.jsx("span",{style:{width:32,color:"#888",fontSize:"0.65rem"},children:i.label}),t.jsx("div",{style:{flex:1,height:6,background:i.bg,borderRadius:3,overflow:"hidden"},children:t.jsx("div",{style:{width:`${h(i.cur,i.max)}%`,height:"100%",background:i.fill,borderRadius:3}})}),t.jsxs("span",{style:{width:60,textAlign:"right",fontSize:"0.65rem",color:"#888"},children:[i.cur,"/",i.max]})]},i.label))}),e.position&&t.jsxs("div",{style:{color:"#555",fontSize:"0.65rem",marginTop:2},children:[(s=e.position.ns)==null?void 0:s.toFixed(1),"N, ",(r=e.position.ew)==null?void 0:r.toFixed(1),"E"]})]},e.character_name)})})})};export{v as VitalSharingWindow};
|
||||
File diff suppressed because one or more lines are too long
34
static/assets/index-BiGwMY76.js
Normal file
34
static/assets/index-BiGwMY76.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -8,9 +8,9 @@
|
|||
<link rel="preload" as="image" href="/dereth.png" />
|
||||
<link rel="preload" as="image" href="/icons/0600127E.png" />
|
||||
<link rel="preload" as="fetch" href="/dungeon_tiles.json" crossorigin="anonymous" />
|
||||
<script type="module" crossorigin src="/assets/index-D3oSGfL8.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/react-DlyoauG8.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CjQam_eg.css">
|
||||
<script type="module" crossorigin src="/assets/index-BiGwMY76.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/react-yfL0ty4i.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BF438D-w.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
1
static/v2/assets/CharacterWindow-DX4iez-v.js
Normal file
1
static/v2/assets/CharacterWindow-DX4iez-v.js
Normal file
File diff suppressed because one or more lines are too long
1
static/v2/assets/CombatPickerWindow-DMOcilt1.js
Normal file
1
static/v2/assets/CombatPickerWindow-DMOcilt1.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{u as c,j as r,D as d}from"./index-BiGwMY76.js";import"./react-yfL0ty4i.js";const p=({id:n,zIndex:i,characters:a})=>{const{openWindow:s}=c(),e=Array.from(a.keys()).sort();return r.jsx(d,{id:n,title:"Combat Stats — Select Character",zIndex:i,width:300,height:400,children:r.jsx("div",{style:{flex:1,overflowY:"auto",padding:6},children:e.length===0?r.jsx("div",{style:{padding:12,color:"#666",textAlign:"center",fontSize:"0.8rem"},children:"No characters online"}):e.map(o=>r.jsx("div",{style:{padding:"5px 8px",cursor:"pointer",borderBottom:"1px solid #222",color:"#ccc",fontSize:"0.82rem"},onMouseEnter:t=>t.currentTarget.style.background="#2a2a2a",onMouseLeave:t=>t.currentTarget.style.background="",onClick:()=>s(`combat-${o}`,`Combat: ${o}`,o),children:o},o))})})};export{p as CombatPickerWindow};
|
||||
1
static/v2/assets/CombatStatsWindow-BOXaCmme.js
Normal file
1
static/v2/assets/CombatStatsWindow-BOXaCmme.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
static/v2/assets/InventoryWindow-C9mxhtJV.js
Normal file
1
static/v2/assets/InventoryWindow-C9mxhtJV.js
Normal file
File diff suppressed because one or more lines are too long
1
static/v2/assets/IssuesWindow-BspsADhQ.js
Normal file
1
static/v2/assets/IssuesWindow-BspsADhQ.js
Normal file
File diff suppressed because one or more lines are too long
1
static/v2/assets/PlayerDashboardWindow-BfZp59uQ.js
Normal file
1
static/v2/assets/PlayerDashboardWindow-BfZp59uQ.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
import{r as d,j as t,D as y}from"./index-D3oSGfL8.js";import"./react-DlyoauG8.js";const b=({id:m,zIndex:u,characters:h})=>{const[o,k]=d.useState("kph"),[c,x]=d.useState(!1),p=d.useMemo(()=>{const e=Array.from(h.values()).filter(s=>s.telemetry).map(s=>{var l,g;const r=s.telemetry;return{name:s.name,kills:r.kills??0,kph:parseInt(r.kills_per_hour)||0,totalKills:r.total_kills??0,rares:r.total_rares??0,sessionRares:r.session_rares??0,deaths:parseInt(r.deaths)||0,totalDeaths:parseInt(r.total_deaths)||0,uptime:((l=r.onlinetime)==null?void 0:l.replace(/^00\./,""))??"",state:r.vt_state??"idle",tapers:parseInt(r.prismatic_taper_count)||0,hp:((g=s.vitals)==null?void 0:g.health_percentage)??0}});return e.sort((s,r)=>{let l=0;switch(o){case"name":l=s.name.localeCompare(r.name);break;case"kills":l=s.kills-r.kills;break;case"kph":l=s.kph-r.kph;break;case"rares":l=s.rares-r.rares;break;case"deaths":l=s.totalDeaths-r.totalDeaths;break;case"uptime":l=s.uptime.localeCompare(r.uptime);break;case"state":l=s.state.localeCompare(r.state);break}return c?l:-l}),e},[h,o,c]),a=e=>{o===e?x(!c):(k(e),x(!1))},i=e=>({padding:"4px 6px",cursor:"pointer",userSelect:"none",color:o===e?"#6af":"#888",fontSize:"0.65rem",fontWeight:600,whiteSpace:"nowrap",borderBottom:"1px solid #444"}),n=e=>o===e?c?" ▲":" ▼":"";return t.jsx(y,{id:m,title:"Player Dashboard",zIndex:u,width:850,height:500,children:t.jsxs("div",{style:{flex:1,overflow:"auto",fontSize:"0.73rem"},children:[t.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[t.jsx("thead",{children:t.jsxs("tr",{style:{position:"sticky",top:0,background:"#1a1a1a",zIndex:1},children:[t.jsxs("th",{style:{...i("name"),textAlign:"left"},onClick:()=>a("name"),children:["Character",n("name")]}),t.jsxs("th",{style:{...i("state"),textAlign:"center"},onClick:()=>a("state"),children:["State",n("state")]}),t.jsxs("th",{style:{...i("kph"),textAlign:"right"},onClick:()=>a("kph"),children:["KPH",n("kph")]}),t.jsxs("th",{style:{...i("kills"),textAlign:"right"},onClick:()=>a("kills"),children:["Session",n("kills")]}),t.jsx("th",{style:{textAlign:"right",padding:"4px 6px",color:"#888",fontSize:"0.65rem",fontWeight:600,borderBottom:"1px solid #444"},children:"Total"}),t.jsxs("th",{style:{...i("rares"),textAlign:"right"},onClick:()=>a("rares"),children:["Rares",n("rares")]}),t.jsxs("th",{style:{...i("deaths"),textAlign:"right"},onClick:()=>a("deaths"),children:["Deaths",n("deaths")]}),t.jsxs("th",{style:{...i("uptime"),textAlign:"right"},onClick:()=>a("uptime"),children:["Uptime",n("uptime")]}),t.jsx("th",{style:{textAlign:"right",padding:"4px 6px",color:"#888",fontSize:"0.65rem",fontWeight:600,borderBottom:"1px solid #444"},children:"HP%"}),t.jsx("th",{style:{textAlign:"right",padding:"4px 6px",color:"#888",fontSize:"0.65rem",fontWeight:600,borderBottom:"1px solid #444"},children:"Tapers"})]})}),t.jsx("tbody",{children:p.map(e=>{const s=e.state.toLowerCase(),r=s==="combat"||s==="hunt";return t.jsxs("tr",{style:{borderBottom:"1px solid #1a1a1a"},children:[t.jsx("td",{style:{padding:"3px 6px",color:"#ccc",fontWeight:500,maxWidth:180,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:e.name}),t.jsx("td",{style:{textAlign:"center",padding:"3px 6px"},children:t.jsx("span",{style:{fontSize:"0.6rem",padding:"1px 6px",borderRadius:3,background:r?"rgba(68,204,68,0.15)":s==="idle"||s==="default"?"rgba(100,100,100,0.2)":"rgba(204,68,68,0.15)",color:r?"#4c4":s==="idle"||s==="default"?"#888":"#c44"},children:e.state})}),t.jsx("td",{style:{textAlign:"right",padding:"3px 6px",color:"#4c4",fontVariantNumeric:"tabular-nums"},children:e.kph.toLocaleString()}),t.jsx("td",{style:{textAlign:"right",padding:"3px 6px",color:"#ccc",fontVariantNumeric:"tabular-nums"},children:e.kills.toLocaleString()}),t.jsx("td",{style:{textAlign:"right",padding:"3px 6px",color:"#888",fontVariantNumeric:"tabular-nums"},children:e.totalKills.toLocaleString()}),t.jsxs("td",{style:{textAlign:"right",padding:"3px 6px",color:"#fc0",fontVariantNumeric:"tabular-nums"},children:[e.rares,e.sessionRares>0?` (${e.sessionRares})`:""]}),t.jsx("td",{style:{textAlign:"right",padding:"3px 6px",color:e.totalDeaths>0?"#c66":"#555",fontVariantNumeric:"tabular-nums"},children:e.totalDeaths}),t.jsx("td",{style:{textAlign:"right",padding:"3px 6px",color:"#888",fontVariantNumeric:"tabular-nums"},children:e.uptime}),t.jsxs("td",{style:{textAlign:"right",padding:"3px 6px",fontVariantNumeric:"tabular-nums",color:e.hp>80?"#4c4":e.hp>40?"#ca0":"#c44"},children:[e.hp.toFixed(0),"%"]}),t.jsx("td",{style:{textAlign:"right",padding:"3px 6px",color:"#888",fontVariantNumeric:"tabular-nums"},children:e.tapers.toLocaleString()})]},e.name)})})]}),p.length===0&&t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"No characters online"})]})})};export{b as PlayerDashboardWindow};
|
||||
1
static/v2/assets/QuestStatusWindow-C_sQPwJQ.js
Normal file
1
static/v2/assets/QuestStatusWindow-C_sQPwJQ.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{r as c,j as t,D as u,a as f}from"./index-BiGwMY76.js";import"./react-yfL0ty4i.js";const j=({id:p,zIndex:x})=>{const[s,h]=c.useState(null);c.useEffect(()=>{const e=async()=>{try{h(await f("/quest-status"))}catch{}};e();const o=setInterval(e,3e4);return()=>clearInterval(o)},[]);const i=s?Object.keys(s.quest_data).sort():[],l=new Set;if(s)for(const e of Object.values(s.quest_data))for(const o of Object.keys(e))l.add(o);const n=Array.from(l).sort();return t.jsx(u,{id:p,title:"Quest Status",zIndex:x,width:780,height:500,children:t.jsx("div",{style:{flex:1,overflow:"auto",fontSize:"0.72rem"},children:s?i.length===0?t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"No quest data available"}):t.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[t.jsx("thead",{children:t.jsxs("tr",{style:{position:"sticky",top:0,background:"#1a1a1a",zIndex:1},children:[t.jsx("th",{style:{textAlign:"left",padding:"4px 8px",borderBottom:"1px solid #444",color:"#888",fontSize:"0.65rem",fontWeight:600,minWidth:140},children:"Character"}),n.map(e=>t.jsx("th",{style:{textAlign:"center",padding:"4px 6px",borderBottom:"1px solid #444",color:"#888",fontSize:"0.6rem",fontWeight:600,maxWidth:120,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},title:e,children:e.replace(" Timer","").replace(" Pickup","")},e))]})}),t.jsx("tbody",{children:i.map(e=>{const o=s.quest_data[e]||{};return t.jsxs("tr",{style:{borderBottom:"1px solid #222"},children:[t.jsx("td",{style:{padding:"3px 8px",color:"#ccc",fontWeight:500,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",maxWidth:160},children:e}),n.map(d=>{const r=o[d],a=r==="READY";return t.jsx("td",{style:{textAlign:"center",padding:"3px 6px",color:a?"#4c4":r?"#ca0":"#333",fontWeight:a?600:400,fontSize:a?"0.7rem":"0.68rem"},children:r||"—"},d)})]},e)})})]}):t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"Loading quest data..."})})})};export{j as QuestStatusWindow};
|
||||
1
static/v2/assets/RadarWindow-BwlMNi8v.js
Normal file
1
static/v2/assets/RadarWindow-BwlMNi8v.js
Normal file
File diff suppressed because one or more lines are too long
1
static/v2/assets/StatsWindow-C5XFDqVx.js
Normal file
1
static/v2/assets/StatsWindow-C5XFDqVx.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{r as o,j as t,D as d}from"./index-BiGwMY76.js";import"./react-yfL0ty4i.js";const c=[{title:"Kills per Hour",id:1},{title:"Memory (MB)",id:2},{title:"CPU (%)",id:3},{title:"Mem Handles",id:4}],m=[{label:"1H",value:"now-1h"},{label:"6H",value:"now-6h"},{label:"24H",value:"now-24h"},{label:"7D",value:"now-7d"}],v=({id:s,charName:a,zIndex:i})=>{const[l,r]=o.useState("now-24h"),n=e=>`/grafana/d-solo/dereth-tracker/dereth-tracker-dashboard?panelId=${e}&var-character=${encodeURIComponent(a)}&from=${l}&to=now&theme=light`;return t.jsxs(d,{id:s,title:`Stats: ${a}`,zIndex:i,width:750,height:480,children:[t.jsx("div",{className:"ml-stats-controls",children:m.map(e=>t.jsx("button",{className:`ml-stats-range-btn ${l===e.value?"active":""}`,onClick:()=>r(e.value),children:e.label},e.value))}),t.jsx("div",{className:"ml-stats-grid",children:c.map(e=>t.jsx("div",{className:"ml-stats-panel",children:t.jsx("iframe",{src:n(e.id),width:"100%",height:"100%",frameBorder:"0",title:e.title})},e.id))})]})};export{v as StatsWindow};
|
||||
1
static/v2/assets/VitalSharingWindow-CSqjZNf-.js
Normal file
1
static/v2/assets/VitalSharingWindow-CSqjZNf-.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{r as n,j as t,D as x,a as m}from"./index-BiGwMY76.js";import"./react-yfL0ty4i.js";const v=({id:o,zIndex:c})=>{const[a,d]=n.useState([]);n.useEffect(()=>{const e=async()=>{try{const s=await m("/vital-sharing/peers");d(s.peers??[])}catch{}};e();const l=setInterval(e,5e3);return()=>clearInterval(l)},[]);const h=(e,l)=>l>0?Math.min(100,e/l*100):0;return t.jsx(x,{id:o,title:"Vital Sharing Network",zIndex:c,width:520,height:450,children:t.jsx("div",{style:{flex:1,overflowY:"auto",padding:6,fontSize:"0.75rem"},children:a.length===0?t.jsx("div",{style:{padding:16,color:"#666",textAlign:"center"},children:"No vital-sharing peers connected"}):a.map(e=>{var l,s,r;return t.jsxs("div",{style:{padding:"6px 8px",marginBottom:4,background:"#1f1f1f",borderRadius:3,border:"1px solid #333"},children:[t.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6,marginBottom:3},children:[t.jsx("span",{style:{color:e.plugin_connected?"#4c4":"#a33",fontSize:"0.8rem"},children:"●"}),t.jsx("strong",{style:{flex:1},children:e.character_name}),e.subscribed&&t.jsx("span",{style:{color:"#6bf",fontSize:"0.65rem"},children:"[subscribed]"})]}),t.jsxs("div",{style:{color:"#666",fontSize:"0.68rem",marginBottom:3},children:["tags: ",((l=e.tags)==null?void 0:l.join(", "))||"none"]}),e.vitals&&e.vitals.max_health>0&&t.jsx("div",{style:{display:"flex",flexDirection:"column",gap:2},children:[{label:"HP",cur:e.vitals.current_health,max:e.vitals.max_health,bg:"#330000",fill:"#c44"},{label:"STA",cur:e.vitals.current_stamina,max:e.vitals.max_stamina,bg:"#331a00",fill:"#ca0"},{label:"MANA",cur:e.vitals.current_mana,max:e.vitals.max_mana,bg:"#001433",fill:"#48f"}].map(i=>t.jsxs("div",{style:{display:"flex",alignItems:"center",gap:4},children:[t.jsx("span",{style:{width:32,color:"#888",fontSize:"0.65rem"},children:i.label}),t.jsx("div",{style:{flex:1,height:6,background:i.bg,borderRadius:3,overflow:"hidden"},children:t.jsx("div",{style:{width:`${h(i.cur,i.max)}%`,height:"100%",background:i.fill,borderRadius:3}})}),t.jsxs("span",{style:{width:60,textAlign:"right",fontSize:"0.65rem",color:"#888"},children:[i.cur,"/",i.max]})]},i.label))}),e.position&&t.jsxs("div",{style:{color:"#555",fontSize:"0.65rem",marginTop:2},children:[(s=e.position.ns)==null?void 0:s.toFixed(1),"N, ",(r=e.position.ew)==null?void 0:r.toFixed(1),"E"]})]},e.character_name)})})})};export{v as VitalSharingWindow};
|
||||
1
static/v2/assets/index-BF438D-w.css
Normal file
1
static/v2/assets/index-BF438D-w.css
Normal file
File diff suppressed because one or more lines are too long
34
static/v2/assets/index-BiGwMY76.js
Normal file
34
static/v2/assets/index-BiGwMY76.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
17
static/v2/assets/react-yfL0ty4i.js
vendored
Normal file
17
static/v2/assets/react-yfL0ty4i.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -8,9 +8,9 @@
|
|||
<link rel="preload" as="image" href="/dereth.png" />
|
||||
<link rel="preload" as="image" href="/icons/0600127E.png" />
|
||||
<link rel="preload" as="fetch" href="/dungeon_tiles.json" crossorigin="anonymous" />
|
||||
<script type="module" crossorigin src="/assets/index-D3oSGfL8.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/react-DlyoauG8.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CjQam_eg.css">
|
||||
<script type="module" crossorigin src="/assets/index-BiGwMY76.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/react-yfL0ty4i.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BF438D-w.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue