// What an entity window should say, and which sections it should show. // // The window used to render the same 13 collapsible sections for every entity, // sorted only by "does it have content" — so a host with 223 relations and // 2.7M metric samples looked exactly like an ingress route with three facts, // and Audit trail carried the same visual weight as Health. Worse, it could // never say *why* something was unhealthy: it rendered checks as configuration // ("ssh-script, every 60s, enabled") rather than as results. // // Both problems are decided here, as pure functions, so they can be tested // without a browser and without a database. import type { Check, Entity, EntityHealth } from '$lib/api' // ─── Verdict ────────────────────────────────────────────────────────────── export interface Verdict { health: EntityHealth | 'unmonitored' /** One line explaining the health, or '' when there is nothing to explain. */ reason: string /** Checks whose own verdict is worse than healthy, worst first. */ failing: Check[] passing: number total: number } const SEVERITY: Record = { down: 0, degraded: 1, stale: 2, unknown: 3, healthy: 4 } function severity(h: string | null | undefined): number { return SEVERITY[h ?? 'unknown'] ?? 3 } /** What a check is actually probing, for use in a human-readable reason. */ export function checkLabel(check: Check): string { const script = (check.config as Record | undefined)?.script if (typeof script === 'string' && script) return script.replace(/\.sh$/, '') return check.kind } /** * Derives the entity's health and a one-line reason from its own checks. * * Mirrors the backend's aggregation (WorstHealthForTarget): an entity is as * healthy as its unhealthiest check. Deriving it here as well means the header * can name the responsible probe, which the entity's stored health alone can * never do. */ export function deriveVerdict(entity: Entity | null, checks: Check[]): Verdict { const enabled = checks.filter((c) => c.enabled) const withVerdict = enabled.filter((c) => c.last_health) if (!entity) { return { health: 'unknown', reason: '', failing: [], passing: 0, total: 0 } } // No checks at all is a distinct state from "checks that all pass" — it is // the coverage gap the unmonitored signal reports, and saying "healthy" // here would be a lie by omission. if (enabled.length === 0) { return { health: 'unmonitored', reason: 'no checks configured for this entity', failing: [], passing: 0, total: 0 } } const failing = withVerdict .filter((c) => c.last_health && c.last_health !== 'healthy') .sort((a, b) => severity(a.last_health) - severity(b.last_health)) const passing = withVerdict.length - failing.length // Prefer the entity's stored health (the backend is authoritative and // accounts for staleness), falling back to the derived worst. const health: EntityHealth = entity.health ?? (failing[0]?.last_health as EntityHealth) ?? 'unknown' if (failing.length === 0) { const pending = enabled.length - withVerdict.length return { health, reason: pending > 0 ? `${passing} of ${enabled.length} checks passing, ${pending} not yet run` : '', failing, passing, total: enabled.length } } // Name the probes, not the count: "ping failing" is actionable in a way that // "1 check failing" is not. const names = failing.slice(0, 2).map(checkLabel) const more = failing.length - names.length const who = names.join(', ') + (more > 0 ? ` +${more} more` : '') const verb = failing[0].last_health === 'down' ? 'failing' : failing[0].last_health return { health, reason: `${who} ${verb} · ${passing} of ${enabled.length} checks passing`, failing, passing, total: enabled.length } } // ─── Section composition ────────────────────────────────────────────────── export type SectionKey = 'content' | 'status' | 'impact' | 'activity' | 'metrics' | 'reference' // Knowledge entities are documents: their content is the point, and they have // no checks, metrics or signals to show. const KNOWLEDGE_TYPES = new Set(['document', 'investigation', 'runbook']) // Records of something that happened, not things that can be observed. // Offering them monitoring sections is meaningless. const RECORD_TYPES = new Set([ 'execution', 'signal', 'check', 'approval', 'classification', 'feedback', 'pattern', 'skill' ]) const INFRASTRUCTURE: SectionKey[] = ['status', 'impact', 'activity', 'metrics', 'reference'] const KNOWLEDGE: SectionKey[] = ['content', 'impact', 'reference'] const RECORD: SectionKey[] = ['activity', 'reference'] /** * The sections this entity type should show, in order. * * Unknown types fall back to the infrastructure list rather than rendering * nothing, so a newly added entity type is never a blank window. */ export function sectionsForType(type: string): SectionKey[] { if (KNOWLEDGE_TYPES.has(type)) return KNOWLEDGE if (RECORD_TYPES.has(type)) return RECORD return INFRASTRUCTURE } /** Whether this type is worth showing monitoring affordances for at all. */ export function isObservable(type: string): boolean { return !KNOWLEDGE_TYPES.has(type) && !RECORD_TYPES.has(type) } // ─── Ask Nomos ──────────────────────────────────────────────────────────── /** * A task prompt scoped to what the operator is currently looking at, so * investigating is one click from seeing rather than a retyped question. */ export function nomosPrompt(entity: Entity, verdict: Verdict): string { if (verdict.health === 'unmonitored') { return `${entity.slug} has no checks configured. Work out what monitoring it should have and set it up.` } if (verdict.failing.length > 0) { return `Investigate ${entity.slug} — it reads ${verdict.health}: ${verdict.reason}. Find the cause and report what you find.` } return `Give me a status summary of ${entity.slug}: what it is, what depends on it, and anything that looks off.` }