Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

1456 lines
43 KiB
JavaScript

import process from 'node:process';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Box, Text, render, useApp, useInput, useStdout } from 'ink';
import { selectDiscoveredSite } from './environment.mjs';
import { formatStatusReport, formatStatusSummary } from './format.mjs';
import { runRepairRelease, runReset, runSupportBundle } from './lifecycle.mjs';
import { collectStatus } from './status.mjs';
import { listAppliancePods, readPodLogsSince, readPodLogsTail } from './workloads.mjs';
const ACTION_GROUPS = [
{
title: 'Operations',
actions: ['Status', 'Workloads'],
},
{
title: 'System',
actions: ['Repair Release', 'Support Bundle', 'Reset'],
},
];
const ACTIONS = ACTION_GROUPS.flatMap((group) => group.actions);
const YES_NO_OPTIONS = ['yes', 'no'];
const BRAND_PRIMARY = 'magentaBright';
const BRAND_SECONDARY = 'cyanBright';
const TEXT_MUTED = 'white';
const COLOR_OK = 'greenBright';
const COLOR_WARN = 'yellowBright';
const COLOR_ERROR = 'redBright';
const WORKLOAD_REFRESH_MS = 5000;
const LOG_POLL_MS = 1500;
const LOG_VIEW_HEIGHT = 16;
const LOG_CHUNK_LINES = 120;
const LOG_MAX_LINES = 400;
function clampIndex(value, length) {
if (length <= 0) {
return 0;
}
if (value < 0) {
return length - 1;
}
if (value >= length) {
return 0;
}
return value;
}
function cycleOption(options, current, delta) {
if (!options.length) {
return current;
}
const index = options.indexOf(current);
const start = index >= 0 ? index : 0;
const next = (start + delta + options.length) % options.length;
return options[next];
}
function lineColor(line = '') {
const normalized = String(line).toLowerCase();
if (
normalized.includes('error') ||
normalized.includes('failed') ||
normalized.includes('unhealthy') ||
normalized.includes('blocked') ||
normalized.includes('timed out') ||
normalized.includes('crashloop') ||
normalized.includes('imagepullbackoff') ||
normalized.includes('not ready')
) {
return COLOR_ERROR;
}
if (
normalized.includes('warning') ||
normalized.includes('pending') ||
normalized.includes('reconciling') ||
normalized.includes('installing') ||
normalized.includes('unknown') ||
normalized.includes('unavailable')
) {
return COLOR_WARN;
}
if (
normalized.includes('healthy') ||
normalized.includes('ready') ||
normalized.includes('api reachable: true') ||
normalized.includes('no blocker detected') ||
normalized.includes('completed successfully') ||
normalized.includes('done:')
) {
return COLOR_OK;
}
return undefined;
}
function splitLabelValue(line = '') {
const text = String(line);
const separatorIndex = text.indexOf(': ');
if (separatorIndex <= 0) {
return null;
}
return {
label: text.slice(0, separatorIndex + 1),
value: text.slice(separatorIndex + 2),
};
}
function truncateText(value, width) {
const text = String(value ?? '');
if (text.length <= width) {
return text.padEnd(width);
}
if (width <= 1) {
return text.slice(0, width);
}
return `${text.slice(0, width - 1)}`;
}
function alignRight(value, width) {
return String(value ?? '').padStart(width);
}
const WORKLOAD_COLUMNS = {
pod: 31,
namespace: 12,
status: 18,
ready: 7,
restarts: 8,
age: 5,
};
function renderLines(lines, keyPrefix, options = {}) {
const { fallbackColor, bold = false } = options;
return lines.map((line, index) =>
{
const text = line || ' ';
const parts = splitLabelValue(text);
const valueColor = lineColor(text) || fallbackColor;
if (!parts) {
return React.createElement(
Text,
{
key: `${keyPrefix}-${index}`,
color: valueColor,
bold: bold && index === 0,
},
text,
);
}
return React.createElement(
Text,
{
key: `${keyPrefix}-${index}`,
bold: bold && index === 0,
},
React.createElement(Text, { color: BRAND_SECONDARY }, `${parts.label} `),
React.createElement(Text, { color: valueColor }, parts.value || ''),
);
},
);
}
function mapProgressEvent(event) {
if (event.type === 'phase') {
return `[${event.phase}] ${event.line || ''}`.trim();
}
if (event.type === 'error') {
return `ERROR: ${event.line}`;
}
if (event.type === 'done') {
return `DONE: ${event.line}`;
}
return event.line || '';
}
function makeResetDefaults(env) {
return {
challenge: '',
expected: `WIPE ${env.site?.siteId || env.suggestedSiteId || 'appliance-single-node'}`,
};
}
function makeRepairReleaseDefaults() {
return {
releaseName: 'alga-core',
cleanupWorkloads: 'yes',
};
}
function makeSupportBundleDefaults() {
return {
outputDir: process.cwd(),
};
}
function summarizeStatus(status) {
if (!status) {
return ['Status unavailable'];
}
return formatStatusSummary(status);
}
function shortStatusReport(status) {
if (!status) {
return ['Unable to collect status'];
}
const report = formatStatusReport(status);
return [
...report.summary,
'',
'Talos:',
...report.host,
'',
'Kubernetes:',
...report.cluster,
'',
'Flux:',
...report.flux,
'',
'Workloads:',
...report.workloads,
];
}
function trimLogLines(lines, maxLines = LOG_MAX_LINES) {
if (lines.length <= maxLines) {
return lines;
}
return lines.slice(lines.length - maxLines);
}
function applyLogTail(nextLines, previous = []) {
const merged = [];
for (const line of nextLines) {
merged.push({
id: `${line.timestamp || 'no-ts'}|${line.text}`,
timestamp: line.timestamp,
text: line.text,
});
}
if (!merged.length) {
return trimLogLines(previous);
}
return trimLogLines(merged);
}
function appendLiveLogLines(previous, incoming) {
if (!incoming.length) {
return previous;
}
const knownIds = new Set(previous.map((line) => line.id));
const next = [...previous];
for (const line of incoming) {
const row = {
id: `${line.timestamp || 'no-ts'}|${line.text}`,
timestamp: line.timestamp,
text: line.text,
};
if (!knownIds.has(row.id)) {
knownIds.add(row.id);
next.push(row);
}
}
return trimLogLines(next);
}
function prependOlderLines(previous, expandedTail) {
if (!expandedTail.length) {
return previous;
}
const normalized = expandedTail.map((line) => ({
id: `${line.timestamp || 'no-ts'}|${line.text}`,
timestamp: line.timestamp,
text: line.text,
}));
const existing = new Set(previous.map((line) => line.id));
const older = normalized.filter((line) => !existing.has(line.id));
if (!older.length) {
return previous;
}
return trimLogLines([...older, ...previous]);
}
function FieldList({ fields, values, selectedIndex }) {
return React.createElement(
Box,
{ flexDirection: 'column' },
...fields.map((field, index) => {
const selected = index === selectedIndex;
const pointer = selected ? '>' : ' ';
const rawValue = values[field.key] ?? '';
const suffix = field.type === 'secret' ? String(rawValue).replace(/./g, '*') : rawValue;
return React.createElement(
Text,
{ key: field.key, bold: selected },
React.createElement(Text, { color: selected ? BRAND_SECONDARY : BRAND_PRIMARY }, `${pointer} ${field.label}: `),
React.createElement(Text, { color: selected ? undefined : TEXT_MUTED }, suffix),
);
}),
);
}
function Header({ env, status }) {
const lines = summarizeStatus(status);
return React.createElement(
Box,
{ borderStyle: 'round', borderColor: BRAND_PRIMARY, paddingX: 1, flexDirection: 'column' },
React.createElement(Text, { bold: true, color: BRAND_PRIMARY }, 'Alga PSA Operator'),
React.createElement(
Text,
{ color: BRAND_SECONDARY },
`Site: ${env.site?.siteId || 'unselected'} Node IP: ${env.nodeIp || 'unknown'}`,
),
React.createElement(
Text,
{ color: TEXT_MUTED },
`Release: ${status?.release?.selectedReleaseVersion || 'unknown'}`,
),
...renderLines(lines.slice(0, 2), 'header-summary', { fallbackColor: TEXT_MUTED }),
);
}
function ActionNav({ selectedIndex, compactLayout }) {
return React.createElement(
Box,
{
borderStyle: 'round',
borderColor: BRAND_SECONDARY,
paddingX: 1,
flexDirection: 'column',
width: compactLayout ? undefined : 26,
minWidth: compactLayout ? undefined : 26,
flexShrink: 0,
},
React.createElement(Text, { bold: true, color: BRAND_PRIMARY }, 'Actions'),
...ACTION_GROUPS.flatMap((group) => {
const section = [
React.createElement(Text, { key: `${group.title}-title`, color: BRAND_SECONDARY, bold: true }, group.title),
];
for (const label of group.actions) {
const index = ACTIONS.indexOf(label);
const selected = index === selectedIndex;
const color = label === 'Reset'
? (selected ? COLOR_ERROR : COLOR_WARN)
: (selected ? BRAND_SECONDARY : TEXT_MUTED);
section.push(
React.createElement(
Text,
{
key: label,
color,
bold: selected,
},
`${selected ? '>' : ' '} ${label}`,
),
);
}
return section;
}),
);
}
function HelpStrip({ view, busy, formName }) {
let text = '↑/↓ move Enter select q quit';
if (view === 'site-select') {
text = '↑/↓ select site Enter confirm q quit';
} else if (view === 'form') {
text = `${formName}: ↑/↓ field ←/→ option type edit Backspace delete Enter confirm Esc back`;
} else if (view === 'confirm') {
text = `${formName}: Enter run Esc cancel`;
} else if (view === 'workloads') {
text = 'Workloads: ↑/↓ select pod Enter logs r refresh Esc home q quit';
} else if (view === 'logs') {
text = 'Logs: ↑/↓ scroll PgUp/PgDn page Enter older near top Esc back q quit';
} else if (view === 'running' || busy) {
text = 'Running action...';
}
return React.createElement(
Box,
{ borderStyle: 'round', borderColor: BRAND_SECONDARY, paddingX: 1 },
React.createElement(Text, { color: TEXT_MUTED }, text),
);
}
function WorkloadsPane({ workloads, workloadIndex, workloadNotice, loadingWorkloads }) {
const rows = workloads?.pods || [];
const start = Math.max(0, Math.min(workloadIndex - 8, Math.max(0, rows.length - 16)));
const visibleRows = rows.slice(start, start + 16);
const headerLine = [
truncateText('Pod', WORKLOAD_COLUMNS.pod),
truncateText('Namespace', WORKLOAD_COLUMNS.namespace),
truncateText('Status', WORKLOAD_COLUMNS.status),
alignRight('Ready', WORKLOAD_COLUMNS.ready),
alignRight('Restarts', WORKLOAD_COLUMNS.restarts),
alignRight('Age', WORKLOAD_COLUMNS.age),
].join(' ');
return React.createElement(
Box,
{ borderStyle: 'round', borderColor: BRAND_PRIMARY, paddingX: 1, flexDirection: 'column', flexGrow: 1 },
React.createElement(Text, { bold: true, color: BRAND_PRIMARY }, 'Workloads'),
React.createElement(
Text,
{ color: TEXT_MUTED },
`Namespace: ${(workloads?.namespaces || []).join(', ') || 'msp'}`,
),
React.createElement(Text, { color: TEXT_MUTED }, `Updated: ${workloads?.fetchedAt || 'pending...'}`),
loadingWorkloads ? React.createElement(Text, { color: COLOR_WARN }, 'Refreshing workload inventory...') : null,
React.createElement(Text, null, ''),
React.createElement(Text, { color: BRAND_SECONDARY, bold: true }, headerLine),
...(rows.length
? visibleRows.map((pod, index) => {
const absolute = start + index;
const selected = absolute === workloadIndex;
const pointer = selected ? '>' : ' ';
return React.createElement(
Text,
{
key: pod.key,
backgroundColor: selected ? BRAND_SECONDARY : undefined,
color: selected ? 'black' : undefined,
bold: selected,
},
React.createElement(Text, { color: selected ? 'black' : BRAND_SECONDARY }, `${pointer} `),
React.createElement(Text, { color: selected ? 'black' : TEXT_MUTED }, `${truncateText(pod.name, WORKLOAD_COLUMNS.pod)} `),
React.createElement(Text, { color: selected ? 'black' : TEXT_MUTED }, `${truncateText(pod.namespace, WORKLOAD_COLUMNS.namespace)} `),
React.createElement(Text, { color: selected ? 'black' : lineColor(pod.status) || TEXT_MUTED }, `${truncateText(pod.status, WORKLOAD_COLUMNS.status)} `),
React.createElement(Text, { color: selected ? 'black' : TEXT_MUTED }, `${alignRight(pod.ready, WORKLOAD_COLUMNS.ready)} `),
React.createElement(Text, { color: selected ? 'black' : TEXT_MUTED }, `${alignRight(pod.restarts, WORKLOAD_COLUMNS.restarts)} `),
React.createElement(Text, { color: selected ? 'black' : TEXT_MUTED }, alignRight(pod.age, WORKLOAD_COLUMNS.age)),
);
})
: [React.createElement(Text, { key: 'empty-workloads', color: TEXT_MUTED }, 'No appliance pods found.')]),
...(workloads?.errors || []).map((errorLine, index) =>
React.createElement(Text, { key: `error-${index}`, color: COLOR_WARN }, errorLine),
),
workloadNotice ? React.createElement(Text, { color: COLOR_WARN }, workloadNotice) : null,
);
}
function LogPane({ selectedPod, logState, logNotice, loadingOlder, loadingLogs }) {
const lines = logState?.lines || [];
const top = Math.max(0, logState?.top ?? 0);
const viewLines = lines.slice(top, top + LOG_VIEW_HEIGHT);
const followMode = !!logState?.follow;
return React.createElement(
Box,
{ borderStyle: 'round', borderColor: BRAND_PRIMARY, paddingX: 1, flexDirection: 'column', flexGrow: 1 },
React.createElement(Text, { bold: true, color: BRAND_PRIMARY }, `Logs: ${selectedPod?.namespace || ''}/${selectedPod?.name || ''}`),
React.createElement(
Text,
{ color: TEXT_MUTED },
`Lines: ${lines.length} Follow: ${followMode ? 'on' : 'paused'} Top: ${top + 1}`,
),
loadingLogs ? React.createElement(Text, { color: COLOR_WARN }, 'Loading logs...') : null,
loadingOlder ? React.createElement(Text, { color: COLOR_WARN }, 'Loading older log chunk...') : null,
React.createElement(Text, null, ''),
...(viewLines.length
? viewLines.map((line, index) =>
React.createElement(
Text,
{
key: `${line.id}-${index}`,
color: TEXT_MUTED,
},
line.text,
),
)
: [React.createElement(Text, { key: 'empty-logs', color: TEXT_MUTED }, 'No logs yet for selected pod.')]),
logNotice ? React.createElement(Text, { color: COLOR_WARN }, logNotice) : null,
);
}
function ProgressPane({ lines, maxLines = 12 }) {
const shown = lines.slice(-maxLines);
return React.createElement(
Box,
{
borderStyle: 'round',
borderColor: BRAND_SECONDARY,
paddingX: 1,
flexDirection: 'column',
minHeight: Math.min(maxLines + 2, 28),
},
React.createElement(Text, { bold: true, color: BRAND_PRIMARY }, 'Live Progress / Install Log'),
...(shown.length
? shown.map((line, index) =>
React.createElement(
Text,
{ key: `${index}-${line.slice(0, 8)}`, color: lineColor(line) || TEXT_MUTED },
line,
),
)
: [React.createElement(Text, { key: 'empty', color: TEXT_MUTED }, 'No lifecycle output yet.')]),
);
}
function RunningSummaryPane({ env, status, formType }) {
const canonical = status?.canonical || status || {};
const rollup = canonical.rollup || {};
const operation = canonical.activeOperations?.[0]?.message || rollup.message || 'Waiting for lifecycle output...';
const statusUrl = canonical.urls?.statusUrl || (env.nodeIp ? `http://${env.nodeIp}:8080` : 'unknown');
const loginUrl = canonical.urls?.loginUrl || env.appUrl || (env.nodeIp ? `http://${env.nodeIp}:3000` : 'unknown');
const blocker = canonical.topBlockers?.[0] || status?.topBlocker || null;
return React.createElement(
Box,
{ borderStyle: 'round', borderColor: BRAND_PRIMARY, paddingX: 1, flexDirection: 'column' },
React.createElement(Text, { bold: true, color: BRAND_PRIMARY }, `${formType || 'Operation'} status`),
React.createElement(Text, { color: BRAND_SECONDARY }, `Status page: ${statusUrl}`),
React.createElement(Text, { color: TEXT_MUTED }, `Login URL: ${loginUrl}`),
React.createElement(Text, { color: rollup.state === 'failed_action_required' ? COLOR_ERROR : BRAND_SECONDARY }, `Install state: ${rollup.state || canonical.status || 'unknown'}`),
React.createElement(Text, { color: TEXT_MUTED }, `Current operation: ${operation}`),
blocker
? React.createElement(Text, { color: COLOR_WARN }, `Blocker: ${blocker.reason || blocker.layer}`)
: React.createElement(Text, { color: TEXT_MUTED }, 'Blocker: none detected yet'),
);
}
function StatusPane({ status }) {
const lines = shortStatusReport(status);
return React.createElement(
Box,
{
borderStyle: 'round',
borderColor: BRAND_PRIMARY,
paddingX: 1,
flexDirection: 'column',
minWidth: 0,
width: undefined,
flexBasis: 44,
flexGrow: 1,
flexShrink: 1,
},
React.createElement(Text, { bold: true, color: BRAND_PRIMARY }, 'Status Dashboard'),
...renderLines(lines.slice(0, 20), 'dashboard', { fallbackColor: TEXT_MUTED }),
);
}
function MainPane({
view,
env,
status,
formType,
formValues,
formIndex,
siteIndex,
pendingAction,
notice,
result,
error,
workloads,
workloadIndex,
workloadNotice,
loadingWorkloads,
selectedPod,
logState,
logNotice,
loadingOlder,
loadingLogs,
}) {
if (view === 'site-select') {
const sites = env.siteIds || [];
return React.createElement(
Box,
{ borderStyle: 'round', borderColor: BRAND_PRIMARY, paddingX: 1, flexDirection: 'column', flexGrow: 1 },
React.createElement(Text, { bold: true, color: BRAND_PRIMARY }, 'Select Appliance Site'),
...sites.map((siteId, index) =>
React.createElement(
Text,
{
key: siteId,
color: index === siteIndex ? BRAND_SECONDARY : TEXT_MUTED,
bold: index === siteIndex,
},
`${index === siteIndex ? '>' : ' '} ${siteId}`,
),
),
React.createElement(Text, null, ''),
React.createElement(
Text,
{ color: TEXT_MUTED },
pendingAction
? `Select a discovered site to continue to ${pendingAction}.`
: 'Select a discovered site to continue.',
),
);
}
if (view === 'status') {
const report = formatStatusReport(status);
return React.createElement(
Box,
{ borderStyle: 'round', borderColor: BRAND_PRIMARY, paddingX: 1, flexDirection: 'column', flexGrow: 1 },
React.createElement(Text, { bold: true, color: BRAND_PRIMARY }, 'Status View'),
...renderLines(report.summary, 'status-summary', { fallbackColor: TEXT_MUTED }),
React.createElement(Text, null, ''),
React.createElement(Text, { bold: true, color: BRAND_SECONDARY }, 'Release'),
...renderLines(report.release, 'status-release', { fallbackColor: TEXT_MUTED }),
React.createElement(Text, null, ''),
React.createElement(Text, { bold: true, color: BRAND_SECONDARY }, 'Config Paths'),
...renderLines(report.paths, 'status-paths', { fallbackColor: TEXT_MUTED }),
);
}
if (view === 'workloads') {
return React.createElement(WorkloadsPane, {
workloads,
workloadIndex,
workloadNotice,
loadingWorkloads,
});
}
if (view === 'logs') {
return React.createElement(LogPane, {
selectedPod,
logState,
logNotice,
loadingOlder,
loadingLogs,
});
}
if (view === 'form') {
const fields = formFields(formType, env);
const title = `${formType} Form`;
return React.createElement(
Box,
{ borderStyle: 'round', borderColor: BRAND_PRIMARY, paddingX: 1, flexDirection: 'column', flexGrow: 1 },
React.createElement(Text, { bold: true, color: BRAND_PRIMARY }, title),
formType === 'Reset'
? React.createElement(
Box,
{ flexDirection: 'column' },
React.createElement(Text, { color: COLOR_ERROR, bold: true }, `Target appliance: ${env.site.siteId}`),
React.createElement(
Text,
{ color: COLOR_ERROR },
'Wipes namespace msp, namespace alga-system, and /var/mnt/alga-data/local-path-provisioner data.',
),
)
: null,
formType === 'Repair Release'
? React.createElement(
Text,
{ color: COLOR_WARN },
'Repairs a stuck alga-core release by cleaning failed workloads and reconciling the HelmRelease.',
)
: null,
React.createElement(FieldList, { fields, values: formValues, selectedIndex: formIndex }),
notice ? React.createElement(Text, { color: COLOR_WARN }, notice) : null,
);
}
if (view === 'confirm') {
return React.createElement(
Box,
{ borderStyle: 'round', borderColor: BRAND_PRIMARY, paddingX: 1, flexDirection: 'column', flexGrow: 1 },
React.createElement(Text, { bold: true, color: BRAND_PRIMARY }, `${formType} Confirmation`),
...Object.entries(formValues).map(([key, value]) =>
React.createElement(Text, { key, color: TEXT_MUTED }, `${key}: ${String(value)}`),
),
formType === 'Repair Release'
? React.createElement(Text, { color: COLOR_WARN }, 'Repair will clean up failed alga-core workloads before reconciling the release.')
: null,
React.createElement(Text, { color: TEXT_MUTED }, 'Press Enter to run, Esc to cancel.'),
);
}
if (view === 'running') {
return React.createElement(
Box,
{ borderStyle: 'round', borderColor: BRAND_PRIMARY, paddingX: 1, flexDirection: 'column', flexGrow: 1 },
React.createElement(Text, { bold: true, color: BRAND_PRIMARY }, `${formType} Running`),
React.createElement(Text, { color: TEXT_MUTED }, 'Streaming lifecycle progress in the panel below...'),
);
}
return React.createElement(
Box,
{ borderStyle: 'round', borderColor: BRAND_PRIMARY, paddingX: 1, flexDirection: 'column', flexGrow: 1 },
React.createElement(Text, { bold: true, color: BRAND_PRIMARY }, 'Home'),
React.createElement(Text, { color: TEXT_MUTED }, `Selected appliance: ${env.site?.siteId || 'unselected'}`),
React.createElement(Text, { color: TEXT_MUTED }, `Node IP: ${env.nodeIp || 'unknown'}`),
React.createElement(Text, { color: TEXT_MUTED }, `Current release: ${status?.release?.selectedReleaseVersion || 'unknown'}`),
React.createElement(Text, { color: TEXT_MUTED }, 'Use arrow keys and Enter to launch a flow.'),
notice ? React.createElement(Text, { color: COLOR_WARN }, notice) : null,
result
? React.createElement(Text, { color: result.ok ? COLOR_OK : COLOR_ERROR, bold: true }, result.message)
: null,
error ? React.createElement(Text, { color: COLOR_ERROR }, error) : null,
);
}
function formFields(formType, env) {
if (formType === 'Reset') {
return [{ key: 'challenge', label: `Type ${makeResetDefaults(env).expected}`, type: 'text' }];
}
if (formType === 'Repair Release') {
return [
{ key: 'releaseName', label: 'Release Name', type: 'text' },
{ key: 'cleanupWorkloads', label: 'Cleanup Failed Workloads', type: 'select', options: YES_NO_OPTIONS },
];
}
if (formType === 'Support Bundle') {
return [{ key: 'outputDir', label: 'Output Directory', type: 'text' }];
}
return [];
}
function initFormValues(formType, env) {
if (formType === 'Reset') {
return makeResetDefaults(env);
}
if (formType === 'Repair Release') {
return makeRepairReleaseDefaults();
}
if (formType === 'Support Bundle') {
return makeSupportBundleDefaults();
}
return {};
}
function normalizeChallenge(value) {
return String(value || '')
.trim()
.replace(/\s+/g, ' ');
}
function TuiApp({ initialEnv, actions, onExit }) {
const { exit } = useApp();
const { stdout } = useStdout();
const [env, setEnv] = useState(initialEnv);
const [status, setStatus] = useState(null);
const [view, setView] = useState('home');
const [actionIndex, setActionIndex] = useState(0);
const [siteIndex, setSiteIndex] = useState(0);
const [pendingAction, setPendingAction] = useState('');
const [formType, setFormType] = useState('');
const [formValues, setFormValues] = useState({});
const [formIndex, setFormIndex] = useState(0);
const [progressLines, setProgressLines] = useState([]);
const [busy, setBusy] = useState(false);
const [notice, setNotice] = useState('');
const [result, setResult] = useState(null);
const [error, setError] = useState('');
const [workloads, setWorkloads] = useState({ fetchedAt: null, namespaces: [], pods: [], errors: [] });
const [loadingWorkloads, setLoadingWorkloads] = useState(false);
const [workloadNotice, setWorkloadNotice] = useState('');
const [workloadIndex, setWorkloadIndex] = useState(0);
const [logState, setLogState] = useState({
lines: [],
top: 0,
follow: true,
tailLines: LOG_CHUNK_LINES,
lastTimestamp: null,
});
const [loadingLogs, setLoadingLogs] = useState(false);
const [loadingOlder, setLoadingOlder] = useState(false);
const [logNotice, setLogNotice] = useState('');
const [viewportWidth, setViewportWidth] = useState(stdout?.columns || process.stdout.columns || 140);
const selectedPodRef = useRef(null);
const compactLayout = viewportWidth < 140;
const showProgressPane = busy || view === 'running' || progressLines.length > 0;
const selectedPod = workloads.pods[workloadIndex] || null;
useEffect(() => {
if (!stdout || typeof stdout.on !== 'function') {
return undefined;
}
let resizeTimer = null;
const handleResize = () => {
if (resizeTimer) {
clearTimeout(resizeTimer);
}
resizeTimer = setTimeout(() => {
setViewportWidth(stdout.columns || process.stdout.columns || 140);
}, 80);
};
stdout.on('resize', handleResize);
return () => {
if (resizeTimer) {
clearTimeout(resizeTimer);
}
stdout.off('resize', handleResize);
};
}, [stdout]);
const refreshStatus = useMemo(
() => async () => {
if (!env.site) {
setStatus(null);
return;
}
try {
const next = await actions.collectStatus(env);
setStatus(next);
setError('');
} catch (err) {
setError(err.message || String(err));
}
},
[actions, env],
);
const refreshWorkloads = useMemo(
() => async () => {
if (!env.site) {
setWorkloads({ fetchedAt: null, namespaces: [], pods: [], errors: [] });
return;
}
setLoadingWorkloads(true);
try {
const next = await actions.listAppliancePods(env);
setWorkloads(next);
setWorkloadNotice('');
setWorkloadIndex((current) => {
if (selectedPodRef.current) {
const byRef = next.pods.findIndex((entry) => entry.key === selectedPodRef.current.key);
if (byRef >= 0) {
return byRef;
}
}
return clampIndex(current, next.pods.length);
});
} catch (err) {
setWorkloadNotice(err.message || String(err));
} finally {
setLoadingWorkloads(false);
}
},
[actions, env],
);
useEffect(() => {
if (!env.siteSelectionRequired && env.site) {
refreshStatus();
}
}, [env, refreshStatus]);
useEffect(() => {
selectedPodRef.current = selectedPod;
}, [selectedPod]);
useEffect(() => {
if (view !== 'workloads') {
return undefined;
}
refreshWorkloads();
const timer = setInterval(() => {
refreshWorkloads();
}, WORKLOAD_REFRESH_MS);
return () => clearInterval(timer);
}, [refreshWorkloads, view]);
useEffect(() => {
if (view !== 'running') {
return undefined;
}
refreshStatus();
const timer = setInterval(() => {
refreshStatus();
}, 5000);
return () => clearInterval(timer);
}, [refreshStatus, view]);
async function executeFormAction() {
setBusy(true);
setView('running');
setNotice('');
setResult(null);
const append = (line) => {
if (!line || !line.trim()) {
return;
}
setProgressLines((prev) => [...prev.slice(-199), line]);
};
try {
let output;
if (formType === 'Reset') {
output = await actions.runReset(env, {
onProgress: (event) => append(mapProgressEvent(event)),
});
} else if (formType === 'Repair Release') {
output = await actions.runRepairRelease(env, {
...formValues,
cleanupWorkloads: formValues.cleanupWorkloads !== 'no',
onProgress: (event) => append(mapProgressEvent(event)),
});
} else if (formType === 'Support Bundle') {
output = await actions.runSupportBundle(env, {
...formValues,
onProgress: (event) => append(mapProgressEvent(event)),
});
} else {
output = { ok: true };
}
if (!output.ok) {
const layer = output.failureLayer ? ` Failure layer: ${output.failureLayer}.` : '';
setResult({
ok: false,
message: `${formType} failed.${layer}`.trim(),
});
} else {
setResult({ ok: true, message: `${formType} completed successfully.` });
}
} catch (err) {
setResult({ ok: false, message: `${formType} failed: ${err.message || String(err)}` });
} finally {
setBusy(false);
setView('home');
setFormType('');
setFormValues({});
setFormIndex(0);
await refreshStatus();
}
}
async function openLogViewerForPod(pod) {
if (!pod) {
setLogNotice('No pod selected.');
return;
}
setLoadingLogs(true);
setLogNotice('');
try {
const fetched = await actions.readPodLogsTail(env, pod, { tailLines: LOG_CHUNK_LINES });
if (!fetched.ok) {
setLogState({
lines: [],
top: 0,
follow: true,
tailLines: LOG_CHUNK_LINES,
lastTimestamp: null,
});
setLogNotice(fetched.error || 'Unable to load pod logs.');
} else {
const lines = applyLogTail(fetched.lines);
const lastTimestamp = fetched.lines.at(-1)?.timestamp || null;
setLogState({
lines,
top: Math.max(0, lines.length - LOG_VIEW_HEIGHT),
follow: true,
tailLines: LOG_CHUNK_LINES,
lastTimestamp,
});
}
setView('logs');
} catch (err) {
setLogNotice(err.message || String(err));
} finally {
setLoadingLogs(false);
}
}
async function loadOlderLogs() {
if (!selectedPod || loadingOlder || loadingLogs) {
return;
}
setLoadingOlder(true);
setLogNotice('');
try {
const nextTail = (logState.tailLines || LOG_CHUNK_LINES) + LOG_CHUNK_LINES;
const fetched = await actions.readPodLogsTail(env, selectedPod, { tailLines: nextTail });
if (!fetched.ok) {
setLogNotice(fetched.error || 'Unable to load older logs.');
return;
}
setLogState((previous) => {
const merged = prependOlderLines(previous.lines, fetched.lines);
const shifted = Math.max(0, merged.length - previous.lines.length);
const lastTimestamp = fetched.lines.at(-1)?.timestamp || previous.lastTimestamp || null;
return {
...previous,
lines: merged,
tailLines: nextTail,
top: previous.top + shifted,
lastTimestamp,
};
});
} catch (err) {
setLogNotice(err.message || String(err));
} finally {
setLoadingOlder(false);
}
}
useEffect(() => {
if (view !== 'logs' || !selectedPod || !logState.follow) {
return undefined;
}
const timer = setInterval(async () => {
try {
const fetched = await actions.readPodLogsSince(env, selectedPod, { sinceTime: logState.lastTimestamp });
if (!fetched.ok || !fetched.lines.length) {
return;
}
setLogState((previous) => {
const lines = appendLiveLogLines(previous.lines, fetched.lines);
const top = Math.max(0, lines.length - LOG_VIEW_HEIGHT);
return {
...previous,
lines,
top,
lastTimestamp: fetched.lines.at(-1)?.timestamp || previous.lastTimestamp || null,
};
});
} catch {
// Keep live follow resilient; surface hard failures only on direct user actions.
}
}, LOG_POLL_MS);
return () => clearInterval(timer);
}, [actions, env, logState.follow, logState.lastTimestamp, selectedPod, view]);
function openAction(action) {
setNotice('');
setResult(null);
if (!env.site) {
if (!(env.siteIds || []).length) {
setNotice('No appliance sites discovered. Run setup from the appliance web UI first.');
setView('home');
return;
}
setPendingAction(action);
setSiteIndex(0);
setView('site-select');
return;
}
if (action === 'Status') {
setView('status');
return;
}
if (action === 'Workloads') {
setView('workloads');
return;
}
setFormType(action);
setFormValues(initFormValues(action, env));
setFormIndex(0);
setView('form');
}
useInput((input, key) => {
if (key.ctrl && input === 'c') {
onExit(0);
exit();
return;
}
if (busy || view === 'running') {
return;
}
if (input === 'q') {
onExit(0);
exit();
return;
}
if (view === 'site-select') {
const sites = env.siteIds || [];
if (!sites.length) {
setView('home');
return;
}
if (key.escape) {
setPendingAction('');
setView('home');
return;
}
if (key.upArrow || input === 'k') {
setSiteIndex((value) => clampIndex(value - 1, sites.length));
return;
}
if (key.downArrow || input === 'j') {
setSiteIndex((value) => clampIndex(value + 1, sites.length));
return;
}
if (key.return) {
const selected = sites[siteIndex] || sites[0];
if (!selected) {
setNotice('No discovered site selected.');
return;
}
const selectedEnv = selectDiscoveredSite(env, selected);
setEnv(selectedEnv);
const action = pendingAction;
setPendingAction('');
if (action === 'Status') {
setView('status');
return;
}
if (action === 'Workloads') {
setView('workloads');
return;
}
if (action) {
setFormType(action);
setFormValues(initFormValues(action, selectedEnv));
setFormIndex(0);
setView('form');
return;
}
setView('home');
}
return;
}
if (view === 'status') {
if (key.escape || input === 'h') {
setView('home');
}
if (input === 'r') {
refreshStatus();
}
return;
}
if (view === 'workloads') {
if (key.escape || input === 'h') {
setView('home');
return;
}
if (key.upArrow || input === 'k') {
setWorkloadIndex((value) => clampIndex(value - 1, workloads.pods.length));
return;
}
if (key.downArrow || input === 'j') {
setWorkloadIndex((value) => clampIndex(value + 1, workloads.pods.length));
return;
}
if (input === 'r') {
refreshWorkloads();
return;
}
if (key.return) {
openLogViewerForPod(selectedPod);
}
return;
}
if (view === 'logs') {
if (key.escape || input === 'h') {
setView('workloads');
return;
}
if (key.pageUp) {
setLogState((previous) => {
const top = Math.max(0, previous.top - LOG_VIEW_HEIGHT);
return {
...previous,
top,
follow: top + LOG_VIEW_HEIGHT >= previous.lines.length,
};
});
return;
}
if (key.pageDown) {
setLogState((previous) => {
const maxTop = Math.max(0, previous.lines.length - LOG_VIEW_HEIGHT);
const top = Math.min(maxTop, previous.top + LOG_VIEW_HEIGHT);
return {
...previous,
top,
follow: top >= maxTop,
};
});
return;
}
if (key.upArrow || input === 'k') {
setLogState((previous) => {
const top = Math.max(0, previous.top - 1);
return {
...previous,
top,
follow: false,
};
});
if ((logState.top || 0) <= 1) {
loadOlderLogs();
}
return;
}
if (key.downArrow || input === 'j') {
setLogState((previous) => {
const maxTop = Math.max(0, previous.lines.length - LOG_VIEW_HEIGHT);
const top = Math.min(maxTop, previous.top + 1);
return {
...previous,
top,
follow: top >= maxTop,
};
});
return;
}
if (key.return && (logState.top || 0) <= 1) {
loadOlderLogs();
}
return;
}
if (view === 'home') {
if (key.upArrow || input === 'k') {
setActionIndex((value) => clampIndex(value - 1, ACTIONS.length));
return;
}
if (key.downArrow || input === 'j') {
setActionIndex((value) => clampIndex(value + 1, ACTIONS.length));
return;
}
if (input === 'r') {
refreshStatus();
return;
}
if (key.return) {
openAction(ACTIONS[actionIndex]);
}
return;
}
if (view === 'form') {
const fields = formFields(formType, env);
if (key.escape) {
setView('home');
setFormType('');
setFormValues({});
setFormIndex(0);
return;
}
if (key.upArrow || input === 'k') {
setFormIndex((value) => clampIndex(value - 1, fields.length));
return;
}
if (key.downArrow || key.tab || input === 'j') {
setFormIndex((value) => clampIndex(value + 1, fields.length));
return;
}
const field = fields[formIndex];
if (!field) {
return;
}
if (field.type === 'select' && (key.leftArrow || key.rightArrow || input === 'h' || input === 'l')) {
const delta = key.rightArrow || input === 'l' ? 1 : -1;
setFormValues((prev) => ({
...prev,
[field.key]: cycleOption(field.options || [], prev[field.key], delta),
}));
return;
}
if (key.return) {
if (formType === 'Reset') {
const expected = makeResetDefaults(env).expected;
if (normalizeChallenge(formValues.challenge) !== normalizeChallenge(expected)) {
setNotice(`Reset confirmation mismatch. Type ${expected}.`);
return;
}
}
setNotice('');
setView('confirm');
return;
}
if (field.type === 'text') {
if (key.backspace || key.delete) {
setFormValues((prev) => ({
...prev,
[field.key]: String(prev[field.key] || '').slice(0, -1),
}));
return;
}
if (input && !key.meta && !key.ctrl) {
setFormValues((prev) => ({
...prev,
[field.key]: `${String(prev[field.key] || '')}${input}`,
}));
}
}
return;
}
if (view === 'confirm') {
if (key.escape) {
setView('form');
return;
}
if (key.return) {
executeFormAction();
}
}
});
if (view === 'running') {
return React.createElement(
Box,
{ flexDirection: 'column', width: '100%' },
React.createElement(RunningSummaryPane, { env, status, formType }),
React.createElement(Box, { marginTop: 1 }, React.createElement(ProgressPane, { lines: progressLines, maxLines: 24 })),
React.createElement(Box, { marginTop: 1 }, React.createElement(HelpStrip, { view, busy, formName: formType || 'Operation' })),
);
}
return React.createElement(
Box,
{ flexDirection: 'column', width: '100%' },
React.createElement(Header, { env, status }),
React.createElement(
Box,
{ flexDirection: compactLayout ? 'column' : 'row', marginTop: 1, width: '100%' },
React.createElement(ActionNav, { selectedIndex: actionIndex, compactLayout }),
React.createElement(
Box,
{
marginLeft: compactLayout ? 0 : 1,
marginTop: compactLayout ? 1 : 0,
flexGrow: 1,
flexBasis: compactLayout ? undefined : 0,
flexShrink: 1,
minWidth: 0,
},
React.createElement(MainPane, {
view,
env,
status,
formType,
formValues,
formIndex,
siteIndex,
pendingAction,
notice,
result,
error,
workloads,
workloadIndex,
workloadNotice,
loadingWorkloads,
selectedPod,
logState,
logNotice,
loadingOlder,
loadingLogs,
}),
),
React.createElement(
Box,
{
marginLeft: compactLayout ? 0 : 1,
marginTop: compactLayout ? 1 : 0,
flexGrow: compactLayout ? 0 : 1,
flexBasis: compactLayout ? undefined : 0,
flexShrink: 1,
minWidth: 0,
},
React.createElement(StatusPane, { status }),
),
),
showProgressPane
? React.createElement(Box, { marginTop: 1 }, React.createElement(ProgressPane, { lines: progressLines, maxLines: 10 }))
: null,
React.createElement(Box, { marginTop: 1 }, React.createElement(HelpStrip, { view, busy, formName: formType || 'Home' })),
);
}
function createTuiActions(overrides = {}) {
return {
collectStatus: overrides.collectStatus || collectStatus,
runRepairRelease: overrides.runRepairRelease || runRepairRelease,
runReset: overrides.runReset || runReset,
runSupportBundle: overrides.runSupportBundle || runSupportBundle,
listAppliancePods: overrides.listAppliancePods || listAppliancePods,
readPodLogsTail: overrides.readPodLogsTail || readPodLogsTail,
readPodLogsSince: overrides.readPodLogsSince || readPodLogsSince,
};
}
export async function runTui(env, options = {}) {
const actions = createTuiActions(options);
let exitCode = 0;
const enterAlt = process.stdout.isTTY;
if (enterAlt) {
process.stdout.write('\u001B[?1049h\u001B[H');
}
try {
const app = render(
React.createElement(TuiApp, {
initialEnv: env,
actions,
onExit: (code) => {
exitCode = code;
},
}),
{
exitOnCtrlC: true,
},
);
await app.waitUntilExit();
return exitCode;
} finally {
if (enterAlt) {
process.stdout.write('\u001B[?1049l');
}
}
}
export { TuiApp, createTuiActions, formFields, initFormValues, mapProgressEvent };