import { useEffect, useState } from "preact/hooks"; import { t } from "../../i18n"; import { api, apiErrorText } from "../../features/customize/api"; import { custTab } from "../../features/customize/actions"; import { nestedEditor } from "../../features/customize/state"; import { asList, asString, confirmAction, dropEnvSnapshots, hint, } from "../../features/customize/host"; import { currentId } from "../../stores/session"; import { environmentStatus, standardProfileReadiness, _environmentStatusPromise, _environmentStatusRefreshFailed, } from "../../stores/customize"; import { _jobPoll } from "../../stores/ui"; import { sanitizeStandardProfileReadiness, standardReadinessStateText, type StandardReadiness, } from "../../features/customize/environment"; import { scheduleTimeout } from "../../features/customize/timers"; import { useAlive, useTimerLease } from "./use-timer-lease"; import { markCustomizeFailed, markCustomizeLoaded } from "../../features/customize/load"; import { Hdr, InfoRow } from "./ui"; async function refreshEnvironmentStatus(): Promise | null> { if (_environmentStatusPromise.value) { return _environmentStatusPromise.value as Promise | null>; } const pending = (async () => { try { const payload = await api("/environments/status"); _environmentStatusRefreshFailed.value = false; environmentStatus.value = payload && typeof payload === "object" ? payload : null; standardProfileReadiness.value = sanitizeStandardProfileReadiness( payload.standard_profile_readiness, ); } catch { _environmentStatusRefreshFailed.value = true; const prev = standardProfileReadiness.value as StandardReadiness | null; if (prev && prev.enabled === true) { standardProfileReadiness.value = { ...prev, ready: false, state: "unavailable", reason: "status_refresh_failed", }; } } return environmentStatus.value as Record | null; })(); _environmentStatusPromise.value = pending; try { return await pending; } finally { _environmentStatusPromise.value = null; } } function ReadinessCard({ readiness, lease, alive, }: { readiness: StandardReadiness; lease: ReturnType; alive: () => boolean; }) { return (
{t("environment.readiness.cardTitle")}
{standardReadinessStateText(readiness)}
{readiness.missing_environments.length ? (
{t("environment.readiness.missingEnvironments")}
    {readiness.missing_environments.map((name) => (
  • {name}
  • ))}
) : null} {Object.entries(readiness.missing_packages).map(([environment, packages]) => packages.length ? (
{t("environment.readiness.missingPackages", environment)}
    {packages.map((packageName) => (
  • {packageName}
  • ))}
) : null, )} {readiness.remediation && readiness.remediation.requires_explicit_action && readiness.remediation.commands.length ? (
{t("environment.readiness.remediation")}
{t("environment.readiness.explicitOnly")}
{readiness.remediation.commands.map((item) => ( ))}
) : null}
); } function CopyCommand({ item, lease, alive, }: { item: { command: string; label: string }; lease: ReturnType; alive: () => boolean; }) { const [copied, setCopied] = useState(false); return (
{item.command}
); } export function ComputeTab() { const alive = useAlive(); const lease = useTimerLease(); const [err, setErr] = useState(null); const [gpu, setGpu] = useState>({ available: false }); const [host, setHost] = useState>({}); const [envs, setEnvs] = useState[]>([]); const [remote, setRemote] = useState | null>(null); const [jobs, setJobs] = useState[]>([]); const [pkg, setPkg] = useState(""); const [installing, setInstalling] = useState(false); const [jobKind, setJobKind] = useState("bash"); const [jobCmd, setJobCmd] = useState(""); const [jobBusy, setJobBusy] = useState(false); const [alias, setAlias] = useState(""); const loadJobs = async () => { let d: Record; try { d = await api("/compute/jobs"); } catch { d = { jobs: [] }; } if (!alive()) return; const list = asList(d.jobs) as Record[]; setJobs(list); const anyRunning = list.some( (j) => j.status === "running" || j.status === "queued", ); if (anyRunning) { const handle = scheduleTimeout( lease, () => { void loadJobs(); }, 1500, ); _jobPoll.value = handle; } }; useEffect(() => { void (async () => { try { const [g, env, h] = await Promise.all([ api("/compute/gpu").catch(() => ({ available: false })), refreshEnvironmentStatus().then((status) => status || { environments: [] }), api("/compute/local/hostinfo").catch(() => ({})), ]); if (!alive()) return; setGpu(g); setHost(h); setEnvs(asList(env && env.environments) as Record[]); markCustomizeLoaded(); try { const info = await api("/compute/remote"); if (alive()) setRemote(info); } catch { /* original swallowed */ } await loadJobs(); } catch (e) { if (!alive()) return; const message = t("versions.load.err", (e as Error).message); setErr(message); markCustomizeFailed(message); } })(); return () => { _jobPoll.value = null; }; }, [alive, lease]); if (err) return
{err}
; const readiness = standardProfileReadiness.value as StandardReadiness | null; const hosts = remote ? (asList(remote.hosts) as Record[]) : []; const taken = new Set(hosts.map((h) => h.alias)); const avail = (remote ? asList(remote.available_aliases) : []).filter( (a) => !taken.has(a), ); return (
{readiness && readiness.enabled ? ( ) : null} {remote ? ( <>
{t("cust.remote.title")}
{t("cust.remote.desc")}
{hosts.map((h) => (
{(h.reachable ? "🟢 " : "🔴 ") + asString(h.label || h.alias)} {" · " + asString(h.provider)}
{asString(h.gpus) || (h.reachable ? "" : t("cust.remote.unreachable"))}
{asList(h.capabilities).length ? ( <> {t("cust.remote.services") + " "} {asList(h.capabilities).map((cp, i) => { const c = cp as Record; return ( {asString(c.name) + (c.engine ? " · " + asString(c.engine) : "") + (c.verified ? " ✓" : "")} ); })} ) : ( {t("cust.remote.noservices")} )}
))}
{t("cust.remote.addName")}
) : null} {envs.map((e) => { const inst = asList(e.packages).filter( (p) => (p as Record).installed, ) as Record[]; return ( p.name) .join("、") + (inst.length > 18 ? " …" : ""), )} /> ); })}
{t("cust.compute.installExtraName")}
setPkg((e.target as HTMLInputElement).value)} />
{t("cust.jobs.submitName")}
setJobCmd((e.target as HTMLInputElement).value)} />
{!jobs.length ? (
{t("cust.jobs.empty")}
) : ( jobs.map((j) => (
{asString(j.status)}{" "} {(asString(j.kind) + " " + asString(j.command)).slice(0, 80)}
{(j.duration_s != null ? j.duration_s + "s" : "") + (j.exit_code != null ? " · exit " + j.exit_code : "") + (j.truncated ? " " + t("cust.jobs.dropped", (j.dropped_bytes || 0).toLocaleString()) : "")}
{j.status === "running" || j.status === "queued" ? ( ) : null}
)) )}
); }