Files
electric-cherry/src/lib/ui/maintenance-presentation.ts
T

87 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { MaintenanceJob, PlutoniumVersion } from '../plutonium.js';
import type { StatusTone } from './status.js';
export interface ScanErrorGroup {
key: string;
title: string;
description: string;
servers: string[];
details: { server: string | null; message: string }[];
}
const scanCauses = [
{ key: 'host', pattern: /(?:host|agent).*(?:offline|unavailable|disconnect|not connected)/i,
title: 'Host agent unavailable', description: 'The host agent could not complete the checks. Check its connection, then retry.' },
{ key: 'timeout', pattern: /timed?\s*out|timeout|no reply/i,
title: 'Checks timed out', description: 'The check did not receive a response in time. Check the server connection, then retry.' },
{ key: 'permissions', pattern: /permission denied|access denied|EACCES|EPERM/i,
title: 'File access denied', description: 'The host agent could not read files needed for the check. Review file permissions on the affected host.' },
{ key: 'files', pattern: /no such file|file.*not found|ENOENT|missing (?:file|directory)|cannot find.*file/i,
title: 'Files unavailable', description: 'Files needed for the check could not be found. Review the affected server’s files and retry.' },
{ key: 'response', pattern: /bad.reply|invalid (?:response|reply)|malformed|could not parse/i,
title: 'Unexpected response', description: 'The response could not be read. Inspect the connection and host logs before retrying.' },
{ key: 'setup', pattern: /register a native host instance/i,
title: 'No managed servers', description: 'Register a host and create a server before running automatic maintenance checks.' }
];
const otherCause = { key: 'other', title: 'Other check failures', description: 'Some checks could not finish. Review the technical details before retrying.' };
/** Worker errors use "server: detail" lines. Only known names become links. */
export function groupMaintenanceScanErrors(raw: string | null | undefined, knownServers: readonly string[]): ScanErrorGroup[] {
if (!raw?.trim()) return [];
const names = [...new Set(knownServers)].sort((a, b) => b.length - a.length);
const groups = new Map<string, ScanErrorGroup>();
for (const line of raw.split(/\r?\n/).map(value => value.trim()).filter(Boolean)) {
const server = names.find(name => line.startsWith(`${name}:`)) ?? null;
const message = server ? line.slice(server.length + 1).trim() : line;
const cause = scanCauses.find(cause => cause.pattern.test(message)) ?? otherCause;
let group = groups.get(cause.key);
if (!group) {
group = { key: cause.key, title: cause.title, description: cause.description, servers: [], details: [] };
groups.set(cause.key, group);
}
if (server && !group.servers.includes(server)) group.servers.push(server);
if (!group.details.some(detail => detail.server === server && detail.message === message)) group.details.push({ server, message });
}
return [...groups.values()];
}
type VersionStatus = NonNullable<PlutoniumVersion['update_status']>;
const versionStatuses: Record<VersionStatus, { label: string; tone: StatusTone }> = {
current: { label: 'Up to date', tone: 'online' },
outdated: { label: 'Update available', tone: 'warning' },
ahead: { label: 'Ahead of published release', tone: 'neutral' },
stopped: { label: 'Intentionally stopped', tone: 'neutral' },
unknown: { label: 'Unverified', tone: 'warning' }
};
export interface MaintenanceVersionGroup {
key: string;
revision: number | null;
status: VersionStatus;
label: string;
tone: StatusTone;
versions: PlutoniumVersion[];
pending: MaintenanceJob[];
updatable: string[];
}
/** Keep verification and pending work distinct even when revision numbers match. */
export function groupMaintenanceVersions(versions: readonly PlutoniumVersion[], jobs: readonly MaintenanceJob[]): MaintenanceVersionGroup[] {
const groups = new Map<string, MaintenanceVersionGroup>();
const pending = jobs.filter(job => !job.server_deleted_at && ['queued', 'restarting'].includes(job.state));
for (const version of versions) {
const status = version.update_status ?? (version.stopped ? 'stopped' : 'unknown');
const key = `${version.revision ?? 'unknown'}:${status}`;
let group = groups.get(key);
if (!group) {
group = { key, revision: version.revision, status, ...versionStatuses[status], versions: [], pending: [], updatable: [] };
groups.set(key, group);
}
group.versions.push(version);
const work = pending.filter(job => job.server === version.server);
group.pending.push(...work);
if (status === 'outdated' && !work.length) group.updatable.push(version.server);
}
const order: Record<VersionStatus, number> = { unknown: 0, outdated: 1, current: 2, ahead: 3, stopped: 4 };
return [...groups.values()].sort((a, b) => order[a.status] - order[b.status] || (b.revision ?? 0) - (a.revision ?? 0));
}