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

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

No files matched your search

-145
View File
@@ -1,145 +0,0 @@
import type { WidgetType } from "./stats-widgets.js";
/**
* Host Metrics layout model. Shared by the frontend tab and the backend
* preferences endpoint. Metric card ids reuse the existing `WidgetType` values
* verbatim (so `enabledWidgets` stays valid for the mobile app); manager card
* ids are new and never enter `statsConfig.enabledWidgets`.
*/
export type HostMetricCardId = WidgetType;
export type HostMetricManagerId =
| "service_manager"
| "process_inspector"
| "log_viewer"
| "cron_manager"
| "package_manager"
| "ssl_manager"
| "firewall_manager"
| "user_manager"
| "health_check"
| "disk_breakdown"
| "systemd_timers"
| "top_memory"
| "wireguard_manager"
| "tailscale_manager";
export type HostMetricsCardId = HostMetricCardId | HostMetricManagerId;
export type HostMetricsColSpan = 1 | 2 | 3;
export interface HostMetricsSlot {
id: HostMetricsCardId;
order: number;
colSpan: HostMetricsColSpan;
height: number | null;
}
export interface HostMetricsLayout {
slots: HostMetricsSlot[];
columns: number;
}
/** Canonical order of metric cards (also the canonical `enabledWidgets` order). */
export const METRIC_CARD_IDS: HostMetricCardId[] = [
"cpu",
"memory",
"disk",
"network",
"uptime",
"system",
"login_stats",
"processes",
"ports",
"firewall",
"temperature",
"gpu",
];
export const MANAGER_CARD_IDS: HostMetricManagerId[] = [
"service_manager",
"process_inspector",
"log_viewer",
"cron_manager",
"package_manager",
"ssl_manager",
"firewall_manager",
"user_manager",
"health_check",
"disk_breakdown",
"systemd_timers",
"top_memory",
"wireguard_manager",
"tailscale_manager",
];
export function isMetricCardId(id: string): id is HostMetricCardId {
return (METRIC_CARD_IDS as string[]).includes(id);
}
export function isManagerCardId(id: string): id is HostMetricManagerId {
return (MANAGER_CARD_IDS as string[]).includes(id);
}
/**
* Derive the flat `enabledWidgets` array (canonical order, deduped, metric ids
* only) from a set of layout slots. This is the single source of truth keeping
* `statsConfig.enabledWidgets` valid for the mobile app.
*/
export function deriveEnabledWidgets(
slots: Array<{ id: string }>,
): WidgetType[] {
const present = new Set(slots.map((s) => s.id));
return METRIC_CARD_IDS.filter((id) => present.has(id));
}
const DEFAULT_COLSPAN: Partial<Record<HostMetricsCardId, HostMetricsColSpan>> =
{
cpu: 1,
memory: 1,
disk: 1,
uptime: 1,
system: 1,
network: 1,
temperature: 1,
processes: 2,
ports: 2,
firewall: 2,
login_stats: 2,
gpu: 2,
};
// All cards default to content/auto height (null); the masonry packs them
// tetris-style and scrollable cards cap their own body height. Users can still
// pin an explicit height by dragging a card's resize handle.
const DEFAULT_HEIGHT: Partial<Record<HostMetricsCardId, number | null>> = {};
/**
* Build a default layout from a list of enabled metric-card ids (e.g. the host's
* current `statsConfig.enabledWidgets`), preserving canonical order. Manager
* cards are not added by default.
*/
export function defaultLayoutFromWidgets(
enabledWidgets: string[],
columns = 3,
): HostMetricsLayout {
const enabled = new Set(enabledWidgets);
const ids = METRIC_CARD_IDS.filter((id) => enabled.has(id));
const slots: HostMetricsSlot[] = ids.map((id, i) => ({
id,
order: i,
colSpan: DEFAULT_COLSPAN[id] ?? 1,
height: DEFAULT_HEIGHT[id] ?? null,
}));
return { slots, columns };
}
export function defaultColSpanFor(id: HostMetricsCardId): HostMetricsColSpan {
return DEFAULT_COLSPAN[id] ?? (isManagerCardId(id) ? 2 : 1);
}
export function defaultHeightFor(id: HostMetricsCardId): number | null {
if (id in DEFAULT_HEIGHT) return DEFAULT_HEIGHT[id] ?? null;
return null;
}
+6 -3
View File
@@ -1,4 +1,3 @@
import type { StatsConfig } from "./stats-widgets.js";
import type { Request } from "express";
import type { RefObject } from "react";
import type { HostAuthOverrides } from "./auth-protocols.js";
@@ -230,7 +229,9 @@ export type Host = {
tunnelConnections: TunnelConnection[];
jumpHosts?: JumpHost[];
quickActions?: QuickAction[];
statsConfig?: string | StatsConfig;
statusCheckEnabled?: boolean;
/** Seconds between status checks; null follows the global setting. */
statusCheckInterval?: number | null;
terminalConfig?: Partial<TerminalConfig>;
notes?: string;
@@ -385,7 +386,9 @@ export interface HostData {
tunnelConnections?: TunnelConnection[];
jumpHosts?: JumpHostData[];
quickActions?: QuickActionData[];
statsConfig?: string | StatsConfig;
statusCheckEnabled?: boolean;
/** Seconds between status checks; null follows the global setting. */
statusCheckInterval?: number | null;
terminalConfig?: Partial<TerminalConfig>;
notes?: string;
-129
View File
@@ -1,129 +0,0 @@
export type WidgetType =
| "cpu"
| "memory"
| "disk"
| "network"
| "uptime"
| "processes"
| "system"
| "login_stats"
| "ports"
| "firewall"
| "temperature"
| "gpu";
export interface ListeningPort {
protocol: "tcp" | "udp";
localAddress: string;
localPort: number;
state?: string;
pid?: number;
process?: string;
}
export interface PortsMetrics {
source: "ss" | "netstat" | "none";
ports: ListeningPort[];
}
export interface FirewallRule {
chain: string;
target: string;
protocol: string;
source: string;
destination: string;
dport?: string;
sport?: string;
state?: string;
interface?: string;
extra?: string;
}
export interface FirewallChain {
name: string;
policy: string;
rules: FirewallRule[];
}
export interface FirewallMetrics {
type: "iptables" | "nftables" | "none";
status: "active" | "inactive" | "unknown";
chains: FirewallChain[];
}
export interface TemperatureSensor {
label: string;
celsius: number;
}
export interface TemperatureMetrics {
source: "sysfs" | "sensors" | "none";
highestCelsius: number | null;
sensors: TemperatureSensor[];
}
export interface GpuDevice {
index: number;
uuid: string;
name: string;
driverVersion: string | null;
utilizationPercent: number | null;
memoryUsedMiB: number | null;
memoryTotalMiB: number | null;
memoryPercent: number | null;
temperatureCelsius: number | null;
powerDrawWatts: number | null;
powerLimitWatts: number | null;
fanPercent: number | null;
}
export interface GpuProcess {
gpuIndex: number | null;
pid: number;
name: string;
memoryUsedMiB: number | null;
}
export interface GpuMetrics {
source: "nvidia-smi" | "none";
gpus: GpuDevice[];
processes: GpuProcess[];
}
export interface StatsConfig {
enabledWidgets: WidgetType[];
statusCheckEnabled: boolean;
statusCheckInterval: number;
useGlobalStatusInterval?: boolean;
metricsEnabled: boolean;
metricsInterval: number;
useGlobalMetricsInterval?: boolean;
disableTcpPing?: boolean;
/** Filesystem mount points to leave out of the disk widget. */
excludedMounts?: string[];
/** Extra paths to monitor, including paths inside bind-mounted containers. */
monitoredMounts?: Array<{ path: string; label?: string }>;
}
export const DEFAULT_STATS_CONFIG: StatsConfig = {
enabledWidgets: [
"cpu",
"memory",
"disk",
"network",
"uptime",
"system",
"login_stats",
"processes",
"ports",
"firewall",
"temperature",
],
statusCheckEnabled: true,
statusCheckInterval: 30,
useGlobalStatusInterval: true,
metricsEnabled: true,
metricsInterval: 30,
useGlobalMetricsInterval: true,
disableTcpPing: false,
};
+3 -2
View File
@@ -1,5 +1,4 @@
import type { TerminalConfig } from "./index.js";
import type { StatsConfig } from "./stats-widgets.js";
import type { HostAuthOverrides } from "./auth-protocols.js";
export type Host = {
@@ -147,7 +146,9 @@ export type Host = {
enabledCards?: string[];
} | null;
statsConfig?: StatsConfig;
statusCheckEnabled?: boolean;
/** Seconds between status checks; null follows the global setting. */
statusCheckInterval?: number | null;
quickActions: { name: string; snippetId: string }[];
enableSsh: boolean;