mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: finish the host-metrics plugin, move host status into core (B16)
This commit is contained in:
1 parent
14eb558b85
commit
0a3f05d99d
308 files changed
+53073
-8985
No files matched your search
@@ -0,0 +1,31 @@
|
||||
import type { HomepageWidgetContribution } from "@termix/plugin-sdk/frontend";
|
||||
|
||||
/** The homepage's grid step, in pixels. */
|
||||
export const GRID_SIZE = 30;
|
||||
|
||||
export type MetricsChartMetric =
|
||||
"cpu" | "memory" | "disk" | "net_rx" | "net_tx";
|
||||
export type MetricsChartRange = "15m" | "1h" | "6h" | "24h";
|
||||
|
||||
export interface MetricsChartConfig {
|
||||
hostId: number;
|
||||
metric: MetricsChartMetric;
|
||||
range: MetricsChartRange;
|
||||
showCurrentValue: boolean;
|
||||
}
|
||||
|
||||
/** What the homepage hands a widget; only the fields this plugin reads. */
|
||||
export interface WidgetComponentProps<C> {
|
||||
widget: { title?: string | null };
|
||||
config: C;
|
||||
}
|
||||
|
||||
export interface WidgetEditFormProps<C> {
|
||||
config: C;
|
||||
onChange: (config: C) => void;
|
||||
}
|
||||
|
||||
export type WidgetDefinition<C> = Omit<
|
||||
HomepageWidgetContribution<C>,
|
||||
"defaultConfig"
|
||||
> & { defaultConfig: C };
|
||||
@@ -0,0 +1,145 @@
|
||||
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 the `enabledWidgets` host setting.
|
||||
*/
|
||||
|
||||
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
|
||||
* the `enabledWidgets` host setting 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 the `enabledWidgets` host setting), 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;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import type { GpuMetrics } from "./stats-widgets.js";
|
||||
|
||||
/** What GET /metrics/:id answers. */
|
||||
export interface CpuMetrics {
|
||||
percent: number | null;
|
||||
cores: number | null;
|
||||
load: [number, number, number] | null;
|
||||
}
|
||||
|
||||
export interface MemoryMetrics {
|
||||
percent: number | null;
|
||||
usedGiB: number | null;
|
||||
totalGiB: number | null;
|
||||
}
|
||||
|
||||
export interface DiskFilesystem {
|
||||
filesystem: string;
|
||||
type: string;
|
||||
mount: string;
|
||||
percent: number | null;
|
||||
usedHuman: string | null;
|
||||
totalHuman: string | null;
|
||||
availableHuman: string | null;
|
||||
usedBytes: number | null;
|
||||
totalBytes: number | null;
|
||||
availableBytes: number | null;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface DiskMetrics {
|
||||
percent: number | null;
|
||||
usedHuman: string | null;
|
||||
totalHuman: string | null;
|
||||
availableHuman?: string | null;
|
||||
mount?: string | null;
|
||||
filesystems?: DiskFilesystem[];
|
||||
}
|
||||
|
||||
export interface NetworkInterface {
|
||||
name: string;
|
||||
ip: string;
|
||||
state: string;
|
||||
rx?: string | null;
|
||||
tx?: string | null;
|
||||
rxBytes?: string | null;
|
||||
txBytes?: string | null;
|
||||
rxRateBps?: number | null;
|
||||
txRateBps?: number | null;
|
||||
}
|
||||
|
||||
export interface ProcessInfo {
|
||||
pid: string;
|
||||
user: string;
|
||||
cpu: string;
|
||||
mem: string;
|
||||
command: string;
|
||||
}
|
||||
|
||||
export interface LoginRecord {
|
||||
user: string;
|
||||
ip: string;
|
||||
time: string;
|
||||
status: "success" | "failed";
|
||||
}
|
||||
|
||||
export interface ListeningPort {
|
||||
protocol: "tcp" | "udp";
|
||||
localAddress: string;
|
||||
localPort: number;
|
||||
state?: string;
|
||||
pid?: number;
|
||||
process?: string;
|
||||
}
|
||||
|
||||
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 type ServerMetrics = {
|
||||
cpu: CpuMetrics;
|
||||
memory: MemoryMetrics;
|
||||
disk: DiskMetrics;
|
||||
network?: { interfaces?: NetworkInterface[] };
|
||||
uptime?: { seconds?: number | null; formatted?: string | null };
|
||||
system?: {
|
||||
hostname?: string | null;
|
||||
os?: string | null;
|
||||
kernel?: string | null;
|
||||
arch?: string | null;
|
||||
};
|
||||
processes?: {
|
||||
total?: number | null;
|
||||
running?: number | null;
|
||||
top?: ProcessInfo[];
|
||||
};
|
||||
login_stats?: {
|
||||
recentLogins?: LoginRecord[];
|
||||
failedLogins?: LoginRecord[];
|
||||
totalLogins?: number;
|
||||
uniqueIPs?: number;
|
||||
};
|
||||
ports?: {
|
||||
source?: "ss" | "netstat" | "none";
|
||||
ports?: ListeningPort[];
|
||||
};
|
||||
firewall?: {
|
||||
type?: "iptables" | "nftables" | "none";
|
||||
status?: "active" | "inactive" | "unknown";
|
||||
chains?: FirewallChain[];
|
||||
};
|
||||
temperature?: {
|
||||
source?: "sysfs" | "sensors" | "none";
|
||||
highestCelsius?: number | null;
|
||||
sensors?: Array<{
|
||||
label: string;
|
||||
celsius: number;
|
||||
}>;
|
||||
};
|
||||
gpu?: GpuMetrics;
|
||||
lastChecked: string;
|
||||
};
|
||||
@@ -0,0 +1,161 @@
|
||||
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[];
|
||||
}
|
||||
|
||||
/** This plugin's host settings, as core stores them in plugin_settings. */
|
||||
export interface HostMetricsSettings {
|
||||
metricsEnabled: boolean;
|
||||
/** Seconds between samples; null follows the admin setting. */
|
||||
metricsInterval: number | null;
|
||||
enabledWidgets: WidgetType[];
|
||||
/** 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_ENABLED_WIDGETS: WidgetType[] = [
|
||||
"cpu",
|
||||
"memory",
|
||||
"disk",
|
||||
"network",
|
||||
"uptime",
|
||||
"system",
|
||||
"login_stats",
|
||||
"processes",
|
||||
"ports",
|
||||
"firewall",
|
||||
"temperature",
|
||||
];
|
||||
|
||||
export const DEFAULT_HOST_METRICS_SETTINGS: HostMetricsSettings = {
|
||||
metricsEnabled: true,
|
||||
metricsInterval: null,
|
||||
enabledWidgets: DEFAULT_ENABLED_WIDGETS,
|
||||
excludedMounts: [],
|
||||
monitoredMounts: [],
|
||||
};
|
||||
|
||||
/** Host settings with defaults filled in and bad values dropped. */
|
||||
export function readHostMetricsSettings(
|
||||
values: Record<string, unknown> | null | undefined,
|
||||
): HostMetricsSettings {
|
||||
const bag = values ?? {};
|
||||
const interval = Number(bag.metricsInterval);
|
||||
return {
|
||||
metricsEnabled: bag.metricsEnabled !== false,
|
||||
metricsInterval:
|
||||
bag.metricsInterval != null &&
|
||||
Number.isInteger(interval) &&
|
||||
interval >= 5 &&
|
||||
interval <= 3600
|
||||
? interval
|
||||
: null,
|
||||
enabledWidgets: Array.isArray(bag.enabledWidgets)
|
||||
? (bag.enabledWidgets.filter(
|
||||
(widget) => typeof widget === "string",
|
||||
) as WidgetType[])
|
||||
: DEFAULT_ENABLED_WIDGETS,
|
||||
excludedMounts: Array.isArray(bag.excludedMounts)
|
||||
? bag.excludedMounts.filter(
|
||||
(mount): mount is string => typeof mount === "string",
|
||||
)
|
||||
: [],
|
||||
monitoredMounts: Array.isArray(bag.monitoredMounts)
|
||||
? (bag.monitoredMounts as unknown[]).filter(
|
||||
(mount): mount is { path: string; label?: string } =>
|
||||
!!mount &&
|
||||
typeof mount === "object" &&
|
||||
typeof (mount as { path?: unknown }).path === "string",
|
||||
)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user