chore: snapshot release workspace
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user