Show unavailable Fleet history explicitly
This commit is contained in:
@@ -150,3 +150,18 @@ DOMContentLoaded/60-second fixture passed both widths. Logs:
|
|||||||
`/tmp/archy-fleet-acceptance-browser-retry.log`. Owned browser/server jobs stopped.
|
`/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
|
This is source-browser evidence, not a production build or real identity/transport
|
||||||
acceptance. Backend provenance qualification and coupled release remain required.
|
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/`.
|
||||||
|
|||||||
@@ -125,6 +125,7 @@
|
|||||||
:node="fleet.selectedNode.value"
|
:node="fleet.selectedNode.value"
|
||||||
:node-id="fleet.selectedNodeId.value"
|
:node-id="fleet.selectedNodeId.value"
|
||||||
:history-loading="fleet.nodeHistoryLoading.value"
|
:history-loading="fleet.nodeHistoryLoading.value"
|
||||||
|
:history-message="fleet.nodeHistoryMessage.value"
|
||||||
:history-labels="fleet.nodeHistoryLabels.value"
|
:history-labels="fleet.nodeHistoryLabels.value"
|
||||||
:cpu-datasets="fleet.nodeHistoryCpuDatasets.value"
|
:cpu-datasets="fleet.nodeHistoryCpuDatasets.value"
|
||||||
:mem-datasets="fleet.nodeHistoryMemDatasets.value"
|
:mem-datasets="fleet.nodeHistoryMemDatasets.value"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- History Charts -->
|
<!-- 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">
|
<div v-if="historyLoading && !historyLabels.length" class="text-white/40 text-sm py-4 text-center mb-4">
|
||||||
Loading history...
|
Loading history...
|
||||||
</div>
|
</div>
|
||||||
@@ -129,6 +130,7 @@ defineProps<{
|
|||||||
node: FleetNode | null
|
node: FleetNode | null
|
||||||
nodeId: string
|
nodeId: string
|
||||||
historyLoading: boolean
|
historyLoading: boolean
|
||||||
|
historyMessage?: string
|
||||||
historyLabels: string[]
|
historyLabels: string[]
|
||||||
cpuDatasets: ChartDataset[]
|
cpuDatasets: ChartDataset[]
|
||||||
memDatasets: ChartDataset[]
|
memDatasets: ChartDataset[]
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { afterEach, expect, it, vi } from 'vitest'
|
|||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import { flushPromises, mount } from '@vue/test-utils'
|
import { flushPromises, mount } from '@vue/test-utils'
|
||||||
import { rpcClient } from '@/api/rpc-client'
|
import { rpcClient } from '@/api/rpc-client'
|
||||||
import { useFleetData } from '../useFleetData'
|
import { useFleetData, normalizeFleetNode } from '../useFleetData'
|
||||||
import FleetAlerts from '../FleetAlerts.vue'
|
import FleetAlerts from '../FleetAlerts.vue'
|
||||||
|
import FleetNodeDetail from '../FleetNodeDetail.vue'
|
||||||
|
|
||||||
vi.mock('@/api/rpc-client', () => ({ rpcClient: { call: vi.fn() } }))
|
vi.mock('@/api/rpc-client', () => ({ rpcClient: { call: vi.fn() } }))
|
||||||
afterEach(() => { sessionStorage.clear(); vi.clearAllMocks() })
|
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)
|
expect(fleet.alertsLoading.value).toBe(false)
|
||||||
} finally { wrapper.unmount() }
|
} 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() }
|
||||||
|
})
|
||||||
|
|||||||
@@ -281,6 +281,7 @@ export function useFleetData() {
|
|||||||
const selectedNodeId = ref<string | null>(cached.selectedNodeId ?? null)
|
const selectedNodeId = ref<string | null>(cached.selectedNodeId ?? null)
|
||||||
const nodeHistory = ref<NodeHistoryEntry[]>([])
|
const nodeHistory = ref<NodeHistoryEntry[]>([])
|
||||||
const nodeHistoryLoading = ref(false)
|
const nodeHistoryLoading = ref(false)
|
||||||
|
const nodeHistoryMessage = ref('')
|
||||||
const autoRefresh = ref(true)
|
const autoRefresh = ref(true)
|
||||||
const lastRefreshed = ref(cached.lastRefreshed ?? '')
|
const lastRefreshed = ref(cached.lastRefreshed ?? '')
|
||||||
const sortBy = ref<SortOption>(cached.sortBy ?? 'status')
|
const sortBy = ref<SortOption>(cached.sortBy ?? 'status')
|
||||||
@@ -423,15 +424,25 @@ export function useFleetData() {
|
|||||||
const request = ++historyRequest
|
const request = ++historyRequest
|
||||||
nodeHistoryLoading.value = true
|
nodeHistoryLoading.value = true
|
||||||
try {
|
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',
|
method: 'telemetry.fleet-node-history',
|
||||||
params: { node_id: nodeId },
|
params: { node_id: nodeId },
|
||||||
})
|
})
|
||||||
if (request === historyRequest && selectedNodeId.value === nodeId) {
|
if (request === historyRequest && selectedNodeId.value === nodeId) {
|
||||||
nodeHistory.value = normalizeNodeHistoryResponse(data)
|
if (data.history_available === false) {
|
||||||
|
nodeHistory.value = []
|
||||||
|
nodeHistoryMessage.value = 'History is unavailable for this node.'
|
||||||
|
} else {
|
||||||
|
nodeHistory.value = normalizeNodeHistoryResponse(data)
|
||||||
|
nodeHistoryMessage.value = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} 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 {
|
} finally {
|
||||||
if (request === historyRequest) nodeHistoryLoading.value = false
|
if (request === historyRequest) nodeHistoryLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -521,6 +532,7 @@ export function useFleetData() {
|
|||||||
historyRequest++
|
historyRequest++
|
||||||
nodeHistory.value = []
|
nodeHistory.value = []
|
||||||
nodeHistoryLoading.value = false
|
nodeHistoryLoading.value = false
|
||||||
|
nodeHistoryMessage.value = ''
|
||||||
if (newId) {
|
if (newId) {
|
||||||
fetchNodeHistory(newId)
|
fetchNodeHistory(newId)
|
||||||
} else {
|
} else {
|
||||||
@@ -599,7 +611,7 @@ export function useFleetData() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
loading, refreshing, errorMessage, nodes, fleetAlerts, alertsLoading, alertsErrorMessage,
|
loading, refreshing, errorMessage, nodes, fleetAlerts, alertsLoading, alertsErrorMessage,
|
||||||
selectedNodeId, selectedNode, nodeHistory, nodeHistoryLoading,
|
selectedNodeId, selectedNode, nodeHistory, nodeHistoryLoading, nodeHistoryMessage,
|
||||||
autoRefresh, lastRefreshed, sortBy, chartWidth, now,
|
autoRefresh, lastRefreshed, sortBy, chartWidth, now,
|
||||||
onlineCount, offlineCount, unknownCount, healthyCount, fleetHealthPct,
|
onlineCount, offlineCount, unknownCount, healthyCount, fleetHealthPct,
|
||||||
avgCpu, avgMem, avgDisk, sortedNodes, allAppIds,
|
avgCpu, avgMem, avgDisk, sortedNodes, allAppIds,
|
||||||
|
|||||||
Reference in New Issue
Block a user