improvement on gm windows, ai
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, h, ref } from "vue";
|
||||
import {
|
||||
useVueTable,
|
||||
getCoreRowModel,
|
||||
@@ -18,6 +18,7 @@ import { useViewerSelectionStore } from "../../ui/stores/viewerSelection";
|
||||
import type { ShipSnapshot } from "../../contractsShips";
|
||||
import type { StationSnapshot } from "../../contractsInfrastructure";
|
||||
import type { FactionSnapshot } from "../../contractsFactions";
|
||||
import type { MarketOrderSnapshot } from "../../contractsEconomy";
|
||||
|
||||
// ── Column ordering composable ─────────────────────────────────────────────
|
||||
|
||||
@@ -85,6 +86,20 @@ const factionMap = computed(() =>
|
||||
new Map(gmStore.factions.map((f) => [f.id, f.label])),
|
||||
);
|
||||
|
||||
const factionColorMap = computed(() =>
|
||||
new Map(gmStore.factions.map((f) => [f.id, f.color])),
|
||||
);
|
||||
|
||||
function renderColorCell(color: string | null | undefined) {
|
||||
const resolved = color && color !== "—" ? color : "#6b7280";
|
||||
return h("div", { class: "flex items-center justify-center" }, [
|
||||
h("span", {
|
||||
class: "inline-block h-3 w-3 rounded-full border border-white/20",
|
||||
style: { backgroundColor: resolved },
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
function titleCaseToken(value: string | null | undefined) {
|
||||
if (!value) return "—";
|
||||
return value
|
||||
@@ -106,6 +121,13 @@ function compactRate(value: number | null | undefined) {
|
||||
return `${sign}${value.toFixed(2)}/s`;
|
||||
}
|
||||
|
||||
function formatCargoAmount(value: number | null | undefined) {
|
||||
if (value == null || Number.isNaN(value)) return "—";
|
||||
const rounded = Math.round(value);
|
||||
if (Math.abs(value - rounded) < 0.005) return String(rounded);
|
||||
return value.toFixed(2).replace(/\.?0+$/, "");
|
||||
}
|
||||
|
||||
function getLeadObjective(faction: FactionSnapshot) {
|
||||
return [...(faction.objectives ?? [])]
|
||||
.sort((left, right) => right.priority - left.priority)
|
||||
@@ -184,6 +206,7 @@ type ShipRow = {
|
||||
id: string;
|
||||
label: string;
|
||||
class: string;
|
||||
factionColor: string;
|
||||
faction: string;
|
||||
system: string;
|
||||
state: string;
|
||||
@@ -201,6 +224,7 @@ const shipRows = computed<ShipRow[]>(() =>
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
class: s.class,
|
||||
factionColor: factionColorMap.value.get(s.factionId) ?? "—",
|
||||
faction: factionMap.value.get(s.factionId) ?? s.factionId,
|
||||
system: s.systemId,
|
||||
state: titleCaseToken(s.state),
|
||||
@@ -218,6 +242,11 @@ const shipColumnHelper = createColumnHelper<ShipRow>();
|
||||
const shipColumns = [
|
||||
shipColumnHelper.accessor("label", { header: "Name" }),
|
||||
shipColumnHelper.accessor("class", { header: "Class" }),
|
||||
shipColumnHelper.accessor("factionColor", {
|
||||
id: "factionColor",
|
||||
header: "Color",
|
||||
cell: (info) => renderColorCell(info.getValue()),
|
||||
}),
|
||||
shipColumnHelper.accessor("faction", { header: "Faction" }),
|
||||
shipColumnHelper.accessor("system", { header: "System" }),
|
||||
shipColumnHelper.accessor("state", { header: "Ship State" }),
|
||||
@@ -226,13 +255,16 @@ const shipColumns = [
|
||||
shipColumnHelper.accessor("phase", { header: "Phase" }),
|
||||
shipColumnHelper.accessor("action", { header: "Current Action" }),
|
||||
shipColumnHelper.accessor("task", { header: "Task" }),
|
||||
shipColumnHelper.accessor("cargo", { header: "Cargo" }),
|
||||
shipColumnHelper.accessor("cargo", {
|
||||
header: "Cargo",
|
||||
cell: (info) => formatCargoAmount(info.getValue()),
|
||||
}),
|
||||
shipColumnHelper.accessor("health", { header: "HP" }),
|
||||
];
|
||||
|
||||
const shipFilter = ref("");
|
||||
const shipSorting = ref<SortingState>([]);
|
||||
const shipOrder = useColumnOrder(["label", "class", "faction", "system", "state", "objective", "behavior", "phase", "action", "task", "cargo", "health"]);
|
||||
const shipOrder = useColumnOrder(["label", "class", "factionColor", "faction", "system", "state", "objective", "behavior", "phase", "action", "task", "cargo", "health"]);
|
||||
|
||||
const shipTable = useVueTable({
|
||||
get data() { return shipRows.value; },
|
||||
@@ -259,22 +291,29 @@ type StationRow = {
|
||||
label: string;
|
||||
category: string;
|
||||
objective: string;
|
||||
factionColor: string;
|
||||
faction: string;
|
||||
system: string;
|
||||
process: string;
|
||||
workforce: string;
|
||||
docked: string;
|
||||
orders: number;
|
||||
orderDetails: MarketOrderSnapshot[];
|
||||
cargo: number;
|
||||
modules: number;
|
||||
};
|
||||
|
||||
const marketOrderMap = computed(() =>
|
||||
new Map(gmStore.marketOrders.map((o) => [o.id, o])),
|
||||
);
|
||||
|
||||
const stationRows = computed<StationRow[]>(() =>
|
||||
gmStore.stations.map((s) => ({
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
category: s.category,
|
||||
objective: titleCaseToken(s.objective),
|
||||
factionColor: factionColorMap.value.get(s.factionId) ?? "—",
|
||||
faction: factionMap.value.get(s.factionId) ?? s.factionId,
|
||||
system: s.systemId,
|
||||
process: s.currentProcesses.length > 0
|
||||
@@ -283,6 +322,10 @@ const stationRows = computed<StationRow[]>(() =>
|
||||
workforce: `${Math.round(s.population)} / ${Math.round(s.populationCapacity)} · ${Math.round(s.workforceEffectiveRatio * 100)}%`,
|
||||
docked: `${s.dockedShips} / ${s.dockingPads}`,
|
||||
orders: s.marketOrderIds.length,
|
||||
orderDetails: s.marketOrderIds.flatMap((id) => {
|
||||
const order = marketOrderMap.value.get(id);
|
||||
return order ? [order] : [];
|
||||
}),
|
||||
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
|
||||
modules: s.installedModules.length,
|
||||
})),
|
||||
@@ -293,19 +336,27 @@ const stationColumns = [
|
||||
stationColumnHelper.accessor("label", { header: "Name" }),
|
||||
stationColumnHelper.accessor("category", { header: "Category" }),
|
||||
stationColumnHelper.accessor("objective", { header: "Objective" }),
|
||||
stationColumnHelper.accessor("factionColor", {
|
||||
id: "factionColor",
|
||||
header: "Color",
|
||||
cell: (info) => renderColorCell(info.getValue()),
|
||||
}),
|
||||
stationColumnHelper.accessor("faction", { header: "Faction" }),
|
||||
stationColumnHelper.accessor("system", { header: "System" }),
|
||||
stationColumnHelper.accessor("process", { header: "Production" }),
|
||||
stationColumnHelper.accessor("workforce", { header: "Workforce" }),
|
||||
stationColumnHelper.accessor("docked", { header: "Docked" }),
|
||||
stationColumnHelper.accessor("orders", { header: "Orders" }),
|
||||
stationColumnHelper.accessor("cargo", { header: "Cargo" }),
|
||||
stationColumnHelper.accessor("cargo", {
|
||||
header: "Cargo",
|
||||
cell: (info) => formatCargoAmount(info.getValue()),
|
||||
}),
|
||||
stationColumnHelper.accessor("modules", { header: "Modules" }),
|
||||
];
|
||||
|
||||
const stationFilter = ref("");
|
||||
const stationSorting = ref<SortingState>([]);
|
||||
const stationOrder = useColumnOrder(["label", "category", "objective", "faction", "system", "process", "workforce", "docked", "orders", "cargo", "modules"]);
|
||||
const stationOrder = useColumnOrder(["label", "category", "objective", "factionColor", "faction", "system", "process", "workforce", "docked", "orders", "cargo", "modules"]);
|
||||
|
||||
const stationTable = useVueTable({
|
||||
get data() { return stationRows.value; },
|
||||
@@ -330,6 +381,7 @@ const stationTable = useVueTable({
|
||||
type FactionRow = {
|
||||
id: string;
|
||||
label: string;
|
||||
color: string;
|
||||
planCycle: number;
|
||||
priority: string;
|
||||
strategicState: string;
|
||||
@@ -353,6 +405,7 @@ const factionRows = computed<FactionRow[]>(() =>
|
||||
return {
|
||||
id: f.id,
|
||||
label: f.label,
|
||||
color: f.color,
|
||||
planCycle: blackboard?.planCycle ?? 0,
|
||||
priority: describeFactionPriority(f),
|
||||
strategicState: describeFactionStrategicState(f),
|
||||
@@ -374,6 +427,10 @@ const factionRows = computed<FactionRow[]>(() =>
|
||||
const factionColumnHelper = createColumnHelper<FactionRow>();
|
||||
const factionColumns = [
|
||||
factionColumnHelper.accessor("label", { header: "Faction" }),
|
||||
factionColumnHelper.accessor("color", {
|
||||
header: "Color",
|
||||
cell: (info) => renderColorCell(info.getValue()),
|
||||
}),
|
||||
factionColumnHelper.accessor("planCycle", { header: "Cycle" }),
|
||||
factionColumnHelper.accessor("priority", { header: "Top Priority" }),
|
||||
factionColumnHelper.accessor("strategicState", { header: "Objective" }),
|
||||
@@ -392,7 +449,7 @@ const factionColumns = [
|
||||
|
||||
const factionFilter = ref("");
|
||||
const factionSorting = ref<SortingState>([]);
|
||||
const factionOrder = useColumnOrder(["label", "planCycle", "priority", "strategicState", "leadStep", "leadTask", "warReadiness", "economy", "threat", "fleets", "systems", "credits", "population", "shipsBuilt", "shipsLost"]);
|
||||
const factionOrder = useColumnOrder(["label", "color", "planCycle", "priority", "strategicState", "leadStep", "leadTask", "warReadiness", "economy", "threat", "fleets", "systems", "credits", "population", "shipsBuilt", "shipsLost"]);
|
||||
|
||||
const factionTable = useVueTable({
|
||||
get data() { return factionRows.value; },
|
||||
@@ -472,6 +529,31 @@ function isShipSelected(id: string) {
|
||||
function isStationSelected(id: string) {
|
||||
return selectedEntityKind.value === "station" && selectedEntityId.value === id;
|
||||
}
|
||||
|
||||
// ── Orders tooltip ─────────────────────────────────────────────────────────
|
||||
|
||||
const ordersTooltip = ref<{
|
||||
visible: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
orders: MarketOrderSnapshot[];
|
||||
}>({ visible: false, x: 0, y: 0, orders: [] });
|
||||
|
||||
function showOrdersTooltip(e: MouseEvent, orders: MarketOrderSnapshot[]) {
|
||||
if (orders.length === 0) return;
|
||||
ordersTooltip.value = { visible: true, x: e.clientX, y: e.clientY, orders };
|
||||
}
|
||||
|
||||
function moveOrdersTooltip(e: MouseEvent) {
|
||||
if (ordersTooltip.value.visible) {
|
||||
ordersTooltip.value.x = e.clientX;
|
||||
ordersTooltip.value.y = e.clientY;
|
||||
}
|
||||
}
|
||||
|
||||
function hideOrdersTooltip() {
|
||||
ordersTooltip.value.visible = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -660,6 +742,15 @@ function isStationSelected(id: string) {
|
||||
>
|
||||
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
||||
</span>
|
||||
<span
|
||||
v-else-if="cell.column.id === 'orders'"
|
||||
:class="row.original.orderDetails.length > 0 ? 'gm-orders-trigger' : ''"
|
||||
@mouseenter="showOrdersTooltip($event, row.original.orderDetails)"
|
||||
@mousemove="moveOrdersTooltip"
|
||||
@mouseleave="hideOrdersTooltip"
|
||||
>
|
||||
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
||||
</span>
|
||||
<FlexRender
|
||||
v-else
|
||||
:render="cell.column.columnDef.cell"
|
||||
@@ -740,4 +831,35 @@ function isStationSelected(id: string) {
|
||||
</div>
|
||||
</div>
|
||||
</GmWindow>
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="ordersTooltip.visible && ordersTooltip.orders.length > 0"
|
||||
class="gm-orders-tooltip"
|
||||
:style="{ left: `${ordersTooltip.x + 14}px`, top: `${ordersTooltip.y + 14}px` }"
|
||||
>
|
||||
<table class="gm-orders-tooltip-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Kind</th>
|
||||
<th>Amount</th>
|
||||
<th>Remaining</th>
|
||||
<th>Valuation</th>
|
||||
<th>State</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="order in ordersTooltip.orders" :key="order.id">
|
||||
<td>{{ order.itemId }}</td>
|
||||
<td>{{ titleCaseToken(order.kind) }}</td>
|
||||
<td class="tabular-nums">{{ order.amount }}</td>
|
||||
<td class="tabular-nums">{{ order.remainingAmount }}</td>
|
||||
<td class="tabular-nums">{{ order.valuation }}</td>
|
||||
<td>{{ titleCaseToken(order.state) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
134
apps/viewer/src/components/gm/GmSettingsWindow.vue
Normal file
134
apps/viewer/src/components/gm/GmSettingsWindow.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import GmWindow from "./GmWindow.vue";
|
||||
import { fetchBalance, updateBalance } from "../../api";
|
||||
import type { BalanceSettings } from "../../contractsBalance";
|
||||
|
||||
const emit = defineEmits<{ close: [] }>();
|
||||
|
||||
type FieldMeta = {
|
||||
key: keyof BalanceSettings;
|
||||
label: string;
|
||||
description: string;
|
||||
step: number;
|
||||
min: number;
|
||||
};
|
||||
|
||||
const FIELDS: FieldMeta[] = [
|
||||
{ key: "simulationSpeedMultiplier", label: "Sim Speed Multiplier", description: "Global speed factor applied to every tick", step: 0.1, min: 0.01 },
|
||||
{ key: "arrivalThreshold", label: "Arrival Threshold", description: "Distance at which a ship is considered arrived", step: 1, min: 0.1 },
|
||||
{ key: "miningRate", label: "Mining Rate", description: "Units of ore extracted per mining cycle", step: 1, min: 0 },
|
||||
{ key: "miningCycleSeconds", label: "Mining Cycle (s)", description: "Duration of one mining cycle", step: 0.1, min: 0.1 },
|
||||
{ key: "transferRate", label: "Transfer Rate", description: "Cargo units transferred per second", step: 1, min: 0 },
|
||||
{ key: "dockingDuration", label: "Docking Duration (s)", description: "Time for a ship to complete docking", step: 0.1, min: 0.1 },
|
||||
{ key: "undockingDuration", label: "Undocking Duration (s)", description: "Time for a ship to complete undocking", step: 0.1, min: 0.1 },
|
||||
{ key: "undockDistance", label: "Undock Distance", description: "Distance traveled when undocking", step: 1, min: 0 },
|
||||
{ key: "yPlane", label: "Y Plane", description: "Vertical height for spatial placement", step: 0.5, min: 0 },
|
||||
];
|
||||
|
||||
const draft = reactive<BalanceSettings>({
|
||||
simulationSpeedMultiplier: 1,
|
||||
yPlane: 0,
|
||||
arrivalThreshold: 0,
|
||||
miningRate: 0,
|
||||
miningCycleSeconds: 0,
|
||||
transferRate: 0,
|
||||
dockingDuration: 0,
|
||||
undockingDuration: 0,
|
||||
undockDistance: 0,
|
||||
});
|
||||
|
||||
const loadError = ref<string | null>(null);
|
||||
const saveError = ref<string | null>(null);
|
||||
const saveStatus = ref<"idle" | "saving" | "saved">("idle");
|
||||
const loaded = ref(false);
|
||||
let savedTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const data = await fetchBalance();
|
||||
Object.assign(draft, data);
|
||||
loadError.value = null;
|
||||
loaded.value = true;
|
||||
} catch {
|
||||
loadError.value = "Failed to load settings";
|
||||
}
|
||||
});
|
||||
|
||||
function sanitizeDraft() {
|
||||
for (const field of FIELDS) {
|
||||
const value = draft[field.key];
|
||||
draft[field.key] = Number.isFinite(value) ? Math.max(field.min, value) : field.min;
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
sanitizeDraft();
|
||||
saveStatus.value = "saving";
|
||||
saveError.value = null;
|
||||
try {
|
||||
const saved = await updateBalance({ ...draft });
|
||||
Object.assign(draft, saved);
|
||||
saveStatus.value = "saved";
|
||||
if (savedTimer !== null) clearTimeout(savedTimer);
|
||||
savedTimer = setTimeout(() => { saveStatus.value = "idle"; }, 2500);
|
||||
} catch {
|
||||
saveError.value = "Failed to save settings";
|
||||
saveStatus.value = "idle";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GmWindow
|
||||
title="Settings"
|
||||
:initial-width="480"
|
||||
:initial-height="460"
|
||||
:initial-x="260"
|
||||
:initial-y="100"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<div class="gm-settings flex h-full flex-col">
|
||||
<div v-if="loadError" class="gm-settings-error mx-4 mt-3 rounded px-3 py-2 text-xs">
|
||||
{{ loadError }}
|
||||
</div>
|
||||
|
||||
<div class="gm-settings-body min-h-0 flex-1 overflow-auto px-4 py-3">
|
||||
<div class="gm-settings-section-title mb-3">Balance</div>
|
||||
|
||||
<div class="gm-settings-grid">
|
||||
<template v-for="field in FIELDS" :key="field.key">
|
||||
<label :for="`gm-setting-${field.key}`" class="gm-settings-label">
|
||||
{{ field.label }}
|
||||
</label>
|
||||
<div class="gm-settings-field-group">
|
||||
<input
|
||||
:id="`gm-setting-${field.key}`"
|
||||
v-model.number="draft[field.key]"
|
||||
type="number"
|
||||
class="gm-settings-input"
|
||||
:step="field.step"
|
||||
:min="field.min"
|
||||
/>
|
||||
<span class="gm-settings-desc">{{ field.description }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gm-settings-footer flex items-center gap-3 px-4 py-3">
|
||||
<span v-if="saveError" class="gm-settings-error-inline flex-1 text-xs">{{ saveError }}</span>
|
||||
<span v-else-if="saveStatus === 'saved'" class="gm-settings-saved flex-1 text-xs">Saved</span>
|
||||
<span v-else class="flex-1" />
|
||||
<button
|
||||
type="button"
|
||||
class="gm-settings-save-btn"
|
||||
:disabled="saveStatus === 'saving' || !loaded"
|
||||
@click="save"
|
||||
>
|
||||
{{ saveStatus === 'saving' ? 'Saving…' : 'Apply' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</GmWindow>
|
||||
</template>
|
||||
166
apps/viewer/src/components/gm/GmTelemetryWindow.vue
Normal file
166
apps/viewer/src/components/gm/GmTelemetryWindow.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import GmWindow from "./GmWindow.vue";
|
||||
import { fetchTelemetry } from "../../api";
|
||||
import type { TelemetrySnapshot } from "../../contractsTelemetry";
|
||||
|
||||
const emit = defineEmits<{ close: [] }>();
|
||||
|
||||
const data = ref<TelemetrySnapshot | null>(null);
|
||||
const error = ref<string | null>(null);
|
||||
const lastUpdatedAt = ref<number | null>(null);
|
||||
const secondsSinceUpdate = ref(0);
|
||||
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let ageTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
async function poll() {
|
||||
try {
|
||||
data.value = await fetchTelemetry();
|
||||
lastUpdatedAt.value = Date.now();
|
||||
error.value = null;
|
||||
} catch {
|
||||
error.value = "Failed to fetch telemetry";
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void poll();
|
||||
pollTimer = setInterval(poll, 2000);
|
||||
ageTimer = setInterval(() => {
|
||||
secondsSinceUpdate.value = lastUpdatedAt.value
|
||||
? Math.floor((Date.now() - lastUpdatedAt.value) / 1000)
|
||||
: 0;
|
||||
}, 500);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (pollTimer !== null) clearInterval(pollTimer);
|
||||
if (ageTimer !== null) clearInterval(ageTimer);
|
||||
});
|
||||
|
||||
function formatUptime(seconds: number) {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
if (h > 0) return `${h}h ${m}m ${s}s`;
|
||||
if (m > 0) return `${m}m ${s}s`;
|
||||
return `${s}s`;
|
||||
}
|
||||
|
||||
function formatNumber(n: number) {
|
||||
return n.toLocaleString("en-US");
|
||||
}
|
||||
|
||||
function cpuBarWidth(pct: number) {
|
||||
return `${Math.min(100, Math.max(0, pct))}%`;
|
||||
}
|
||||
|
||||
function cpuBarClass(pct: number) {
|
||||
if (pct >= 80) return "gm-telemetry-bar--high";
|
||||
if (pct >= 50) return "gm-telemetry-bar--mid";
|
||||
return "gm-telemetry-bar--low";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GmWindow
|
||||
title="Server Telemetry"
|
||||
:initial-width="460"
|
||||
:initial-height="380"
|
||||
:initial-x="200"
|
||||
:initial-y="120"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<div class="gm-telemetry flex h-full flex-col overflow-auto px-4 py-3">
|
||||
<!-- Error state -->
|
||||
<div v-if="error" class="gm-telemetry-error mb-3 rounded px-3 py-2 text-xs">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-else-if="!data" class="flex flex-1 items-center justify-center text-xs opacity-40">
|
||||
Loading…
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- PROCESS section -->
|
||||
<div class="gm-telemetry-section mb-4">
|
||||
<div class="gm-telemetry-section-title mb-2">Process</div>
|
||||
<div class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5">
|
||||
<span class="gm-telemetry-label">CPU</span>
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="gm-telemetry-value w-10 text-right">{{ data.process.cpuPercent.toFixed(1) }}%</span>
|
||||
<span class="gm-telemetry-bar-track flex-1">
|
||||
<span
|
||||
class="gm-telemetry-bar"
|
||||
:class="cpuBarClass(data.process.cpuPercent)"
|
||||
:style="{ width: cpuBarWidth(data.process.cpuPercent) }"
|
||||
/>
|
||||
</span>
|
||||
<span class="gm-telemetry-dim">/ {{ data.process.processorCount }} cores</span>
|
||||
</span>
|
||||
|
||||
<span class="gm-telemetry-label">Working set</span>
|
||||
<span class="gm-telemetry-value">{{ data.process.workingSetMb.toFixed(1) }} MB</span>
|
||||
|
||||
<span class="gm-telemetry-label">GC memory</span>
|
||||
<span class="gm-telemetry-value">{{ data.process.gcMemoryMb.toFixed(1) }} MB</span>
|
||||
|
||||
<span class="gm-telemetry-label">Threads</span>
|
||||
<span class="gm-telemetry-value">{{ data.process.threadCount }}</span>
|
||||
|
||||
<span class="gm-telemetry-label">Uptime</span>
|
||||
<span class="gm-telemetry-value">{{ formatUptime(data.process.uptimeSeconds) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SIMULATION section -->
|
||||
<div class="gm-telemetry-section mb-4">
|
||||
<div class="gm-telemetry-section-title mb-2">Simulation</div>
|
||||
<div class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5">
|
||||
<span class="gm-telemetry-label">Sequence</span>
|
||||
<span class="gm-telemetry-value font-mono">{{ formatNumber(data.simulation.sequence) }}</span>
|
||||
|
||||
<span class="gm-telemetry-label">Connected clients</span>
|
||||
<span class="gm-telemetry-value">
|
||||
<span
|
||||
class="gm-telemetry-clients-dot"
|
||||
:class="data.simulation.connectedClients > 0 ? 'gm-telemetry-clients-dot--active' : ''"
|
||||
/>
|
||||
{{ data.simulation.connectedClients }}
|
||||
</span>
|
||||
|
||||
<span class="gm-telemetry-label">Delta history</span>
|
||||
<span class="gm-telemetry-value">{{ data.simulation.deltaHistoryCount }} / 256</span>
|
||||
|
||||
<span class="gm-telemetry-label">Tick interval</span>
|
||||
<span class="gm-telemetry-value">{{ data.simulation.tickIntervalMs }} ms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RUNTIME section -->
|
||||
<div class="gm-telemetry-section mb-4">
|
||||
<div class="gm-telemetry-section-title mb-2">Runtime</div>
|
||||
<div class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5">
|
||||
<span class="gm-telemetry-label">.NET</span>
|
||||
<span class="gm-telemetry-value">{{ data.runtime.frameworkDescription }}</span>
|
||||
|
||||
<span class="gm-telemetry-label">GC collections</span>
|
||||
<span class="gm-telemetry-value font-mono">
|
||||
G0 {{ formatNumber(data.runtime.gcGen0) }} ·
|
||||
G1 {{ formatNumber(data.runtime.gcGen1) }} ·
|
||||
G2 {{ formatNumber(data.runtime.gcGen2) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="mt-auto flex items-center justify-between pt-2 text-[10px] opacity-40">
|
||||
<span>Updated {{ secondsSinceUpdate }}s ago</span>
|
||||
<span>Polling every 2s</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</GmWindow>
|
||||
</template>
|
||||
Reference in New Issue
Block a user