Files
Netcatty/application/state/useMonitorRuntime.ts
T
陈大猫 3f35872638 Refine monitoring layout, disk details and refresh controls (#3704)
* Polish monitoring panels and preserve host icons

* Simplify monitoring layout and show every disk partition

* Simplify list rows and add resizable monitoring overlays

* feat(monitor): add modal drawers, sampled hover values and faster refresh

* feat(monitor): reset run history and support saved drag ordering

* perf(monitor): stream history deltas and keep fast samples lightweight

* fix(monitor): separate chart histories across connection identities

* refine(monitor): show disk capacity on hover and use inline sidebars

* fix(monitor): omit unchanged stats from polling deltas

* test(monitor): freeze time in full-history delta checks

* perf(monitor): bound chart paths and reuse unchanged histories
2026-10-17 18:59:38 +08:00

199 lines
6.4 KiB
TypeScript

import { hostRestrictsExtraSshChannels } from "../../domain/host";
import { useEffect, useRef, useState } from "react";
import { buildHostConnectionTestPlan } from "../../domain/hostConnectionTest";
import { resolveEffectiveTerminalHost } from "../../domain/terminalHostResolution";
import { getEffectiveKnownHosts } from "../../infrastructure/syncHelpers";
import { netcattyBridge } from "../../infrastructure/services/netcattyBridge";
import {
useAppLockChrome,
useAppSettingsRuntime,
useAppVaultRuntime,
} from "./appRuntimeBridge";
import { useMonitorPanels } from "./monitorPanelsStorage";
import {
monitorActions,
getMonitorHistoryCursor,
publishMonitorSnapshot,
publishMonitorUnavailable,
} from "./monitorRuntimeStore";
import type { MonitorPanelDemand } from "../../domain/monitorRuntime";
/** Root lifetime, independent of Vault navigation. No runtime state is persisted. */
export function useMonitorRuntime() {
const panels = useMonitorPanels();
const {
hosts,
keys,
identities,
groupConfigs,
proxyProfiles,
knownHosts,
isInitialized,
} = useAppVaultRuntime();
const { terminalSettings } = useAppSettingsRuntime();
const { locked } = useAppLockChrome();
const [running, setRunning] = useState(() => new Map<string, string>());
const [retryVersion, setRetryVersion] = useState(0);
const epoch = useRef<number | null>(null);
const runtimeId = useRef<string | null>(null);
const credentials = useRef(
new Map<string, { username: string; password: string }>(),
);
useEffect(() => {
monitorActions.start = (id) =>
setRunning((prev) => new Map(prev).set(id, crypto.randomUUID()));
monitorActions.stop = (id) =>
setRunning((prev) => {
const next = new Map(prev);
next.delete(id);
return next;
});
monitorActions.stopAll = () => {
credentials.current.clear();
setRunning(new Map());
void netcattyBridge.get()?.monitorStopAll?.();
};
monitorActions.retry = (id, override) => {
if (override) credentials.current.set(id, override);
void netcattyBridge
.get()
?.monitorRetry?.(id)
.then(() => setRetryVersion((v) => v + 1));
};
const offline = () => monitorActions.stopAll();
window.addEventListener("offline", offline);
return () => {
window.removeEventListener("offline", offline);
void netcattyBridge.get()?.monitorUpdate?.([]);
};
}, []);
useEffect(() => {
if (locked || !isInitialized) {
credentials.current.clear();
setRunning(new Map());
}
}, [locked, isInitialized]);
useEffect(() => {
const bridge = netcattyBridge.get();
if (!bridge?.monitorUpdate) {
publishMonitorUnavailable();
return;
}
const demand: MonitorPanelDemand[] =
locked || !isInitialized
? []
: panels
.filter((p) => running.has(p.id))
.map((panel) => ({
id: panel.id,
runId: running.get(panel.id)!,
interval: panel.interval,
hosts: panel.hostIds.map((id) => {
const host = hosts.find((h) => h.id === id);
if (
!host ||
(host.protocol && host.protocol !== "ssh") ||
hostRestrictsExtraSshChannels(host)
)
return { id };
const effective = resolveEffectiveTerminalHost({
host,
groupConfigs,
proxyProfiles,
});
const plan = buildHostConnectionTestPlan({
host,
hosts,
keys,
identities,
groupConfigs,
proxyProfiles,
knownHosts: getEffectiveKnownHosts(knownHosts) ?? [],
terminalSettings,
sessionId: `monitor-${id}`,
bootEpoch: 0,
authOverride: credentials.current.has(id)
? { ...credentials.current.get(id), authMethod: "password" }
: null,
});
if (!plan.ok) return { id, status: "error" as const };
if (plan.needsCredentialReentry)
return { id, status: "login" as const };
return {
id,
options: {
...plan.options,
singleChannelSsh: hostRestrictsExtraSshChannels(effective),
sshAuthReadyTimeoutMs: 120000,
agentForwarding: effective.agentForwarding,
},
};
}),
}));
const activeHostIds = new Set(
demand.flatMap((p) => p.hosts.map((h) => h.id)),
);
for (const id of credentials.current.keys())
if (!activeHostIds.has(id)) credentials.current.delete(id);
void bridge
.monitorUpdate(demand)
.then(publishMonitorSnapshot)
.catch(() => {
credentials.current.clear();
setRunning((prev) => (prev.size ? new Map() : prev));
publishMonitorUnavailable();
});
}, [
panels,
running,
hosts,
keys,
identities,
groupConfigs,
proxyProfiles,
knownHosts,
terminalSettings,
locked,
isInitialized,
retryVersion,
]);
useEffect(() => {
const bridge = netcattyBridge.get();
let disposed = false;
let pending = false;
const poll = async () => {
if (pending || !bridge?.monitorSnapshot) return;
pending = true;
try {
const snapshot = await bridge.monitorSnapshot(getMonitorHistoryCursor());
if (disposed) return;
if (
(epoch.current !== null && epoch.current !== snapshot.epoch) ||
(runtimeId.current !== null &&
runtimeId.current !== snapshot.runtimeId)
) {
credentials.current.clear();
setRunning(new Map());
}
epoch.current = snapshot.epoch;
runtimeId.current = snapshot.runtimeId;
publishMonitorSnapshot(snapshot);
} catch {
credentials.current.clear();
setRunning((prev) => (prev.size ? new Map() : prev));
publishMonitorUnavailable();
} finally {
pending = false;
}
};
void poll();
const timer = setInterval(() => {
void poll();
}, 1000);
return () => {
disposed = true;
clearInterval(timer);
};
}, []);
}