'use client'; // Client-side rolling history of `HermesOpsSnapshot`s — Phase 6 trend cards. // We keep just a handful of derived metrics per snapshot (warning count, // healthy-instance count, the freshest backup-commit timestamp per // instance) so the localStorage payload stays tiny. Capped at 24 entries // (24 minutes of 1-minute polls — long enough to spot a trend, short // enough that an old entry doesn't pollute "what's happening right now"). import type { HermesOpsSnapshot } from './api'; export interface HermesOpsHistoryEntry { /** ISO-8601 generation time, unique enough for a key. */ generatedAt: string; /** Total warning count from the snapshot. */ warningCount: number; /** Number of instances passing all five health checks (matches the panel). */ healthyInstances: number; /** Per-instance freshest-backup-commit ISO timestamp, or null when unknown. */ backupFreshness: Record; } const STORAGE_KEY = 'bytelyst.hermesOpsHistory.v1'; const MAX_ENTRIES = 24; function safeRead(): HermesOpsHistoryEntry[] { if (typeof window === 'undefined') return []; try { const raw = window.localStorage.getItem(STORAGE_KEY); if (!raw) return []; const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) return []; // Guard each entry shape so a stale schema doesn't crash callers. return parsed.filter( (e) => e && typeof e.generatedAt === 'string' && typeof e.warningCount === 'number' && typeof e.healthyInstances === 'number', ); } catch { return []; } } function safeWrite(entries: HermesOpsHistoryEntry[]): void { if (typeof window === 'undefined') return; try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(entries)); } catch { // Quota exceeded / private mode — drop silently. Trend cards degrade // gracefully to "no history yet". } } /** * Record a new snapshot's derived metrics. Returns the trimmed history * AFTER the new entry is appended. De-dupes on `generatedAt` so the same * snapshot rendered twice (cache hit) doesn't double-count. */ export function recordHermesOpsSnapshot(snapshot: HermesOpsSnapshot): HermesOpsHistoryEntry[] { const existing = safeRead(); if (existing.some((e) => e.generatedAt === snapshot.generatedAt)) { return existing; } const healthyInstances = snapshot.instances.filter( (instance) => instance.gateway.active && instance.dashboard.active && instance.backup.timer.active && instance.backup.repo.clean && instance.google.workspaceToken, ).length; const backupFreshness: Record = {}; for (const instance of snapshot.instances) { backupFreshness[instance.id] = instance.backup.repo.lastCommitAt ?? null; } const entry: HermesOpsHistoryEntry = { generatedAt: snapshot.generatedAt, warningCount: snapshot.warnings.length, healthyInstances, backupFreshness, }; const next = [...existing, entry].slice(-MAX_ENTRIES); safeWrite(next); return next; } /** Read the current history (no side effects). */ export function getHermesOpsHistory(): HermesOpsHistoryEntry[] { return safeRead(); }