import { HEALTH_ORDER, isBackupStale, type MeterLevel, meterLevel, ratio, type ServerHealth, serverHealth, } from '@/components/proxmox/proxmox-status'; import type { ProxmoxServer } from '@/lib/proxmox-api'; /** * Reine Funktionen der Proxmox-Kachel (quick-260924-i8v) — ohne React, * damit jede Verzweigung einzeln pruefbar ist (Muster `picture-frame-config.ts`). * Die Statuslogik selbst (Zustand je Server, Schwellen, Sortierung) kommt * aus `components/proxmox/proxmox-status.ts` und wird hier nicht kopiert. * * Grundregel wie dort: ein unbekannter Wert ist nie 0. */ export interface HealthSummary { /** Es gibt Server, und keiner weicht von „in Ordnung“ ab. */ allOk: boolean; /** Alle Zustaende ausser `ok` mit Anzahl > 0, in `HEALTH_ORDER`. */ entries: Array<{ health: ServerHealth; count: number }>; /** Schlimmster vorhandene Zustand (erster in `HEALTH_ORDER` mit Anzahl > 0). */ worst: ServerHealth | null; } /** * Zusammenfassung fuer die Zeile unter dem Balken: „Alles in Ordnung“ oder * die Abweichungen („1 nicht erreichbar, 1 mit Warnung“). Die Anzahl `ok` * wird bewusst nicht genannt — die Zeile soll sagen, was zu tun ist. */ export function healthSummary(counts: Record): HealthSummary { const entries = HEALTH_ORDER.filter((h) => h !== 'ok' && counts[h] > 0).map((health) => ({ health, count: counts[health], })); const total = HEALTH_ORDER.reduce((sum, h) => sum + counts[h], 0); const worst = HEALTH_ORDER.find((h) => counts[h] > 0) ?? null; return { allOk: total > 0 && entries.length === 0, entries, worst }; } export interface ProxmoxWidgetConfig { /** Leer = keine Kopfzeile. */ title: string; /** Leer = alle Server. */ serverIds: string[]; } /** * Konfiguration abwehrend lesen (T-I8V-03): die API prueft Kachel- * Konfigurationen nicht inhaltlich, `config` kann also alles enthalten. * Nicht-Strings und leere Kennungen fallen weg, doppelte zaehlen einmal. */ export function resolveProxmoxWidgetConfig(config: Record): ProxmoxWidgetConfig { const title = typeof config.title === 'string' ? config.title : ''; const rawIds = Array.isArray(config.serverIds) ? config.serverIds : []; const serverIds: string[] = []; for (const id of rawIds) { if (typeof id === 'string' && id !== '' && !serverIds.includes(id)) serverIds.push(id); } return { title, serverIds }; } /** * Serverauswahl anwenden. Keine Auswahl = alle Server. Sind nur noch * Kennungen geloeschter Server ausgewaehlt, ist `selectionGone` gesetzt — * die Kachel sagt das dann, statt still „alle“ oder gar nichts zu zeigen. * Die Reihenfolge der gelieferten Liste bleibt erhalten. */ export function selectServers( servers: readonly ProxmoxServer[], serverIds: readonly string[], ): { servers: ProxmoxServer[]; selectionGone: boolean } { if (serverIds.length === 0) return { servers: [...servers], selectionGone: false }; const selected = servers.filter((s) => serverIds.includes(s.id)); return { servers: selected, selectionGone: selected.length === 0 }; } /** Die eine Kennzahl rechts in einer Serverzeile. */ export type KeyFigure = | { kind: 'status'; health: 'down' | 'idle' | 'orphan' } | { kind: 'guests'; running: number; total: number } | { kind: 'noGuests' } | { kind: 'load'; fraction: number; level: MeterLevel } | { kind: 'backup'; at: number; stale: boolean } | { kind: 'noBackup' } | { kind: 'mailIn'; count: number } | { kind: 'unknown' }; function isKnownNumber(value: unknown): value is number { return typeof value === 'number' && !Number.isNaN(value); } function maxKnown(values: Array): number | null { let max: number | null = null; for (const value of values) { if (isKnownNumber(value) && (max === null || value > max)) max = value; } return max; } /** * Kennzahl je Server. Zuerst der Zustand: nicht erreichbar, noch nicht * abgefragt und verwaist zeigen immer nur den Zustand (ein verwaister * Server zeigt keine alten Messwerte, wie auf der Modulseite). * * - PVE: bei Warnung der hoechste bekannte Anteil aus Knoten-CPU, * Knoten-RAM und Speicher (das ist der Grund der Warnung), sonst die * laufenden Gaeste. * - PBS: die AELTESTE bekannte letzte Sicherung ueber alle Datenspeicher — * sie ist der Grund fuer „Sicherung zu alt“. * - PMG: eingehende Mails. */ export function widgetKeyFigure(server: ProxmoxServer, now: number = Date.now()): KeyFigure { const health = serverHealth(server, now); if (health === 'down' || health === 'idle' || health === 'orphan') { return { kind: 'status', health }; } const metrics = server.status?.metrics ?? null; if (metrics === null) return { kind: 'unknown' }; if (metrics.productType === 'pve') { if (health === 'warn') { const fraction = maxKnown([ ...metrics.nodes.map((n) => (isKnownNumber(n.cpu) ? n.cpu : null)), ...metrics.nodes.map((n) => ratio(n.mem, n.maxmem)), ...metrics.storages.map((s) => ratio(s.disk, s.maxdisk)), ]); if (fraction !== null) return { kind: 'load', fraction, level: meterLevel(fraction) }; } if (!isKnownNumber(metrics.guestsRunning) || !isKnownNumber(metrics.guestsStopped)) { return { kind: 'unknown' }; } const total = metrics.guestsRunning + metrics.guestsStopped; if (total === 0) return { kind: 'noGuests' }; return { kind: 'guests', running: metrics.guestsRunning, total }; } if (metrics.productType === 'pbs') { if (metrics.datastores.length === 0) return { kind: 'unknown' }; let oldest: number | null = null; for (const ds of metrics.datastores) { if (isKnownNumber(ds.lastBackupAt) && (oldest === null || ds.lastBackupAt < oldest)) { oldest = ds.lastBackupAt; } } if (oldest === null) return { kind: 'noBackup' }; return { kind: 'backup', at: oldest, stale: isBackupStale(oldest, now) }; } if (!isKnownNumber(metrics.countIn)) return { kind: 'unknown' }; return { kind: 'mailIn', count: metrics.countIn }; }