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

1406 lines
45 KiB
TypeScript

"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
Activity,
Boxes,
ScrollText,
Server,
SlidersHorizontal,
} from "lucide-react";
import { AlgaLogo } from "./AlgaLogo";
import { LogoutButton } from "./auth/LogoutButton";
import styles from "./status.module.css";
type RawTierMap = Record<
string,
boolean | { ready?: boolean; status?: string }
>;
type Blocker = {
severity?: string;
component?: string;
layer?: string;
reason?: string;
nextAction?: string;
loginBlocking?: boolean;
};
type EventItem = {
type?: string;
reason?: string;
namespace?: string;
involvedObject?: string;
message?: string;
timestamp?: string | null;
};
type SetupConfigResponse = { mode?: string };
type StatusResponse = {
status?: string;
// True when setup is blocked on a correctable install code; the UI offers a
// "re-enter your install code" action and the /setup form is reachable again.
setupReEditable?: boolean;
rollup?: { state?: string; message?: string; nextAction?: string } | null;
currentPhase?: string;
urls?: { statusUrl?: string | null; loginUrl?: string | null };
activeOperations?: Array<{
component?: string;
image?: string | null;
message?: string;
elapsedSeconds?: number | null;
}>;
tiers?: RawTierMap;
readinessTiers?: RawTierMap;
topBlockers?: Blocker[];
failures?: Array<{
category?: string;
phase?: string;
suspectedCause?: string;
suggestedNextStep?: string;
}>;
bootstrap?: {
job?: {
name?: string | null;
state?: string;
failed?: boolean;
completed?: boolean;
};
logs?: { available?: boolean; tail?: string[]; detectedErrors?: string[] };
};
recentEvents?: EventItem[];
installState?: {
status?: string;
phase?: string;
lastAction?: string;
updatedAt?: string;
};
kubernetes?: {
nodes?: Array<{ name?: string; ready?: boolean }>;
podCount?: number;
jobCount?: number;
helmReleaseCount?: number;
warnings?: string[];
};
diagnostics?: Array<{
name?: string;
ok?: boolean;
status?: number;
command?: string;
stdout?: string;
stderr?: string;
}>;
};
type NamespaceItem = { name: string; phase?: string };
type Deployment = {
namespace: string;
name: string;
readyReplicas: number;
replicas: number;
updatedReplicas: number;
availableReplicas: number;
generation: number;
observedGeneration: number;
strategy?: string;
images?: string[];
revision?: string | null;
conditions?: Array<{
type?: string;
status?: string;
reason?: string;
message?: string;
}>;
replicaSets?: Array<{
name: string;
revision?: string | null;
replicas: number;
readyReplicas: number;
availableReplicas: number;
createdAt?: string | null;
images?: string[];
}>;
};
type Pod = {
namespace: string;
name: string;
phase: string;
reason?: string | null;
ready: boolean;
readyContainers: number;
totalContainers: number;
restarts: number;
node?: string | null;
podIP?: string | null;
createdAt?: string | null;
containers: Array<{
name: string;
image?: string;
ready?: boolean;
restarts?: number;
state?: Record<string, unknown> | null;
}>;
};
type Tab = "overview" | "deployments" | "pods" | "logs";
type LogLoadOptions = { preserveScroll?: boolean; scrollToEnd?: boolean };
function apiPath(
path: string,
params: Record<string, string | number | boolean | null | undefined> = {},
) {
const search = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== null && value !== undefined && value !== "")
search.set(key, String(value));
}
const qs = search.toString();
return qs ? `${path}?${qs}` : path;
}
function badgeClass(value?: string | boolean) {
const normalized = String(value ?? "unknown");
if (["loading"].includes(normalized)) return styles.loading;
if (
[
"true",
"fully_healthy",
"ready_to_log_in",
"ready_with_background_issues",
"healthy",
"Running",
"Succeeded",
"ready",
"True",
].includes(normalized)
)
return styles.ready;
if (
[
"installing",
"progressing",
"unknown",
"Pending",
"ContainerCreating",
"PodInitializing",
"setup-queued",
"not_fully_healthy",
].includes(normalized)
)
return styles.installing;
if (
[
"false",
"not ready",
"warning",
"background",
"degraded_background_services",
"Unknown",
].includes(normalized)
)
return styles.warning;
return styles.failed;
}
function tierEntries(status: StatusResponse | null) {
const source = status?.readinessTiers || status?.tiers || {};
return Object.entries(source).map(([name, value]) => {
if (typeof value === "boolean")
return [
name,
{ ready: value, status: value ? "ready" : "not ready" },
] as const;
return [
name,
{
ready: Boolean(value?.ready),
status: value?.status || (value?.ready ? "ready" : "not ready"),
},
] as const;
});
}
function blockers(status: StatusResponse | null): Blocker[] {
if (status?.topBlockers?.length) return status.topBlockers;
return (status?.failures || []).map((failure) => ({
severity:
failure.category === "background-services" ? "background" : "critical",
component: failure.category,
layer: failure.phase,
reason: failure.suspectedCause,
nextAction: failure.suggestedNextStep,
loginBlocking: failure.category !== "background-services",
}));
}
function ageFrom(date?: string | null) {
if (!date) return "—";
const seconds = Math.max(
0,
Math.floor((Date.now() - new Date(date).getTime()) / 1000),
);
if (seconds < 60) return `${seconds}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
return `${Math.floor(seconds / 86400)}d`;
}
function elapsedLabel(seconds?: number | null) {
if (seconds === null || seconds === undefined)
return "elapsed time unavailable";
if (seconds < 60) return `${Math.max(0, Math.floor(seconds))}s elapsed`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m elapsed`;
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m elapsed`;
}
const statusTabs = [
{ value: "overview", label: "Overview", Icon: Activity },
{ value: "deployments", label: "Deployments", Icon: Boxes },
{ value: "pods", label: "Pods", Icon: Server },
{ value: "logs", label: "Logs", Icon: ScrollText },
] satisfies Array<{ value: Tab; label: string; Icon: typeof Activity }>;
function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function highlightLine(line: string, search: string) {
if (!search) return line;
const parts = line.split(new RegExp(`(${escapeRegExp(search)})`, "ig"));
return (
<>
{parts.map((part, index) =>
part.toLowerCase() === search.toLowerCase() ? (
<mark key={index}>{part}</mark>
) : (
part
),
)}
</>
);
}
function SkeletonRows({
rows = 6,
columns = 6,
}: {
rows?: number;
columns?: number;
}) {
return (
<>
{Array.from({ length: rows }).map((_, row) => (
<tr key={row} className={styles.skeletonRow}>
{Array.from({ length: columns }).map((__, col) => (
<td key={col}>
<span className={styles.skeletonCell} />
</td>
))}
</tr>
))}
</>
);
}
function SkeletonBlock({ lines = 6 }: { lines?: number }) {
return (
<div className={styles.skeletonBlock}>
{Array.from({ length: lines }).map((_, index) => (
<span key={index} className={styles.skeletonLineDark} />
))}
</div>
);
}
export default function StatusPage() {
const [activeTab, setActiveTab] = useState<Tab>("overview");
const [status, setStatus] = useState<StatusResponse | null>(null);
const [setupMode, setSetupMode] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [recovering, setRecovering] = useState(false);
const [recoverMsg, setRecoverMsg] = useState<string | null>(null);
const [confirmRecover, setConfirmRecover] = useState(false);
const [namespaces, setNamespaces] = useState<NamespaceItem[]>([]);
const [namespace, setNamespace] = useState("msp");
const [deployments, setDeployments] = useState<Deployment[]>([]);
const [pods, setPods] = useState<Pod[]>([]);
const [selectedPod, setSelectedPod] = useState("");
const [selectedContainer, setSelectedContainer] = useState("");
const [deploymentFilter, setDeploymentFilter] = useState("");
const [podFilter, setPodFilter] = useState("");
const [logFilter, setLogFilter] = useState("");
const [logSearch, setLogSearch] = useState("");
const [activeMatch, setActiveMatch] = useState(0);
const [logTail, setLogTail] = useState(200);
const [logLines, setLogLines] = useState<string[]>([]);
const [logError, setLogError] = useState<string | null>(null);
const [loadingStatus, setLoadingStatus] = useState(true);
const [loadingNamespaces, setLoadingNamespaces] = useState(true);
const [loadingDeployments, setLoadingDeployments] = useState(false);
const [loadingPods, setLoadingPods] = useState(false);
const [loadingLogs, setLoadingLogs] = useState(false);
const logPaneRef = useRef<HTMLPreElement | null>(null);
const lineRefs = useRef<Array<HTMLDivElement | null>>([]);
const pendingLogScroll = useRef<null | {
mode: "preserve" | "end";
previousScrollHeight: number;
previousScrollTop: number;
}>(null);
const statusRequestInFlight = useRef(false);
const statusAbortController = useRef<AbortController | null>(null);
const loadStatus = useCallback(async () => {
if (statusRequestInFlight.current) return;
const controller = new AbortController();
statusRequestInFlight.current = true;
statusAbortController.current = controller;
setLoadingStatus(true);
try {
const response = await fetch(apiPath("/api/status"), {
cache: "no-store",
signal: controller.signal,
});
if (response.status === 401) {
window.location.reload();
return;
}
if (!response.ok) throw new Error("Status API unavailable.");
setStatus(await response.json());
setError(null);
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") return;
setError(err instanceof Error ? err.message : String(err));
} finally {
if (statusAbortController.current === controller) {
statusRequestInFlight.current = false;
statusAbortController.current = null;
setLoadingStatus(false);
}
}
}, []);
const recoverBootstrap = useCallback(async () => {
if (recovering) return;
setRecovering(true);
setRecoverMsg(null);
try {
const response = await fetch(apiPath("/api/recover"), {
method: "POST",
cache: "no-store",
});
const data = await response.json().catch(() => ({}));
if (!response.ok)
throw new Error(data.error || "Failed to trigger recovery.");
setRecoverMsg(data.message || "Recovery triggered.");
loadStatus();
} catch (err) {
setRecoverMsg(err instanceof Error ? err.message : String(err));
} finally {
setRecovering(false);
}
}, [recovering, loadStatus]);
const loadNamespaces = useCallback(async () => {
setLoadingNamespaces(true);
try {
const response = await fetch(apiPath("/api/k8s/namespaces"), {
cache: "no-store",
});
if (!response.ok) return;
const data = await response.json();
setNamespaces(data.namespaces || []);
} catch {
/* cluster may not exist yet */
} finally {
setLoadingNamespaces(false);
}
}, []);
const loadPods = useCallback(async () => {
setLoadingPods(true);
try {
const response = await fetch(apiPath("/api/k8s/pods", { namespace }), {
cache: "no-store",
});
if (!response.ok) return;
const data = await response.json();
const nextPods = data.pods || [];
setPods(nextPods);
if (!selectedPod && nextPods.length) {
setSelectedPod(nextPods[0].name);
setSelectedContainer(nextPods[0].containers?.[0]?.name || "");
}
} catch {
/* ignore transient Kubernetes failures */
} finally {
setLoadingPods(false);
}
}, [namespace, selectedPod]);
const loadDeployments = useCallback(async () => {
setLoadingDeployments(true);
try {
const response = await fetch(
apiPath("/api/k8s/deployments", { namespace }),
{ cache: "no-store" },
);
if (!response.ok) return;
const data = await response.json();
setDeployments(data.deployments || []);
} catch {
/* ignore transient Kubernetes failures */
} finally {
setLoadingDeployments(false);
}
}, [namespace]);
function applyPendingLogScroll() {
const pending = pendingLogScroll.current;
const pane = logPaneRef.current;
if (!pending || !pane) return;
if (pending.mode === "end") {
pane.scrollTop = pane.scrollHeight;
} else {
pane.scrollTop =
pane.scrollHeight -
pending.previousScrollHeight +
pending.previousScrollTop;
}
pendingLogScroll.current = null;
}
const loadLogs = useCallback(
async (tail = logTail, options: LogLoadOptions = {}) => {
if (!selectedPod) return;
const pane = logPaneRef.current;
const previousScrollHeight = pane?.scrollHeight || 0;
const previousScrollTop = pane?.scrollTop || 0;
setLoadingLogs(true);
try {
setLogError(null);
const response = await fetch(
apiPath("/api/k8s/logs", {
namespace,
pod: selectedPod,
container: selectedContainer,
tail,
}),
{ cache: "no-store" },
);
if (!response.ok)
throw new Error(
(await response.json()).error || "Unable to read logs.",
);
const data = await response.json();
if (options.preserveScroll) {
pendingLogScroll.current = {
mode: "preserve",
previousScrollHeight,
previousScrollTop,
};
} else if (options.scrollToEnd) {
pendingLogScroll.current = {
mode: "end",
previousScrollHeight,
previousScrollTop,
};
}
setLogLines(data.lines || []);
setLogTail(tail);
window.setTimeout(applyPendingLogScroll, 50);
window.setTimeout(applyPendingLogScroll, 250);
} catch (err) {
setLogError(err instanceof Error ? err.message : String(err));
} finally {
setLoadingLogs(false);
}
},
[logTail, namespace, selectedContainer, selectedPod],
);
useEffect(() => {
let cancelled = false;
async function loadSetupMode() {
try {
const response = await fetch(apiPath("/api/setup/config"), {
cache: "no-store",
});
if (response.status === 401) {
window.location.reload();
return;
}
if (!response.ok) return;
const data = (await response.json()) as SetupConfigResponse;
if (cancelled) return;
setSetupMode(data.mode || null);
if (data.mode === "setup") {
window.location.replace("/setup/");
}
} catch {
// Keep the status page usable if setup config is temporarily unavailable.
}
}
loadSetupMode();
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
loadStatus();
loadNamespaces();
const timer = setInterval(loadStatus, 15000);
return () => {
clearInterval(timer);
statusAbortController.current?.abort();
};
}, [loadNamespaces, loadStatus]);
useEffect(() => {
if (activeTab === "deployments") loadDeployments();
if (activeTab === "pods" || activeTab === "logs") loadPods();
}, [activeTab, loadDeployments, loadPods]);
useEffect(() => {
if (activeTab === "logs") loadLogs(200, { scrollToEnd: true });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab, namespace, selectedPod, selectedContainer]);
useEffect(() => {
requestAnimationFrame(applyPendingLogScroll);
}, [logLines, loadingLogs, activeTab]);
const selectedPodData = pods.find((pod) => pod.name === selectedPod);
const visibleDeployments = deployments.filter((deployment) =>
`${deployment.namespace}/${deployment.name} ${deployment.images?.join(" ")}`
.toLowerCase()
.includes(deploymentFilter.toLowerCase()),
);
const visiblePods = pods.filter((pod) =>
`${pod.namespace}/${pod.name} ${pod.phase} ${pod.containers.map((c) => c.image).join(" ")}`
.toLowerCase()
.includes(podFilter.toLowerCase()),
);
const filteredLogLines = logLines.filter(
(line) =>
!logFilter || line.toLowerCase().includes(logFilter.toLowerCase()),
);
const matchLineIndexes = useMemo(
() =>
logSearch
? filteredLogLines.reduce<number[]>((matches, line, index) => {
if (line.toLowerCase().includes(logSearch.toLowerCase()))
matches.push(index);
return matches;
}, [])
: [],
[filteredLogLines, logSearch],
);
const searchMatches = matchLineIndexes.length;
const state =
status?.rollup?.state ||
status?.status ||
status?.installState?.status ||
"loading";
const blockerList = blockers(status);
const runningOperations = status?.activeOperations || [];
const primaryOperation = runningOperations[0];
const currentPhase =
status?.currentPhase || status?.installState?.phase || "loading";
const nextAction =
status?.rollup?.nextAction ||
status?.installState?.lastAction ||
"Waiting for the next appliance update";
useEffect(() => {
setActiveMatch(0);
}, [logSearch, logFilter, logLines]);
function handleLogScroll() {
const pane = logPaneRef.current;
if (!pane || loadingLogs || pane.scrollTop > 80 || logTail >= 10000) return;
loadLogs(logTail + 200, { preserveScroll: true });
}
function jumpMatch(direction: 1 | -1) {
if (searchMatches === 0) return;
const next = (activeMatch + direction + searchMatches) % searchMatches;
setActiveMatch(next);
window.setTimeout(() => {
lineRefs.current[matchLineIndexes[next]]?.scrollIntoView({
block: "center",
});
}, 0);
}
return (
<main className={styles.shell}>
<aside className={styles.sidebar}>
<div className={styles.brand}>
<span className={styles.logo}>
<AlgaLogo className={styles.logoSvg} />
</span>
<span className={styles.brandText}>
<span>Alga PSA</span>
<small>Setup status</small>
</span>
</div>
<nav
className={styles.nav}
aria-label="Alga PSA setup status tabs"
role="tablist"
>
{statusTabs.map(({ value, label, Icon }) => (
<button
key={value}
type="button"
role="tab"
id={`appliance-tab-${value}`}
aria-selected={activeTab === value}
aria-controls={`appliance-panel-${value}`}
className={activeTab === value ? styles.activeTab : ""}
onClick={() => setActiveTab(value)}
>
<Icon className={styles.navIcon} aria-hidden="true" />
<span>{label}</span>
</button>
))}
</nav>
<a className={styles.setupLink} href="/setup/">
<SlidersHorizontal className={styles.navIcon} aria-hidden="true" />
<span>Setup</span>
</a>
<LogoutButton />
</aside>
<section className={styles.workspace}>
<header className={styles.commandBar}>
<div>
<div className={styles.eyebrow}>Alga PSA appliance</div>
<h1>
{status?.rollup?.message ||
error ||
"Reading live appliance status…"}
</h1>
</div>
<span className={`${styles.statusPill} ${badgeClass(state)}`}>
{state}
</span>
</header>
{setupMode === "setup" ? (
<div className={styles.setupCta} role="status">
<div>
<strong>Initial setup is waiting for you</strong>
<p>
Enter the setup token, then your install code, to finish
configuring this appliance.
</p>
</div>
<a className={styles.primaryButton} href="/setup/">
Continue setup
</a>
</div>
) : null}
{status?.setupReEditable ? (
<div className={styles.setupCta} role="alert">
<div>
<strong>Install code needs attention</strong>
<p>
The install code could not be redeemed it may be invalid,
expired, or already used. Re-issue a fresh code from the portal,
then re-enter it to continue.
</p>
</div>
<a className={styles.primaryButton} href="/setup/">
Re-enter install code
</a>
</div>
) : null}
{activeTab === "overview" ? (
<div
id="appliance-panel-overview"
role="tabpanel"
aria-labelledby="appliance-tab-overview"
className={styles.grid}
>
<article className={`${styles.panel} ${styles.wide}`}>
<h2>What is running now</h2>
{loadingStatus && !status ? (
<SkeletonBlock lines={4} />
) : (
<div className={styles.runSummary}>
<div className={styles.runHeader}>
<div>
<strong>
{primaryOperation?.component || currentPhase}
</strong>
<p className={styles.runMessage}>
{primaryOperation?.message || nextAction}
</p>
</div>
<span
className={`${styles.statusPill} ${badgeClass(state)}`}
>
{state}
</span>
</div>
<div className={styles.runMeta}>
<span>Phase: {currentPhase}</span>
<span>
{elapsedLabel(primaryOperation?.elapsedSeconds)}
</span>
<span>
Login:{" "}
{status?.urls?.loginUrl
? "available"
: "not available yet"}
</span>
<span>{status?.kubernetes?.podCount ?? 0} pods</span>
</div>
{runningOperations.length > 1 ? (
<ul
className={styles.runList}
aria-label="Other active operations"
>
{runningOperations.slice(1, 4).map((op, index) => (
<li key={`${op.component}-${index}`}>
{op.component}: {op.message}
</li>
))}
</ul>
) : null}
</div>
)}
</article>
<article className={styles.panel}>
<h2>Next checkpoint</h2>
{loadingStatus && !status ? (
<SkeletonBlock lines={4} />
) : (
<dl className={styles.kv}>
<div>
<dt>Current phase</dt>
<dd>{currentPhase}</dd>
</div>
<div>
<dt>Next action</dt>
<dd>{nextAction}</dd>
</div>
<div>
<dt>Login URL</dt>
<dd>{status?.urls?.loginUrl || "Not available yet"}</dd>
</div>
<div>
<dt>Cluster objects</dt>
<dd>
{status?.kubernetes?.podCount ?? 0} pods ·{" "}
{status?.kubernetes?.helmReleaseCount ?? 0} releases
</dd>
</div>
</dl>
)}
</article>
<article className={styles.panel}>
<h2>Blockers</h2>
{loadingStatus && !status ? (
<SkeletonBlock lines={3} />
) : blockerList.length === 0 ? (
<p className={styles.muted}>
No action-required blockers detected.
</p>
) : (
blockerList.map((blocker, index) => (
<div
className={`${styles.blocker} ${blocker.loginBlocking === false ? styles.backgroundBlocker : ""}`}
key={index}
>
<strong>{blocker.component || blocker.layer}</strong>
<p>{blocker.reason}</p>
<small>{blocker.nextAction}</small>
</div>
))
)}
</article>
<article className={styles.panel}>
<h2>Readiness tiers</h2>
{loadingStatus && !status ? (
<SkeletonBlock lines={6} />
) : (
<div className={styles.tiers}>
{tierEntries(status).map(([name, tier]) => (
<div className={styles.tier} key={name}>
<strong>{name}</strong>
<span
className={`${styles.badge} ${badgeClass(tier.ready)}`}
>
{tier.ready ? "ready" : "not ready"}
</span>
<small>{tier.status}</small>
</div>
))}
</div>
)}
</article>
<article className={styles.panel}>
<h2>Active operations</h2>
{loadingStatus && !status ? (
<SkeletonBlock lines={3} />
) : runningOperations.length === 0 ? (
<p className={styles.muted}>
No active image pull or long-running pod operation detected.
</p>
) : (
runningOperations.map((op, index) => (
<div className={styles.operation} key={index}>
<strong>{op.component}</strong>
<p>{op.message}</p>
</div>
))
)}
</article>
<article className={styles.panel}>
<h2>Recovery</h2>
<p className={styles.muted}>
Re-runs the application bootstrap database migrations,
onboarding seeds, and creation of the initial tenant and admin
user (only when no user exists yet). Use this if setup finished
but you cannot log in because the initial account was never
created.
</p>
<div className={styles.toolbar}>
<button
type="button"
className={styles.actionButton}
disabled={recovering}
onClick={() => {
if (!confirmRecover) {
setConfirmRecover(true);
} else {
setConfirmRecover(false);
recoverBootstrap();
}
}}
>
{recovering
? "Triggering…"
: confirmRecover
? "Confirm re-run"
: "Re-run application bootstrap"}
</button>
{confirmRecover && !recovering ? (
<button
type="button"
onClick={() => setConfirmRecover(false)}
>
Cancel
</button>
) : null}
</div>
{recoverMsg ? (
<p className={styles.helpText}>{recoverMsg}</p>
) : null}
</article>
<article className={`${styles.panel} ${styles.wide}`}>
<h2>Recent Kubernetes events</h2>
{loadingStatus && !status ? (
<SkeletonBlock lines={5} />
) : (
<div className={styles.eventList}>
{(status?.recentEvents || [])
.slice(-10)
.reverse()
.map((event, index) => (
<div className={styles.event} key={index}>
<b>
{event.type} {event.reason}
</b>
<span>
{event.namespace} · {event.involvedObject}
</span>
<p>{event.message}</p>
</div>
))}
{(status?.recentEvents || []).length === 0 ? (
<p className={styles.muted}>
Kubernetes events are not available yet.
</p>
) : null}
</div>
)}
</article>
<details className={styles.advancedSupport}>
<summary className={styles.advancedSummary}>
Advanced support diagnostics
</summary>
<p className={styles.helpText}>
Use this raw payload when support asks for exact appliance
state.
</p>
{loadingStatus && !status ? (
<SkeletonBlock lines={12} />
) : (
<pre className={styles.raw}>
{JSON.stringify(status || { error }, null, 2)}
</pre>
)}
</details>
</div>
) : null}
{activeTab === "deployments" ? (
<section
id="appliance-panel-deployments"
role="tabpanel"
aria-labelledby="appliance-tab-deployments"
className={styles.panel}
>
<Toolbar
namespace={namespace}
namespaces={namespaces}
loadingNamespaces={loadingNamespaces}
onNamespace={setNamespace}
filter={deploymentFilter}
onFilter={setDeploymentFilter}
onRefresh={loadDeployments}
/>
<div className={styles.tableWrap}>
<table>
<thead>
<tr>
<th>Deployment</th>
<th>Ready</th>
<th>Revision</th>
<th>Strategy</th>
<th>Images</th>
<th>History</th>
</tr>
</thead>
<tbody>
{loadingDeployments ? (
<SkeletonRows columns={6} rows={7} />
) : visibleDeployments.length === 0 ? (
<tr>
<td colSpan={6} className={styles.emptyCell}>
No deployments found.
</td>
</tr>
) : (
visibleDeployments.map((deployment) => (
<tr key={`${deployment.namespace}/${deployment.name}`}>
<td>
<b>{deployment.name}</b>
<small>{deployment.namespace}</small>
</td>
<td>
<span
className={`${styles.badge} ${badgeClass(deployment.readyReplicas === deployment.replicas)}`}
>
{deployment.readyReplicas}/{deployment.replicas}
</span>
</td>
<td>{deployment.revision || "—"}</td>
<td>{deployment.strategy}</td>
<td>
{deployment.images?.map((image) => (
<code key={image}>{image}</code>
))}
</td>
<td>
<div className={styles.history}>
{deployment.replicaSets?.slice(0, 4).map((rs) => (
<span key={rs.name}>
r{rs.revision || "?"} {rs.readyReplicas}/
{rs.replicas} · {ageFrom(rs.createdAt)}
</span>
))}
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
) : null}
{activeTab === "pods" ? (
<section
id="appliance-panel-pods"
role="tabpanel"
aria-labelledby="appliance-tab-pods"
className={styles.panel}
>
<Toolbar
namespace={namespace}
namespaces={namespaces}
loadingNamespaces={loadingNamespaces}
onNamespace={setNamespace}
filter={podFilter}
onFilter={setPodFilter}
onRefresh={loadPods}
/>
<div className={styles.tableWrap}>
<table>
<thead>
<tr>
<th>Pod</th>
<th>Status</th>
<th>Ready</th>
<th>Restarts</th>
<th>Node/IP</th>
<th>Containers</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{loadingPods ? (
<SkeletonRows columns={7} rows={8} />
) : visiblePods.length === 0 ? (
<tr>
<td colSpan={7} className={styles.emptyCell}>
No pods found.
</td>
</tr>
) : (
visiblePods.map((pod) => (
<tr
key={`${pod.namespace}/${pod.name}`}
onClick={() => {
setNamespace(pod.namespace);
setSelectedPod(pod.name);
setSelectedContainer(pod.containers[0]?.name || "");
setActiveTab("logs");
}}
>
<td>
<b>{pod.name}</b>
<small>{pod.namespace}</small>
</td>
<td>
<span
className={`${styles.badge} ${badgeClass(pod.phase)}`}
>
{pod.phase}
</span>
</td>
<td>
{pod.readyContainers}/{pod.totalContainers}
</td>
<td>{pod.restarts}</td>
<td>
<small>
{pod.node || "—"}
<br />
{pod.podIP || "—"}
</small>
</td>
<td>
{pod.containers.map((container) => (
<code key={container.name}>{container.name}</code>
))}
</td>
<td>
<button
type="button"
onClick={(event) => {
event.stopPropagation();
setNamespace(pod.namespace);
setSelectedPod(pod.name);
setSelectedContainer(
pod.containers[0]?.name || "",
);
setActiveTab("logs");
}}
>
View logs
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
) : null}
{activeTab === "logs" ? (
<section
id="appliance-panel-logs"
role="tabpanel"
aria-labelledby="appliance-tab-logs"
className={styles.panel}
>
<div className={styles.logControls}>
<Dropdown
ariaLabel="Namespace"
value={namespace}
disabled={loadingNamespaces}
onChange={(value) => {
setNamespace(value);
setSelectedPod("");
}}
options={[
{ value: "msp", label: "msp" },
...namespaces
.filter((ns) => ns.name !== "msp")
.map((ns) => ({ value: ns.name, label: ns.name })),
]}
/>
<Dropdown
ariaLabel="Pod"
value={selectedPod}
disabled={loadingPods}
placeholder="Select pod"
onChange={(value) => {
setSelectedPod(value);
setSelectedContainer("");
}}
options={pods.map((pod) => ({
value: pod.name,
label: pod.name,
}))}
/>
<Dropdown
ariaLabel="Container"
value={selectedContainer}
disabled={loadingPods || !selectedPodData}
placeholder="Select container"
onChange={setSelectedContainer}
options={(selectedPodData?.containers || []).map(
(container) => ({
value: container.name,
label: container.name,
}),
)}
/>
<button
type="button"
onClick={() => loadLogs(logTail, { scrollToEnd: true })}
>
Refresh
</button>
<span className={styles.muted}>
tail {logTail} · scroll up for older lines
</span>
</div>
<div className={styles.logControls}>
<input
aria-label="Filter visible log lines"
value={logFilter}
onChange={(event) => setLogFilter(event.target.value)}
placeholder="Filter visible log lines"
/>
<input
aria-label="Search and highlight log lines"
value={logSearch}
onChange={(event) => setLogSearch(event.target.value)}
placeholder="Search and highlight"
/>
<button
type="button"
disabled={searchMatches === 0}
onClick={() => jumpMatch(-1)}
>
Previous
</button>
<button
type="button"
disabled={searchMatches === 0}
onClick={() => jumpMatch(1)}
>
Next
</button>
<span className={styles.matchCount}>
{searchMatches
? `${activeMatch + 1}/${searchMatches}`
: "0 matches"}
</span>
</div>
{logError ? <div className={styles.alert}>{logError}</div> : null}
{loadingLogs && logLines.length === 0 ? (
<div className={styles.logPane}>
<SkeletonBlock lines={18} />
</div>
) : (
<pre
className={styles.logPane}
ref={logPaneRef}
onScroll={handleLogScroll}
>
{filteredLogLines.map((line, index) => {
const matchIndex = matchLineIndexes.indexOf(index);
const isMatch = matchIndex >= 0;
const isActive = isMatch && matchIndex === activeMatch;
return (
<div
ref={(el) => {
lineRefs.current[index] = el;
}}
key={`${index}-${line.slice(0, 20)}`}
className={`${isMatch ? styles.matchLine : ""} ${isActive ? styles.activeMatchLine : ""}`}
>
{highlightLine(line, logSearch)}
</div>
);
})}
</pre>
)}
</section>
) : null}
</section>
</main>
);
}
type DropdownOption = { value: string; label: string };
// Custom dropdown used instead of a native <select>. Native select popups are
// unreliable inside the Electron browser pane (they fail to open or are
// dismissed by the 15s status-poll re-render). This renders the option list in
// the page DOM and keeps its open state across parent re-renders.
function Dropdown({
value,
options,
onChange,
disabled,
ariaLabel,
placeholder,
}: {
value: string;
options: DropdownOption[];
onChange: (value: string) => void;
disabled?: boolean;
ariaLabel: string;
placeholder?: string;
}) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement | null>(null);
const selected = options.find((option) => option.value === value);
useEffect(() => {
if (!open) return;
const onPointerDown = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node))
setOpen(false);
};
const onKey = (event: KeyboardEvent) => {
if (event.key === "Escape") setOpen(false);
};
document.addEventListener("mousedown", onPointerDown);
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("mousedown", onPointerDown);
document.removeEventListener("keydown", onKey);
};
}, [open]);
useEffect(() => {
if (disabled) setOpen(false);
}, [disabled]);
return (
<div className={styles.dropdown} ref={ref}>
<button
type="button"
className={styles.dropdownButton}
aria-haspopup="listbox"
aria-expanded={open}
aria-label={ariaLabel}
disabled={disabled}
onClick={() => setOpen((prev) => !prev)}
>
<span className={styles.dropdownLabel}>
{selected?.label ?? (value || placeholder || "—")}
</span>
<span className={styles.dropdownCaret} aria-hidden="true">
</span>
</button>
{open && !disabled ? (
<ul
className={styles.dropdownMenu}
role="listbox"
aria-label={ariaLabel}
>
{options.length === 0 ? (
<li className={styles.dropdownOption} aria-disabled="true">
No options
</li>
) : (
options.map((option) => (
<li
key={option.value}
role="option"
aria-selected={option.value === value}
className={`${styles.dropdownOption} ${option.value === value ? styles.dropdownOptionActive : ""}`}
onClick={() => {
onChange(option.value);
setOpen(false);
}}
>
{option.label}
</li>
))
)}
</ul>
) : null}
</div>
);
}
function Toolbar({
namespace,
namespaces,
loadingNamespaces,
onNamespace,
filter,
onFilter,
onRefresh,
}: {
namespace: string;
namespaces: NamespaceItem[];
loadingNamespaces?: boolean;
onNamespace: (value: string) => void;
filter: string;
onFilter: (value: string) => void;
onRefresh: () => void;
}) {
const options: DropdownOption[] = [
{ value: "all", label: "all namespaces" },
{ value: "msp", label: "msp" },
...namespaces
.filter((ns) => ns.name !== "msp")
.map((ns) => ({ value: ns.name, label: ns.name })),
];
return (
<div className={styles.toolbar}>
<Dropdown
ariaLabel="Namespace"
value={namespace}
disabled={loadingNamespaces}
onChange={onNamespace}
options={options}
/>
<input
aria-label="Filter by name, image, or state"
value={filter}
onChange={(event) => onFilter(event.target.value)}
placeholder="Filter by name, image, state…"
/>
<button type="button" onClick={onRefresh}>
Refresh
</button>
</div>
);
}