bug fixing and deploy and build diagnostics
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
/** Composable encapsulating fleet telemetry data fetching, types, and utilities */
|
||||
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import type { ChartDataset } from '@/components/LineChart.vue'
|
||||
|
||||
// --- Types ---
|
||||
|
||||
export interface FleetNode {
|
||||
node_id: string
|
||||
version: string
|
||||
uptime_secs: number
|
||||
cpu_cores: number
|
||||
cpu_pct: number
|
||||
mem_pct: number
|
||||
disk_pct: number
|
||||
container_count: number
|
||||
running_count: number
|
||||
federation_peers: number
|
||||
recent_alerts: Array<{ rule: string; message: string; timestamp: string }>
|
||||
containers: Array<{ id: string; state: string; version: string }>
|
||||
reported_at: string
|
||||
}
|
||||
|
||||
export interface FleetAlert {
|
||||
node_id: string
|
||||
rule: string
|
||||
message: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface NodeHistoryEntry {
|
||||
timestamp: string
|
||||
cpu_pct: number
|
||||
mem_pct: number
|
||||
disk_pct: number
|
||||
}
|
||||
|
||||
export type SortOption = 'status' | 'last-seen' | 'name'
|
||||
|
||||
// --- Utility Functions ---
|
||||
|
||||
export function formatUptime(secs: number): string {
|
||||
if (secs < 60) return `${secs}s`
|
||||
const days = Math.floor(secs / 86400)
|
||||
const hours = Math.floor((secs % 86400) / 3600)
|
||||
const mins = Math.floor((secs % 3600) / 60)
|
||||
if (days > 0) return `${days}d ${hours}h`
|
||||
if (hours > 0) return `${hours}h ${mins}m`
|
||||
return `${mins}m`
|
||||
}
|
||||
|
||||
export function timeAgo(dateStr: string): string {
|
||||
const now = Date.now()
|
||||
const then = new Date(dateStr).getTime()
|
||||
const diffMs = now - then
|
||||
if (diffMs < 0) return 'just now'
|
||||
const diffSecs = Math.floor(diffMs / 1000)
|
||||
if (diffSecs < 60) return `${diffSecs}s ago`
|
||||
const diffMins = Math.floor(diffSecs / 60)
|
||||
if (diffMins < 60) return `${diffMins}m ago`
|
||||
const diffHours = Math.floor(diffMins / 60)
|
||||
if (diffHours < 24) return `${diffHours}h ago`
|
||||
const diffDays = Math.floor(diffHours / 24)
|
||||
return `${diffDays}d ago`
|
||||
}
|
||||
|
||||
export function isOnline(reportedAt: string): boolean {
|
||||
const thirtyMinMs = 30 * 60 * 1000
|
||||
return Date.now() - new Date(reportedAt).getTime() < thirtyMinMs
|
||||
}
|
||||
|
||||
export function healthBarClass(pct: number): string {
|
||||
if (pct >= 85) return 'monitoring-bar-danger'
|
||||
if (pct >= 60) return 'monitoring-bar-warn'
|
||||
return 'monitoring-bar-ok'
|
||||
}
|
||||
|
||||
export function healthTextClass(pct: number): string {
|
||||
if (pct >= 85) return 'fleet-text-danger'
|
||||
if (pct >= 60) return 'fleet-text-warn'
|
||||
return ''
|
||||
}
|
||||
|
||||
export function alertSeverityDot(rule: string): string {
|
||||
const critical = ['container_crash', 'disk_critical', 'node_offline']
|
||||
if (critical.includes(rule)) return 'bg-red-400'
|
||||
return 'bg-orange-400'
|
||||
}
|
||||
|
||||
export function alertTypeLabel(rule: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
container_crash: 'Container Crash',
|
||||
disk_critical: 'Disk Critical',
|
||||
disk_warning: 'Disk Warning',
|
||||
ram_high: 'High RAM',
|
||||
cpu_high: 'High CPU',
|
||||
node_offline: 'Node Offline',
|
||||
version_mismatch: 'Version Mismatch',
|
||||
}
|
||||
return labels[rule] ?? rule
|
||||
}
|
||||
|
||||
export function formatTimestamp(ts: string): string {
|
||||
const d = new Date(ts)
|
||||
return d.toLocaleString()
|
||||
}
|
||||
|
||||
export function getContainerState(node: FleetNode, appId: string): string | null {
|
||||
const container = node.containers.find(c => c.id === appId)
|
||||
if (!container) return null
|
||||
return container.state
|
||||
}
|
||||
|
||||
export const SORT_OPTIONS: Array<{ label: string; value: SortOption }> = [
|
||||
{ label: 'Status', value: 'status' },
|
||||
{ label: 'Last Seen', value: 'last-seen' },
|
||||
{ label: 'Name', value: 'name' },
|
||||
]
|
||||
|
||||
// --- Composable ---
|
||||
|
||||
export function useFleetData() {
|
||||
const loading = ref(true)
|
||||
const errorMessage = ref('')
|
||||
const nodes = ref<FleetNode[]>([])
|
||||
const fleetAlerts = ref<FleetAlert[]>([])
|
||||
const alertsLoading = ref(false)
|
||||
const selectedNodeId = ref<string | null>(null)
|
||||
const nodeHistory = ref<NodeHistoryEntry[]>([])
|
||||
const nodeHistoryLoading = ref(false)
|
||||
const autoRefresh = ref(true)
|
||||
const lastRefreshed = ref('')
|
||||
const sortBy = ref<SortOption>('status')
|
||||
const chartWidth = ref(300)
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
// --- Computed ---
|
||||
|
||||
const onlineCount = computed(() => nodes.value.filter(n => isOnline(n.reported_at)).length)
|
||||
const offlineCount = computed(() => nodes.value.length - onlineCount.value)
|
||||
const healthyCount = computed(() => nodes.value.filter(n => n.recent_alerts.length === 0).length)
|
||||
|
||||
const fleetHealthPct = computed(() => {
|
||||
if (!nodes.value.length) return 0
|
||||
return Math.round((healthyCount.value / nodes.value.length) * 100)
|
||||
})
|
||||
|
||||
const avgCpu = computed(() => {
|
||||
if (!nodes.value.length) return 0
|
||||
return nodes.value.reduce((sum, n) => sum + n.cpu_pct, 0) / nodes.value.length
|
||||
})
|
||||
|
||||
const avgMem = computed(() => {
|
||||
if (!nodes.value.length) return 0
|
||||
return nodes.value.reduce((sum, n) => sum + n.mem_pct, 0) / nodes.value.length
|
||||
})
|
||||
|
||||
const avgDisk = computed(() => {
|
||||
if (!nodes.value.length) return 0
|
||||
return nodes.value.reduce((sum, n) => sum + n.disk_pct, 0) / nodes.value.length
|
||||
})
|
||||
|
||||
const selectedNode = computed(() => {
|
||||
if (!selectedNodeId.value) return null
|
||||
return nodes.value.find(n => n.node_id === selectedNodeId.value) ?? null
|
||||
})
|
||||
|
||||
const sortedNodes = computed(() => {
|
||||
const sorted = [...nodes.value]
|
||||
switch (sortBy.value) {
|
||||
case 'status':
|
||||
sorted.sort((a, b) => {
|
||||
const aOnline = isOnline(a.reported_at)
|
||||
const bOnline = isOnline(b.reported_at)
|
||||
if (aOnline !== bOnline) return aOnline ? 1 : -1
|
||||
return new Date(b.reported_at).getTime() - new Date(a.reported_at).getTime()
|
||||
})
|
||||
break
|
||||
case 'last-seen':
|
||||
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))
|
||||
break
|
||||
}
|
||||
return sorted
|
||||
})
|
||||
|
||||
const allAppIds = computed(() => {
|
||||
const appSet = new Set<string>()
|
||||
for (const node of nodes.value) {
|
||||
for (const c of node.containers) {
|
||||
appSet.add(c.id)
|
||||
}
|
||||
}
|
||||
return Array.from(appSet).sort()
|
||||
})
|
||||
|
||||
// Node history chart datasets
|
||||
const nodeHistoryLabels = computed(() => {
|
||||
return nodeHistory.value.map(h => {
|
||||
const d = new Date(h.timestamp)
|
||||
return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`
|
||||
})
|
||||
})
|
||||
|
||||
const nodeHistoryCpuDatasets = computed<ChartDataset[]>(() => [{
|
||||
label: 'CPU',
|
||||
data: nodeHistory.value.map(h => h.cpu_pct),
|
||||
color: '#fb923c',
|
||||
}])
|
||||
|
||||
const nodeHistoryMemDatasets = computed<ChartDataset[]>(() => [{
|
||||
label: 'RAM',
|
||||
data: nodeHistory.value.map(h => h.mem_pct),
|
||||
color: '#3b82f6',
|
||||
}])
|
||||
|
||||
const nodeHistoryDiskDatasets = computed<ChartDataset[]>(() => [{
|
||||
label: 'Disk',
|
||||
data: nodeHistory.value.map(h => h.disk_pct),
|
||||
color: '#a78bfa',
|
||||
}])
|
||||
|
||||
// --- Data Fetching ---
|
||||
|
||||
async function fetchFleetStatus() {
|
||||
try {
|
||||
const data = await rpcClient.call<{ nodes: FleetNode[] }>({
|
||||
method: 'telemetry.fleet-status',
|
||||
})
|
||||
if (data?.nodes) {
|
||||
nodes.value = data.nodes
|
||||
lastRefreshed.value = new Date().toISOString()
|
||||
}
|
||||
} catch (err) {
|
||||
if (loading.value) {
|
||||
errorMessage.value = err instanceof Error ? err.message : 'Failed to load fleet data'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchFleetAlerts() {
|
||||
alertsLoading.value = true
|
||||
try {
|
||||
const data = await rpcClient.call<{ alerts: FleetAlert[] }>({
|
||||
method: 'telemetry.fleet-alerts',
|
||||
})
|
||||
if (data?.alerts) {
|
||||
fleetAlerts.value = data.alerts
|
||||
}
|
||||
} catch {
|
||||
// Non-critical, retry on next poll
|
||||
} finally {
|
||||
alertsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchNodeHistory(nodeId: string) {
|
||||
nodeHistoryLoading.value = true
|
||||
nodeHistory.value = []
|
||||
try {
|
||||
const data = await rpcClient.call<{ history: NodeHistoryEntry[] }>({
|
||||
method: 'telemetry.fleet-node-history',
|
||||
params: { node_id: nodeId },
|
||||
})
|
||||
if (data?.history) {
|
||||
nodeHistory.value = data.history
|
||||
}
|
||||
} catch {
|
||||
// Non-critical
|
||||
} finally {
|
||||
nodeHistoryLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
loading.value = !nodes.value.length
|
||||
errorMessage.value = ''
|
||||
await Promise.all([fetchFleetStatus(), fetchFleetAlerts()])
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function selectNode(nodeId: string) {
|
||||
if (selectedNodeId.value === nodeId) {
|
||||
selectedNodeId.value = null
|
||||
nodeHistory.value = []
|
||||
} else {
|
||||
selectedNodeId.value = nodeId
|
||||
fetchNodeHistory(nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAutoRefresh() {
|
||||
autoRefresh.value = !autoRefresh.value
|
||||
if (autoRefresh.value) {
|
||||
startAutoRefresh()
|
||||
} else {
|
||||
stopAutoRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
function startAutoRefresh() {
|
||||
stopAutoRefresh()
|
||||
pollTimer = setInterval(async () => {
|
||||
await Promise.all([fetchFleetStatus(), fetchFleetAlerts()])
|
||||
if (selectedNodeId.value) {
|
||||
await fetchNodeHistory(selectedNodeId.value)
|
||||
}
|
||||
}, 60000)
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function exportFleetData() {
|
||||
const exportData = {
|
||||
exported_at: new Date().toISOString(),
|
||||
nodes: nodes.value,
|
||||
alerts: fleetAlerts.value,
|
||||
}
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `fleet-telemetry-${new Date().toISOString().slice(0, 10)}.json`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
function updateChartWidth() {
|
||||
const container = document.querySelector('.glass-card')
|
||||
if (container) {
|
||||
const cardWidth = container.clientWidth
|
||||
chartWidth.value = Math.max(Math.floor((cardWidth - 80) / 3), 200)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch node history when selection changes
|
||||
watch(selectedNodeId, (newId) => {
|
||||
if (newId) {
|
||||
fetchNodeHistory(newId)
|
||||
} else {
|
||||
nodeHistory.value = []
|
||||
}
|
||||
})
|
||||
|
||||
// --- Lifecycle ---
|
||||
|
||||
onMounted(async () => {
|
||||
updateChartWidth()
|
||||
window.addEventListener('resize', updateChartWidth)
|
||||
await refreshAll()
|
||||
if (autoRefresh.value) {
|
||||
startAutoRefresh()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAutoRefresh()
|
||||
window.removeEventListener('resize', updateChartWidth)
|
||||
})
|
||||
|
||||
return {
|
||||
loading, errorMessage, nodes, fleetAlerts, alertsLoading,
|
||||
selectedNodeId, selectedNode, nodeHistory, nodeHistoryLoading,
|
||||
autoRefresh, lastRefreshed, sortBy, chartWidth,
|
||||
onlineCount, offlineCount, healthyCount, fleetHealthPct,
|
||||
avgCpu, avgMem, avgDisk, sortedNodes, allAppIds,
|
||||
nodeHistoryLabels, nodeHistoryCpuDatasets, nodeHistoryMemDatasets, nodeHistoryDiskDatasets,
|
||||
refreshAll, selectNode, toggleAutoRefresh, exportFleetData,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user