From 19b70430a10bf6225bb206087a171508d4e1c898 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Mon, 21 Sep 2026 11:30:46 -0500 Subject: [PATCH] feat: ship host metrics as a first-party in-process plugin --- .gitignore | 2 +- plugins/docker/frontend/index.mjs | 5 +- .../host-metrics/backend}/alert-engine.ts | 8 +- .../backend}/automation-bridge.ts | 4 +- .../host-metrics/backend}/helpers.ts | 2 +- .../host-metrics/backend}/history-routes.ts | 8 +- plugins/host-metrics/backend/index.mjs | 66 +++++++ .../host-metrics/backend}/managers/cron.ts | 4 +- .../backend}/managers/firewall.ts | 6 +- .../host-metrics/backend}/managers/health.ts | 10 +- .../host-metrics/backend}/managers/index.ts | 2 +- .../host-metrics/backend}/managers/logs.ts | 12 +- .../host-metrics/backend/managers/packages.ts | 71 +++++++ .../backend}/managers/processes.ts | 10 +- .../backend}/managers/route-helpers.ts | 10 +- .../backend}/managers/services.ts | 6 +- .../backend}/managers/simple-reads.ts | 2 +- .../host-metrics/backend}/managers/ssl.ts | 11 +- .../backend}/managers/tailscale.ts | 6 +- .../host-metrics/backend}/managers/types.ts | 2 +- .../host-metrics/backend}/managers/users.ts | 9 +- .../backend}/managers/wireguard.ts | 4 +- .../backend}/preferences-routes.ts | 10 +- .../host-metrics/backend/routes.ts | 180 ++++++++++-------- .../host-metrics/backend}/sessions.ts | 2 +- .../host-metrics/backend}/settings-routes.ts | 4 +- .../host-metrics/backend}/viewer-routes.ts | 8 +- .../backend}/widgets/cpu-collector.ts | 2 +- .../backend}/widgets/disk-collector.ts | 2 +- .../backend}/widgets/firewall-collector.ts | 4 +- .../backend}/widgets/gpu-collector.ts | 5 +- .../backend}/widgets/login-stats-collector.ts | 2 +- .../backend}/widgets/memory-collector.ts | 2 +- .../backend}/widgets/network-collector.ts | 2 +- .../backend}/widgets/ports-collector.ts | 4 +- .../backend}/widgets/processes-collector.ts | 2 +- .../backend}/widgets/system-collector.ts | 2 +- .../backend}/widgets/temperature-collector.ts | 5 +- .../backend}/widgets/uptime-collector.ts | 2 +- .../host-metrics/frontend}/HostMetricsApp.tsx | 2 +- .../host-metrics/frontend}/HostMetricsTab.tsx | 2 +- .../frontend}/cards/CardTimeTabs.tsx | 0 .../host-metrics/frontend}/cards/CpuCard.tsx | 2 +- .../host-metrics/frontend}/cards/DiskCard.tsx | 2 +- .../frontend}/cards/FilesystemPicker.tsx | 0 .../frontend}/cards/FirewallCard.tsx | 2 +- .../host-metrics/frontend}/cards/GpuCard.tsx | 2 +- .../frontend}/cards/LoginStatsCard.tsx | 2 +- .../frontend}/cards/MemoryCard.tsx | 2 +- .../frontend}/cards/NetworkCard.tsx | 2 +- .../frontend}/cards/PortsCard.tsx | 2 +- .../frontend}/cards/ProcessesCard.tsx | 2 +- .../frontend}/cards/SystemCard.tsx | 2 +- .../frontend}/cards/TemperatureCard.tsx | 2 +- .../frontend}/cards/UptimeCard.tsx | 2 +- .../frontend}/cards/gpu-history.ts | 0 .../host-metrics/frontend}/cards/index.tsx | 0 .../cards/managers/CronManagerCard.tsx | 0 .../cards/managers/FirewallManagerCard.tsx | 0 .../cards/managers/HealthCheckCard.tsx | 0 .../cards/managers/LogViewerCard.tsx | 0 .../cards/managers/ManagerCardShell.tsx | 2 +- .../cards/managers/ManagerToolbar.tsx | 0 .../cards/managers/PackageManagerCard.tsx | 0 .../cards/managers/ProcessInspectorCard.tsx | 0 .../cards/managers/ServiceManagerCard.tsx | 0 .../cards/managers/SimpleManagerCards.tsx | 0 .../cards/managers/SslManagerCard.tsx | 0 .../cards/managers/TailscaleManagerCard.tsx | 0 .../cards/managers/UserManagerCard.tsx | 0 .../cards/managers/WireGuardManagerCard.tsx | 0 .../cards/managers/useManagerData.ts | 0 .../hooks/useHostMetricsPreferences.ts | 0 plugins/host-metrics/frontend/index.mjs | 49 +++++ .../frontend}/metrics-change-key.ts | 0 .../frontend}/temperature-preference.ts | 0 plugins/host-metrics/manifest.json | 30 +++ scripts/copy-bundled-plugins.cjs | 52 +++-- src/backend/ai/tools/executor.ts | 2 +- src/backend/automations/actions/index.ts | 4 +- src/backend/automations/docker-watcher.ts | 2 +- src/backend/database/routes/fleet-routes.ts | 10 +- src/backend/database/routes/proxmox.ts | 2 +- src/backend/hosts/host-key-verifier.ts | 2 +- .../common-utils.ts | 0 .../exec-elevated.ts | 2 +- .../host-status.ts | 0 .../package-commands.ts} | 66 +------ .../managers => metrics-shared}/platform.ts | 2 +- .../{metrics => metrics-shared}/state.ts | 0 .../managers => metrics-shared}/validation.ts | 0 .../hosts/metrics/proxmox-stats-polling.ts | 2 +- .../proxmox/cluster-health-collector.ts | 2 +- .../metrics/proxmox/collect-proxmox-stats.ts | 2 +- .../hosts/metrics/proxmox/guests-collector.ts | 2 +- .../metrics/proxmox/node-network-collector.ts | 2 +- .../metrics/proxmox/node-status-collector.ts | 2 +- .../metrics/proxmox/storage-collector.ts | 2 +- src/backend/hosts/ssh-client-factory.ts | 3 +- src/backend/plugins/first-party.ts | 8 + src/backend/plugins/index.ts | 2 +- src/backend/starter.ts | 8 +- .../tests/ai/proposal-executor.test.ts | 2 +- .../database/routes/fleet-routes.test.ts | 14 +- .../database/routes/proxmox-sudo.test.ts | 2 +- .../tests/hosts/host-key-recovery.test.ts | 2 +- .../common-utils.test.ts | 2 +- .../exec-elevated.test.ts | 4 +- .../hosts/metrics-shared}/host-status.test.ts | 2 +- .../{metrics => metrics-shared}/state.test.ts | 2 +- .../proxmox/cluster-health-collector.test.ts | 2 +- .../proxmox/collect-proxmox-stats.test.ts | 2 +- .../metrics/proxmox/guests-collector.test.ts | 2 +- .../proxmox/node-network-collector.test.ts | 2 +- .../proxmox/node-status-collector.test.ts | 2 +- .../metrics/proxmox/storage-collector.test.ts | 2 +- .../tests/plugins/bundled-plugin.test.ts | 50 +++++ src/backend/tests/plugins/events.test.ts | 2 +- src/backend/tests/plugins/first-party.test.ts | 22 ++- .../alert-engine-superseded.test.ts | 2 +- .../host-metrics}/helpers.test.ts | 2 +- .../login-alert-dispatch.test.ts | 7 +- .../login-alert-route-order.test.ts | 5 +- .../host-metrics}/managers/managers.test.ts | 26 +-- .../host-metrics}/status-poll-auth.test.ts | 5 +- .../widgets/cpu-collector.test.ts | 2 +- .../widgets/disk-collector.test.ts | 2 +- .../widgets/gpu-collector.test.ts | 2 +- .../widgets/network-collector.test.ts | 2 +- .../widgets/temperature-collector.test.ts | 2 +- .../crypto-migration/automations-migration.ts | 20 +- src/main.tsx | 2 +- src/types/ui-preferences.ts | 2 +- .../metric-card.tsx} | 0 .../proxmox-stats/cards/ClusterHealthCard.tsx | 2 +- .../proxmox-stats/cards/NodeNetworkCard.tsx | 2 +- .../proxmox-stats/cards/StoragePoolsCard.tsx | 2 +- src/ui/shell/tabUtils.tsx | 2 +- src/ui/sidebar/FleetsPanel.tsx | 16 +- .../host-metrics/HostMetricsTab.test.tsx | 4 +- .../host-metrics/cards}/GpuCard.test.tsx | 2 +- .../host-metrics/cards}/gpu-history.test.ts | 2 +- .../host-metrics/metrics-change-key.test.ts | 2 +- .../temperature-preference.test.ts | 2 +- tsconfig.plugins.json | 5 +- 145 files changed, 675 insertions(+), 349 deletions(-) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/alert-engine.ts (97%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/automation-bridge.ts (91%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/helpers.ts (94%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/history-routes.ts (93%) create mode 100644 plugins/host-metrics/backend/index.mjs rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/managers/cron.ts (95%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/managers/firewall.ts (93%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/managers/health.ts (94%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/managers/index.ts (95%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/managers/logs.ts (90%) create mode 100644 plugins/host-metrics/backend/managers/packages.ts rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/managers/processes.ts (92%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/managers/route-helpers.ts (81%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/managers/services.ts (93%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/managers/simple-reads.ts (97%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/managers/ssl.ts (96%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/managers/tailscale.ts (94%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/managers/types.ts (89%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/managers/users.ts (93%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/managers/wireguard.ts (97%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/preferences-routes.ts (95%) rename src/backend/hosts/metrics/index.ts => plugins/host-metrics/backend/routes.ts (95%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/sessions.ts (96%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/settings-routes.ts (97%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/viewer-routes.ts (96%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/widgets/cpu-collector.ts (98%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/widgets/disk-collector.ts (99%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/widgets/firewall-collector.ts (97%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/widgets/gpu-collector.ts (97%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/widgets/login-stats-collector.ts (97%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/widgets/memory-collector.ts (98%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/widgets/network-collector.ts (99%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/widgets/ports-collector.ts (96%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/widgets/processes-collector.ts (95%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/widgets/system-collector.ts (96%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/widgets/temperature-collector.ts (96%) rename {src/backend/hosts/metrics => plugins/host-metrics/backend}/widgets/uptime-collector.ts (97%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/HostMetricsApp.tsx (94%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/HostMetricsTab.tsx (99%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/CardTimeTabs.tsx (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/CpuCard.tsx (98%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/DiskCard.tsx (98%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/FilesystemPicker.tsx (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/FirewallCard.tsx (97%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/GpuCard.tsx (99%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/LoginStatsCard.tsx (97%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/MemoryCard.tsx (98%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/NetworkCard.tsx (99%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/PortsCard.tsx (98%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/ProcessesCard.tsx (96%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/SystemCard.tsx (95%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/TemperatureCard.tsx (98%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/UptimeCard.tsx (94%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/gpu-history.ts (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/index.tsx (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/managers/CronManagerCard.tsx (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/managers/FirewallManagerCard.tsx (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/managers/HealthCheckCard.tsx (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/managers/LogViewerCard.tsx (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/managers/ManagerCardShell.tsx (97%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/managers/ManagerToolbar.tsx (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/managers/PackageManagerCard.tsx (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/managers/ProcessInspectorCard.tsx (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/managers/ServiceManagerCard.tsx (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/managers/SimpleManagerCards.tsx (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/managers/SslManagerCard.tsx (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/managers/TailscaleManagerCard.tsx (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/managers/UserManagerCard.tsx (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/managers/WireGuardManagerCard.tsx (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/cards/managers/useManagerData.ts (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/hooks/useHostMetricsPreferences.ts (100%) create mode 100644 plugins/host-metrics/frontend/index.mjs rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/metrics-change-key.ts (100%) rename {src/ui/features/host-metrics => plugins/host-metrics/frontend}/temperature-preference.ts (100%) create mode 100644 plugins/host-metrics/manifest.json rename src/backend/hosts/{metrics/widgets => metrics-shared}/common-utils.ts (100%) rename src/backend/hosts/{metrics/managers => metrics-shared}/exec-elevated.ts (98%) rename src/backend/hosts/{metrics => metrics-shared}/host-status.ts (100%) rename src/backend/hosts/{metrics/managers/packages.ts => metrics-shared/package-commands.ts} (52%) rename src/backend/hosts/{metrics/managers => metrics-shared}/platform.ts (97%) rename src/backend/hosts/{metrics => metrics-shared}/state.ts (100%) rename src/backend/hosts/{metrics/managers => metrics-shared}/validation.ts (100%) rename src/backend/tests/hosts/{metrics/widgets => metrics-shared}/common-utils.test.ts (95%) rename src/backend/tests/hosts/{metrics/managers => metrics-shared}/exec-elevated.test.ts (96%) rename src/backend/{hosts/metrics => tests/hosts/metrics-shared}/host-status.test.ts (96%) rename src/backend/tests/hosts/{metrics => metrics-shared}/state.test.ts (99%) rename src/backend/tests/{hosts/metrics => plugins/host-metrics}/alert-engine-superseded.test.ts (97%) rename src/backend/tests/{hosts/metrics => plugins/host-metrics}/helpers.test.ts (98%) rename src/backend/tests/{hosts/metrics => plugins/host-metrics}/login-alert-dispatch.test.ts (96%) rename src/backend/tests/{hosts/metrics => plugins/host-metrics}/login-alert-route-order.test.ts (90%) rename src/backend/tests/{hosts => plugins/host-metrics}/managers/managers.test.ts (93%) rename src/backend/tests/{hosts/metrics => plugins/host-metrics}/status-poll-auth.test.ts (92%) rename src/backend/tests/{hosts/metrics => plugins/host-metrics}/widgets/cpu-collector.test.ts (94%) rename src/backend/tests/{hosts/metrics => plugins/host-metrics}/widgets/disk-collector.test.ts (99%) rename src/backend/tests/{hosts/metrics => plugins/host-metrics}/widgets/gpu-collector.test.ts (98%) rename src/backend/tests/{hosts/metrics => plugins/host-metrics}/widgets/network-collector.test.ts (97%) rename src/backend/tests/{hosts/metrics => plugins/host-metrics}/widgets/temperature-collector.test.ts (92%) rename src/ui/{features/host-metrics/cards/MetricCard.tsx => components/metric-card.tsx} (100%) rename src/ui/tests/{features => plugins}/host-metrics/HostMetricsTab.test.tsx (94%) rename src/ui/tests/{features/host-metrics => plugins/host-metrics/cards}/GpuCard.test.tsx (97%) rename src/ui/tests/{features/host-metrics => plugins/host-metrics/cards}/gpu-history.test.ts (94%) rename src/ui/tests/{features => plugins}/host-metrics/metrics-change-key.test.ts (94%) rename src/ui/tests/{features => plugins}/host-metrics/temperature-preference.test.ts (90%) diff --git a/.gitignore b/.gitignore index 427895808..7b94e7a4a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ coverage # imports for type-checking (src/types, src/backend/utils, repositories...); # scripts/copy-bundled-plugins.cjs removes those after every compile, this # is a safety net for running tsc -p tsconfig.plugins.json directly. -/plugins/*/backend/*.js +/plugins/*/backend/**/*.js /src/**/*.js .vscode/* diff --git a/plugins/docker/frontend/index.mjs b/plugins/docker/frontend/index.mjs index 0575e87f3..ba6761f93 100644 --- a/plugins/docker/frontend/index.mjs +++ b/plugins/docker/frontend/index.mjs @@ -45,9 +45,8 @@ export async function register({ }); if (registerHostEditorTab) { - const { HostDockerTab } = await import( - "../../../src/ui/sidebar/HostEditorFeatureTabs.tsx" - ); + const { HostDockerTab } = + await import("../../../src/ui/sidebar/HostEditorFeatureTabs.tsx"); registerHostEditorTab({ id: hostEditorTabId, labelKey: "hosts.tabDocker", diff --git a/src/backend/hosts/metrics/alert-engine.ts b/plugins/host-metrics/backend/alert-engine.ts similarity index 97% rename from src/backend/hosts/metrics/alert-engine.ts rename to plugins/host-metrics/backend/alert-engine.ts index 8699dce07..3fcb1ae67 100644 --- a/src/backend/hosts/metrics/alert-engine.ts +++ b/plugins/host-metrics/backend/alert-engine.ts @@ -1,11 +1,11 @@ -import { createCurrentAlertRepository } from "../../database/repositories/factory.js"; -import { statsLogger } from "../../utils/logger.js"; +import { createCurrentAlertRepository } from "../../../src/backend/database/repositories/factory.js"; +import { statsLogger } from "../../../src/backend/utils/logger.js"; import { sendNotification, type AlertPayload, type NotificationChannel, -} from "../../utils/notification-sender.js"; -import { sendDiscord } from "../../utils/discord-sender.js"; +} from "../../../src/backend/utils/notification-sender.js"; +import { sendDiscord } from "../../../src/backend/utils/discord-sender.js"; type AlertTriggerType = | "host_offline" diff --git a/src/backend/hosts/metrics/automation-bridge.ts b/plugins/host-metrics/backend/automation-bridge.ts similarity index 91% rename from src/backend/hosts/metrics/automation-bridge.ts rename to plugins/host-metrics/backend/automation-bridge.ts index b559c7af1..53ba78ce5 100644 --- a/src/backend/hosts/metrics/automation-bridge.ts +++ b/plugins/host-metrics/backend/automation-bridge.ts @@ -11,8 +11,8 @@ * `hosts/automation-events.ts` -- that one is shared across features. */ -import { pluginEvents, TOPICS } from "../../plugins/events.js"; -import type { MetricsSnapshot } from "../../automations/conditions.js"; +import { pluginEvents, TOPICS } from "../../../src/backend/plugins/events.js"; +import type { MetricsSnapshot } from "../../../src/backend/automations/conditions.js"; export interface HostMetricsPayload { hostId: number; diff --git a/src/backend/hosts/metrics/helpers.ts b/plugins/host-metrics/backend/helpers.ts similarity index 94% rename from src/backend/hosts/metrics/helpers.ts rename to plugins/host-metrics/backend/helpers.ts index d10b62828..48ac7f77a 100644 --- a/src/backend/hosts/metrics/helpers.ts +++ b/plugins/host-metrics/backend/helpers.ts @@ -1,5 +1,5 @@ import type { Client } from "ssh2"; -import { usesIssuedCertificate } from "../issued-certificate-auth.js"; +import { usesIssuedCertificate } from "../../../src/backend/hosts/issued-certificate-auth.js"; export type StatsCapableHost = { connectionType?: string; diff --git a/src/backend/hosts/metrics/history-routes.ts b/plugins/host-metrics/backend/history-routes.ts similarity index 93% rename from src/backend/hosts/metrics/history-routes.ts rename to plugins/host-metrics/backend/history-routes.ts index 03fe2840d..58e66211b 100644 --- a/src/backend/hosts/metrics/history-routes.ts +++ b/plugins/host-metrics/backend/history-routes.ts @@ -1,8 +1,8 @@ import type { Express, RequestHandler } from "express"; -import type { AuthenticatedRequest } from "../../../types/index.js"; -import { createCurrentHostMetricsHistoryRepository } from "../../database/repositories/factory.js"; -import { statsLogger } from "../../utils/logger.js"; -import type { HostAction } from "../../utils/permission-manager.js"; +import type { AuthenticatedRequest } from "../../../src/types/index.js"; +import { createCurrentHostMetricsHistoryRepository } from "../../../src/backend/database/repositories/factory.js"; +import { statsLogger } from "../../../src/backend/utils/logger.js"; +import type { HostAction } from "../../../src/backend/utils/permission-manager.js"; type HistoryRoutesDeps = { validateHostId: RequestHandler; diff --git a/plugins/host-metrics/backend/index.mjs b/plugins/host-metrics/backend/index.mjs new file mode 100644 index 000000000..327331f43 --- /dev/null +++ b/plugins/host-metrics/backend/index.mjs @@ -0,0 +1,66 @@ +/** + * Host Metrics - first-party, in-process plugin. + * + * This plugin runs on the main thread rather than in a worker for the same + * reason ssh-terminal and docker do (see src/backend/plugins/first-party.ts): + * its polling manager holds long-lived ssh2.Client connections across a + * whole fleet of hosts, reused between polling ticks, and its connect flow + * implements the same jump-host/SOCKS5/TOTP/Warpgate/OPKSSH/Vault + * keyboard-interactive handshakes the terminal does. None of that can cross + * the structured-clone postMessage boundary a worker plugin talks over, and + * the worker ctx.ssh primitive (connect + single exec-and-collect) has no + * hook for a session that outlives one request. + * + * routes.ts is what used to be src/backend/hosts/metrics/index.ts, moved + * here and rewritten from a module-scope SIGINT/SIGTERM + listenOnServicePort + * call into startHostMetricsService()/shutdown(), called from + * activate()/deactivate() below. + * + * Proxmox-related code (metrics/proxmox/*, proxmox-stats-*) stays in core at + * src/backend/hosts/metrics/ rather than moving here -- it is planned as its + * own future plugin, so this move does not touch it. + */ + +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +/** + * Loads a module by its path relative to this file's own directory. + * + * The plugin ships beside dist/backend, so in a built server the compiled + * .js for the moved host-metrics files (routes.js, ...) sits right next to + * this .mjs, and core modules resolve via ../../../backend/backend/... from + * there (dist/plugins/host-metrics/backend -> dist/backend/backend). Under + * vitest/dev nothing imports this plugin directly (the loader loads + * in-process plugins with a plain dynamic import, no tsx loader involved), + * so the only layout that needs to resolve here is the built one. + */ +async function loadRelative(relativePath) { + const here = path.dirname(fileURLToPath(import.meta.url)); + const target = path.join(here, `${relativePath}.js`); + return import(new URL(`file://${target.replace(/\\/g, "/")}`).href); +} + +let routesModule = null; +let httpServer = null; + +export async function activate(ctx) { + routesModule = await loadRelative("routes"); + + httpServer = routesModule.startHostMetricsService(); + + ctx.log.info(`Host Metrics listening on ${routesModule.PORT ?? 30005}`); +} + +export async function deactivate() { + if (routesModule) { + routesModule.shutdown(); + routesModule = null; + } + + if (httpServer) { + const server = httpServer; + httpServer = null; + await new Promise((resolve) => server.close(() => resolve())); + } +} diff --git a/src/backend/hosts/metrics/managers/cron.ts b/plugins/host-metrics/backend/managers/cron.ts similarity index 95% rename from src/backend/hosts/metrics/managers/cron.ts rename to plugins/host-metrics/backend/managers/cron.ts index 08ac8d024..362e7f596 100644 --- a/src/backend/hosts/metrics/managers/cron.ts +++ b/plugins/host-metrics/backend/managers/cron.ts @@ -1,7 +1,7 @@ import type { Express } from "express"; -import { execCommand } from "../widgets/common-utils.js"; +import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; -import { shellSingleQuote } from "./exec-elevated.js"; +import { shellSingleQuote } from "../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; import type { ManagerRoutesDeps } from "./types.js"; export interface CronEntry { diff --git a/src/backend/hosts/metrics/managers/firewall.ts b/plugins/host-metrics/backend/managers/firewall.ts similarity index 93% rename from src/backend/hosts/metrics/managers/firewall.ts rename to plugins/host-metrics/backend/managers/firewall.ts index 535c289b6..afeeb1461 100644 --- a/src/backend/hosts/metrics/managers/firewall.ts +++ b/plugins/host-metrics/backend/managers/firewall.ts @@ -1,6 +1,6 @@ import type { Express } from "express"; import { collectFirewallMetrics } from "../widgets/firewall-collector.js"; -import { execElevated } from "./exec-elevated.js"; +import { execElevated } from "../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; import { isValidPort, @@ -8,8 +8,8 @@ import { isValidFirewallTarget, type IpProtocol, type FirewallTarget, -} from "./validation.js"; -import { detectPlatform } from "./platform.js"; +} from "../../../../src/backend/hosts/metrics-shared/validation.js"; +import { detectPlatform } from "../../../../src/backend/hosts/metrics-shared/platform.js"; import type { ManagerRoutesDeps } from "./types.js"; export interface FirewallRuleSpec { diff --git a/src/backend/hosts/metrics/managers/health.ts b/plugins/host-metrics/backend/managers/health.ts similarity index 94% rename from src/backend/hosts/metrics/managers/health.ts rename to plugins/host-metrics/backend/managers/health.ts index 6c8abf7ff..f72612dcc 100644 --- a/src/backend/hosts/metrics/managers/health.ts +++ b/plugins/host-metrics/backend/managers/health.ts @@ -1,11 +1,11 @@ import type { Express } from "express"; import type { Client } from "ssh2"; -import type { AuthenticatedRequest } from "../../../../types/index.js"; -import { execCommand } from "../widgets/common-utils.js"; -import { createCurrentHostHealthRepository } from "../../../database/repositories/factory.js"; +import type { AuthenticatedRequest } from "../../../../src/types/index.js"; +import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; +import { createCurrentHostHealthRepository } from "../../../../src/backend/database/repositories/factory.js"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; -import { shellSingleQuote } from "./exec-elevated.js"; -import { isValidPort } from "./validation.js"; +import { shellSingleQuote } from "../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; +import { isValidPort } from "../../../../src/backend/hosts/metrics-shared/validation.js"; import type { ManagerRoutesDeps } from "./types.js"; import { AlertEngine } from "../alert-engine.js"; import { notifyAutomationHealthCheck } from "../automation-bridge.js"; diff --git a/src/backend/hosts/metrics/managers/index.ts b/plugins/host-metrics/backend/managers/index.ts similarity index 95% rename from src/backend/hosts/metrics/managers/index.ts rename to plugins/host-metrics/backend/managers/index.ts index f4e79d1ee..da5c6d1e8 100644 --- a/src/backend/hosts/metrics/managers/index.ts +++ b/plugins/host-metrics/backend/managers/index.ts @@ -1,5 +1,5 @@ import type { Express } from "express"; -import { detectPlatform } from "./platform.js"; +import { detectPlatform } from "../../../../src/backend/hosts/metrics-shared/platform.js"; import { managerHandler } from "./route-helpers.js"; import type { ManagerRoutesDeps } from "./types.js"; import { registerServiceRoutes } from "./services.js"; diff --git a/src/backend/hosts/metrics/managers/logs.ts b/plugins/host-metrics/backend/managers/logs.ts similarity index 90% rename from src/backend/hosts/metrics/managers/logs.ts rename to plugins/host-metrics/backend/managers/logs.ts index c1e88efbd..9da3c8264 100644 --- a/src/backend/hosts/metrics/managers/logs.ts +++ b/plugins/host-metrics/backend/managers/logs.ts @@ -1,8 +1,14 @@ import type { Express } from "express"; -import { execCommand } from "../widgets/common-utils.js"; -import { execElevated, shellSingleQuote } from "./exec-elevated.js"; +import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; +import { + execElevated, + shellSingleQuote, +} from "../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; -import { isAllowedPath, isValidSystemdUnit } from "./validation.js"; +import { + isAllowedPath, + isValidSystemdUnit, +} from "../../../../src/backend/hosts/metrics-shared/validation.js"; import type { ManagerRoutesDeps } from "./types.js"; /** Directories from which arbitrary log files may be tailed. */ diff --git a/plugins/host-metrics/backend/managers/packages.ts b/plugins/host-metrics/backend/managers/packages.ts new file mode 100644 index 000000000..1a1c7d71c --- /dev/null +++ b/plugins/host-metrics/backend/managers/packages.ts @@ -0,0 +1,71 @@ +import type { Express } from "express"; +import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; +import { execElevated } from "../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; +import { managerHandler, ManagerInputError } from "./route-helpers.js"; +import { isValidPackageName } from "../../../../src/backend/hosts/metrics-shared/validation.js"; +import { detectPlatform } from "../../../../src/backend/hosts/metrics-shared/platform.js"; +import { + buildListUpgradableCommand, + buildPackageActionCommand, + parseUpgradable, + type PackageAction, +} from "../../../../src/backend/hosts/metrics-shared/package-commands.js"; +import type { ManagerRoutesDeps } from "./types.js"; + +export function registerPackageRoutes( + app: Express, + { validateHostId, runOnHost }: ManagerRoutesDeps, +): void { + app.get( + "/host-metrics/managers/packages/:id", + validateHostId, + managerHandler(runOnHost, "connect", "packages_list", async (client) => { + const platform = await detectPlatform(client); + const cmd = buildListUpgradableCommand(platform.pkg); + if (!cmd) return { pkg: platform.pkg, upgradable: [] }; + const { stdout } = await execCommand(client, cmd, 60000); + return { + pkg: platform.pkg, + upgradable: parseUpgradable(platform.pkg, stdout), + }; + }), + ); + + app.post( + "/host-metrics/managers/packages/:id/action", + validateHostId, + managerHandler( + runOnHost, + "connect", + "packages_action", + async (client, host, req) => { + const { action, pkg: name } = req.body as { + action?: PackageAction; + pkg?: string; + }; + if ( + action !== "upgrade-all" && + action !== "install" && + action !== "upgrade" + ) { + throw new ManagerInputError("Invalid action"); + } + if (action !== "upgrade-all" && !isValidPackageName(name)) { + throw new ManagerInputError("Invalid package name"); + } + const platform = await detectPlatform(client); + const cmd = buildPackageActionCommand(platform.pkg, action, name); + if (!cmd) throw new ManagerInputError("No supported package manager"); + // Package operations can be slow; allow up to 10 minutes. + const result = await execElevated(client, cmd, host.sudoPassword, { + forceSudo: true, + timeoutMs: 600000, + }); + return { + success: result.code === 0, + output: (result.stdout || result.stderr).slice(-8000), + }; + }, + ), + ); +} diff --git a/src/backend/hosts/metrics/managers/processes.ts b/plugins/host-metrics/backend/managers/processes.ts similarity index 92% rename from src/backend/hosts/metrics/managers/processes.ts rename to plugins/host-metrics/backend/managers/processes.ts index 5e1e20452..6fc6df337 100644 --- a/src/backend/hosts/metrics/managers/processes.ts +++ b/plugins/host-metrics/backend/managers/processes.ts @@ -1,8 +1,12 @@ import type { Express } from "express"; -import { execCommand } from "../widgets/common-utils.js"; -import { execElevated } from "./exec-elevated.js"; +import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; +import { execElevated } from "../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; -import { isValidPid, isValidSignal, type Signal } from "./validation.js"; +import { + isValidPid, + isValidSignal, + type Signal, +} from "../../../../src/backend/hosts/metrics-shared/validation.js"; import type { ManagerRoutesDeps } from "./types.js"; export interface ProcessRow { diff --git a/src/backend/hosts/metrics/managers/route-helpers.ts b/plugins/host-metrics/backend/managers/route-helpers.ts similarity index 81% rename from src/backend/hosts/metrics/managers/route-helpers.ts rename to plugins/host-metrics/backend/managers/route-helpers.ts index 1de07c64e..3677d4a80 100644 --- a/src/backend/hosts/metrics/managers/route-helpers.ts +++ b/plugins/host-metrics/backend/managers/route-helpers.ts @@ -1,11 +1,11 @@ -import { getErrorMessage } from "../../../utils/error-message.js"; +import { getErrorMessage } from "../../../../src/backend/utils/error-message.js"; import type { Request, Response } from "express"; import type { Client } from "ssh2"; -import type { AuthenticatedRequest } from "../../../../types/index.js"; -import { statsLogger } from "../../../utils/logger.js"; -import { ElevationError } from "./exec-elevated.js"; +import type { AuthenticatedRequest } from "../../../../src/types/index.js"; +import { statsLogger } from "../../../../src/backend/utils/logger.js"; +import { ElevationError } from "../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; import type { ManagerHost, RunOnHost } from "./types.js"; -import type { HostAction } from "../../../utils/permission-manager.js"; +import type { HostAction } from "../../../../src/backend/utils/permission-manager.js"; export class AccessDeniedError extends Error { constructor(message = "No access to this host") { diff --git a/src/backend/hosts/metrics/managers/services.ts b/plugins/host-metrics/backend/managers/services.ts similarity index 93% rename from src/backend/hosts/metrics/managers/services.ts rename to plugins/host-metrics/backend/managers/services.ts index 724bc5170..6502a39f4 100644 --- a/src/backend/hosts/metrics/managers/services.ts +++ b/plugins/host-metrics/backend/managers/services.ts @@ -1,12 +1,12 @@ import type { Express } from "express"; -import { execCommand } from "../widgets/common-utils.js"; -import { execElevated } from "./exec-elevated.js"; +import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; +import { execElevated } from "../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; import { isValidSystemdUnit, isValidServiceAction, type ServiceAction, -} from "./validation.js"; +} from "../../../../src/backend/hosts/metrics-shared/validation.js"; import type { ManagerRoutesDeps } from "./types.js"; export interface SystemdService { diff --git a/src/backend/hosts/metrics/managers/simple-reads.ts b/plugins/host-metrics/backend/managers/simple-reads.ts similarity index 97% rename from src/backend/hosts/metrics/managers/simple-reads.ts rename to plugins/host-metrics/backend/managers/simple-reads.ts index aa5dd2a4e..f74e1b0c2 100644 --- a/src/backend/hosts/metrics/managers/simple-reads.ts +++ b/plugins/host-metrics/backend/managers/simple-reads.ts @@ -1,5 +1,5 @@ import type { Express } from "express"; -import { execCommand } from "../widgets/common-utils.js"; +import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; import { managerHandler } from "./route-helpers.js"; import type { ManagerRoutesDeps } from "./types.js"; diff --git a/src/backend/hosts/metrics/managers/ssl.ts b/plugins/host-metrics/backend/managers/ssl.ts similarity index 96% rename from src/backend/hosts/metrics/managers/ssl.ts rename to plugins/host-metrics/backend/managers/ssl.ts index 96248bd89..6927f0a92 100644 --- a/src/backend/hosts/metrics/managers/ssl.ts +++ b/plugins/host-metrics/backend/managers/ssl.ts @@ -1,13 +1,16 @@ import type { Express } from "express"; -import { execCommand } from "../widgets/common-utils.js"; -import { execElevated, shellSingleQuote } from "./exec-elevated.js"; +import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; +import { + execElevated, + shellSingleQuote, +} from "../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; import { isValidDomain, isValidDnsProvider, isAllowedPath, -} from "./validation.js"; -import { detectPlatform } from "./platform.js"; +} from "../../../../src/backend/hosts/metrics-shared/validation.js"; +import { detectPlatform } from "../../../../src/backend/hosts/metrics-shared/platform.js"; import type { ManagerRoutesDeps } from "./types.js"; export type AcmeClient = "certbot" | "acme.sh"; diff --git a/src/backend/hosts/metrics/managers/tailscale.ts b/plugins/host-metrics/backend/managers/tailscale.ts similarity index 94% rename from src/backend/hosts/metrics/managers/tailscale.ts rename to plugins/host-metrics/backend/managers/tailscale.ts index 59f58ecab..b7166c55a 100644 --- a/src/backend/hosts/metrics/managers/tailscale.ts +++ b/plugins/host-metrics/backend/managers/tailscale.ts @@ -1,9 +1,9 @@ import type { Express } from "express"; -import { execCommand } from "../widgets/common-utils.js"; -import { execElevated } from "./exec-elevated.js"; +import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; +import { execElevated } from "../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; import type { ManagerRoutesDeps } from "./types.js"; -import { isValidTailscaleAction } from "./validation.js"; +import { isValidTailscaleAction } from "../../../../src/backend/hosts/metrics-shared/validation.js"; export interface TailscalePeer { hostname: string; diff --git a/src/backend/hosts/metrics/managers/types.ts b/plugins/host-metrics/backend/managers/types.ts similarity index 89% rename from src/backend/hosts/metrics/managers/types.ts rename to plugins/host-metrics/backend/managers/types.ts index 09fbee052..486b36824 100644 --- a/src/backend/hosts/metrics/managers/types.ts +++ b/plugins/host-metrics/backend/managers/types.ts @@ -1,6 +1,6 @@ import type { Client } from "ssh2"; import type { RequestHandler } from "express"; -import type { HostAction } from "../../../utils/permission-manager.js"; +import type { HostAction } from "../../../../src/backend/utils/permission-manager.js"; /** Minimal host shape managers need (includes the decrypted sudo password). */ export interface ManagerHost { diff --git a/src/backend/hosts/metrics/managers/users.ts b/plugins/host-metrics/backend/managers/users.ts similarity index 93% rename from src/backend/hosts/metrics/managers/users.ts rename to plugins/host-metrics/backend/managers/users.ts index 764f53ec6..8df13a739 100644 --- a/src/backend/hosts/metrics/managers/users.ts +++ b/plugins/host-metrics/backend/managers/users.ts @@ -1,8 +1,11 @@ import type { Express } from "express"; -import { execCommand } from "../widgets/common-utils.js"; -import { execElevated } from "./exec-elevated.js"; +import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; +import { execElevated } from "../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; -import { isValidUsername, isValidGroupName } from "./validation.js"; +import { + isValidUsername, + isValidGroupName, +} from "../../../../src/backend/hosts/metrics-shared/validation.js"; import type { ManagerRoutesDeps } from "./types.js"; export interface SystemUser { diff --git a/src/backend/hosts/metrics/managers/wireguard.ts b/plugins/host-metrics/backend/managers/wireguard.ts similarity index 97% rename from src/backend/hosts/metrics/managers/wireguard.ts rename to plugins/host-metrics/backend/managers/wireguard.ts index 8304c6334..dc199712e 100644 --- a/src/backend/hosts/metrics/managers/wireguard.ts +++ b/plugins/host-metrics/backend/managers/wireguard.ts @@ -1,11 +1,11 @@ import type { Express } from "express"; -import { execElevated } from "./exec-elevated.js"; +import { execElevated } from "../../../../src/backend/hosts/metrics-shared/exec-elevated.js"; import { managerHandler, ManagerInputError } from "./route-helpers.js"; import type { ManagerRoutesDeps } from "./types.js"; import { isValidWireGuardInterface, isValidWireGuardAction, -} from "./validation.js"; +} from "../../../../src/backend/hosts/metrics-shared/validation.js"; export interface WireGuardPeer { publicKey: string; diff --git a/src/backend/hosts/metrics/preferences-routes.ts b/plugins/host-metrics/backend/preferences-routes.ts similarity index 95% rename from src/backend/hosts/metrics/preferences-routes.ts rename to plugins/host-metrics/backend/preferences-routes.ts index 3a6e785a0..9bb9ef6f1 100644 --- a/src/backend/hosts/metrics/preferences-routes.ts +++ b/plugins/host-metrics/backend/preferences-routes.ts @@ -1,13 +1,13 @@ import type { Express, RequestHandler } from "express"; -import type { AuthenticatedRequest } from "../../../types/index.js"; -import { createCurrentHostMetricsPreferenceRepository } from "../../database/repositories/factory.js"; -import { statsLogger } from "../../utils/logger.js"; +import type { AuthenticatedRequest } from "../../../src/types/index.js"; +import { createCurrentHostMetricsPreferenceRepository } from "../../../src/backend/database/repositories/factory.js"; +import { statsLogger } from "../../../src/backend/utils/logger.js"; import { deriveEnabledWidgets, defaultLayoutFromWidgets, type HostMetricsLayout, -} from "../../../types/host-metrics.js"; -import type { HostAction } from "../../utils/permission-manager.js"; +} from "../../../src/types/host-metrics.js"; +import type { HostAction } from "../../../src/backend/utils/permission-manager.js"; interface PrefStatsConfig { enabledWidgets?: string[]; diff --git a/src/backend/hosts/metrics/index.ts b/plugins/host-metrics/backend/routes.ts similarity index 95% rename from src/backend/hosts/metrics/index.ts rename to plugins/host-metrics/backend/routes.ts index 23ec9681a..8c6294909 100644 --- a/src/backend/hosts/metrics/index.ts +++ b/plugins/host-metrics/backend/routes.ts @@ -1,30 +1,33 @@ -import { getErrorMessage } from "../../utils/error-message.js"; -import { usesIssuedCertificate } from "../issued-certificate-auth.js"; +import { getErrorMessage } from "../../../src/backend/utils/error-message.js"; +import { usesIssuedCertificate } from "../../../src/backend/hosts/issued-certificate-auth.js"; import express from "express"; import net from "net"; -import { createCorsMiddleware } from "../../utils/cors-config.js"; -import { createCompressionMiddleware } from "../../utils/compression-config.js"; +import { createCorsMiddleware } from "../../../src/backend/utils/cors-config.js"; +import { createCompressionMiddleware } from "../../../src/backend/utils/compression-config.js"; import cookieParser from "cookie-parser"; import { Client, type ConnectConfig } from "ssh2"; -import { SSH_ALGORITHMS } from "../../utils/ssh-algorithms.js"; +import { SSH_ALGORITHMS } from "../../../src/backend/utils/ssh-algorithms.js"; import { pickResolvedPassword, pickResolvedUsername, -} from "../credential-username.js"; +} from "../../../src/backend/hosts/credential-username.js"; import { getCurrentSettingValue, createCurrentHostMetricsHistoryRepository, createCurrentHostResolutionRepository, -} from "../../database/repositories/factory.js"; -import { statsLogger } from "../../utils/logger.js"; -import { DataCrypto } from "../../utils/data-crypto.js"; -import { AuthManager } from "../../utils/auth-manager.js"; -import { PermissionManager } from "../../utils/permission-manager.js"; -import type { AuthenticatedRequest, ProxyNode } from "../../../types/index.js"; +} from "../../../src/backend/database/repositories/factory.js"; +import { statsLogger } from "../../../src/backend/utils/logger.js"; +import { DataCrypto } from "../../../src/backend/utils/data-crypto.js"; +import { AuthManager } from "../../../src/backend/utils/auth-manager.js"; +import { PermissionManager } from "../../../src/backend/utils/permission-manager.js"; +import type { + AuthenticatedRequest, + ProxyNode, +} from "../../../src/types/index.js"; import type { LogEntry, ConnectionStage, -} from "../../../types/connection-log.js"; +} from "../../../src/types/connection-log.js"; import { collectCpuMetrics, clearCpuSampleCache, @@ -41,7 +44,7 @@ import { import { collectUptimeMetrics } from "./widgets/uptime-collector.js"; import { collectProcessesMetrics } from "./widgets/processes-collector.js"; import { collectSystemMetrics } from "./widgets/system-collector.js"; -import { detectPlatform } from "./widgets/common-utils.js"; +import { detectPlatform } from "../../../src/backend/hosts/metrics-shared/common-utils.js"; import { collectLoginStats } from "./widgets/login-stats-collector.js"; import { collectPortsMetrics } from "./widgets/ports-collector.js"; import { collectFirewallMetrics } from "./widgets/firewall-collector.js"; @@ -50,31 +53,34 @@ import { collectGpuMetrics, type GpuMetrics } from "./widgets/gpu-collector.js"; import { createSocks5Connection, type SOCKS5Config, -} from "../../utils/socks5-helper.js"; -import { preparePrivateKeyForSSH2 } from "../../utils/ssh-key-utils.js"; -import { SSHHostKeyVerifier } from "../host-key-verifier.js"; -import { connectionPool, withConnection } from "../ssh-connection-pool.js"; +} from "../../../src/backend/utils/socks5-helper.js"; +import { preparePrivateKeyForSSH2 } from "../../../src/backend/utils/ssh-key-utils.js"; +import { SSHHostKeyVerifier } from "../../../src/backend/hosts/host-key-verifier.js"; +import { + connectionPool, + withConnection, +} from "../../../src/backend/hosts/ssh-connection-pool.js"; import { registerHostMetricsSettingsRoutes } from "./settings-routes.js"; import { registerHostMetricsViewerRoutes } from "./viewer-routes.js"; import { registerHostMetricsPreferencesRoutes } from "./preferences-routes.js"; import { registerHostMetricsHistoryRoutes } from "./history-routes.js"; -import { registerProxmoxStatsRoutes } from "./proxmox-stats-routes.js"; -import { registerProxmoxStatsHistoryRoutes } from "./proxmox-stats-history-routes.js"; -import { ProxmoxPollingManager } from "./proxmox-stats-polling.js"; -import type { HostSessionStatusPayload } from "../host-session-status.js"; -import { pluginEvents, TOPICS } from "../../plugins/events.js"; +import { registerProxmoxStatsRoutes } from "../../../src/backend/hosts/metrics/proxmox-stats-routes.js"; +import { registerProxmoxStatsHistoryRoutes } from "../../../src/backend/hosts/metrics/proxmox-stats-history-routes.js"; +import { ProxmoxPollingManager } from "../../../src/backend/hosts/metrics/proxmox-stats-polling.js"; +import type { HostSessionStatusPayload } from "../../../src/backend/hosts/host-session-status.js"; +import { pluginEvents, TOPICS } from "../../../src/backend/plugins/events.js"; import { AlertEngine } from "./alert-engine.js"; import { notifyAutomationMetrics, notifyAutomationStatus, } from "./automation-bridge.js"; -import { notifyAutomationInternalEvent } from "../automation-events.js"; +import { notifyAutomationInternalEvent } from "../../../src/backend/hosts/automation-events.js"; import { registerManagerRoutes } from "./managers/index.js"; -import { resolveSshConnectConfigHost } from "../ssh-dns.js"; +import { resolveSshConnectConfigHost } from "../../../src/backend/hosts/ssh-dns.js"; import { AccessDeniedError } from "./managers/route-helpers.js"; import type { ManagerHost } from "./managers/types.js"; -import { createJumpHostChain } from "../jump-host-chain.js"; -import { resolveHostById } from "../host-resolver.js"; +import { createJumpHostChain } from "../../../src/backend/hosts/jump-host-chain.js"; +import { resolveHostById } from "../../../src/backend/hosts/host-resolver.js"; import { isTcpPingEnabled, parseStatusHostIds, @@ -86,8 +92,8 @@ import { isHostKeyVerificationError, statusAfterAuthentication, statusAfterReachabilityCheck, -} from "./host-status.js"; -import { createConnectionLog } from "../connection-log.js"; +} from "../../../src/backend/hosts/metrics-shared/host-status.js"; +import { createConnectionLog } from "../../../src/backend/hosts/connection-log.js"; import { cleanupMetricsSession, getSessionKey, @@ -107,8 +113,8 @@ import { pollingBackoff, requestQueue, statusPollLimiter, -} from "./state.js"; -import { listenOnServicePort } from "../../utils/service-listen.js"; +} from "../../../src/backend/hosts/metrics-shared/state.js"; +import { listenOnServicePort } from "../../../src/backend/utils/service-listen.js"; const authManager = AuthManager.getInstance(); const permissionManager = PermissionManager.getInstance(); @@ -1076,7 +1082,7 @@ function validateHostId( next(); } -const app = express(); +export const app = express(); app.set("trust proxy", "loopback"); app.use(createCompressionMiddleware()); app.use(createCorsMiddleware()); @@ -1100,8 +1106,9 @@ app.post("/internal/login-alert", async (req, res) => { ) { return res.status(403).json({ error: "Forbidden" }); } - const systemCrypto = (await import("../../utils/system-crypto.js")) - .SystemCrypto; + const systemCrypto = ( + await import("../../../src/backend/utils/system-crypto.js") + ).SystemCrypto; const expectedToken = await systemCrypto.getInstance().getInternalAuthToken(); const token = req.headers["x-internal-auth"]; if (!token || token !== expectedToken) { @@ -1248,7 +1255,7 @@ async function resolveHostCredentials( if (isSharedHost) { const { SharedHostSecretsManager } = - await import("../../utils/shared-host-secrets-manager.js"); + await import("../../../src/backend/utils/shared-host-secrets-manager.js"); const sharedCred = await SharedHostSecretsManager.getInstance().getSecretForUser( host.id as number, @@ -1515,18 +1522,20 @@ function createSshFactory(host: SSHHostWithCredentials): () => Promise { // Set up OPKSSH cert auth if needed (requires client instance) if (usesIssuedCertificate(host.authType) && host.userId) { - const { getOPKSSHToken } = await import("../opkssh-auth.js"); + const { getOPKSSHToken } = + await import("../../../src/backend/hosts/opkssh-auth.js"); const token = await getOPKSSHToken(host.userId, host.id); if (!token) { throw new Error( "OPKSSH authentication required. Please open a Terminal connection first.", ); } - const { setupOPKSSHCertAuth } = await import("../opkssh-cert-auth.js"); + const { setupOPKSSHCertAuth } = + await import("../../../src/backend/hosts/opkssh-cert-auth.js"); await setupOPKSSHCertAuth(config, client, token, host.username); } else if (host.authType === "vault") { const { setupVaultSshSignerAuth } = - await import("../vault-ssh-connect.js"); + await import("../../../src/backend/hosts/vault-ssh-connect.js"); await setupVaultSshSignerAuth(config, client, host); } @@ -2446,7 +2455,8 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => { const client = new Client(); if (usesIssuedCertificate(host.authType) && host.userId) { - const { getOPKSSHToken } = await import("../opkssh-auth.js"); + const { getOPKSSHToken } = + await import("../../../src/backend/hosts/opkssh-auth.js"); const token = await getOPKSSHToken(host.userId, host.id); if (!token) { connectionLogs.push( @@ -2462,11 +2472,12 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => { connectionLogs, }); } - const { setupOPKSSHCertAuth } = await import("../opkssh-cert-auth.js"); + const { setupOPKSSHCertAuth } = + await import("../../../src/backend/hosts/opkssh-cert-auth.js"); await setupOPKSSHCertAuth(config, client, token, host.username); } else if (host.authType === "vault") { const { setupVaultSshSignerAuth } = - await import("../vault-ssh-connect.js"); + await import("../../../src/backend/hosts/vault-ssh-connect.js"); await setupVaultSshSignerAuth(config, client, host); } @@ -3035,7 +3046,7 @@ app.post("/metrics/connect-totp", async (req, res) => { // Lets automations keep metrics flowing for hosts they watch, through the same // viewer refcount the UI uses rather than around it. -import("../../automations/headless-viewer.js") +import("../../../src/backend/automations/headless-viewer.js") .then(({ setViewerRegistry }) => { setViewerRegistry({ registerViewer: (hostId, sessionId, userId) => @@ -3146,41 +3157,56 @@ registerManagerRoutes(app, { }, }); -process.on("SIGINT", () => { +export const PORT = 30005; + +let cleanupInterval: NodeJS.Timeout | undefined; + +/** + * Starts the Host Metrics HTTP service. Called from the plugin's activate() + * instead of running at module scope, so enabling/disabling the plugin + * actually starts and stops the listener. + */ +export function startHostMetricsService(): ReturnType< + typeof listenOnServicePort +> { + return listenOnServicePort({ + app, + port: PORT, + logger: statsLogger, + serviceName: "metrics", + onListening: async () => { + try { + await authManager.initialize(); + } catch (err) { + statsLogger.error("Failed to initialize AuthManager", err, { + operation: "auth_init_error", + }); + } + + cleanupInterval = setInterval( + () => { + authFailureTracker.cleanup(); + pollingBackoff.cleanup(); + }, + 10 * 60 * 1000, + ); + }, + }); +} + +/** + * Releases everything the service holds: polling timers, pooled SSH + * connections and the periodic auth-failure cleanup. Called from the + * plugin's deactivate(), replacing the old SIGINT/SIGTERM handlers -- the + * process itself no longer owns this service's shutdown, the plugin + * lifecycle does. + */ +export function shutdown(): void { + if (cleanupInterval) { + clearInterval(cleanupInterval); + cleanupInterval = undefined; + } pollingManager.destroy(); proxmoxPollingManager.destroy(); connectionPool.destroy(); - process.exit(0); -}); - -process.on("SIGTERM", () => { - pollingManager.destroy(); - proxmoxPollingManager.destroy(); - connectionPool.destroy(); - process.exit(0); -}); - -const PORT = 30005; -listenOnServicePort({ - app, - port: PORT, - logger: statsLogger, - serviceName: "metrics", - onListening: async () => { - try { - await authManager.initialize(); - } catch (err) { - statsLogger.error("Failed to initialize AuthManager", err, { - operation: "auth_init_error", - }); - } - - setInterval( - () => { - authFailureTracker.cleanup(); - pollingBackoff.cleanup(); - }, - 10 * 60 * 1000, - ); - }, -}); +} diff --git a/src/backend/hosts/metrics/sessions.ts b/plugins/host-metrics/backend/sessions.ts similarity index 96% rename from src/backend/hosts/metrics/sessions.ts rename to plugins/host-metrics/backend/sessions.ts index c23b6f35f..6c9809a77 100644 --- a/src/backend/hosts/metrics/sessions.ts +++ b/plugins/host-metrics/backend/sessions.ts @@ -1,5 +1,5 @@ import type { Client, ConnectConfig } from "ssh2"; -import { statsLogger } from "../../utils/logger.js"; +import { statsLogger } from "../../../src/backend/utils/logger.js"; export interface MetricsSession { client: Client; diff --git a/src/backend/hosts/metrics/settings-routes.ts b/plugins/host-metrics/backend/settings-routes.ts similarity index 97% rename from src/backend/hosts/metrics/settings-routes.ts rename to plugins/host-metrics/backend/settings-routes.ts index f7f64ace1..7bf58c488 100644 --- a/src/backend/hosts/metrics/settings-routes.ts +++ b/plugins/host-metrics/backend/settings-routes.ts @@ -1,6 +1,6 @@ import type { Express, RequestHandler } from "express"; -import { statsLogger } from "../../utils/logger.js"; -import { createCurrentSettingsRepository } from "../../database/repositories/factory.js"; +import { statsLogger } from "../../../src/backend/utils/logger.js"; +import { createCurrentSettingsRepository } from "../../../src/backend/database/repositories/factory.js"; type HostMetricsSettingsConfig = { statusCheckInterval: number; diff --git a/src/backend/hosts/metrics/viewer-routes.ts b/plugins/host-metrics/backend/viewer-routes.ts similarity index 96% rename from src/backend/hosts/metrics/viewer-routes.ts rename to plugins/host-metrics/backend/viewer-routes.ts index b6086ad90..f0e49e5b6 100644 --- a/src/backend/hosts/metrics/viewer-routes.ts +++ b/plugins/host-metrics/backend/viewer-routes.ts @@ -1,8 +1,8 @@ -import { getErrorMessage } from "../../utils/error-message.js"; +import { getErrorMessage } from "../../../src/backend/utils/error-message.js"; import type { Express } from "express"; -import type { AuthenticatedRequest } from "../../../types/index.js"; -import { statsLogger } from "../../utils/logger.js"; -import { DataCrypto } from "../../utils/data-crypto.js"; +import type { AuthenticatedRequest } from "../../../src/types/index.js"; +import { statsLogger } from "../../../src/backend/utils/logger.js"; +import { DataCrypto } from "../../../src/backend/utils/data-crypto.js"; type ViewerStatsConfig = { metricsEnabled: boolean; diff --git a/src/backend/hosts/metrics/widgets/cpu-collector.ts b/plugins/host-metrics/backend/widgets/cpu-collector.ts similarity index 98% rename from src/backend/hosts/metrics/widgets/cpu-collector.ts rename to plugins/host-metrics/backend/widgets/cpu-collector.ts index 70d35fcd4..f8948880b 100644 --- a/src/backend/hosts/metrics/widgets/cpu-collector.ts +++ b/plugins/host-metrics/backend/widgets/cpu-collector.ts @@ -4,7 +4,7 @@ import { execPowerShell, toFixedNum, type HostPlatform, -} from "./common-utils.js"; +} from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; /** * CPU percent is derived from the delta against the previous poll's diff --git a/src/backend/hosts/metrics/widgets/disk-collector.ts b/plugins/host-metrics/backend/widgets/disk-collector.ts similarity index 99% rename from src/backend/hosts/metrics/widgets/disk-collector.ts rename to plugins/host-metrics/backend/widgets/disk-collector.ts index 13f6b9508..bcaee108e 100644 --- a/src/backend/hosts/metrics/widgets/disk-collector.ts +++ b/plugins/host-metrics/backend/widgets/disk-collector.ts @@ -4,7 +4,7 @@ import { execPowerShell, toFixedNum, type HostPlatform, -} from "./common-utils.js"; +} from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; const PSEUDO_FS_RE = /^(tmpfs|devtmpfs|overlay|udev|none|shm)$/; diff --git a/src/backend/hosts/metrics/widgets/firewall-collector.ts b/plugins/host-metrics/backend/widgets/firewall-collector.ts similarity index 97% rename from src/backend/hosts/metrics/widgets/firewall-collector.ts rename to plugins/host-metrics/backend/widgets/firewall-collector.ts index 10d816ef8..657db71f3 100644 --- a/src/backend/hosts/metrics/widgets/firewall-collector.ts +++ b/plugins/host-metrics/backend/widgets/firewall-collector.ts @@ -1,10 +1,10 @@ import type { Client } from "ssh2"; -import { execCommand } from "./common-utils.js"; +import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; import type { FirewallMetrics, FirewallChain, FirewallRule, -} from "../../../../types/stats-widgets.js"; +} from "../../../../src/types/stats-widgets.js"; function parseIptablesRule(line: string): FirewallRule | null { if (!line.startsWith("-A ")) return null; diff --git a/src/backend/hosts/metrics/widgets/gpu-collector.ts b/plugins/host-metrics/backend/widgets/gpu-collector.ts similarity index 97% rename from src/backend/hosts/metrics/widgets/gpu-collector.ts rename to plugins/host-metrics/backend/widgets/gpu-collector.ts index cc5f1bc82..0fcd475ea 100644 --- a/src/backend/hosts/metrics/widgets/gpu-collector.ts +++ b/plugins/host-metrics/backend/widgets/gpu-collector.ts @@ -1,5 +1,8 @@ import type { Client } from "ssh2"; -import { execCommand, toFixedNum } from "./common-utils.js"; +import { + execCommand, + toFixedNum, +} from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; export interface GpuDevice { index: number; diff --git a/src/backend/hosts/metrics/widgets/login-stats-collector.ts b/plugins/host-metrics/backend/widgets/login-stats-collector.ts similarity index 97% rename from src/backend/hosts/metrics/widgets/login-stats-collector.ts rename to plugins/host-metrics/backend/widgets/login-stats-collector.ts index 3fd02e3ac..e6bb3b98f 100644 --- a/src/backend/hosts/metrics/widgets/login-stats-collector.ts +++ b/plugins/host-metrics/backend/widgets/login-stats-collector.ts @@ -1,5 +1,5 @@ import type { Client } from "ssh2"; -import { execCommand } from "./common-utils.js"; +import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; export interface LoginRecord { user: string; diff --git a/src/backend/hosts/metrics/widgets/memory-collector.ts b/plugins/host-metrics/backend/widgets/memory-collector.ts similarity index 98% rename from src/backend/hosts/metrics/widgets/memory-collector.ts rename to plugins/host-metrics/backend/widgets/memory-collector.ts index bd60bcfee..dfa91fc56 100644 --- a/src/backend/hosts/metrics/widgets/memory-collector.ts +++ b/plugins/host-metrics/backend/widgets/memory-collector.ts @@ -5,7 +5,7 @@ import { toFixedNum, kibToGiB, type HostPlatform, -} from "./common-utils.js"; +} from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; async function collectDarwinMemoryMetrics(client: Client): Promise<{ percent: number | null; diff --git a/src/backend/hosts/metrics/widgets/network-collector.ts b/plugins/host-metrics/backend/widgets/network-collector.ts similarity index 99% rename from src/backend/hosts/metrics/widgets/network-collector.ts rename to plugins/host-metrics/backend/widgets/network-collector.ts index 7fd0d1170..ae43d051d 100644 --- a/src/backend/hosts/metrics/widgets/network-collector.ts +++ b/plugins/host-metrics/backend/widgets/network-collector.ts @@ -3,7 +3,7 @@ import { execCommand, execPowerShell, type HostPlatform, -} from "./common-utils.js"; +} from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; export interface NetworkCounters { rx: string; diff --git a/src/backend/hosts/metrics/widgets/ports-collector.ts b/plugins/host-metrics/backend/widgets/ports-collector.ts similarity index 96% rename from src/backend/hosts/metrics/widgets/ports-collector.ts rename to plugins/host-metrics/backend/widgets/ports-collector.ts index 157137614..743084a47 100644 --- a/src/backend/hosts/metrics/widgets/ports-collector.ts +++ b/plugins/host-metrics/backend/widgets/ports-collector.ts @@ -1,9 +1,9 @@ import type { Client } from "ssh2"; -import { execCommand } from "./common-utils.js"; +import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; import type { PortsMetrics, ListeningPort, -} from "../../../../types/stats-widgets.js"; +} from "../../../../src/types/stats-widgets.js"; function parseSsOutput(output: string): ListeningPort[] { const ports: ListeningPort[] = []; diff --git a/src/backend/hosts/metrics/widgets/processes-collector.ts b/plugins/host-metrics/backend/widgets/processes-collector.ts similarity index 95% rename from src/backend/hosts/metrics/widgets/processes-collector.ts rename to plugins/host-metrics/backend/widgets/processes-collector.ts index 63d3c0592..33980edc9 100644 --- a/src/backend/hosts/metrics/widgets/processes-collector.ts +++ b/plugins/host-metrics/backend/widgets/processes-collector.ts @@ -1,5 +1,5 @@ import type { Client } from "ssh2"; -import { execCommand } from "./common-utils.js"; +import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; export async function collectProcessesMetrics(client: Client): Promise<{ total: number | null; diff --git a/src/backend/hosts/metrics/widgets/system-collector.ts b/plugins/host-metrics/backend/widgets/system-collector.ts similarity index 96% rename from src/backend/hosts/metrics/widgets/system-collector.ts rename to plugins/host-metrics/backend/widgets/system-collector.ts index 5031e8915..cb269da1e 100644 --- a/src/backend/hosts/metrics/widgets/system-collector.ts +++ b/plugins/host-metrics/backend/widgets/system-collector.ts @@ -3,7 +3,7 @@ import { execCommand, execPowerShell, type HostPlatform, -} from "./common-utils.js"; +} from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; async function collectWindowsSystemMetrics(client: Client): Promise<{ hostname: string | null; diff --git a/src/backend/hosts/metrics/widgets/temperature-collector.ts b/plugins/host-metrics/backend/widgets/temperature-collector.ts similarity index 96% rename from src/backend/hosts/metrics/widgets/temperature-collector.ts rename to plugins/host-metrics/backend/widgets/temperature-collector.ts index fb18c7d08..4ef51a584 100644 --- a/src/backend/hosts/metrics/widgets/temperature-collector.ts +++ b/plugins/host-metrics/backend/widgets/temperature-collector.ts @@ -1,5 +1,8 @@ import type { Client } from "ssh2"; -import { execCommand, toFixedNum } from "./common-utils.js"; +import { + execCommand, + toFixedNum, +} from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; export interface TemperatureSensor { label: string; diff --git a/src/backend/hosts/metrics/widgets/uptime-collector.ts b/plugins/host-metrics/backend/widgets/uptime-collector.ts similarity index 97% rename from src/backend/hosts/metrics/widgets/uptime-collector.ts rename to plugins/host-metrics/backend/widgets/uptime-collector.ts index 317321d2f..0c585e6ab 100644 --- a/src/backend/hosts/metrics/widgets/uptime-collector.ts +++ b/plugins/host-metrics/backend/widgets/uptime-collector.ts @@ -3,7 +3,7 @@ import { execCommand, execPowerShell, type HostPlatform, -} from "./common-utils.js"; +} from "../../../../src/backend/hosts/metrics-shared/common-utils.js"; function formatUptime(uptimeSeconds: number): string { const days = Math.floor(uptimeSeconds / 86400); diff --git a/src/ui/features/host-metrics/HostMetricsApp.tsx b/plugins/host-metrics/frontend/HostMetricsApp.tsx similarity index 94% rename from src/ui/features/host-metrics/HostMetricsApp.tsx rename to plugins/host-metrics/frontend/HostMetricsApp.tsx index a947ef487..18ac2f35f 100644 --- a/src/ui/features/host-metrics/HostMetricsApp.tsx +++ b/plugins/host-metrics/frontend/HostMetricsApp.tsx @@ -1,6 +1,6 @@ import React from "react"; import { useTranslation } from "react-i18next"; -import { HostMetricsTab } from "@/features/host-metrics/HostMetricsTab.tsx"; +import { HostMetricsTab } from "./HostMetricsTab.tsx"; import { FullScreenAppWrapper } from "@/features/FullScreenAppWrapper.tsx"; import { ConnectionScreen } from "@/components/connection/ConnectionScreen.tsx"; diff --git a/src/ui/features/host-metrics/HostMetricsTab.tsx b/plugins/host-metrics/frontend/HostMetricsTab.tsx similarity index 99% rename from src/ui/features/host-metrics/HostMetricsTab.tsx rename to plugins/host-metrics/frontend/HostMetricsTab.tsx index 9bad7b33c..817369d0f 100644 --- a/src/ui/features/host-metrics/HostMetricsTab.tsx +++ b/plugins/host-metrics/frontend/HostMetricsTab.tsx @@ -1,4 +1,4 @@ -import { getErrorMessage } from "../../lib/error-message.js"; +import { getErrorMessage } from "@/lib/error-message.js"; /* eslint-disable react-hooks/exhaustive-deps */ import React from "react"; import { Separator } from "@/components/separator.tsx"; diff --git a/src/ui/features/host-metrics/cards/CardTimeTabs.tsx b/plugins/host-metrics/frontend/cards/CardTimeTabs.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/CardTimeTabs.tsx rename to plugins/host-metrics/frontend/cards/CardTimeTabs.tsx diff --git a/src/ui/features/host-metrics/cards/CpuCard.tsx b/plugins/host-metrics/frontend/cards/CpuCard.tsx similarity index 98% rename from src/ui/features/host-metrics/cards/CpuCard.tsx rename to plugins/host-metrics/frontend/cards/CpuCard.tsx index 2824c6c80..ad29b1e44 100644 --- a/src/ui/features/host-metrics/cards/CpuCard.tsx +++ b/plugins/host-metrics/frontend/cards/CpuCard.tsx @@ -3,7 +3,7 @@ import { Cpu } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { ServerMetrics } from "@/main-axios"; import { RadialGauge, Sparkline, MiniStat } from "@/components/charts"; -import { MetricCard } from "./MetricCard"; +import { MetricCard } from "@/components/metric-card"; import { LineChart, type LineChartSeries } from "@/components/charts/LineChart"; import { getMetricsHistory, diff --git a/src/ui/features/host-metrics/cards/DiskCard.tsx b/plugins/host-metrics/frontend/cards/DiskCard.tsx similarity index 98% rename from src/ui/features/host-metrics/cards/DiskCard.tsx rename to plugins/host-metrics/frontend/cards/DiskCard.tsx index e778f828c..976427581 100644 --- a/src/ui/features/host-metrics/cards/DiskCard.tsx +++ b/plugins/host-metrics/frontend/cards/DiskCard.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"; import type { ServerMetrics } from "@/main-axios"; import { FilesystemPicker } from "./FilesystemPicker"; import { RadialGauge, Sparkline, MiniStat } from "@/components/charts"; -import { MetricCard } from "./MetricCard"; +import { MetricCard } from "@/components/metric-card"; import { LineChart, type LineChartSeries } from "@/components/charts/LineChart"; import { getMetricsHistory, diff --git a/src/ui/features/host-metrics/cards/FilesystemPicker.tsx b/plugins/host-metrics/frontend/cards/FilesystemPicker.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/FilesystemPicker.tsx rename to plugins/host-metrics/frontend/cards/FilesystemPicker.tsx diff --git a/src/ui/features/host-metrics/cards/FirewallCard.tsx b/plugins/host-metrics/frontend/cards/FirewallCard.tsx similarity index 97% rename from src/ui/features/host-metrics/cards/FirewallCard.tsx rename to plugins/host-metrics/frontend/cards/FirewallCard.tsx index db570a038..a21229fe2 100644 --- a/src/ui/features/host-metrics/cards/FirewallCard.tsx +++ b/plugins/host-metrics/frontend/cards/FirewallCard.tsx @@ -1,7 +1,7 @@ import { ShieldCheck } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { ServerMetrics } from "@/main-axios"; -import { MetricCard } from "./MetricCard"; +import { MetricCard } from "@/components/metric-card"; export function FirewallCard({ metrics }: { metrics: ServerMetrics | null }) { const { t } = useTranslation(); diff --git a/src/ui/features/host-metrics/cards/GpuCard.tsx b/plugins/host-metrics/frontend/cards/GpuCard.tsx similarity index 99% rename from src/ui/features/host-metrics/cards/GpuCard.tsx rename to plugins/host-metrics/frontend/cards/GpuCard.tsx index b73a56ebb..7a53588bb 100644 --- a/src/ui/features/host-metrics/cards/GpuCard.tsx +++ b/plugins/host-metrics/frontend/cards/GpuCard.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"; import type { ServerMetrics } from "@/main-axios"; import type { GpuDevice, GpuProcess } from "@/types/stats-widgets"; import { MiniStat, RadialGauge, Sparkline, StatRow } from "@/components/charts"; -import { MetricCard } from "./MetricCard"; +import { MetricCard } from "@/components/metric-card"; import type { GpuHistories, GpuHistory } from "./gpu-history"; const NA = "N/A"; diff --git a/src/ui/features/host-metrics/cards/LoginStatsCard.tsx b/plugins/host-metrics/frontend/cards/LoginStatsCard.tsx similarity index 97% rename from src/ui/features/host-metrics/cards/LoginStatsCard.tsx rename to plugins/host-metrics/frontend/cards/LoginStatsCard.tsx index dbb9cd19c..fdd659b2d 100644 --- a/src/ui/features/host-metrics/cards/LoginStatsCard.tsx +++ b/plugins/host-metrics/frontend/cards/LoginStatsCard.tsx @@ -1,7 +1,7 @@ import { UserCheck, UserX } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { ServerMetrics } from "@/main-axios"; -import { MetricCard } from "./MetricCard"; +import { MetricCard } from "@/components/metric-card"; export function LoginStatsCard({ metrics }: { metrics: ServerMetrics | null }) { const { t } = useTranslation(); diff --git a/src/ui/features/host-metrics/cards/MemoryCard.tsx b/plugins/host-metrics/frontend/cards/MemoryCard.tsx similarity index 98% rename from src/ui/features/host-metrics/cards/MemoryCard.tsx rename to plugins/host-metrics/frontend/cards/MemoryCard.tsx index 8d9e53ca6..fee2e4502 100644 --- a/src/ui/features/host-metrics/cards/MemoryCard.tsx +++ b/plugins/host-metrics/frontend/cards/MemoryCard.tsx @@ -3,7 +3,7 @@ import { MemoryStick } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { ServerMetrics } from "@/main-axios"; import { RadialGauge, Sparkline, MiniStat } from "@/components/charts"; -import { MetricCard } from "./MetricCard"; +import { MetricCard } from "@/components/metric-card"; import { LineChart, type LineChartSeries } from "@/components/charts/LineChart"; import { getMetricsHistory, diff --git a/src/ui/features/host-metrics/cards/NetworkCard.tsx b/plugins/host-metrics/frontend/cards/NetworkCard.tsx similarity index 99% rename from src/ui/features/host-metrics/cards/NetworkCard.tsx rename to plugins/host-metrics/frontend/cards/NetworkCard.tsx index 2938afcee..f595f85fb 100644 --- a/src/ui/features/host-metrics/cards/NetworkCard.tsx +++ b/plugins/host-metrics/frontend/cards/NetworkCard.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { Cable, Container, Network, Wifi, WifiOff } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { ServerMetrics } from "@/main-axios"; -import { MetricCard } from "./MetricCard"; +import { MetricCard } from "@/components/metric-card"; import { LineChart, type LineChartSeries } from "@/components/charts/LineChart"; import { getMetricsHistory, diff --git a/src/ui/features/host-metrics/cards/PortsCard.tsx b/plugins/host-metrics/frontend/cards/PortsCard.tsx similarity index 98% rename from src/ui/features/host-metrics/cards/PortsCard.tsx rename to plugins/host-metrics/frontend/cards/PortsCard.tsx index 4410cf558..9e41b3fd4 100644 --- a/src/ui/features/host-metrics/cards/PortsCard.tsx +++ b/plugins/host-metrics/frontend/cards/PortsCard.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from "react"; import { Unplug, Search } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { ServerMetrics, ListeningPort } from "@/main-axios"; -import { MetricCard } from "./MetricCard"; +import { MetricCard } from "@/components/metric-card"; import { Select2 } from "@/components/select2"; function formatAddress(addr: string) { diff --git a/src/ui/features/host-metrics/cards/ProcessesCard.tsx b/plugins/host-metrics/frontend/cards/ProcessesCard.tsx similarity index 96% rename from src/ui/features/host-metrics/cards/ProcessesCard.tsx rename to plugins/host-metrics/frontend/cards/ProcessesCard.tsx index 95eb91103..cb3f834a8 100644 --- a/src/ui/features/host-metrics/cards/ProcessesCard.tsx +++ b/plugins/host-metrics/frontend/cards/ProcessesCard.tsx @@ -2,7 +2,7 @@ import { Cpu } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { ServerMetrics } from "@/main-axios"; import { StatRow } from "@/components/charts"; -import { MetricCard } from "./MetricCard"; +import { MetricCard } from "@/components/metric-card"; export function ProcessesCard({ metrics }: { metrics: ServerMetrics | null }) { const { t } = useTranslation(); diff --git a/src/ui/features/host-metrics/cards/SystemCard.tsx b/plugins/host-metrics/frontend/cards/SystemCard.tsx similarity index 95% rename from src/ui/features/host-metrics/cards/SystemCard.tsx rename to plugins/host-metrics/frontend/cards/SystemCard.tsx index 9cb70b291..9cbda0872 100644 --- a/src/ui/features/host-metrics/cards/SystemCard.tsx +++ b/plugins/host-metrics/frontend/cards/SystemCard.tsx @@ -2,7 +2,7 @@ import { Server } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { ServerMetrics } from "@/main-axios"; import { StatRow } from "@/components/charts"; -import { MetricCard } from "./MetricCard"; +import { MetricCard } from "@/components/metric-card"; export function SystemCard({ metrics }: { metrics: ServerMetrics | null }) { const { t } = useTranslation(); diff --git a/src/ui/features/host-metrics/cards/TemperatureCard.tsx b/plugins/host-metrics/frontend/cards/TemperatureCard.tsx similarity index 98% rename from src/ui/features/host-metrics/cards/TemperatureCard.tsx rename to plugins/host-metrics/frontend/cards/TemperatureCard.tsx index fd1a1308c..974ee7716 100644 --- a/src/ui/features/host-metrics/cards/TemperatureCard.tsx +++ b/plugins/host-metrics/frontend/cards/TemperatureCard.tsx @@ -2,7 +2,7 @@ import { Thermometer } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { ServerMetrics } from "@/main-axios"; import { StatRow } from "@/components/charts"; -import { MetricCard } from "./MetricCard"; +import { MetricCard } from "@/components/metric-card"; import { useEffect, useState } from "react"; import { selectTemperatureSensor, diff --git a/src/ui/features/host-metrics/cards/UptimeCard.tsx b/plugins/host-metrics/frontend/cards/UptimeCard.tsx similarity index 94% rename from src/ui/features/host-metrics/cards/UptimeCard.tsx rename to plugins/host-metrics/frontend/cards/UptimeCard.tsx index b6e5ab062..47c9b4d3c 100644 --- a/src/ui/features/host-metrics/cards/UptimeCard.tsx +++ b/plugins/host-metrics/frontend/cards/UptimeCard.tsx @@ -1,7 +1,7 @@ import { Clock } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { ServerMetrics } from "@/main-axios"; -import { MetricCard } from "./MetricCard"; +import { MetricCard } from "@/components/metric-card"; export function UptimeCard({ metrics }: { metrics: ServerMetrics | null }) { const { t } = useTranslation(); diff --git a/src/ui/features/host-metrics/cards/gpu-history.ts b/plugins/host-metrics/frontend/cards/gpu-history.ts similarity index 100% rename from src/ui/features/host-metrics/cards/gpu-history.ts rename to plugins/host-metrics/frontend/cards/gpu-history.ts diff --git a/src/ui/features/host-metrics/cards/index.tsx b/plugins/host-metrics/frontend/cards/index.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/index.tsx rename to plugins/host-metrics/frontend/cards/index.tsx diff --git a/src/ui/features/host-metrics/cards/managers/CronManagerCard.tsx b/plugins/host-metrics/frontend/cards/managers/CronManagerCard.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/managers/CronManagerCard.tsx rename to plugins/host-metrics/frontend/cards/managers/CronManagerCard.tsx diff --git a/src/ui/features/host-metrics/cards/managers/FirewallManagerCard.tsx b/plugins/host-metrics/frontend/cards/managers/FirewallManagerCard.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/managers/FirewallManagerCard.tsx rename to plugins/host-metrics/frontend/cards/managers/FirewallManagerCard.tsx diff --git a/src/ui/features/host-metrics/cards/managers/HealthCheckCard.tsx b/plugins/host-metrics/frontend/cards/managers/HealthCheckCard.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/managers/HealthCheckCard.tsx rename to plugins/host-metrics/frontend/cards/managers/HealthCheckCard.tsx diff --git a/src/ui/features/host-metrics/cards/managers/LogViewerCard.tsx b/plugins/host-metrics/frontend/cards/managers/LogViewerCard.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/managers/LogViewerCard.tsx rename to plugins/host-metrics/frontend/cards/managers/LogViewerCard.tsx diff --git a/src/ui/features/host-metrics/cards/managers/ManagerCardShell.tsx b/plugins/host-metrics/frontend/cards/managers/ManagerCardShell.tsx similarity index 97% rename from src/ui/features/host-metrics/cards/managers/ManagerCardShell.tsx rename to plugins/host-metrics/frontend/cards/managers/ManagerCardShell.tsx index 98ac0914e..3a3099a1b 100644 --- a/src/ui/features/host-metrics/cards/managers/ManagerCardShell.tsx +++ b/plugins/host-metrics/frontend/cards/managers/ManagerCardShell.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from "react"; import { RefreshCw, AlertTriangle } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/button"; -import { MetricCard } from "../MetricCard"; +import { MetricCard } from "@/components/metric-card"; import type { ManagerError } from "./useManagerData"; export function ManagerCardShell({ diff --git a/src/ui/features/host-metrics/cards/managers/ManagerToolbar.tsx b/plugins/host-metrics/frontend/cards/managers/ManagerToolbar.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/managers/ManagerToolbar.tsx rename to plugins/host-metrics/frontend/cards/managers/ManagerToolbar.tsx diff --git a/src/ui/features/host-metrics/cards/managers/PackageManagerCard.tsx b/plugins/host-metrics/frontend/cards/managers/PackageManagerCard.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/managers/PackageManagerCard.tsx rename to plugins/host-metrics/frontend/cards/managers/PackageManagerCard.tsx diff --git a/src/ui/features/host-metrics/cards/managers/ProcessInspectorCard.tsx b/plugins/host-metrics/frontend/cards/managers/ProcessInspectorCard.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/managers/ProcessInspectorCard.tsx rename to plugins/host-metrics/frontend/cards/managers/ProcessInspectorCard.tsx diff --git a/src/ui/features/host-metrics/cards/managers/ServiceManagerCard.tsx b/plugins/host-metrics/frontend/cards/managers/ServiceManagerCard.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/managers/ServiceManagerCard.tsx rename to plugins/host-metrics/frontend/cards/managers/ServiceManagerCard.tsx diff --git a/src/ui/features/host-metrics/cards/managers/SimpleManagerCards.tsx b/plugins/host-metrics/frontend/cards/managers/SimpleManagerCards.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/managers/SimpleManagerCards.tsx rename to plugins/host-metrics/frontend/cards/managers/SimpleManagerCards.tsx diff --git a/src/ui/features/host-metrics/cards/managers/SslManagerCard.tsx b/plugins/host-metrics/frontend/cards/managers/SslManagerCard.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/managers/SslManagerCard.tsx rename to plugins/host-metrics/frontend/cards/managers/SslManagerCard.tsx diff --git a/src/ui/features/host-metrics/cards/managers/TailscaleManagerCard.tsx b/plugins/host-metrics/frontend/cards/managers/TailscaleManagerCard.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/managers/TailscaleManagerCard.tsx rename to plugins/host-metrics/frontend/cards/managers/TailscaleManagerCard.tsx diff --git a/src/ui/features/host-metrics/cards/managers/UserManagerCard.tsx b/plugins/host-metrics/frontend/cards/managers/UserManagerCard.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/managers/UserManagerCard.tsx rename to plugins/host-metrics/frontend/cards/managers/UserManagerCard.tsx diff --git a/src/ui/features/host-metrics/cards/managers/WireGuardManagerCard.tsx b/plugins/host-metrics/frontend/cards/managers/WireGuardManagerCard.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/managers/WireGuardManagerCard.tsx rename to plugins/host-metrics/frontend/cards/managers/WireGuardManagerCard.tsx diff --git a/src/ui/features/host-metrics/cards/managers/useManagerData.ts b/plugins/host-metrics/frontend/cards/managers/useManagerData.ts similarity index 100% rename from src/ui/features/host-metrics/cards/managers/useManagerData.ts rename to plugins/host-metrics/frontend/cards/managers/useManagerData.ts diff --git a/src/ui/features/host-metrics/hooks/useHostMetricsPreferences.ts b/plugins/host-metrics/frontend/hooks/useHostMetricsPreferences.ts similarity index 100% rename from src/ui/features/host-metrics/hooks/useHostMetricsPreferences.ts rename to plugins/host-metrics/frontend/hooks/useHostMetricsPreferences.ts diff --git a/plugins/host-metrics/frontend/index.mjs b/plugins/host-metrics/frontend/index.mjs new file mode 100644 index 000000000..9234c1c39 --- /dev/null +++ b/plugins/host-metrics/frontend/index.mjs @@ -0,0 +1,49 @@ +/** + * Frontend half of the host-metrics plugin. + * + * Registers the Host Metrics tab through the existing extension seams + * (registerRailItem / registerTabComponent), the same way ssh-terminal's and + * docker's frontend do. + * + * HostEditorStatsTab itself stays in src/ui/sidebar/HostEditorStatsTab.tsx + * rather than moving into this plugin: it takes the same {form, setField} + * pair every other host editor tab does, and those types (HostEditorForm, + * the setField signature) are core types HostEditor.tsx owns -- the same + * reason docker leaves HostDockerTab in core. + * + * register() is called when the plugin is enabled, unregister() when it is + * disabled. As of this change neither is actually invoked anywhere yet, the + * same as ssh-terminal's and docker's frontend entries -- there is no + * frontend plugin loader yet. This file follows the same shape so it is + * ready the moment that loader exists. + */ + +export const id = "host-metrics"; +export const tabId = "host-metrics"; + +export async function register({ + registerRailItem, + registerTabComponent, + icons, +}) { + registerTabComponent(tabId, () => + import("./HostMetricsTab.tsx").then((m) => ({ + default: m.HostMetricsTab, + })), + ); + + registerRailItem({ + id: tabId, + icon: icons.Activity, + labelKey: "nav.hostMetrics", + kind: "tab", + }); +} + +export async function unregister({ + unregisterRailItem, + unregisterTabComponent, +}) { + unregisterRailItem(tabId); + unregisterTabComponent(tabId); +} diff --git a/src/ui/features/host-metrics/metrics-change-key.ts b/plugins/host-metrics/frontend/metrics-change-key.ts similarity index 100% rename from src/ui/features/host-metrics/metrics-change-key.ts rename to plugins/host-metrics/frontend/metrics-change-key.ts diff --git a/src/ui/features/host-metrics/temperature-preference.ts b/plugins/host-metrics/frontend/temperature-preference.ts similarity index 100% rename from src/ui/features/host-metrics/temperature-preference.ts rename to plugins/host-metrics/frontend/temperature-preference.ts diff --git a/plugins/host-metrics/manifest.json b/plugins/host-metrics/manifest.json new file mode 100644 index 000000000..395c6296b --- /dev/null +++ b/plugins/host-metrics/manifest.json @@ -0,0 +1,30 @@ +{ + "id": "host-metrics", + "name": "Host Metrics", + "version": "1.0.0", + "description": "Live CPU, memory, disk, network and process metrics, plus host management (services, packages, firewall, cron, users, SSL, logs, WireGuard, Tailscale).", + "author": { "name": "Termix" }, + "license": "MIT", + "repository": "https://github.com/Termix-SSH/Termix", + "category": "Monitoring", + "icon": "Activity", + "engine": { "termix": ">=2.9.0", "api": "1" }, + "capabilities": { + "backend": true, + "frontend": true, + "electron": true, + "platforms": ["linux", "win32", "darwin"] + }, + "permissions": ["process:transport-owner"], + "contributes": { + "tabs": [ + { + "id": "host-metrics", + "titleKey": "nav.hostMetrics", + "icon": "Activity", + "openFrom": ["host-context-menu", "palette"] + } + ] + }, + "sidecars": [] +} diff --git a/scripts/copy-bundled-plugins.cjs b/scripts/copy-bundled-plugins.cjs index 0586f96d4..cf5582ea1 100644 --- a/scripts/copy-bundled-plugins.cjs +++ b/scripts/copy-bundled-plugins.cjs @@ -6,19 +6,21 @@ * tsc only emits .ts, so without a copy step the plugins directory simply * would not exist in dist and the loader would find nothing. * - * The docker plugin is the exception: its backend is real TypeScript, - * physically relocated from src/backend/hosts/docker/ rather than kept as - * hand-written JS, because it is ~4000 lines of typed SSH/session logic that - * is not worth hand-transpiling. tsconfig.plugins.json compiles the plugin - * backend TypeScript to a sibling .js next to its source. Its imports - * into core (e.g. "../../../src/backend/utils/logger.js") are written - * relative to the TypeScript SOURCE tree so tsc can type-check them, since - * plugins/ is not under tsconfig.node.json's rootDir and cannot be added to - * it without changing every existing dist/backend/backend/... path in the - * codebase. That means the emitted .js still points at src/backend/, which - * does not exist in a built server -- only its compiled counterpart at + * docker and host-metrics are the exception: their backends are real + * TypeScript, physically relocated from src/backend/hosts/docker/ and + * src/backend/hosts/metrics/ rather than kept as hand-written JS, because + * each is thousands of lines of typed SSH/session logic that is not worth + * hand-transpiling. tsconfig.plugins.json compiles each plugin backend's + * TypeScript to a sibling .js next to its source. Their imports into core + * (e.g. "../../../src/backend/utils/logger.js") are written relative to the + * TypeScript SOURCE tree so tsc can type-check them, since plugins/ is not + * under tsconfig.node.json's rootDir and cannot be added to it without + * changing every existing dist/backend/backend/... path in the codebase. + * That means the emitted .js still points at src/backend/, which does not + * exist in a built server -- only its compiled counterpart at * dist/backend/backend/ does. rewriteCoreImports() below corrects that one - * prefix after compilation, before the directory is copied into dist/. + * prefix after compilation, for every plugins/*\/backend directory tsc + * compiled, before the directory is copied into dist/. * * getBundledPluginsDir() in src/backend/plugins/paths.ts resolves * dist/backend/backend/plugins -> dist/plugins, which is where this writes. @@ -64,7 +66,9 @@ function rewriteCoreImports(dir) { const original = fs.readFileSync(entryPath, "utf8"); if (!original.includes(SRC_BACKEND_PREFIX)) continue; - const rewritten = original.split(SRC_BACKEND_PREFIX).join(COMPILED_BACKEND_PREFIX); + const rewritten = original + .split(SRC_BACKEND_PREFIX) + .join(COMPILED_BACKEND_PREFIX); fs.writeFileSync(entryPath, rewritten); } } @@ -111,8 +115,28 @@ function removeStrayEmits(dir) { } } +/** + * Every plugin directory with a backend/ that tsc actually compiled (i.e. it + * has a .ts file in it, not just a hand-written index.mjs). Scanned rather + * than hardcoded so a future plugin only needs a tsconfig.plugins.json entry, + * not a second call site here. + */ +function compiledPluginBackendDirs() { + return fs + .readdirSync(source, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => path.join(source, entry.name, "backend")) + .filter( + (dir) => + fs.existsSync(dir) && + fs.readdirSync(dir).some((name) => name.endsWith(".ts")), + ); +} + compilePluginBackends(); -rewriteCoreImports(path.join(source, "docker", "backend")); +for (const dir of compiledPluginBackendDirs()) { + rewriteCoreImports(dir); +} removeStrayEmits(path.join(root, "src")); fs.rmSync(destination, { recursive: true, force: true }); diff --git a/src/backend/ai/tools/executor.ts b/src/backend/ai/tools/executor.ts index 1459e0e0e..01d768fe7 100644 --- a/src/backend/ai/tools/executor.ts +++ b/src/backend/ai/tools/executor.ts @@ -7,7 +7,7 @@ import { } from "../../database/repositories/factory.js"; import { validateDefinition } from "../../database/routes/automations.js"; import { resolveHostById } from "../../hosts/host-resolver.js"; -import { execCommand } from "../../hosts/metrics/widgets/common-utils.js"; +import { execCommand } from "../../hosts/metrics-shared/common-utils.js"; import { createFleetSshFactory, getFleetPoolKey, diff --git a/src/backend/automations/actions/index.ts b/src/backend/automations/actions/index.ts index f288df487..54fd4279a 100644 --- a/src/backend/automations/actions/index.ts +++ b/src/backend/automations/actions/index.ts @@ -2,11 +2,11 @@ import { DEFAULT_STEP_TIMEOUT_MS, type Step, } from "../../../types/automations.js"; -import { execCommand } from "../../hosts/metrics/widgets/common-utils.js"; +import { execCommand } from "../../hosts/metrics-shared/common-utils.js"; import { execElevated, shellSingleQuote, -} from "../../hosts/metrics/managers/exec-elevated.js"; +} from "../../hosts/metrics-shared/exec-elevated.js"; import { createFleetSshFactory, getFleetPoolKey, diff --git a/src/backend/automations/docker-watcher.ts b/src/backend/automations/docker-watcher.ts index 1626faab5..a5cd6816a 100644 --- a/src/backend/automations/docker-watcher.ts +++ b/src/backend/automations/docker-watcher.ts @@ -2,7 +2,7 @@ import type { AutomationDefinition } from "../../types/automations.js"; import { createCurrentAutomationRepository } from "../database/repositories/factory.js"; import { resolveHostById } from "../hosts/host-resolver.js"; import { DataCrypto } from "../utils/data-crypto.js"; -import { execCommand } from "../hosts/metrics/widgets/common-utils.js"; +import { execCommand } from "../hosts/metrics-shared/common-utils.js"; import { createFleetSshFactory, getFleetPoolKey, diff --git a/src/backend/database/routes/fleet-routes.ts b/src/backend/database/routes/fleet-routes.ts index 568e59c34..2232e0c93 100644 --- a/src/backend/database/routes/fleet-routes.ts +++ b/src/backend/database/routes/fleet-routes.ts @@ -28,14 +28,14 @@ import { createFleetSshFactory, } from "../../hosts/ssh-client-factory.js"; import { withConnection } from "../../hosts/ssh-connection-pool.js"; -import { execCommand } from "../../hosts/metrics/widgets/common-utils.js"; -import { detectPlatform } from "../../hosts/metrics/managers/platform.js"; +import { execCommand } from "../../hosts/metrics-shared/common-utils.js"; +import { detectPlatform } from "../../hosts/metrics-shared/platform.js"; import { execElevated, ElevationError, -} from "../../hosts/metrics/managers/exec-elevated.js"; -import { buildPackageActionCommand } from "../../hosts/metrics/managers/packages.js"; -import { isValidPackageName } from "../../hosts/metrics/managers/validation.js"; +} from "../../hosts/metrics-shared/exec-elevated.js"; +import { buildPackageActionCommand } from "../../hosts/metrics-shared/package-commands.js"; +import { isValidPackageName } from "../../hosts/metrics-shared/validation.js"; import { resolveSnippetCommand } from "./snippets-execution.js"; const router = express.Router(); diff --git a/src/backend/database/routes/proxmox.ts b/src/backend/database/routes/proxmox.ts index 7e6234f4f..02f8ce3cb 100644 --- a/src/backend/database/routes/proxmox.ts +++ b/src/backend/database/routes/proxmox.ts @@ -18,7 +18,7 @@ import { serializeProxmoxJumpHosts, } from "./proxmox-jump-hosts.js"; import { isSafeNodeName } from "../../hosts/proxmox-shared.js"; -import { execElevated } from "../../hosts/metrics/managers/exec-elevated.js"; +import { execElevated } from "../../hosts/metrics-shared/exec-elevated.js"; const router = express.Router(); const proxmoxLogger = logger; diff --git a/src/backend/hosts/host-key-verifier.ts b/src/backend/hosts/host-key-verifier.ts index 80ef19fdd..db77263a3 100644 --- a/src/backend/hosts/host-key-verifier.ts +++ b/src/backend/hosts/host-key-verifier.ts @@ -1,4 +1,4 @@ -import { authFailureTracker } from "./metrics/state.js"; +import { authFailureTracker } from "./metrics-shared/state.js"; import type { WebSocket } from "ws"; import { createCurrentHostResolutionRepository } from "../database/repositories/factory.js"; import { sshLogger } from "../utils/logger.js"; diff --git a/src/backend/hosts/metrics/widgets/common-utils.ts b/src/backend/hosts/metrics-shared/common-utils.ts similarity index 100% rename from src/backend/hosts/metrics/widgets/common-utils.ts rename to src/backend/hosts/metrics-shared/common-utils.ts diff --git a/src/backend/hosts/metrics/managers/exec-elevated.ts b/src/backend/hosts/metrics-shared/exec-elevated.ts similarity index 98% rename from src/backend/hosts/metrics/managers/exec-elevated.ts rename to src/backend/hosts/metrics-shared/exec-elevated.ts index ed9589059..7bbde7870 100644 --- a/src/backend/hosts/metrics/managers/exec-elevated.ts +++ b/src/backend/hosts/metrics-shared/exec-elevated.ts @@ -1,5 +1,5 @@ import type { Client } from "ssh2"; -import { execCommand } from "../widgets/common-utils.js"; +import { execCommand } from "./common-utils.js"; export type ElevationErrorCode = "SUDO_REQUIRED" | "SUDO_FAILED" | "NOT_SUDOER"; diff --git a/src/backend/hosts/metrics/host-status.ts b/src/backend/hosts/metrics-shared/host-status.ts similarity index 100% rename from src/backend/hosts/metrics/host-status.ts rename to src/backend/hosts/metrics-shared/host-status.ts diff --git a/src/backend/hosts/metrics/managers/packages.ts b/src/backend/hosts/metrics-shared/package-commands.ts similarity index 52% rename from src/backend/hosts/metrics/managers/packages.ts rename to src/backend/hosts/metrics-shared/package-commands.ts index 2dff740c4..37d933728 100644 --- a/src/backend/hosts/metrics/managers/packages.ts +++ b/src/backend/hosts/metrics-shared/package-commands.ts @@ -1,10 +1,4 @@ -import type { Express } from "express"; -import { execCommand } from "../widgets/common-utils.js"; -import { execElevated } from "./exec-elevated.js"; -import { managerHandler, ManagerInputError } from "./route-helpers.js"; -import { isValidPackageName } from "./validation.js"; -import { detectPlatform, type PackageManager } from "./platform.js"; -import type { ManagerRoutesDeps } from "./types.js"; +import type { PackageManager } from "./platform.js"; export interface UpgradablePackage { name: string; @@ -88,61 +82,3 @@ export function buildPackageActionCommand( return null; } } - -export function registerPackageRoutes( - app: Express, - { validateHostId, runOnHost }: ManagerRoutesDeps, -): void { - app.get( - "/host-metrics/managers/packages/:id", - validateHostId, - managerHandler(runOnHost, "connect", "packages_list", async (client) => { - const platform = await detectPlatform(client); - const cmd = buildListUpgradableCommand(platform.pkg); - if (!cmd) return { pkg: platform.pkg, upgradable: [] }; - const { stdout } = await execCommand(client, cmd, 60000); - return { - pkg: platform.pkg, - upgradable: parseUpgradable(platform.pkg, stdout), - }; - }), - ); - - app.post( - "/host-metrics/managers/packages/:id/action", - validateHostId, - managerHandler( - runOnHost, - "connect", - "packages_action", - async (client, host, req) => { - const { action, pkg: name } = req.body as { - action?: PackageAction; - pkg?: string; - }; - if ( - action !== "upgrade-all" && - action !== "install" && - action !== "upgrade" - ) { - throw new ManagerInputError("Invalid action"); - } - if (action !== "upgrade-all" && !isValidPackageName(name)) { - throw new ManagerInputError("Invalid package name"); - } - const platform = await detectPlatform(client); - const cmd = buildPackageActionCommand(platform.pkg, action, name); - if (!cmd) throw new ManagerInputError("No supported package manager"); - // Package operations can be slow; allow up to 10 minutes. - const result = await execElevated(client, cmd, host.sudoPassword, { - forceSudo: true, - timeoutMs: 600000, - }); - return { - success: result.code === 0, - output: (result.stdout || result.stderr).slice(-8000), - }; - }, - ), - ); -} diff --git a/src/backend/hosts/metrics/managers/platform.ts b/src/backend/hosts/metrics-shared/platform.ts similarity index 97% rename from src/backend/hosts/metrics/managers/platform.ts rename to src/backend/hosts/metrics-shared/platform.ts index 5d51c8419..37e109eed 100644 --- a/src/backend/hosts/metrics/managers/platform.ts +++ b/src/backend/hosts/metrics-shared/platform.ts @@ -1,5 +1,5 @@ import type { Client } from "ssh2"; -import { execCommand } from "../widgets/common-utils.js"; +import { execCommand } from "./common-utils.js"; export type PackageManager = "apt" | "dnf" | "yum" | "pacman" | null; diff --git a/src/backend/hosts/metrics/state.ts b/src/backend/hosts/metrics-shared/state.ts similarity index 100% rename from src/backend/hosts/metrics/state.ts rename to src/backend/hosts/metrics-shared/state.ts diff --git a/src/backend/hosts/metrics/managers/validation.ts b/src/backend/hosts/metrics-shared/validation.ts similarity index 100% rename from src/backend/hosts/metrics/managers/validation.ts rename to src/backend/hosts/metrics-shared/validation.ts diff --git a/src/backend/hosts/metrics/proxmox-stats-polling.ts b/src/backend/hosts/metrics/proxmox-stats-polling.ts index 87c9d78cf..b4a74cba7 100644 --- a/src/backend/hosts/metrics/proxmox-stats-polling.ts +++ b/src/backend/hosts/metrics/proxmox-stats-polling.ts @@ -3,7 +3,7 @@ import { createCurrentProxmoxNodeHistoryRepository, getCurrentSettingValue, } from "../../database/repositories/factory.js"; -import { ConcurrentLimiter } from "./state.js"; +import { ConcurrentLimiter } from "../metrics-shared/state.js"; import { collectProxmoxStats, type ProxmoxStatsSnapshot, diff --git a/src/backend/hosts/metrics/proxmox/cluster-health-collector.ts b/src/backend/hosts/metrics/proxmox/cluster-health-collector.ts index 6b048186e..306121a56 100644 --- a/src/backend/hosts/metrics/proxmox/cluster-health-collector.ts +++ b/src/backend/hosts/metrics/proxmox/cluster-health-collector.ts @@ -1,5 +1,5 @@ import type { Client } from "ssh2"; -import { execCommand } from "../widgets/common-utils.js"; +import { execCommand } from "../../metrics-shared/common-utils.js"; export interface ProxmoxClusterNodeEntry { name: string; diff --git a/src/backend/hosts/metrics/proxmox/collect-proxmox-stats.ts b/src/backend/hosts/metrics/proxmox/collect-proxmox-stats.ts index 7f822d169..6e6237d1e 100644 --- a/src/backend/hosts/metrics/proxmox/collect-proxmox-stats.ts +++ b/src/backend/hosts/metrics/proxmox/collect-proxmox-stats.ts @@ -1,5 +1,5 @@ import type { Client } from "ssh2"; -import { execCommand } from "../widgets/common-utils.js"; +import { execCommand } from "../../metrics-shared/common-utils.js"; import { isSafeNodeName } from "../../proxmox-shared.js"; import { collectProxmoxNodeStatus, diff --git a/src/backend/hosts/metrics/proxmox/guests-collector.ts b/src/backend/hosts/metrics/proxmox/guests-collector.ts index 704f36cf9..11781dcff 100644 --- a/src/backend/hosts/metrics/proxmox/guests-collector.ts +++ b/src/backend/hosts/metrics/proxmox/guests-collector.ts @@ -1,5 +1,5 @@ import type { Client } from "ssh2"; -import { execCommand, toFixedNum } from "../widgets/common-utils.js"; +import { execCommand, toFixedNum } from "../../metrics-shared/common-utils.js"; import { isSafeNodeName } from "../../proxmox-shared.js"; export interface ProxmoxGuestSummaryEntry { diff --git a/src/backend/hosts/metrics/proxmox/node-network-collector.ts b/src/backend/hosts/metrics/proxmox/node-network-collector.ts index faa950269..a0746deee 100644 --- a/src/backend/hosts/metrics/proxmox/node-network-collector.ts +++ b/src/backend/hosts/metrics/proxmox/node-network-collector.ts @@ -1,5 +1,5 @@ import type { Client } from "ssh2"; -import { execCommand } from "../widgets/common-utils.js"; +import { execCommand } from "../../metrics-shared/common-utils.js"; import { isSafeNodeName } from "../../proxmox-shared.js"; export interface ProxmoxNodeNetworkInterface { diff --git a/src/backend/hosts/metrics/proxmox/node-status-collector.ts b/src/backend/hosts/metrics/proxmox/node-status-collector.ts index 4b6fb0063..d0b0ab80d 100644 --- a/src/backend/hosts/metrics/proxmox/node-status-collector.ts +++ b/src/backend/hosts/metrics/proxmox/node-status-collector.ts @@ -1,5 +1,5 @@ import type { Client } from "ssh2"; -import { execCommand, toFixedNum } from "../widgets/common-utils.js"; +import { execCommand, toFixedNum } from "../../metrics-shared/common-utils.js"; import { isSafeNodeName } from "../../proxmox-shared.js"; export interface ProxmoxNodeStatusResult { diff --git a/src/backend/hosts/metrics/proxmox/storage-collector.ts b/src/backend/hosts/metrics/proxmox/storage-collector.ts index ff30624ad..0f4c136fa 100644 --- a/src/backend/hosts/metrics/proxmox/storage-collector.ts +++ b/src/backend/hosts/metrics/proxmox/storage-collector.ts @@ -1,5 +1,5 @@ import type { Client } from "ssh2"; -import { execCommand, toFixedNum } from "../widgets/common-utils.js"; +import { execCommand, toFixedNum } from "../../metrics-shared/common-utils.js"; import { isSafeNodeName } from "../../proxmox-shared.js"; export interface ProxmoxStoragePoolEntry { diff --git a/src/backend/hosts/ssh-client-factory.ts b/src/backend/hosts/ssh-client-factory.ts index 040be98be..0441166ea 100644 --- a/src/backend/hosts/ssh-client-factory.ts +++ b/src/backend/hosts/ssh-client-factory.ts @@ -14,7 +14,8 @@ import { resolveSshConnectConfigHost } from "./ssh-dns.js"; /** * Non-interactive SSH connection helpers shared by fleet execution. * - * Mirrors buildSshConfig/createSshFactory/getPoolKey in hosts/metrics/index.ts, + * Mirrors buildSshConfig/createSshFactory/getPoolKey in + * plugins/host-metrics/backend/routes.ts, * minus the keyboard-interactive TOTP prompt handling (metrics/terminal-only) * and OPKSSH/Vault cert setup (not supported as fleet auth for v1 - those flows * need an interactive browser step that has no place in a batch fleet run). diff --git a/src/backend/plugins/first-party.ts b/src/backend/plugins/first-party.ts index 74a643ad8..2e6288eca 100644 --- a/src/backend/plugins/first-party.ts +++ b/src/backend/plugins/first-party.ts @@ -38,10 +38,18 @@ * session manager reuses a live ssh2.Client across many REST calls, and its * SSH connect flow implements TOTP/Warpgate as a stateful multi-request * handshake the worker ctx.ssh API has no hook for. + * + * host-metrics is here for the same reason: its polling manager holds + * long-lived ssh2 clients across a whole fleet of hosts, reused between + * polling ticks rather than opened per request, and its connect flow + * implements the same jump-host/SOCKS5/TOTP/Warpgate/OPKSSH/Vault + * keyboard-interactive handshakes the terminal does -- none of which the + * worker ctx.ssh API (connect + single exec-and-collect) has a hook for. */ export const FIRST_PARTY_PLUGIN_IDS: ReadonlySet = new Set([ "ssh-terminal", "docker", + "host-metrics", ]); export const TRANSPORT_OWNER_CAPABILITY = "process:transport-owner"; diff --git a/src/backend/plugins/index.ts b/src/backend/plugins/index.ts index 0f42ffbae..0196a1a40 100644 --- a/src/backend/plugins/index.ts +++ b/src/backend/plugins/index.ts @@ -86,7 +86,7 @@ function createBroker(): PluginBroker { import("../hosts/host-resolver.js"), import("../hosts/ssh-connection-pool.js"), import("../hosts/ssh-client-factory.js"), - import("../hosts/metrics/widgets/common-utils.js"), + import("../hosts/metrics-shared/common-utils.js"), ]); // resolveHostById performs the RBAC check (canAccessHost) and returns diff --git a/src/backend/starter.ts b/src/backend/starter.ts index 971c2d976..e03196cfb 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -295,12 +295,12 @@ async function provisionLocalDesktopUserIfNeeded(): Promise { const { serverReady } = await import("./database/database.js"); await serverReady; - // Terminal and docker are deliberately absent: the ssh-terminal and - // docker plugins start their own servers, so disabling either plugin - // stops its WS/HTTP server. See plugins/ssh-terminal and plugins/docker. + // Terminal, docker and host-metrics are deliberately absent: the + // ssh-terminal, docker and host-metrics plugins start their own servers, + // so disabling any of them stops its WS/HTTP server. See + // plugins/ssh-terminal, plugins/docker and plugins/host-metrics. await import("./hosts/tunnel/index.js"); await import("./hosts/file-manager/index.js"); - await import("./hosts/metrics/index.js"); await import("./hosts/tmux/index.js"); await import("./hosts/serial.js"); await import("./services/dashboard.js"); diff --git a/src/backend/tests/ai/proposal-executor.test.ts b/src/backend/tests/ai/proposal-executor.test.ts index 06d32ff37..e19943f03 100644 --- a/src/backend/tests/ai/proposal-executor.test.ts +++ b/src/backend/tests/ai/proposal-executor.test.ts @@ -30,7 +30,7 @@ vi.mock("../../hosts/host-resolver.js", () => ({ })); const execCommand = vi.fn(); -vi.mock("../../hosts/metrics/widgets/common-utils.js", () => ({ +vi.mock("../../hosts/metrics-shared/common-utils.js", () => ({ execCommand: (...args: unknown[]) => execCommand(...args), })); vi.mock("../../hosts/ssh-client-factory.js", () => ({ diff --git a/src/backend/tests/database/routes/fleet-routes.test.ts b/src/backend/tests/database/routes/fleet-routes.test.ts index 5f97b189a..3b5e0f183 100644 --- a/src/backend/tests/database/routes/fleet-routes.test.ts +++ b/src/backend/tests/database/routes/fleet-routes.test.ts @@ -73,15 +73,15 @@ vi.mock("../../../hosts/ssh-connection-pool.js", () => ({ ) => fn({}), })); -vi.mock("../../../hosts/metrics/widgets/common-utils.js", () => ({ +vi.mock("../../../hosts/metrics-shared/common-utils.js", () => ({ execCommand: vi.fn(async () => ({ stdout: "ok", stderr: "", code: 0 })), })); -vi.mock("../../../hosts/metrics/managers/platform.js", () => ({ +vi.mock("../../../hosts/metrics-shared/platform.js", () => ({ detectPlatform: vi.fn(async () => ({ pkg: "apt", osPrettyName: "Debian" })), })); -vi.mock("../../../hosts/metrics/managers/exec-elevated.js", async () => { +vi.mock("../../../hosts/metrics-shared/exec-elevated.js", async () => { class ElevationError extends Error { code: string; constructor(code: string, message: string) { @@ -95,11 +95,11 @@ vi.mock("../../../hosts/metrics/managers/exec-elevated.js", async () => { }; }); -vi.mock("../../../hosts/metrics/managers/packages.js", () => ({ +vi.mock("../../../hosts/metrics-shared/package-commands.js", () => ({ buildPackageActionCommand: vi.fn(() => "apt-get install -y foo"), })); -vi.mock("../../../hosts/metrics/managers/validation.js", () => ({ +vi.mock("../../../hosts/metrics-shared/validation.js", () => ({ isValidPackageName: (v: unknown) => typeof v === "string" && v.length > 0, })); @@ -320,7 +320,7 @@ describe("POST /:id/packages", () => { it("surfaces an ElevationError as a per-host error, not a request failure", async () => { const { execElevated, ElevationError } = - await import("../../../hosts/metrics/managers/exec-elevated.js"); + await import("../../../hosts/metrics-shared/exec-elevated.js"); (execElevated as ReturnType).mockRejectedValue( new ElevationError("SUDO_REQUIRED", "sudo password required"), ); @@ -347,7 +347,7 @@ describe("POST /:id/packages", () => { }); const { execElevated } = - await import("../../../hosts/metrics/managers/exec-elevated.js"); + await import("../../../hosts/metrics-shared/exec-elevated.js"); (execElevated as ReturnType).mockResolvedValue({ code: 0, stdout: "done", diff --git a/src/backend/tests/database/routes/proxmox-sudo.test.ts b/src/backend/tests/database/routes/proxmox-sudo.test.ts index 7cd2a1bae..a9c72495d 100644 --- a/src/backend/tests/database/routes/proxmox-sudo.test.ts +++ b/src/backend/tests/database/routes/proxmox-sudo.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { Client } from "ssh2"; const execElevated = vi.fn(); -vi.mock("../../../hosts/metrics/managers/exec-elevated.js", () => ({ +vi.mock("../../../hosts/metrics-shared/exec-elevated.js", () => ({ execElevated: (...args: unknown[]) => execElevated(...args), })); diff --git a/src/backend/tests/hosts/host-key-recovery.test.ts b/src/backend/tests/hosts/host-key-recovery.test.ts index d5bdf1330..9513112af 100644 --- a/src/backend/tests/hosts/host-key-recovery.test.ts +++ b/src/backend/tests/hosts/host-key-recovery.test.ts @@ -2,7 +2,7 @@ import { EventEmitter } from "node:events"; import type { WebSocket } from "ws"; import { afterEach, describe, expect, it, vi } from "vitest"; import { SSHHostKeyVerifier } from "../../hosts/host-key-verifier.js"; -import { authFailureTracker } from "../../hosts/metrics/state.js"; +import { authFailureTracker } from "../../hosts/metrics-shared/state.js"; const { updateHostKey } = vi.hoisted(() => ({ updateHostKey: vi.fn() })); vi.mock("../../database/repositories/factory.js", () => ({ diff --git a/src/backend/tests/hosts/metrics/widgets/common-utils.test.ts b/src/backend/tests/hosts/metrics-shared/common-utils.test.ts similarity index 95% rename from src/backend/tests/hosts/metrics/widgets/common-utils.test.ts rename to src/backend/tests/hosts/metrics-shared/common-utils.test.ts index 5ca94d3c5..07747753a 100644 --- a/src/backend/tests/hosts/metrics/widgets/common-utils.test.ts +++ b/src/backend/tests/hosts/metrics-shared/common-utils.test.ts @@ -3,7 +3,7 @@ import { toFixedNum, kibToGiB, encodePowerShellCommand, -} from "../../../../hosts/metrics/widgets/common-utils.js"; +} from "../../../hosts/metrics-shared/common-utils.js"; describe("toFixedNum", () => { it("rounds to the requested digit count", () => { diff --git a/src/backend/tests/hosts/metrics/managers/exec-elevated.test.ts b/src/backend/tests/hosts/metrics-shared/exec-elevated.test.ts similarity index 96% rename from src/backend/tests/hosts/metrics/managers/exec-elevated.test.ts rename to src/backend/tests/hosts/metrics-shared/exec-elevated.test.ts index bed2bf2e4..ee97a3ae4 100644 --- a/src/backend/tests/hosts/metrics/managers/exec-elevated.test.ts +++ b/src/backend/tests/hosts/metrics-shared/exec-elevated.test.ts @@ -1,14 +1,14 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; const execCommand = vi.fn(); -vi.mock("../../../../hosts/metrics/widgets/common-utils.js", () => ({ +vi.mock("../../../hosts/metrics-shared/common-utils.js", () => ({ execCommand: (...args: unknown[]) => execCommand(...args), })); import { execElevated, ElevationError, -} from "../../../../hosts/metrics/managers/exec-elevated.js"; +} from "../../../hosts/metrics-shared/exec-elevated.js"; import type { Client } from "ssh2"; const fakeClient = {} as Client; diff --git a/src/backend/hosts/metrics/host-status.test.ts b/src/backend/tests/hosts/metrics-shared/host-status.test.ts similarity index 96% rename from src/backend/hosts/metrics/host-status.test.ts rename to src/backend/tests/hosts/metrics-shared/host-status.test.ts index 83268f3fc..0f04ff099 100644 --- a/src/backend/hosts/metrics/host-status.test.ts +++ b/src/backend/tests/hosts/metrics-shared/host-status.test.ts @@ -3,7 +3,7 @@ import { isHostKeyVerificationError, statusAfterAuthentication, statusAfterReachabilityCheck, -} from "./host-status.js"; +} from "../../../hosts/metrics-shared/host-status.js"; describe("isHostKeyVerificationError", () => { it.each([ diff --git a/src/backend/tests/hosts/metrics/state.test.ts b/src/backend/tests/hosts/metrics-shared/state.test.ts similarity index 99% rename from src/backend/tests/hosts/metrics/state.test.ts rename to src/backend/tests/hosts/metrics-shared/state.test.ts index 6721a7e62..6c3be064e 100644 --- a/src/backend/tests/hosts/metrics/state.test.ts +++ b/src/backend/tests/hosts/metrics-shared/state.test.ts @@ -5,7 +5,7 @@ import { ConcurrentLimiter, HostPollCache, metricsConcurrencyFor, -} from "../../../hosts/metrics/state.js"; +} from "../../../hosts/metrics-shared/state.js"; describe("auth failure tracking", () => { it("permanently pauses polling after a host key mismatch", () => { diff --git a/src/backend/tests/hosts/metrics/proxmox/cluster-health-collector.test.ts b/src/backend/tests/hosts/metrics/proxmox/cluster-health-collector.test.ts index c469ec2db..117a2eeaa 100644 --- a/src/backend/tests/hosts/metrics/proxmox/cluster-health-collector.test.ts +++ b/src/backend/tests/hosts/metrics/proxmox/cluster-health-collector.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; const execCommand = vi.fn(); -vi.mock("../../../../hosts/metrics/widgets/common-utils.js", () => ({ +vi.mock("../../../../hosts/metrics-shared/common-utils.js", () => ({ execCommand: (...args: unknown[]) => execCommand(...args), })); diff --git a/src/backend/tests/hosts/metrics/proxmox/collect-proxmox-stats.test.ts b/src/backend/tests/hosts/metrics/proxmox/collect-proxmox-stats.test.ts index 54a20adf0..731a5ffcf 100644 --- a/src/backend/tests/hosts/metrics/proxmox/collect-proxmox-stats.test.ts +++ b/src/backend/tests/hosts/metrics/proxmox/collect-proxmox-stats.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; const execCommand = vi.fn(); -vi.mock("../../../../hosts/metrics/widgets/common-utils.js", () => ({ +vi.mock("../../../../hosts/metrics-shared/common-utils.js", () => ({ execCommand: (...args: unknown[]) => execCommand(...args), toFixedNum: (n: number | null | undefined, digits = 2) => { if (typeof n !== "number" || !Number.isFinite(n)) return null; diff --git a/src/backend/tests/hosts/metrics/proxmox/guests-collector.test.ts b/src/backend/tests/hosts/metrics/proxmox/guests-collector.test.ts index fbacffa9a..3fb62d94c 100644 --- a/src/backend/tests/hosts/metrics/proxmox/guests-collector.test.ts +++ b/src/backend/tests/hosts/metrics/proxmox/guests-collector.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; const execCommand = vi.fn(); -vi.mock("../../../../hosts/metrics/widgets/common-utils.js", () => ({ +vi.mock("../../../../hosts/metrics-shared/common-utils.js", () => ({ execCommand: (...args: unknown[]) => execCommand(...args), toFixedNum: (n: number | null | undefined, digits = 2) => { if (typeof n !== "number" || !Number.isFinite(n)) return null; diff --git a/src/backend/tests/hosts/metrics/proxmox/node-network-collector.test.ts b/src/backend/tests/hosts/metrics/proxmox/node-network-collector.test.ts index b120cc79d..6884d0329 100644 --- a/src/backend/tests/hosts/metrics/proxmox/node-network-collector.test.ts +++ b/src/backend/tests/hosts/metrics/proxmox/node-network-collector.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; const execCommand = vi.fn(); -vi.mock("../../../../hosts/metrics/widgets/common-utils.js", () => ({ +vi.mock("../../../../hosts/metrics-shared/common-utils.js", () => ({ execCommand: (...args: unknown[]) => execCommand(...args), })); diff --git a/src/backend/tests/hosts/metrics/proxmox/node-status-collector.test.ts b/src/backend/tests/hosts/metrics/proxmox/node-status-collector.test.ts index 79113fc70..3909f673b 100644 --- a/src/backend/tests/hosts/metrics/proxmox/node-status-collector.test.ts +++ b/src/backend/tests/hosts/metrics/proxmox/node-status-collector.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; const execCommand = vi.fn(); -vi.mock("../../../../hosts/metrics/widgets/common-utils.js", () => ({ +vi.mock("../../../../hosts/metrics-shared/common-utils.js", () => ({ execCommand: (...args: unknown[]) => execCommand(...args), toFixedNum: (n: number | null | undefined, digits = 2) => { if (typeof n !== "number" || !Number.isFinite(n)) return null; diff --git a/src/backend/tests/hosts/metrics/proxmox/storage-collector.test.ts b/src/backend/tests/hosts/metrics/proxmox/storage-collector.test.ts index 857d55142..c721f7e1a 100644 --- a/src/backend/tests/hosts/metrics/proxmox/storage-collector.test.ts +++ b/src/backend/tests/hosts/metrics/proxmox/storage-collector.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; const execCommand = vi.fn(); -vi.mock("../../../../hosts/metrics/widgets/common-utils.js", () => ({ +vi.mock("../../../../hosts/metrics-shared/common-utils.js", () => ({ execCommand: (...args: unknown[]) => execCommand(...args), toFixedNum: (n: number | null | undefined, digits = 2) => { if (typeof n !== "number" || !Number.isFinite(n)) return null; diff --git a/src/backend/tests/plugins/bundled-plugin.test.ts b/src/backend/tests/plugins/bundled-plugin.test.ts index ac1a68351..2d741e67a 100644 --- a/src/backend/tests/plugins/bundled-plugin.test.ts +++ b/src/backend/tests/plugins/bundled-plugin.test.ts @@ -136,3 +136,53 @@ describe("bundled docker plugin", () => { expect(source).toMatch(/export async function deactivate/); }); }); + +describe("bundled host-metrics plugin", () => { + it("ships a directory with a manifest and both entry points", () => { + const dir = path.join(getBundledPluginsDir(), "host-metrics"); + + expect(fs.existsSync(path.join(dir, "manifest.json"))).toBe(true); + expect(fs.existsSync(path.join(dir, "backend", "index.mjs"))).toBe(true); + expect(fs.existsSync(path.join(dir, "frontend", "index.mjs"))).toBe(true); + }); + + it("has a manifest that passes the real validator", () => { + const { manifest, errors } = parseManifest( + readBundledManifest("host-metrics"), + ); + + expect(errors).toEqual([]); + expect(manifest?.id).toBe("host-metrics"); + expect(manifest?.category).toBe("Monitoring"); + }); + + it("declares the transport-owner capability and qualifies for the tier", () => { + const { manifest } = parseManifest(readBundledManifest("host-metrics")); + + expect(manifest?.permissions).toContain(TRANSPORT_OWNER_CAPABILITY); + expect(isFirstParty("host-metrics")).toBe(true); + expect(runsInProcess("host-metrics", manifest!.permissions)).toBe(true); + }); + + it("contributes the host-metrics tab the shell registers", () => { + const { manifest } = parseManifest(readBundledManifest("host-metrics")); + const tab = manifest?.contributes?.tabs?.[0]; + + // The shell keys tab content off this id, so it has to stay "host-metrics". + expect(tab?.id).toBe("host-metrics"); + expect(tab?.openFrom).toContain("host-context-menu"); + }); + + it("exports activate and deactivate from its backend entry", () => { + const entry = path.join( + getBundledPluginsDir(), + "host-metrics", + "backend", + "index.mjs", + ); + const source = fs.readFileSync(entry, "utf8"); + + expect(source).toMatch(/export async function activate/); + expect(source).toMatch(/export async function deactivate/); + }); +}); diff --git a/src/backend/tests/plugins/events.test.ts b/src/backend/tests/plugins/events.test.ts index e5ebc5375..ab8558b8a 100644 --- a/src/backend/tests/plugins/events.test.ts +++ b/src/backend/tests/plugins/events.test.ts @@ -137,7 +137,7 @@ describe("hosts publishers route through the bus", () => { it("notifyAutomationStatus and notifyAutomationMetrics emit their topics", async () => { const { notifyAutomationStatus, notifyAutomationMetrics } = - await import("../../hosts/metrics/automation-bridge.js"); + await import("../../../../plugins/host-metrics/backend/automation-bridge.js"); const status = vi.fn(); const metrics = vi.fn(); diff --git a/src/backend/tests/plugins/first-party.test.ts b/src/backend/tests/plugins/first-party.test.ts index 6e64c0582..37aec29e9 100644 --- a/src/backend/tests/plugins/first-party.test.ts +++ b/src/backend/tests/plugins/first-party.test.ts @@ -39,16 +39,18 @@ function manifest(overrides: Record = {}) { } describe("first-party allowlist", () => { - it("contains only ssh-terminal and docker", () => { + it("contains only ssh-terminal, docker and host-metrics", () => { expect([...FIRST_PARTY_PLUGIN_IDS].sort()).toEqual([ "docker", + "host-metrics", "ssh-terminal", ]); }); - it("recognises ssh-terminal and docker and nothing else", () => { + it("recognises ssh-terminal, docker and host-metrics and nothing else", () => { expect(isFirstParty("ssh-terminal")).toBe(true); expect(isFirstParty("docker")).toBe(true); + expect(isFirstParty("host-metrics")).toBe(true); expect(isFirstParty("ssh-terminal-pro")).toBe(false); expect(isFirstParty("community-plugin")).toBe(false); expect(isFirstParty("")).toBe(false); @@ -61,11 +63,15 @@ describe("runsInProcess", () => { true, ); expect(runsInProcess("docker", [TRANSPORT_OWNER_CAPABILITY])).toBe(true); + expect(runsInProcess("host-metrics", [TRANSPORT_OWNER_CAPABILITY])).toBe( + true, + ); // On the list but not asking for it: stays in a worker, so the manifest // remains an honest description of what the plugin does. expect(runsInProcess("ssh-terminal", ["hosts.read"])).toBe(false); expect(runsInProcess("docker", ["hosts.read"])).toBe(false); + expect(runsInProcess("host-metrics", ["hosts.read"])).toBe(false); // Asking for it but not on the list. expect( @@ -121,6 +127,18 @@ describe("manifest gate on the reserved capability", () => { expect(parsed?.id).toBe("docker"); }); + it("allows it for host-metrics", () => { + const { manifest: parsed, errors } = parseManifest( + manifest({ + id: "host-metrics", + permissions: [TRANSPORT_OWNER_CAPABILITY], + }), + ); + + expect(errors).toEqual([]); + expect(parsed?.id).toBe("host-metrics"); + }); + it("still accepts an ordinary manifest that never mentions it", () => { const { manifest: parsed, errors } = parseManifest( manifest({ permissions: ["hosts.read"] }), diff --git a/src/backend/tests/hosts/metrics/alert-engine-superseded.test.ts b/src/backend/tests/plugins/host-metrics/alert-engine-superseded.test.ts similarity index 97% rename from src/backend/tests/hosts/metrics/alert-engine-superseded.test.ts rename to src/backend/tests/plugins/host-metrics/alert-engine-superseded.test.ts index 876a9ba3e..01079c40f 100644 --- a/src/backend/tests/hosts/metrics/alert-engine-superseded.test.ts +++ b/src/backend/tests/plugins/host-metrics/alert-engine-superseded.test.ts @@ -35,7 +35,7 @@ vi.mock("../../../utils/discord-sender.js", () => ({ })); const alertEngineModule = - await import("../../../hosts/metrics/alert-engine.js"); + await import("../../../../../plugins/host-metrics/backend/alert-engine.js"); const cpuRule = { id: 1, diff --git a/src/backend/tests/hosts/metrics/helpers.test.ts b/src/backend/tests/plugins/host-metrics/helpers.test.ts similarity index 98% rename from src/backend/tests/hosts/metrics/helpers.test.ts rename to src/backend/tests/plugins/host-metrics/helpers.test.ts index 0981c727e..ec181d5ad 100644 --- a/src/backend/tests/hosts/metrics/helpers.test.ts +++ b/src/backend/tests/plugins/host-metrics/helpers.test.ts @@ -5,7 +5,7 @@ import { isTcpPingEnabled, parseStatusHostIds, tcpPingThroughJumpHost, -} from "../../../hosts/metrics/helpers.js"; +} from "../../../../../plugins/host-metrics/backend/helpers.js"; import { createConnectionLog } from "../../../hosts/connection-log.js"; describe("supportsMetrics", () => { diff --git a/src/backend/tests/hosts/metrics/login-alert-dispatch.test.ts b/src/backend/tests/plugins/host-metrics/login-alert-dispatch.test.ts similarity index 96% rename from src/backend/tests/hosts/metrics/login-alert-dispatch.test.ts rename to src/backend/tests/plugins/host-metrics/login-alert-dispatch.test.ts index bf807745e..039e1c56a 100644 --- a/src/backend/tests/hosts/metrics/login-alert-dispatch.test.ts +++ b/src/backend/tests/plugins/host-metrics/login-alert-dispatch.test.ts @@ -5,11 +5,14 @@ import { beforeEach, expect, it, vi } from "vitest"; // Load the production route without starting metrics polling or SSH services. const source = readFileSync( - new URL("../../../hosts/metrics/index.ts", import.meta.url), + new URL( + "../../../../../plugins/host-metrics/backend/routes.ts", + import.meta.url, + ), "utf8", ); const ast = ts.createSourceFile( - "index.ts", + "routes.ts", source, ts.ScriptTarget.Latest, true, diff --git a/src/backend/tests/hosts/metrics/login-alert-route-order.test.ts b/src/backend/tests/plugins/host-metrics/login-alert-route-order.test.ts similarity index 90% rename from src/backend/tests/hosts/metrics/login-alert-route-order.test.ts rename to src/backend/tests/plugins/host-metrics/login-alert-route-order.test.ts index 11f4c422b..34068bc44 100644 --- a/src/backend/tests/hosts/metrics/login-alert-route-order.test.ts +++ b/src/backend/tests/plugins/host-metrics/login-alert-route-order.test.ts @@ -11,7 +11,10 @@ import path from "path"; describe("metrics service /internal/login-alert route order", () => { it("is registered before the global auth middleware", () => { const source = fs.readFileSync( - path.resolve(__dirname, "../../../hosts/metrics/index.ts"), + path.resolve( + __dirname, + "../../../../../plugins/host-metrics/backend/routes.ts", + ), "utf8", ); diff --git a/src/backend/tests/hosts/managers/managers.test.ts b/src/backend/tests/plugins/host-metrics/managers/managers.test.ts similarity index 93% rename from src/backend/tests/hosts/managers/managers.test.ts rename to src/backend/tests/plugins/host-metrics/managers/managers.test.ts index fa598ae78..ad108aa1d 100644 --- a/src/backend/tests/hosts/managers/managers.test.ts +++ b/src/backend/tests/plugins/host-metrics/managers/managers.test.ts @@ -2,8 +2,8 @@ import { describe, it, expect } from "vitest"; import { buildSudoCommand, shellSingleQuote, -} from "../../../hosts/metrics/managers/exec-elevated.js"; -import { parsePlatformProbe } from "../../../hosts/metrics/managers/platform.js"; +} from "../../../../hosts/metrics-shared/exec-elevated.js"; +import { parsePlatformProbe } from "../../../../hosts/metrics-shared/platform.js"; import { isValidSystemdUnit, isValidPid, @@ -15,53 +15,53 @@ import { isValidSignal, isValidServiceAction, isAllowedPath, -} from "../../../hosts/metrics/managers/validation.js"; +} from "../../../../hosts/metrics-shared/validation.js"; import { parseServiceList, buildServiceActionCommand, -} from "../../../hosts/metrics/managers/services.js"; +} from "../../../../../../plugins/host-metrics/backend/managers/services.js"; import { parseProcessList, buildKillCommand, -} from "../../../hosts/metrics/managers/processes.js"; +} from "../../../../../../plugins/host-metrics/backend/managers/processes.js"; import { parseDfMounts, parseTopMemory, -} from "../../../hosts/metrics/managers/simple-reads.js"; +} from "../../../../../../plugins/host-metrics/backend/managers/simple-reads.js"; import { parseCrontab, serializeCrontab, isValidCronSchedule, buildApplyCrontabCommand, -} from "../../../hosts/metrics/managers/cron.js"; +} from "../../../../../../plugins/host-metrics/backend/managers/cron.js"; import { buildPackageActionCommand, parseUpgradable, buildListUpgradableCommand, -} from "../../../hosts/metrics/managers/packages.js"; +} from "../../../../hosts/metrics-shared/package-commands.js"; import { buildIssueCommand, buildRenewCommand, buildRevokeCommand, isValidCertName, parseCertbotCertificates, -} from "../../../hosts/metrics/managers/ssl.js"; +} from "../../../../../../plugins/host-metrics/backend/managers/ssl.js"; import { buildIptablesRuleCommand, buildNftRuleCommand, -} from "../../../hosts/metrics/managers/firewall.js"; +} from "../../../../../../plugins/host-metrics/backend/managers/firewall.js"; import { parsePasswd, parseSudoers, -} from "../../../hosts/metrics/managers/users.js"; +} from "../../../../../../plugins/host-metrics/backend/managers/users.js"; import { buildHealthCheckCommand, parseHealthResult, -} from "../../../hosts/metrics/managers/health.js"; +} from "../../../../../../plugins/host-metrics/backend/managers/health.js"; import { buildTailCommand, clampLines, -} from "../../../hosts/metrics/managers/logs.js"; +} from "../../../../../../plugins/host-metrics/backend/managers/logs.js"; describe("exec-elevated", () => { it("single-quotes and escapes for the shell", () => { diff --git a/src/backend/tests/hosts/metrics/status-poll-auth.test.ts b/src/backend/tests/plugins/host-metrics/status-poll-auth.test.ts similarity index 92% rename from src/backend/tests/hosts/metrics/status-poll-auth.test.ts rename to src/backend/tests/plugins/host-metrics/status-poll-auth.test.ts index a6c9fdcc8..4cd6f63f3 100644 --- a/src/backend/tests/hosts/metrics/status-poll-auth.test.ts +++ b/src/backend/tests/plugins/host-metrics/status-poll-auth.test.ts @@ -13,7 +13,10 @@ import path from "path"; describe("metrics service status polling", () => { it("never authenticates over SSH from pollHostStatus, on the first poll or any later one", () => { const source = fs.readFileSync( - path.resolve(__dirname, "../../../hosts/metrics/index.ts"), + path.resolve( + __dirname, + "../../../../../plugins/host-metrics/backend/routes.ts", + ), "utf8", ); diff --git a/src/backend/tests/hosts/metrics/widgets/cpu-collector.test.ts b/src/backend/tests/plugins/host-metrics/widgets/cpu-collector.test.ts similarity index 94% rename from src/backend/tests/hosts/metrics/widgets/cpu-collector.test.ts rename to src/backend/tests/plugins/host-metrics/widgets/cpu-collector.test.ts index 8a8a49cf4..7e9ce42ea 100644 --- a/src/backend/tests/hosts/metrics/widgets/cpu-collector.test.ts +++ b/src/backend/tests/plugins/host-metrics/widgets/cpu-collector.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest"; import { parseCpuLine, parseDarwinCpuUsageLine, -} from "../../../../hosts/metrics/widgets/cpu-collector.js"; +} from "../../../../../../plugins/host-metrics/backend/widgets/cpu-collector.js"; describe("parseCpuLine", () => { it("parses a standard /proc/stat cpu line", () => { diff --git a/src/backend/tests/hosts/metrics/widgets/disk-collector.test.ts b/src/backend/tests/plugins/host-metrics/widgets/disk-collector.test.ts similarity index 99% rename from src/backend/tests/hosts/metrics/widgets/disk-collector.test.ts rename to src/backend/tests/plugins/host-metrics/widgets/disk-collector.test.ts index 2289dce7c..8780e0660 100644 --- a/src/backend/tests/hosts/metrics/widgets/disk-collector.test.ts +++ b/src/backend/tests/plugins/host-metrics/widgets/disk-collector.test.ts @@ -9,7 +9,7 @@ import { parseWindowsDiskJson, parseDarwinMountTypes, parseDarwinDfRows, -} from "../../../../hosts/metrics/widgets/disk-collector.js"; +} from "../../../../../../plugins/host-metrics/backend/widgets/disk-collector.js"; describe("parseDfLines", () => { it("parses df -T -P output into rows", () => { diff --git a/src/backend/tests/hosts/metrics/widgets/gpu-collector.test.ts b/src/backend/tests/plugins/host-metrics/widgets/gpu-collector.test.ts similarity index 98% rename from src/backend/tests/hosts/metrics/widgets/gpu-collector.test.ts rename to src/backend/tests/plugins/host-metrics/widgets/gpu-collector.test.ts index 1f3a3b9d8..1f7a94faf 100644 --- a/src/backend/tests/hosts/metrics/widgets/gpu-collector.test.ts +++ b/src/backend/tests/plugins/host-metrics/widgets/gpu-collector.test.ts @@ -3,7 +3,7 @@ import { parseNvidiaSmiGpus, parseNvidiaSmiOutput, parseNvidiaSmiProcesses, -} from "../../../../hosts/metrics/widgets/gpu-collector.js"; +} from "../../../../../../plugins/host-metrics/backend/widgets/gpu-collector.js"; const RTX_UUID = "GPU-3b8f2c1a-5d4e-7f60-8a9b-0c1d2e3f4a5b"; const H100_UUID = "GPU-9a8b7c6d-5e4f-3a2b-1c0d-e9f8a7b6c5d4"; diff --git a/src/backend/tests/hosts/metrics/widgets/network-collector.test.ts b/src/backend/tests/plugins/host-metrics/widgets/network-collector.test.ts similarity index 97% rename from src/backend/tests/hosts/metrics/widgets/network-collector.test.ts rename to src/backend/tests/plugins/host-metrics/widgets/network-collector.test.ts index a092a456a..e84b6f9f9 100644 --- a/src/backend/tests/hosts/metrics/widgets/network-collector.test.ts +++ b/src/backend/tests/plugins/host-metrics/widgets/network-collector.test.ts @@ -5,7 +5,7 @@ import { parseDarwinIfconfig, parseDarwinNetstat, parseWindowsAdapterJson, -} from "../../../../hosts/metrics/widgets/network-collector.js"; +} from "../../../../../../plugins/host-metrics/backend/widgets/network-collector.js"; const PROC_NET = `Inter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed diff --git a/src/backend/tests/hosts/metrics/widgets/temperature-collector.test.ts b/src/backend/tests/plugins/host-metrics/widgets/temperature-collector.test.ts similarity index 92% rename from src/backend/tests/hosts/metrics/widgets/temperature-collector.test.ts rename to src/backend/tests/plugins/host-metrics/widgets/temperature-collector.test.ts index 629940bee..9bd2fa963 100644 --- a/src/backend/tests/hosts/metrics/widgets/temperature-collector.test.ts +++ b/src/backend/tests/plugins/host-metrics/widgets/temperature-collector.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { parseSensorsOutput, parseSysfsThermalOutput, -} from "../../../../hosts/metrics/widgets/temperature-collector.js"; +} from "../../../../../../plugins/host-metrics/backend/widgets/temperature-collector.js"; describe("temperature collectors", () => { it("parses sysfs thermal zone output", () => { diff --git a/src/backend/utils/crypto-migration/automations-migration.ts b/src/backend/utils/crypto-migration/automations-migration.ts index 7ff074977..b80718a98 100644 --- a/src/backend/utils/crypto-migration/automations-migration.ts +++ b/src/backend/utils/crypto-migration/automations-migration.ts @@ -120,11 +120,27 @@ export async function runAutomationsMigration(): Promise { try { - const { markAlertEngineSuperseded } = - await import("../../hosts/metrics/alert-engine.js"); + const { getBundledPluginsDir } = await import("../../plugins/paths.js"); + const path = await import("node:path"); + const { pathToFileURL } = await import("node:url"); + + const entry = path.join( + getBundledPluginsDir(), + "host-metrics", + "backend", + "alert-engine.js", + ); + const { markAlertEngineSuperseded } = await import( + pathToFileURL(entry).href + ); markAlertEngineSuperseded(); } catch { // If it cannot be loaded there is nothing running to stand down. diff --git a/src/main.tsx b/src/main.tsx index f09e572de..3949bd217 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -44,7 +44,7 @@ const TunnelApp = lazy(() => import("@/features/tunnel/TunnelApp").then((m) => ({ default: m.default })), ); const HostMetricsApp = lazy(() => - import("@/features/host-metrics/HostMetricsApp").then((m) => ({ + import("../plugins/host-metrics/frontend/HostMetricsApp").then((m) => ({ default: m.default, })), ); diff --git a/src/types/ui-preferences.ts b/src/types/ui-preferences.ts index 7311111fe..a9ce40f6b 100644 --- a/src/types/ui-preferences.ts +++ b/src/types/ui-preferences.ts @@ -182,7 +182,7 @@ const BALANCED_DASHBOARD_CARDS = [ // the edge of the screen. They stay available in the Add card tray. const ADVANCED_DASHBOARD_CARDS = [...BALANCED_DASHBOARD_CARDS, "service_links"]; -/** Host metrics card ids, mirroring CARD_DEFINITIONS in features/host-metrics/cards. */ +/** Host metrics card ids, mirroring CARD_DEFINITIONS in plugins/host-metrics/frontend/cards. */ const SIMPLE_HOST_METRICS_CARDS = ["cpu", "memory", "disk"]; const BALANCED_HOST_METRICS_CARDS = [ "cpu", diff --git a/src/ui/features/host-metrics/cards/MetricCard.tsx b/src/ui/components/metric-card.tsx similarity index 100% rename from src/ui/features/host-metrics/cards/MetricCard.tsx rename to src/ui/components/metric-card.tsx diff --git a/src/ui/features/proxmox-stats/cards/ClusterHealthCard.tsx b/src/ui/features/proxmox-stats/cards/ClusterHealthCard.tsx index ff4ae63db..8c0620f58 100644 --- a/src/ui/features/proxmox-stats/cards/ClusterHealthCard.tsx +++ b/src/ui/features/proxmox-stats/cards/ClusterHealthCard.tsx @@ -2,7 +2,7 @@ import { Network } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { ProxmoxStatsSnapshot } from "@/types/proxmox"; import { cn } from "@/lib/utils"; -import { MetricCard } from "@/features/host-metrics/cards/MetricCard"; +import { MetricCard } from "@/components/metric-card"; export function ClusterHealthCard({ snapshot, diff --git a/src/ui/features/proxmox-stats/cards/NodeNetworkCard.tsx b/src/ui/features/proxmox-stats/cards/NodeNetworkCard.tsx index 8b4133935..e48ed4ce2 100644 --- a/src/ui/features/proxmox-stats/cards/NodeNetworkCard.tsx +++ b/src/ui/features/proxmox-stats/cards/NodeNetworkCard.tsx @@ -2,7 +2,7 @@ import { Network } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { ProxmoxStatsSnapshot } from "@/types/proxmox"; import { StatRow } from "@/components/charts"; -import { MetricCard } from "@/features/host-metrics/cards/MetricCard"; +import { MetricCard } from "@/components/metric-card"; export function NodeNetworkCard({ snapshot, diff --git a/src/ui/features/proxmox-stats/cards/StoragePoolsCard.tsx b/src/ui/features/proxmox-stats/cards/StoragePoolsCard.tsx index 9ed14a328..21c8f6f39 100644 --- a/src/ui/features/proxmox-stats/cards/StoragePoolsCard.tsx +++ b/src/ui/features/proxmox-stats/cards/StoragePoolsCard.tsx @@ -1,7 +1,7 @@ import { Database } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { ProxmoxStatsSnapshot } from "@/types/proxmox"; -import { MetricCard } from "@/features/host-metrics/cards/MetricCard"; +import { MetricCard } from "@/components/metric-card"; export function StoragePoolsCard({ snapshot, diff --git a/src/ui/shell/tabUtils.tsx b/src/ui/shell/tabUtils.tsx index 1b5c6eb7b..753a52b37 100644 --- a/src/ui/shell/tabUtils.tsx +++ b/src/ui/shell/tabUtils.tsx @@ -95,7 +95,7 @@ const loadWebEndpointTab = () => })); const WebEndpointTab = lazy(loadWebEndpointTab); const loadHostMetricsTab = () => - import("@/features/host-metrics/HostMetricsTab").then((m) => ({ + import("../../../plugins/host-metrics/frontend/HostMetricsTab").then((m) => ({ default: m.HostMetricsTab, })); const HostMetricsTab = lazy(loadHostMetricsTab); diff --git a/src/ui/sidebar/FleetsPanel.tsx b/src/ui/sidebar/FleetsPanel.tsx index f322d265c..438d8931b 100644 --- a/src/ui/sidebar/FleetsPanel.tsx +++ b/src/ui/sidebar/FleetsPanel.tsx @@ -39,7 +39,11 @@ import { import { FOLDER_COLORS } from "@/lib/theme"; import { getSSHHosts } from "@/api/ssh-host-management-api"; import { getUserList } from "@/api/user-management-api"; -import { getRoles, type SharePermissionLevel, type ShareTarget } from "@/api/rbac-api"; +import { + getRoles, + type SharePermissionLevel, + type ShareTarget, +} from "@/api/rbac-api"; import { Select2 } from "@/components/select2"; import { DropdownMenu, @@ -685,9 +689,7 @@ function FleetShareDialog({ - setPermissionLevel( - e.target.value as SharePermissionLevel, - ) + setPermissionLevel(e.target.value as SharePermissionLevel) } className="h-8 w-full px-2.5 text-xs border border-border bg-background hover:bg-muted/40 transition-colors" > @@ -1304,11 +1306,7 @@ function FleetDetail({ {t("newUi.sidebar.fleets.shareFleet")} - diff --git a/src/ui/tests/features/host-metrics/HostMetricsTab.test.tsx b/src/ui/tests/plugins/host-metrics/HostMetricsTab.test.tsx similarity index 94% rename from src/ui/tests/features/host-metrics/HostMetricsTab.test.tsx rename to src/ui/tests/plugins/host-metrics/HostMetricsTab.test.tsx index ed6c4d985..9d015c2f8 100644 --- a/src/ui/tests/features/host-metrics/HostMetricsTab.test.tsx +++ b/src/ui/tests/plugins/host-metrics/HostMetricsTab.test.tsx @@ -56,7 +56,7 @@ vi.mock("@/hooks/use-confirmation.ts", () => ({ })); vi.mock( - "../../../features/host-metrics/hooks/useHostMetricsPreferences.ts", + "../../../../../plugins/host-metrics/frontend/hooks/useHostMetricsPreferences.ts", () => ({ useHostMetricsPreferences: () => ({ layout: { @@ -69,7 +69,7 @@ vi.mock( }), ); -import { HostMetricsTab } from "../../../features/host-metrics/HostMetricsTab"; +import { HostMetricsTab } from "../../../../../plugins/host-metrics/frontend/HostMetricsTab"; beforeAll(() => { globalThis.ResizeObserver ??= class { diff --git a/src/ui/tests/features/host-metrics/GpuCard.test.tsx b/src/ui/tests/plugins/host-metrics/cards/GpuCard.test.tsx similarity index 97% rename from src/ui/tests/features/host-metrics/GpuCard.test.tsx rename to src/ui/tests/plugins/host-metrics/cards/GpuCard.test.tsx index 5c0ce75e2..3e18881f0 100644 --- a/src/ui/tests/features/host-metrics/GpuCard.test.tsx +++ b/src/ui/tests/plugins/host-metrics/cards/GpuCard.test.tsx @@ -8,7 +8,7 @@ vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => key }), })); -import { GpuCard } from "../../../features/host-metrics/cards/GpuCard"; +import { GpuCard } from "../../../../../../plugins/host-metrics/frontend/cards/GpuCard"; const GPU_METRICS: GpuMetrics = { source: "nvidia-smi", diff --git a/src/ui/tests/features/host-metrics/gpu-history.test.ts b/src/ui/tests/plugins/host-metrics/cards/gpu-history.test.ts similarity index 94% rename from src/ui/tests/features/host-metrics/gpu-history.test.ts rename to src/ui/tests/plugins/host-metrics/cards/gpu-history.test.ts index 4cd419fc8..a197aadca 100644 --- a/src/ui/tests/features/host-metrics/gpu-history.test.ts +++ b/src/ui/tests/plugins/host-metrics/cards/gpu-history.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import type { GpuDevice } from "@/types/stats-widgets"; -import { appendGpuHistories } from "../../../features/host-metrics/cards/gpu-history"; +import { appendGpuHistories } from "../../../../../../plugins/host-metrics/frontend/cards/gpu-history"; function gpu( index: number, diff --git a/src/ui/tests/features/host-metrics/metrics-change-key.test.ts b/src/ui/tests/plugins/host-metrics/metrics-change-key.test.ts similarity index 94% rename from src/ui/tests/features/host-metrics/metrics-change-key.test.ts rename to src/ui/tests/plugins/host-metrics/metrics-change-key.test.ts index 64d78f0b3..b264ca912 100644 --- a/src/ui/tests/features/host-metrics/metrics-change-key.test.ts +++ b/src/ui/tests/plugins/host-metrics/metrics-change-key.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import type { ServerMetrics } from "@/main-axios"; import type { GpuMetrics } from "@/types/stats-widgets"; -import { metricsChangeKey } from "../../../features/host-metrics/metrics-change-key"; +import { metricsChangeKey } from "../../../../../plugins/host-metrics/frontend/metrics-change-key"; function poll(gpu: GpuMetrics): ServerMetrics { return { diff --git a/src/ui/tests/features/host-metrics/temperature-preference.test.ts b/src/ui/tests/plugins/host-metrics/temperature-preference.test.ts similarity index 90% rename from src/ui/tests/features/host-metrics/temperature-preference.test.ts rename to src/ui/tests/plugins/host-metrics/temperature-preference.test.ts index 480e785b4..4f404d9f9 100644 --- a/src/ui/tests/features/host-metrics/temperature-preference.test.ts +++ b/src/ui/tests/plugins/host-metrics/temperature-preference.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { selectTemperatureSensor, temperaturePreferenceKey, -} from "../../../features/host-metrics/temperature-preference"; +} from "../../../../../plugins/host-metrics/frontend/temperature-preference"; const sensors = [ { label: "k10temp: Tctl", celsius: 55 }, diff --git a/tsconfig.plugins.json b/tsconfig.plugins.json index 35df1c0e5..7d8b25ed9 100644 --- a/tsconfig.plugins.json +++ b/tsconfig.plugins.json @@ -26,6 +26,9 @@ "allowUnusedLabels": true, "allowUnreachableCode": true }, - "include": ["plugins/docker/backend/**/*.ts"], + "include": [ + "plugins/docker/backend/**/*.ts", + "plugins/host-metrics/backend/**/*.ts" + ], "exclude": ["src/**/*.test.ts"] }