feat: finish the host-metrics plugin, move host status into core (B16)

This commit is contained in:
LukeGus committed 2026-09-24 11:21:23 -05:00
1 parent 14eb558b85
commit 0a3f05d99d
308 files changed
+53073 -8985

No files matched your search

+5 -13
View File
@@ -29,8 +29,8 @@ import {
updatePasswordResetAllowed,
getSessionTimeout,
updateSessionTimeout,
getGlobalMonitoringSettings,
updateGlobalMonitoringSettings,
getStatusCheckSettings,
updateStatusCheckSettings,
getLogLevel,
updateLogLevel,
getOidcAutoProvision,
@@ -134,7 +134,6 @@ export function AdminSettingsPanel({
const [allowPasswordReset, setAllowPasswordReset] = useState(true);
const [sessionTimeout, setSessionTimeout] = useState("24");
const [statusInterval, setStatusInterval] = useState("60");
const [metricsInterval, setMetricsInterval] = useState("30");
const [logLevel, setLogLevel] = useState("info");
const [analyticsEnabled, setAnalyticsEnabled] = useState(true);
const [analyticsLocked, setAnalyticsLocked] = useState(false);
@@ -359,7 +358,7 @@ export function AdminSettingsPanel({
getPasswordLoginAllowed(),
getPasswordResetAllowed(),
getSessionTimeout(),
getGlobalMonitoringSettings(),
getStatusCheckSettings(),
getLogLevel(),
getOidcAutoProvision(),
getSecondFactorAfterExternalLogin(),
@@ -392,7 +391,6 @@ export function AdminSettingsPanel({
setSessionTimeout(String(timeout.value.timeoutHours));
if (monitoring.status === "fulfilled") {
setStatusInterval(String(monitoring.value.statusCheckInterval));
setMetricsInterval(String(monitoring.value.metricsInterval));
}
if (level.status === "fulfilled") setLogLevel(level.value.level);
@@ -630,16 +628,12 @@ export function AdminSettingsPanel({
async function handleSaveMonitoring() {
const status = parseInt(statusInterval, 10);
const metrics = parseInt(metricsInterval, 10);
if (isNaN(status) || isNaN(metrics)) {
if (isNaN(status) || status < 5 || status > 3600) {
toast.error(t("admin.monitoringIntervalInvalid"));
return;
}
try {
await updateGlobalMonitoringSettings({
statusCheckInterval: status,
metricsInterval: metrics,
});
await updateStatusCheckSettings({ statusCheckInterval: status });
toast.success(t("admin.monitoringSaved"));
} catch {
toast.error(t("admin.monitoringSaveFailed"));
@@ -1070,8 +1064,6 @@ export function AdminSettingsPanel({
handleSaveSessionTimeout={handleSaveSessionTimeout}
statusInterval={statusInterval}
setStatusInterval={setStatusInterval}
metricsInterval={metricsInterval}
setMetricsInterval={setMetricsInterval}
handleSaveMonitoring={handleSaveMonitoring}
logLevel={logLevel}
handleSaveLogLevel={handleSaveLogLevel}
+1 -35
View File
@@ -57,8 +57,6 @@ type GeneralSettingsSectionProps = {
handleSaveSessionTimeout: () => void;
statusInterval: string;
setStatusInterval: Dispatch<SetStateAction<string>>;
metricsInterval: string;
setMetricsInterval: Dispatch<SetStateAction<string>>;
handleSaveMonitoring: () => void;
logLevel: string;
handleSaveLogLevel: (level: string) => void;
@@ -98,8 +96,6 @@ export function AdminGeneralSettingsSection({
handleSaveSessionTimeout,
statusInterval,
setStatusInterval,
metricsInterval,
setMetricsInterval,
handleSaveMonitoring,
logLevel,
handleSaveLogLevel,
@@ -352,22 +348,6 @@ export function AdminGeneralSettingsSection({
<span className="text-xs text-muted-foreground">
{t("admin.sec")}
</span>
</div>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
{t("admin.metrics")}
</label>
<div className="flex items-center gap-2">
<Input
type="number"
value={metricsInterval}
onChange={(e) => setMetricsInterval(e.target.value)}
className="w-20 text-sm"
/>
<span className="text-xs text-muted-foreground">
{t("admin.sec")}
</span>
<Button
variant="outline"
size="sm"
@@ -972,22 +952,8 @@ export function AdminHostDefaultsSection({
<div className="flex flex-col gap-2 border-t border-border pt-3">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
{t("admin.hostDefaultsMetrics")}
{t("admin.hostDefaultsStatus")}
</span>
<SettingRow
label={t("admin.hostDefaultsMetricsEnabled")}
description={t("admin.hostDefaultsMetricsEnabledDesc")}
>
<AdminToggle
on={defaults.metricsEnabled ?? true}
onToggle={() =>
setDefaults((p) => ({
...p,
metricsEnabled: !(p.metricsEnabled ?? true),
}))
}
/>
</SettingRow>
<SettingRow
label={t("admin.hostDefaultsStatusCheckEnabled")}
description={t("admin.hostDefaultsStatusCheckEnabledDesc")}
+5 -24
View File
@@ -282,29 +282,9 @@ export function createHostEditorForm(
telnetAuthType: (host?.telnetAuthType ??
(host?.telnetCredentialId ? "credential" : "direct")) as
"direct" | "credential",
statsConfig: host?.statsConfig ?? {
statusCheckEnabled: d?.statusCheckEnabled ?? true,
statusCheckInterval: 60,
useGlobalStatusInterval: true,
metricsEnabled: d?.metricsEnabled ?? true,
metricsInterval: 30,
useGlobalMetricsInterval: true,
enabledWidgets: [
"cpu",
"memory",
"disk",
"network",
"uptime",
"system",
"login_stats",
"processes",
"ports",
"firewall",
"temperature",
],
excludedMounts: [] as string[],
monitoredMounts: [] as Array<{ path: string; label?: string }>,
},
statusCheckEnabled:
host?.statusCheckEnabled ?? d?.statusCheckEnabled ?? true,
statusCheckInterval: (host?.statusCheckInterval ?? null) as number | null,
// Host-scope plugin settings, keyed by plugin id. Loaded with the host and
// saved through the plugin's own scoped route, not in the host payload.
@@ -546,7 +526,8 @@ export function buildHostEditorPayload(
name: a.name,
snippetId: Number(a.snippetId),
})),
statsConfig: form.statsConfig,
statusCheckEnabled: form.statusCheckEnabled,
statusCheckInterval: form.statusCheckInterval,
terminalConfig: protocols.enableSsh ? terminalOverrides : null,
};
}
+48
View File
@@ -7,6 +7,7 @@ import { PasswordInput } from "@/components/password-input";
import { FakeSwitch, SectionCard, SettingRow } from "@/components/section-card";
import type { Host } from "@/types/ui-types";
import {
Activity,
Globe,
LayoutGrid,
Plus,
@@ -843,6 +844,53 @@ export function HostEditorGeneralTab({
</div>
</SectionCard>
<SectionCard
title={t("hosts.statusChecksLabel")}
icon={<Activity className="size-3.5" />}
>
<div className="flex flex-col gap-0 py-1">
<SettingRow
label={t("hosts.enableStatusChecks")}
description={t("hosts.enableStatusChecksDesc")}
>
<FakeSwitch
checked={form.statusCheckEnabled}
onChange={(value) => setField("statusCheckEnabled", value)}
/>
</SettingRow>
{form.statusCheckEnabled && (
<SettingRow
label={t("hosts.useGlobalInterval")}
description={t("hosts.useGlobalIntervalDesc")}
>
<FakeSwitch
checked={form.statusCheckInterval === null}
onChange={(useGlobal) =>
setField("statusCheckInterval", useGlobal ? null : 60)
}
/>
</SettingRow>
)}
{form.statusCheckEnabled && form.statusCheckInterval !== null && (
<SettingRow
label={t("hosts.checkIntervalS")}
description={t("hosts.checkIntervalDesc")}
>
<Input
type="number"
min={5}
max={86400}
value={form.statusCheckInterval}
onChange={(e) =>
setField("statusCheckInterval", Number(e.target.value))
}
className="w-20 h-7 text-xs text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/>
</SettingRow>
)}
</div>
</SectionCard>
<SectionCard
title={t("hosts.connectionToolbar")}
icon={<LayoutGrid className="size-3.5" />}
+2 -1
View File
@@ -150,7 +150,8 @@ export function sshHostToHost(h: SSHHostWithStatus): Host {
terminalConfig: parsedTerminalConfig as Host["terminalConfig"],
hasSudoPassword:
!!host.hasSudoPassword || !!parsedTerminalConfig?.sudoPassword,
statsConfig: parseJson(h.statsConfig) as Host["statsConfig"],
statusCheckEnabled: h.statusCheckEnabled !== false,
statusCheckInterval: h.statusCheckInterval ?? null,
forceKeyboardInteractive: h.forceKeyboardInteractive ?? false,
useSocks5: h.useSocks5,
socks5Host: h.socks5Host,
+2 -1
View File
@@ -57,7 +57,8 @@ export const FIELD_GROUP_KEYS: Record<FieldGroup, string[]> = {
"forceKeyboardInteractive",
],
advanced: [
"statsConfig",
"statusCheckEnabled",
"statusCheckInterval",
"dockerConfig",
"webUiConfig",
"proxmoxConfig",
+4 -20
View File
@@ -17,7 +17,7 @@ function makeHost(overrides: Partial<Host> = {}): Host {
enableRdp: false,
enableVnc: false,
enableTelnet: false,
statsConfig: { statusCheckEnabled: true },
statusCheckEnabled: true,
...overrides,
} as unknown as Host;
}
@@ -56,15 +56,7 @@ describe("buildStatusTooltip", () => {
});
it("returns 'Monitoring disabled' when status check is disabled", () => {
const host = makeHost({
statsConfig: {
enabledWidgets: [],
statusCheckEnabled: false,
statusCheckInterval: 30,
metricsEnabled: false,
metricsInterval: 30,
},
});
const host = makeHost({ statusCheckEnabled: false });
const tooltip = buildStatusTooltip(host, "online", t);
expect(tooltip).toBe("Monitoring disabled");
});
@@ -106,20 +98,12 @@ describe("buildStatusTooltip", () => {
describe("statusCheckEnabled", () => {
it("returns true when statusCheckEnabled is not set (default)", () => {
const host = makeHost({ statsConfig: undefined });
const host = makeHost({ statusCheckEnabled: undefined });
expect(statusCheckEnabled(host)).toBe(true);
});
it("returns false when statusCheckEnabled is explicitly false", () => {
const host = makeHost({
statsConfig: {
enabledWidgets: [],
statusCheckEnabled: false,
statusCheckInterval: 30,
metricsEnabled: false,
metricsInterval: 30,
},
});
const host = makeHost({ statusCheckEnabled: false });
expect(statusCheckEnabled(host)).toBe(false);
});
});
+1 -1
View File
@@ -89,7 +89,7 @@ import { shell } from "@/plugin-host/shell-bridge";
import type { TabShellCallbacks } from "@/shell/tab-registry";
export function statusCheckEnabled(host: Host): boolean {
return host.statsConfig?.statusCheckEnabled !== false;
return host.statusCheckEnabled !== false;
}
export function buildStatusTooltip(
+2 -1
View File
@@ -642,7 +642,8 @@ export function SidebarTree({
name: a.name,
snippetId: Number(a.snippetId),
})),
statsConfig: host.statsConfig,
statusCheckEnabled: host.statusCheckEnabled,
statusCheckInterval: host.statusCheckInterval,
terminalConfig: host.terminalConfig ?? null,
};
const created = await createSSHHost(duplicateHost);