feat(panel): GameProfile registry + real-data dashboard (remove all mock/fake data)

DashboardView now renders the REAL server from useServerStore (connection/config + live WebSocket stats) + real 24h history from /analytics/timeseries, with honest EmptyStates ('install the companion agent') when there is no data. DELETED _dashboardMock.ts (the fake 8-server fleet/feed/wipes). PlayersChart hardened: removed the DEFAULT_SERIES fallback, renders an 'awaiting telemetry' empty state instead of a fabricated curve. New gameProfiles.ts: real per-game capability/terminology/stat registry (rust/conan/soulmask/dune; dune managementModel=docker-compose), ready to wire when the backend gains a per-license game field. No fake data. Build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-06-11 04:52:12 -04:00
parent be57d2839a
commit f2b09b281a
4 changed files with 594 additions and 478 deletions

View File

@@ -1,10 +1,15 @@
<script setup lang="ts">
/**
* PlayersChart — themed ECharts area chart of players online.
* Reads the live design tokens (--accent etc.) from CSS so it matches the
* active theme/game, and re-renders when data-game / data-theme flip on <html>.
*
* Requires real `data` — there is NO fallback series. When `data` is absent
* or empty, an "awaiting telemetry" placeholder is shown instead of the chart.
* This is intentional: fabricated curves mislead operators.
*
* Reads live design tokens (--accent etc.) from CSS so it matches the active
* theme/game, and re-renders when data-game / data-theme flip on <html>.
*/
import { onMounted, onBeforeUnmount, useTemplateRef } from 'vue'
import { computed, onMounted, onBeforeUnmount, useTemplateRef } from 'vue'
import * as echarts from 'echarts'
const props = withDefaults(
@@ -12,29 +17,26 @@ const props = withDefaults(
{ height: 200, max: 200 },
)
const hasData = computed(() => Array.isArray(props.data) && props.data.length > 0)
const el = useTemplateRef<HTMLDivElement>('el')
let chart: echarts.ECharts | null = null
let ro: ResizeObserver | null = null
let mo: MutationObserver | null = null
const DEFAULT_SERIES = [
60, 52, 44, 38, 33, 30, 34, 46, 62, 78, 92, 104,
118, 126, 131, 138, 142, 151, 168, 182, 176, 150, 112, 84,
]
function cssVar(name: string, node?: HTMLElement): string {
return getComputedStyle(node || document.documentElement).getPropertyValue(name).trim()
}
function render(): void {
if (!chart || !el.value) return
if (!chart || !el.value || !hasData.value) return
const node = el.value
const accent = cssVar('--accent', node) || '#f26622'
const grid = cssVar('--border-subtle', node) || 'rgba(255,255,255,0.06)'
const text = cssVar('--text-tertiary', node) || '#767d89'
const mono = 'JetBrains Mono, monospace'
const hours = Array.from({ length: 24 }, (_, i) => `${String(i).padStart(2, '0')}:00`)
const series = props.data ?? DEFAULT_SERIES
const series = props.data as number[]
chart.setOption({
animationDuration: 700,
@@ -77,6 +79,7 @@ function render(): void {
onMounted(() => {
if (!el.value) return
if (!hasData.value) return // empty-state slot renders instead
chart = echarts.init(el.value, undefined, { renderer: 'canvas' })
render()
ro = new ResizeObserver(() => chart?.resize())
@@ -94,5 +97,33 @@ onBeforeUnmount(() => {
</script>
<template>
<div ref="el" :style="{ width: '100%', height: height + 'px' }" />
<!-- Real data: render the ECharts canvas -->
<div v-if="hasData" ref="el" :style="{ width: '100%', height: height + 'px' }" />
<!-- No data: honest empty state never show a fabricated curve -->
<div
v-else
class="pc-empty"
:style="{ height: height + 'px' }"
>
<svg class="pc-empty__icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
</svg>
<span class="pc-empty__label">Awaiting telemetry</span>
<span class="pc-empty__sub">Player data will appear once the server connects and reports stats</span>
</div>
</template>
<style scoped>
.pc-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
width: 100%;
color: var(--text-muted);
}
.pc-empty__icon { margin-bottom: 4px; opacity: 0.5; }
.pc-empty__label { font-size: var(--text-sm); font-weight: 500; color: var(--text-tertiary); }
.pc-empty__sub { font-size: var(--text-xs); color: var(--text-muted); max-width: 280px; text-align: center; line-height: 1.5; }
</style>