Show unavailable Fleet history explicitly

This commit is contained in:
archipelago
2026-10-07 20:42:18 -04:00
parent f5a1806ae3
commit fd98b38364
5 changed files with 64 additions and 5 deletions
@@ -150,3 +150,18 @@ DOMContentLoaded/60-second fixture passed both widths. Logs:
`/tmp/archy-fleet-acceptance-browser-retry.log`. Owned browser/server jobs stopped.
This is source-browser evidence, not a production build or real identity/transport
acceptance. Backend provenance qualification and coupled release remain required.
The backend provenance follow-up explicitly returns `history_available=false`
for federation identities whose collector history cannot be trusted. The UI now
consumes that flag: it clears any supplied history entries and displays “History
is unavailable for this node.” Changing selection clears the prior message;
legacy `history`/`entries` forms remain supported. A failed history refresh marks
retained history or unavailability instead of silently implying success. A new
composable plus real detail-component fixture verifies the unavailable flag wins
even when entries accompany it, and does not leak into a newly selected node.
Focused Fleet suite after this follow-up: **29 tests in five files passed**;
log `/tmp/archy-fleet-history-final.log`.
The exact app-project typecheck also passed again after the history follow-up,
with the same 2 GB bound; log `/tmp/archy-fleet-history-typecheck.log`.
Completed logs and browser fixture sources are archived at
`~/.local/state/archipelago/release-qualification/fleet-acceptance-20261007/`.
+1
View File
@@ -125,6 +125,7 @@
:node="fleet.selectedNode.value"
:node-id="fleet.selectedNodeId.value"
:history-loading="fleet.nodeHistoryLoading.value"
:history-message="fleet.nodeHistoryMessage.value"
:history-labels="fleet.nodeHistoryLabels.value"
:cpu-datasets="fleet.nodeHistoryCpuDatasets.value"
:mem-datasets="fleet.nodeHistoryMemDatasets.value"
@@ -32,6 +32,7 @@
</div>
<!-- History Charts -->
<p v-if="historyMessage" role="status" class="text-sm text-amber-200 mb-4">{{ historyMessage }}</p>
<div v-if="historyLoading && !historyLabels.length" class="text-white/40 text-sm py-4 text-center mb-4">
Loading history...
</div>
@@ -129,6 +130,7 @@ defineProps<{
node: FleetNode | null
nodeId: string
historyLoading: boolean
historyMessage?: string
historyLabels: string[]
cpuDatasets: ChartDataset[]
memDatasets: ChartDataset[]
@@ -2,8 +2,9 @@ import { afterEach, expect, it, vi } from 'vitest'
import { defineComponent } from 'vue'
import { flushPromises, mount } from '@vue/test-utils'
import { rpcClient } from '@/api/rpc-client'
import { useFleetData } from '../useFleetData'
import { useFleetData, normalizeFleetNode } from '../useFleetData'
import FleetAlerts from '../FleetAlerts.vue'
import FleetNodeDetail from '../FleetNodeDetail.vue'
vi.mock('@/api/rpc-client', () => ({ rpcClient: { call: vi.fn() } }))
afterEach(() => { sessionStorage.clear(); vi.clearAllMocks() })
@@ -82,3 +83,31 @@ it('ignores an older alerts failure after a newer successful refresh', async ()
expect(fleet.alertsLoading.value).toBe(false)
} finally { wrapper.unmount() }
})
it('reports explicitly unavailable history and clears the message when changing nodes', async () => {
vi.mocked(rpcClient.call).mockImplementation(({ method, params }) => {
if (method !== 'telemetry.fleet-node-history') return Promise.resolve({ nodes: [], alerts: [] }) as never
return Promise.resolve((params as { node_id: string }).node_id === 'trusted'
? { history_available: false, entries: [{ timestamp: 'must-not-render', cpu_pct: 99 }] }
: { entries: [{ timestamp: 'legacy-available', cpu_pct: 12 }] }) as never
})
let fleet!: ReturnType<typeof useFleetData>
const wrapper = mount(defineComponent({ setup() { fleet = useFleetData(); return () => null } }))
try {
await flushPromises()
fleet.selectNode('trusted'); await flushPromises()
expect(fleet.nodeHistoryMessage.value).toContain('unavailable')
expect(fleet.nodeHistory.value).toEqual([])
const detail = mount(FleetNodeDetail, { props: {
node: normalizeFleetNode({ node_id: 'trusted' }), nodeId: 'trusted',
historyLoading: false, historyMessage: fleet.nodeHistoryMessage.value,
historyLabels: [], cpuDatasets: [], memDatasets: [], diskDatasets: [], chartWidth: 300,
}, global: { mocks: { $ver: (v: string) => v }, stubs: { LineChart: true } } })
expect(detail.get('[role="status"]').text()).toContain('History is unavailable')
expect(detail.text()).not.toContain('CPU History')
detail.unmount()
fleet.selectNode('legacy'); await flushPromises()
expect(fleet.nodeHistoryMessage.value).toBe('')
expect(fleet.nodeHistory.value[0]?.timestamp).toBe('legacy-available')
} finally { wrapper.unmount() }
})
+15 -3
View File
@@ -281,6 +281,7 @@ export function useFleetData() {
const selectedNodeId = ref<string | null>(cached.selectedNodeId ?? null)
const nodeHistory = ref<NodeHistoryEntry[]>([])
const nodeHistoryLoading = ref(false)
const nodeHistoryMessage = ref('')
const autoRefresh = ref(true)
const lastRefreshed = ref(cached.lastRefreshed ?? '')
const sortBy = ref<SortOption>(cached.sortBy ?? 'status')
@@ -423,15 +424,25 @@ export function useFleetData() {
const request = ++historyRequest
nodeHistoryLoading.value = true
try {
const data = await rpcClient.call<{ history?: NodeHistoryEntry[]; entries?: NodeHistoryEntry[] }>({
const data = await rpcClient.call<{ history?: NodeHistoryEntry[]; entries?: NodeHistoryEntry[]; history_available?: boolean }>({
method: 'telemetry.fleet-node-history',
params: { node_id: nodeId },
})
if (request === historyRequest && selectedNodeId.value === nodeId) {
if (data.history_available === false) {
nodeHistory.value = []
nodeHistoryMessage.value = 'History is unavailable for this node.'
} else {
nodeHistory.value = normalizeNodeHistoryResponse(data)
nodeHistoryMessage.value = ''
}
}
} catch {
// Non-critical
if (request === historyRequest && selectedNodeId.value === nodeId) {
nodeHistoryMessage.value = nodeHistory.value.length
? 'History refresh failed. Showing the last received history.'
: 'History is unavailable for this node.'
}
} finally {
if (request === historyRequest) nodeHistoryLoading.value = false
}
@@ -521,6 +532,7 @@ export function useFleetData() {
historyRequest++
nodeHistory.value = []
nodeHistoryLoading.value = false
nodeHistoryMessage.value = ''
if (newId) {
fetchNodeHistory(newId)
} else {
@@ -599,7 +611,7 @@ export function useFleetData() {
return {
loading, refreshing, errorMessage, nodes, fleetAlerts, alertsLoading, alertsErrorMessage,
selectedNodeId, selectedNode, nodeHistory, nodeHistoryLoading,
selectedNodeId, selectedNode, nodeHistory, nodeHistoryLoading, nodeHistoryMessage,
autoRefresh, lastRefreshed, sortBy, chartWidth, now,
onlineCount, offlineCount, unknownCount, healthyCount, fleetHealthPct,
avgCpu, avgMem, avgDisk, sortedNodes, allAppIds,