mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: ship host metrics as a first-party in-process plugin
This commit is contained in:
1 parent
a2479c19b0
commit
19b70430a1
145 files changed
+675
-349
No files matched your search
+1
-1
@@ -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/*
|
||||
|
||||
@@ -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",
|
||||
|
||||
+4
-4
@@ -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"
|
||||
+2
-2
@@ -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;
|
||||
@@ -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;
|
||||
+4
-4
@@ -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;
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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 {
|
||||
+3
-3
@@ -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 {
|
||||
+5
-5
@@ -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";
|
||||
+1
-1
@@ -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";
|
||||
+9
-3
@@ -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. */
|
||||
@@ -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),
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
+7
-3
@@ -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 {
|
||||
+5
-5
@@ -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") {
|
||||
+3
-3
@@ -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 {
|
||||
+1
-1
@@ -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";
|
||||
|
||||
+7
-4
@@ -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";
|
||||
+3
-3
@@ -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;
|
||||
+1
-1
@@ -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 {
|
||||
+6
-3
@@ -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 {
|
||||
+2
-2
@@ -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;
|
||||
+5
-5
@@ -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[];
|
||||
@@ -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<Client> {
|
||||
|
||||
// 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,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
+2
-2
@@ -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;
|
||||
+4
-4
@@ -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;
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -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)$/;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
+4
-1
@@ -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;
|
||||
+1
-1
@@ -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;
|
||||
+1
-1
@@ -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;
|
||||
+1
-1
@@ -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;
|
||||
+2
-2
@@ -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[] = [];
|
||||
+1
-1
@@ -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;
|
||||
+1
-1
@@ -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;
|
||||
+4
-1
@@ -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;
|
||||
+1
-1
@@ -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);
|
||||
+1
-1
@@ -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";
|
||||
|
||||
+1
-1
@@ -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";
|
||||
File renamed without changes.
+1
-1
@@ -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,
|
||||
+1
-1
@@ -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,
|
||||
File renamed without changes.
+1
-1
@@ -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();
|
||||
+1
-1
@@ -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";
|
||||
+1
-1
@@ -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();
|
||||
+1
-1
@@ -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,
|
||||
+1
-1
@@ -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,
|
||||
+1
-1
@@ -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) {
|
||||
+1
-1
@@ -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();
|
||||
+1
-1
@@ -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();
|
||||
+1
-1
@@ -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,
|
||||
+1
-1
@@ -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();
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -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({
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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);
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -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": []
|
||||
}
|
||||
@@ -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 });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
File renamed without changes.
+1
-1
@@ -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";
|
||||
|
||||
File renamed without changes.
+1
-65
@@ -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),
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
|
||||
File renamed without changes.
File renamed without changes.
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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<string> = new Set([
|
||||
"ssh-terminal",
|
||||
"docker",
|
||||
"host-metrics",
|
||||
]);
|
||||
|
||||
export const TRANSPORT_OWNER_CAPABILITY = "process:transport-owner";
|
||||
|
||||
Loaded 100 of 145 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user