chore: snapshot release workspace

This commit is contained in:
archipelago
2026-06-12 03:00:15 -04:00
parent 6a30ff11bd
commit d6f108d818
76 changed files with 792 additions and 3613 deletions
+10 -6
View File
@@ -2,13 +2,21 @@
<div v-if="node" class="glass-card p-5 mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-medium text-white/80">
Node Detail <span class="font-mono">{{ nodeId.slice(0, 8) }}</span>
Node Detail <span>{{ fleetNodeDisplayName(node) }}</span>
</h3>
<button class="glass-button text-xs px-3 py-1" @click="$emit('close')">Close</button>
</div>
<!-- Node Info Summary -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div class="monitoring-stat-card">
<p class="text-xs text-white/50 uppercase tracking-wide">Hostname</p>
<p class="text-lg font-bold text-white truncate">{{ node.hostname || 'Unknown' }}</p>
</div>
<div class="monitoring-stat-card">
<p class="text-xs text-white/50 uppercase tracking-wide">Address</p>
<p class="text-lg font-bold text-white truncate">{{ node.server_url || nodeId.slice(0, 8) }}</p>
</div>
<div class="monitoring-stat-card">
<p class="text-xs text-white/50 uppercase tracking-wide">Version</p>
<p class="text-lg font-bold text-white">v{{ node.version }}</p>
@@ -17,10 +25,6 @@
<p class="text-xs text-white/50 uppercase tracking-wide">Uptime</p>
<p class="text-lg font-bold text-white">{{ formatUptime(node.uptime_secs) }}</p>
</div>
<div class="monitoring-stat-card">
<p class="text-xs text-white/50 uppercase tracking-wide">CPU Cores</p>
<p class="text-lg font-bold text-white">{{ node.cpu_cores }}</p>
</div>
<div class="monitoring-stat-card">
<p class="text-xs text-white/50 uppercase tracking-wide">Federation Peers</p>
<p class="text-lg font-bold text-white">{{ node.federation_peers }}</p>
@@ -119,7 +123,7 @@
<script setup lang="ts">
import LineChart from '@/components/LineChart.vue'
import type { ChartDataset } from '@/components/LineChart.vue'
import { type FleetNode, formatUptime, alertSeverityDot, formatTimestamp } from './useFleetData'
import { type FleetNode, formatUptime, alertSeverityDot, formatTimestamp, fleetNodeDisplayName } from './useFleetData'
defineProps<{
node: FleetNode | null
+5 -2
View File
@@ -34,10 +34,13 @@
class="fleet-status-dot"
:class="isOnline(node.reported_at) ? 'fleet-dot-online' : 'fleet-dot-offline'"
></span>
<span class="text-sm font-mono text-white">{{ node.node_id.slice(0, 8) }}</span>
<span class="text-sm font-semibold text-white truncate">{{ fleetNodeDisplayName(node) }}</span>
</div>
<span class="fleet-version-badge">v{{ node.version }}</span>
</div>
<div class="mb-3 truncate text-xs text-white/40">
{{ fleetNodeSubtitle(node) }}
</div>
<div class="space-y-2 mb-3">
<div class="fleet-metric-row">
@@ -91,7 +94,7 @@
<script setup lang="ts">
import {
type FleetNode, type SortOption, SORT_OPTIONS,
isOnline, healthBarClass, formatUptime, timeAgo,
isOnline, healthBarClass, formatUptime, timeAgo, fleetNodeDisplayName, fleetNodeSubtitle,
} from './useFleetData'
defineProps<{
@@ -1,9 +1,20 @@
import { describe, expect, it, vi } from 'vitest'
import { isOnline, normalizeFleetNode, normalizeNodeHistoryResponse, sortFleetNodes, type FleetNode } from '../useFleetData'
import {
fleetNodeDisplayName,
fleetNodeSubtitle,
isOnline,
normalizeFleetNode,
normalizeNodeHistoryResponse,
sortFleetNodes,
type FleetNode,
} from '../useFleetData'
function node(id: string, reportedAt: string): FleetNode {
return {
node_id: id,
node_name: null,
hostname: null,
server_url: null,
version: '1.8-alpha',
uptime_secs: 60,
cpu_cores: 4,
@@ -49,10 +60,12 @@ describe('fleet data helpers', () => {
})
it('sorts by name alphabetically', () => {
expect(sortFleetNodes([
node('zulu', '2026-06-10T11:59:00Z'),
node('alpha', '2026-06-10T11:59:00Z'),
], 'name').map(n => n.node_id)).toEqual(['alpha', 'zulu'])
const zulu = node('zulu', '2026-06-10T11:59:00Z')
zulu.node_name = 'Workshop'
const alpha = node('alpha', '2026-06-10T11:59:00Z')
alpha.node_name = 'Kitchen'
expect(sortFleetNodes([zulu, alpha], 'name').map(n => n.node_id)).toEqual(['alpha', 'zulu'])
})
it('normalizes older telemetry reports with missing metric and container fields', () => {
@@ -63,6 +76,9 @@ describe('fleet data helpers', () => {
})
expect(normalized.node_id).toBe('legacy-node')
expect(normalized.node_name).toBeNull()
expect(normalized.hostname).toBeNull()
expect(normalized.server_url).toBeNull()
expect(normalized.cpu_pct).toBe(0)
expect(normalized.mem_pct).toBe(0)
expect(normalized.disk_pct).toBe(0)
@@ -70,6 +86,28 @@ describe('fleet data helpers', () => {
expect(normalized.recent_alerts).toEqual([])
})
it('uses node name, hostname, then node id for fleet display labels', () => {
const named = normalizeFleetNode({
node_id: 'abcdef123456',
node_name: 'Kitchen Node',
hostname: 'kitchen-node',
server_url: 'https://192.168.1.20',
})
const hostOnly = normalizeFleetNode({
node_id: '123456abcdef',
hostname: 'workshop-node',
server_url: 'https://192.168.1.21',
})
const idOnly = normalizeFleetNode({ node_id: 'feedfacecafebeef' })
expect(fleetNodeDisplayName(named)).toBe('Kitchen Node')
expect(fleetNodeSubtitle(named)).toBe('kitchen-node')
expect(fleetNodeDisplayName(hostOnly)).toBe('workshop-node')
expect(fleetNodeSubtitle(hostOnly)).toBe('https://192.168.1.21')
expect(fleetNodeDisplayName(idOnly)).toBe('feedface')
expect(fleetNodeSubtitle(idOnly)).toBe('feedfacecafebeef')
})
it('normalizes node history responses from backend entries or legacy history fields', () => {
const entry = { timestamp: '2026-06-10T11:59:00Z', cpu_pct: 1, mem_pct: 2, disk_pct: 3 }
+18 -1
View File
@@ -8,6 +8,9 @@ import type { ChartDataset } from '@/components/LineChart.vue'
export interface FleetNode {
node_id: string
node_name?: string | null
hostname?: string | null
server_url?: string | null
version: string
uptime_secs: number
cpu_cores: number
@@ -112,6 +115,17 @@ export function getContainerState(node: FleetNode, appId: string): string | null
return container.state
}
export function fleetNodeDisplayName(node: FleetNode): string {
const name = node.node_name?.trim() || node.hostname?.trim()
return name || node.node_id.slice(0, 8)
}
export function fleetNodeSubtitle(node: FleetNode): string {
const host = node.hostname?.trim()
if (host && host !== fleetNodeDisplayName(node)) return host
return node.server_url?.trim() || node.node_id
}
export const SORT_OPTIONS: Array<{ label: string; value: SortOption }> = [
{ label: 'Status', value: 'status' },
{ label: 'Last Seen', value: 'last-seen' },
@@ -133,7 +147,7 @@ export function sortFleetNodes(nodes: FleetNode[], sortBy: SortOption): FleetNod
sorted.sort((a, b) => new Date(b.reported_at).getTime() - new Date(a.reported_at).getTime())
break
case 'name':
sorted.sort((a, b) => a.node_id.localeCompare(b.node_id))
sorted.sort((a, b) => fleetNodeDisplayName(a).localeCompare(fleetNodeDisplayName(b)))
break
}
return sorted
@@ -146,6 +160,9 @@ function numberOrZero(value: unknown): number {
export function normalizeFleetNode(node: Partial<FleetNode>): FleetNode {
return {
node_id: typeof node.node_id === 'string' ? node.node_id : 'unknown',
node_name: typeof node.node_name === 'string' ? node.node_name : null,
hostname: typeof node.hostname === 'string' ? node.hostname : null,
server_url: typeof node.server_url === 'string' ? node.server_url : null,
version: typeof node.version === 'string' ? node.version : 'unknown',
uptime_secs: numberOrZero(node.uptime_secs),
cpu_cores: numberOrZero(node.cpu_cores),