feat(beszel): add Beszel integration with system monitoring widgets (#5900)
This commit is contained in:
1 parent
5d1f268438
commit
a4db9cf0dc
41 files changed
+3474
-14
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
|
||||
import { AdGuardHomeIntegration } from "../adguard-home/adguard-home-integration";
|
||||
import { BeszelIntegration } from "../beszel/beszel-integration";
|
||||
import { AnchorIntegration } from "../anchor/anchor-integration";
|
||||
import { CodebergIntegration } from "../codeberg/codeberg-integration";
|
||||
import { CoolifyIntegration } from "../coolify/coolify-integration";
|
||||
@@ -125,6 +126,7 @@ export const integrationCreators = {
|
||||
speedtestTracker: SpeedtestTrackerIntegration,
|
||||
umami: UmamiIntegration,
|
||||
peaNut: PeaNutIntegration,
|
||||
beszel: BeszelIntegration,
|
||||
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
|
||||
|
||||
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
import { ResponseError } from "@homarr/common/server";
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||
|
||||
import type { IntegrationInput, IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import type { SessionStore } from "../base/session-store";
|
||||
import { createSessionStore } from "../base/session-store";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type {
|
||||
BeszelAlert,
|
||||
BeszelAlertHistory,
|
||||
BeszelAuthResponse,
|
||||
BeszelContainer,
|
||||
BeszelContainerStatsRecord,
|
||||
BeszelSmartDevice,
|
||||
BeszelSystem,
|
||||
BeszelSystemDetails,
|
||||
BeszelSystemdService,
|
||||
BeszelSystemStatsRecord,
|
||||
CreateAlertInput,
|
||||
PocketBaseListResponse,
|
||||
UpdateAlertInput,
|
||||
} from "./beszel-types";
|
||||
|
||||
const escapeFilterValue = (value: string) => value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
||||
|
||||
interface BeszelSession {
|
||||
token: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export class BeszelIntegration extends Integration {
|
||||
private readonly sessionStore: SessionStore<BeszelSession>;
|
||||
|
||||
constructor(integration: IntegrationInput) {
|
||||
super(integration);
|
||||
this.sessionStore = createSessionStore(integration);
|
||||
}
|
||||
|
||||
private async authenticateAsync(): Promise<BeszelSession> {
|
||||
const existingSession = await this.sessionStore.getAsync();
|
||||
if (existingSession) {
|
||||
return existingSession;
|
||||
}
|
||||
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/collections/users/auth-with-password"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
identity: this.getSecretValue("username"),
|
||||
password: this.getSecretValue("password"),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ResponseError(response);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as BeszelAuthResponse;
|
||||
const session: BeszelSession = { token: data.token, userId: data.record.id };
|
||||
await this.sessionStore.setAsync(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
private async fetchWithAuthAsync(url: URL, options: { method?: string; body?: string } = {}) {
|
||||
let session = await this.authenticateAsync();
|
||||
|
||||
const doFetch = (token: string) =>
|
||||
fetchWithTrustedCertificatesAsync(url, {
|
||||
method: options.method ?? "GET",
|
||||
headers: {
|
||||
Authorization: token,
|
||||
...(options.body ? { "Content-Type": "application/json" } : {}),
|
||||
},
|
||||
body: options.body,
|
||||
});
|
||||
|
||||
let response = await doFetch(session.token);
|
||||
|
||||
if (response.status === 401) {
|
||||
await this.sessionStore.clearAsync();
|
||||
session = await this.authenticateAsync();
|
||||
response = await doFetch(session.token);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ResponseError(response);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async getSystemsAsync(): Promise<BeszelSystem[]> {
|
||||
const response = await this.fetchWithAuthAsync(
|
||||
this.url("/api/collections/systems/records", { perPage: "500", sort: "-updated" }),
|
||||
);
|
||||
const data = (await response.json()) as PocketBaseListResponse<BeszelSystem>;
|
||||
return data.items;
|
||||
}
|
||||
|
||||
public async getSystemDetailsAsync(systemId: string): Promise<BeszelSystemDetails> {
|
||||
const response = await this.fetchWithAuthAsync(
|
||||
this.url(`/api/collections/system_details/records/${systemId}` as `/${string}`),
|
||||
);
|
||||
return (await response.json()) as BeszelSystemDetails;
|
||||
}
|
||||
|
||||
public async getSystemStatsAsync(systemId: string, type = "1m", perPage = 60): Promise<BeszelSystemStatsRecord[]> {
|
||||
const filter = `system='${escapeFilterValue(systemId)}' && type='${escapeFilterValue(type)}'`;
|
||||
const response = await this.fetchWithAuthAsync(
|
||||
this.url("/api/collections/system_stats/records", {
|
||||
filter,
|
||||
sort: "-created",
|
||||
perPage: String(perPage),
|
||||
}),
|
||||
);
|
||||
const data = (await response.json()) as PocketBaseListResponse<BeszelSystemStatsRecord>;
|
||||
return data.items;
|
||||
}
|
||||
|
||||
public async getContainersAsync(systemId: string): Promise<BeszelContainer[]> {
|
||||
const response = await this.fetchWithAuthAsync(
|
||||
this.url("/api/collections/containers/records", {
|
||||
filter: `system='${escapeFilterValue(systemId)}'`,
|
||||
sort: "-updated",
|
||||
perPage: "500",
|
||||
}),
|
||||
);
|
||||
const data = (await response.json()) as PocketBaseListResponse<BeszelContainer>;
|
||||
return data.items;
|
||||
}
|
||||
|
||||
public async getContainerStatsAsync(
|
||||
systemId: string,
|
||||
type = "1m",
|
||||
perPage = 60,
|
||||
): Promise<BeszelContainerStatsRecord[]> {
|
||||
const filter = `system='${escapeFilterValue(systemId)}' && type='${escapeFilterValue(type)}'`;
|
||||
const response = await this.fetchWithAuthAsync(
|
||||
this.url("/api/collections/container_stats/records", {
|
||||
filter,
|
||||
sort: "-created",
|
||||
perPage: String(perPage),
|
||||
}),
|
||||
);
|
||||
const data = (await response.json()) as PocketBaseListResponse<BeszelContainerStatsRecord>;
|
||||
return data.items;
|
||||
}
|
||||
|
||||
public async getSmartDevicesAsync(systemId: string): Promise<BeszelSmartDevice[]> {
|
||||
const response = await this.fetchWithAuthAsync(
|
||||
this.url("/api/collections/smart_devices/records", {
|
||||
filter: `system='${escapeFilterValue(systemId)}'`,
|
||||
}),
|
||||
);
|
||||
const data = (await response.json()) as PocketBaseListResponse<BeszelSmartDevice>;
|
||||
return data.items;
|
||||
}
|
||||
|
||||
public async getSystemdServicesAsync(systemId: string): Promise<BeszelSystemdService[]> {
|
||||
const response = await this.fetchWithAuthAsync(
|
||||
this.url("/api/collections/systemd_services/records", {
|
||||
filter: `system='${escapeFilterValue(systemId)}'`,
|
||||
sort: "name",
|
||||
}),
|
||||
);
|
||||
const data = (await response.json()) as PocketBaseListResponse<BeszelSystemdService>;
|
||||
return data.items;
|
||||
}
|
||||
|
||||
public async getAlertsAsync(systemId?: string): Promise<BeszelAlert[]> {
|
||||
const params: Record<string, string> = { perPage: "500" };
|
||||
if (systemId) {
|
||||
params.filter = `system='${escapeFilterValue(systemId)}'`;
|
||||
}
|
||||
const response = await this.fetchWithAuthAsync(this.url("/api/collections/alerts/records", params));
|
||||
const data = (await response.json()) as PocketBaseListResponse<BeszelAlert>;
|
||||
return data.items;
|
||||
}
|
||||
|
||||
public async getAlertHistoryAsync(systemId?: string, perPage = 50): Promise<BeszelAlertHistory[]> {
|
||||
const params: Record<string, string> = {
|
||||
sort: "-created",
|
||||
perPage: String(perPage),
|
||||
};
|
||||
if (systemId) {
|
||||
params.filter = `system='${escapeFilterValue(systemId)}'`;
|
||||
}
|
||||
const response = await this.fetchWithAuthAsync(this.url("/api/collections/alerts_history/records", params));
|
||||
const data = (await response.json()) as PocketBaseListResponse<BeszelAlertHistory>;
|
||||
return data.items;
|
||||
}
|
||||
|
||||
public async createAlertAsync(systemId: string, input: CreateAlertInput): Promise<BeszelAlert> {
|
||||
const session = await this.authenticateAsync();
|
||||
const response = await this.fetchWithAuthAsync(this.url("/api/collections/alerts/records"), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
system: systemId,
|
||||
user: session.userId,
|
||||
name: input.name,
|
||||
value: input.value,
|
||||
min: input.min ?? 0,
|
||||
triggered: false,
|
||||
}),
|
||||
});
|
||||
return (await response.json()) as BeszelAlert;
|
||||
}
|
||||
|
||||
public async updateAlertAsync(alertId: string, input: UpdateAlertInput): Promise<BeszelAlert> {
|
||||
const response = await this.fetchWithAuthAsync(
|
||||
this.url(`/api/collections/alerts/records/${alertId}` as `/${string}`),
|
||||
{
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(input),
|
||||
},
|
||||
);
|
||||
return (await response.json()) as BeszelAlert;
|
||||
}
|
||||
|
||||
public async deleteAlertAsync(alertId: string): Promise<void> {
|
||||
await this.fetchWithAuthAsync(this.url(`/api/collections/alerts/records/${alertId}` as `/${string}`), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
public async pauseSystemAsync(systemId: string): Promise<void> {
|
||||
await this.fetchWithAuthAsync(this.url(`/api/collections/systems/records/${systemId}` as `/${string}`), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status: "paused" }),
|
||||
});
|
||||
}
|
||||
|
||||
public async resumeSystemAsync(systemId: string): Promise<void> {
|
||||
await this.fetchWithAuthAsync(this.url(`/api/collections/systems/records/${systemId}` as `/${string}`), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status: "pending" }),
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteSystemAsync(systemId: string): Promise<void> {
|
||||
await this.fetchWithAuthAsync(this.url(`/api/collections/systems/records/${systemId}` as `/${string}`), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const response = await input.fetchAsync(this.url("/api/collections/users/auth-with-password"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
identity: this.getSecretValue("username"),
|
||||
password: this.getSecretValue("password"),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return TestConnectionError.StatusResult({ status: response.status, url: response.url });
|
||||
}
|
||||
|
||||
const data = (await response.json()) as BeszelAuthResponse;
|
||||
if (!data.token) {
|
||||
return TestConnectionError.UnauthorizedResult(401);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
export interface BeszelAuthResponse {
|
||||
token: string;
|
||||
record: {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
verified: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type BeszelSystemStatus = "up" | "down" | "paused" | "pending";
|
||||
|
||||
/**
|
||||
* Real-time system info snapshot from Beszel agent.
|
||||
* Returned as `system.info` on each BeszelSystem record.
|
||||
* All resource values are percentages or already-scaled summaries.
|
||||
*/
|
||||
export interface BeszelSystemInfo {
|
||||
/** hostname */
|
||||
h?: string;
|
||||
/** CPU usage (%) */
|
||||
cpu: number;
|
||||
/** temperature (°C), overall */
|
||||
t?: number;
|
||||
/** CPU core count */
|
||||
c?: number;
|
||||
/** CPU thread count */
|
||||
ct?: number;
|
||||
/** CPU model string */
|
||||
m?: string;
|
||||
/** load average [1m, 5m, 15m] */
|
||||
la?: [number, number, number];
|
||||
/** OS identifier string */
|
||||
o?: string;
|
||||
/** uptime (seconds) */
|
||||
u: number;
|
||||
/** memory usage (%) */
|
||||
mp: number;
|
||||
/** disk usage (%) */
|
||||
dp: number;
|
||||
/** battery [level%, charging state] */
|
||||
bat?: [number, number];
|
||||
/** bandwidth — public interface (Mbps, legacy). Use bb when available */
|
||||
b?: number;
|
||||
/** bandwidth — public interface (bytes/s, newer format). Preferred over b */
|
||||
bb?: number;
|
||||
/** agent version */
|
||||
v: string;
|
||||
/** podman mode */
|
||||
p?: boolean;
|
||||
/** GPU usage (%) */
|
||||
g?: number;
|
||||
/** disk temperature (°C) */
|
||||
dt?: number;
|
||||
/** OS type identifier */
|
||||
os?: number;
|
||||
/** extra filesystem usage (%) keyed by mount path */
|
||||
efs?: Record<string, number>;
|
||||
/** systemd services [running, total] */
|
||||
sv?: [number, number];
|
||||
}
|
||||
|
||||
export interface BeszelSystem {
|
||||
id: string;
|
||||
name: string;
|
||||
host: string;
|
||||
port: string;
|
||||
status: BeszelSystemStatus;
|
||||
info: BeszelSystemInfo;
|
||||
users?: string[];
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface BeszelSystemDetails {
|
||||
id: string;
|
||||
system: string;
|
||||
hostname: string;
|
||||
kernel: string;
|
||||
cores: number;
|
||||
threads: number;
|
||||
cpu: string;
|
||||
os: number;
|
||||
os_name: string;
|
||||
arch: string;
|
||||
memory: number;
|
||||
podman: boolean;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Historical system stats record from Beszel PocketBase.
|
||||
* Stored per-interval (1m, 10m, 20m, 120m, 480m depending on time range).
|
||||
*
|
||||
* Units:
|
||||
* - CPU/GPU/memory/disk percentages: 0–100
|
||||
* - Memory/disk absolute values (mu, du, etc.): bytes
|
||||
* - Disk I/O (dr, dw): bytes/s
|
||||
* - Network (ns, nr): bytes/s — all interfaces (including loopback, docker bridges)
|
||||
* - Bandwidth (b): bytes/s [sent, recv] — public interfaces only. Prefer b over ns/nr for user-facing charts
|
||||
*/
|
||||
export interface BeszelSystemStats {
|
||||
/** CPU usage (%) */
|
||||
cpu: number;
|
||||
/** CPU usage max (%) */
|
||||
cpum?: number;
|
||||
/** per-core busy (%) */
|
||||
cpub?: number[];
|
||||
/** per-core speed (MHz) */
|
||||
cpus?: number[];
|
||||
/** load average [1m, 5m, 15m] */
|
||||
la?: [number, number, number];
|
||||
/** total memory (bytes) */
|
||||
m: number;
|
||||
/** memory used (bytes) */
|
||||
mu: number;
|
||||
/** memory usage (%) */
|
||||
mp: number;
|
||||
/** memory buffer/cache (bytes) */
|
||||
mb: number;
|
||||
/** memory max used (bytes) */
|
||||
mm?: number;
|
||||
/** memory zfs arc (bytes) */
|
||||
mz?: number;
|
||||
/** total swap (bytes) */
|
||||
s: number;
|
||||
/** swap used (bytes) */
|
||||
su: number;
|
||||
/** total disk (bytes) */
|
||||
d: number;
|
||||
/** disk used (bytes) */
|
||||
du: number;
|
||||
/** disk usage (%) */
|
||||
dp: number;
|
||||
/** disk read (bytes/s) */
|
||||
dr: number;
|
||||
/** disk write (bytes/s) */
|
||||
dw: number;
|
||||
/** disk read max (bytes/s) */
|
||||
drm?: number;
|
||||
/** disk write max (bytes/s) */
|
||||
dwm?: number;
|
||||
/** disk IOPS [read, write] */
|
||||
dio?: [number, number];
|
||||
/** disk IOPS max [read, write] */
|
||||
diom?: [number, number];
|
||||
/** network sent — all interfaces (bytes/s) */
|
||||
ns: number;
|
||||
/** network received — all interfaces (bytes/s) */
|
||||
nr: number;
|
||||
/** bandwidth — public interfaces only (bytes/s) [sent, recv]. Prefer over ns/nr */
|
||||
b?: [number, number];
|
||||
/** network sent max (bytes/s) */
|
||||
nsm?: number;
|
||||
/** network received max (bytes/s) */
|
||||
nrm?: number;
|
||||
/** bandwidth max [sent, recv] (bytes/s) */
|
||||
bm?: [number, number];
|
||||
/** temperatures keyed by sensor name (°C) */
|
||||
t?: Record<string, number>;
|
||||
/** extra filesystem stats keyed by mount path */
|
||||
efs?: Record<string, BeszelExtraFsStats>;
|
||||
/** GPU stats keyed by device id */
|
||||
g?: Record<string, BeszelGPUData>;
|
||||
/** battery [level%, charging state] */
|
||||
bat?: [number, number];
|
||||
/** network interfaces [rx bytes/s, tx bytes/s, rx max, tx max] */
|
||||
ni?: Record<string, [number, number, number, number]>;
|
||||
}
|
||||
|
||||
export interface BeszelExtraFsStats {
|
||||
/** total disk (bytes) */
|
||||
d: number;
|
||||
/** disk used (bytes) */
|
||||
du: number;
|
||||
/** read (bytes/s) */
|
||||
r: number;
|
||||
/** write (bytes/s) */
|
||||
w: number;
|
||||
/** read max (bytes/s) */
|
||||
rm: number;
|
||||
/** write max (bytes/s) */
|
||||
wm: number;
|
||||
/** read (IOPS) */
|
||||
rb: number;
|
||||
/** write (IOPS) */
|
||||
wb: number;
|
||||
/** read max (IOPS) */
|
||||
rbm: number;
|
||||
/** write max (IOPS) */
|
||||
wbm: number;
|
||||
}
|
||||
|
||||
export interface BeszelGPUData {
|
||||
/** GPU name/model */
|
||||
n: string;
|
||||
/** memory used (bytes) */
|
||||
mu?: number;
|
||||
/** memory total (bytes) */
|
||||
mt?: number;
|
||||
/** GPU utilization (%) */
|
||||
u: number;
|
||||
/** power draw (W) */
|
||||
p?: number;
|
||||
/** power peak (W) */
|
||||
pp?: number;
|
||||
/** encoder utilization keyed by engine name (%) */
|
||||
e?: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface BeszelSystemStatsRecord {
|
||||
id: string;
|
||||
system: string;
|
||||
stats: BeszelSystemStats;
|
||||
type: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface BeszelContainer {
|
||||
id: string;
|
||||
system: string;
|
||||
name: string;
|
||||
image: string;
|
||||
status: string;
|
||||
health: number;
|
||||
cpu: number;
|
||||
memory: number;
|
||||
net: number;
|
||||
ports?: string;
|
||||
updated: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-container resource stats from Beszel agent.
|
||||
* One entry per container in a BeszelContainerStatsRecord.
|
||||
*
|
||||
* Network fields: b (bandwidth) is preferred when available; fall back to ns/nr.
|
||||
*/
|
||||
export interface BeszelContainerStats {
|
||||
/** container name */
|
||||
n: string;
|
||||
/** CPU usage (%) */
|
||||
c: number;
|
||||
/** memory usage (bytes) */
|
||||
m: number;
|
||||
/** network sent (bytes/s) — legacy, use b when available */
|
||||
ns?: number;
|
||||
/** network received (bytes/s) — legacy, use b when available */
|
||||
nr?: number;
|
||||
/** network bandwidth [sent, recv] (bytes/s) — preferred over ns/nr */
|
||||
b?: [number, number];
|
||||
}
|
||||
|
||||
export interface BeszelContainerStatsRecord {
|
||||
id: string;
|
||||
system: string;
|
||||
stats: BeszelContainerStats[];
|
||||
type: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface BeszelSmartDevice {
|
||||
id: string;
|
||||
system: string;
|
||||
name: string;
|
||||
model: string;
|
||||
state: string;
|
||||
capacity: number;
|
||||
temp: number;
|
||||
firmware: string;
|
||||
serial: string;
|
||||
type: string;
|
||||
hours: number;
|
||||
cycles: number;
|
||||
attributes: BeszelSmartAttribute[];
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface BeszelSmartAttribute {
|
||||
/** attribute ID */
|
||||
id?: number;
|
||||
/** attribute name */
|
||||
n: string;
|
||||
/** current value */
|
||||
v: number;
|
||||
/** worst value */
|
||||
w?: number;
|
||||
/** threshold */
|
||||
t?: number;
|
||||
/** raw value (numeric) */
|
||||
rv?: number;
|
||||
/** raw value (string) */
|
||||
rs?: string;
|
||||
/** when failed */
|
||||
wf?: string;
|
||||
}
|
||||
|
||||
export interface BeszelSystemdService {
|
||||
id: string;
|
||||
system: string;
|
||||
name: string;
|
||||
state: number;
|
||||
sub: number;
|
||||
cpu: number;
|
||||
cpuPeak: number;
|
||||
memory: number;
|
||||
memPeak: number;
|
||||
updated: number;
|
||||
}
|
||||
|
||||
export interface BeszelAlert {
|
||||
id: string;
|
||||
user: string;
|
||||
system: string;
|
||||
name: string;
|
||||
triggered: boolean;
|
||||
value: number;
|
||||
min: number;
|
||||
}
|
||||
|
||||
export interface BeszelAlertHistory {
|
||||
id: string;
|
||||
alert: string;
|
||||
user: string;
|
||||
system: string;
|
||||
name: string;
|
||||
val: number;
|
||||
created: string;
|
||||
resolved?: string | null;
|
||||
}
|
||||
|
||||
export interface CreateAlertInput {
|
||||
name: string;
|
||||
value: number;
|
||||
min?: number;
|
||||
}
|
||||
|
||||
export interface UpdateAlertInput {
|
||||
value?: number;
|
||||
min?: number;
|
||||
}
|
||||
|
||||
export interface PocketBaseListResponse<T> {
|
||||
page: number;
|
||||
perPage: number;
|
||||
totalItems: number;
|
||||
totalPages: number;
|
||||
items: T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized view model for a Beszel-monitored system.
|
||||
* Constructed by mapToSystemRow() in request-handler from BeszelSystem + BeszelSystemDetails.
|
||||
* This is the shape consumed by grid/table widgets — all values are pre-scaled for display.
|
||||
*/
|
||||
export interface BeszelSystemRow {
|
||||
id: string;
|
||||
name: string;
|
||||
status: BeszelSystemStatus;
|
||||
/** CPU usage (%) */
|
||||
cpu: number;
|
||||
/** memory usage (%) */
|
||||
memory: number;
|
||||
/** disk usage (%) */
|
||||
disk: number;
|
||||
/** GPU usage (%) */
|
||||
gpu: number;
|
||||
/** load average [1m, 5m, 15m] */
|
||||
loadAvg: [number, number, number] | null;
|
||||
/** public interface bandwidth (bytes/s). From info.bb or info.b * 1M */
|
||||
netBytes: number;
|
||||
/** temperature (°C) */
|
||||
temp: number | null;
|
||||
/** battery [level%, charging state] */
|
||||
battery: [number, number] | null;
|
||||
/** running systemd services count */
|
||||
services: number;
|
||||
/** uptime (seconds) */
|
||||
uptime: number;
|
||||
agentVersion: string;
|
||||
hostname: string;
|
||||
cpuModel: string;
|
||||
cores: number;
|
||||
/** total memory (bytes) */
|
||||
memoryTotal: number;
|
||||
osName: string;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// @vitest-environment node
|
||||
import { beforeAll, describe, expect, test, vi } from "vitest";
|
||||
|
||||
vi.hoisted(() => {
|
||||
process.env.SKIP_ENV_VALIDATION = "true";
|
||||
process.env.SECRET_ENCRYPTION_KEY = "ff3f4f7ce30e870c9630de9e5d244ffa81101a24ed0dfe5f064beb53a7e684f1";
|
||||
process.env.ENABLE_DNS_CACHING = "false";
|
||||
});
|
||||
|
||||
const BESZEL_URL = process.env.BESZEL_TEST_URL ?? "http://localhost:8090";
|
||||
const BESZEL_EMAIL = process.env.BESZEL_TEST_EMAIL ?? "";
|
||||
const BESZEL_PASSWORD = process.env.BESZEL_TEST_PASSWORD ?? "";
|
||||
|
||||
vi.mock("@homarr/redis", () => ({
|
||||
createGetSetChannel: () => ({
|
||||
getAsync: vi.fn().mockResolvedValue(null),
|
||||
setAsync: vi.fn().mockResolvedValue(undefined),
|
||||
removeAsync: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/core/infrastructure/logs", () => ({
|
||||
createLogger: () => ({
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
ErrorWithMetadata: class extends Error {},
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/core/infrastructure/http", () => ({
|
||||
fetchWithTrustedCertificatesAsync: (url: URL | string, init?: RequestInit) => fetch(url, init),
|
||||
createAxiosCertificateInstanceAsync: vi.fn().mockResolvedValue({}),
|
||||
createCertificateAgentAsync: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/core/infrastructure/certificates", () => ({
|
||||
getTrustedCertificateHostnamesAsync: vi.fn().mockResolvedValue([]),
|
||||
getAllTrustedCertificatesAsync: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
import { BeszelIntegration } from "../beszel-integration";
|
||||
|
||||
function createBeszelIntegration() {
|
||||
return new BeszelIntegration({
|
||||
id: "test-beszel",
|
||||
name: "Test Beszel",
|
||||
url: BESZEL_URL,
|
||||
externalUrl: null,
|
||||
decryptedSecrets: [
|
||||
{ kind: "username", value: BESZEL_EMAIL },
|
||||
{ kind: "password", value: BESZEL_PASSWORD },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const shouldRun = BESZEL_EMAIL && BESZEL_PASSWORD;
|
||||
|
||||
describe.skipIf(!shouldRun)("BeszelIntegration (real instance)", () => {
|
||||
let integration: BeszelIntegration;
|
||||
let systemId: string;
|
||||
|
||||
beforeAll(() => {
|
||||
integration = createBeszelIntegration();
|
||||
});
|
||||
|
||||
describe("getSystemsAsync", () => {
|
||||
test("returns an array of systems", async () => {
|
||||
const systems = await integration.getSystemsAsync();
|
||||
expect(systems).toBeInstanceOf(Array);
|
||||
expect(systems.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("each system has required fields", async () => {
|
||||
const systems = await integration.getSystemsAsync();
|
||||
const system = systems[0];
|
||||
if (!system) {
|
||||
throw new Error("Expected at least one system");
|
||||
}
|
||||
systemId = system.id;
|
||||
|
||||
expect(system.id).toBeDefined();
|
||||
expect(system.name).toBeDefined();
|
||||
expect(system.status).toMatch(/^(up|down|paused|pending)$/);
|
||||
expect(system.info).toBeDefined();
|
||||
expect(typeof system.info.cpu).toBe("number");
|
||||
expect(typeof system.info.mp).toBe("number");
|
||||
expect(typeof system.info.dp).toBe("number");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSystemDetailsAsync", () => {
|
||||
test("returns system details with hardware info", async () => {
|
||||
const systems = await integration.getSystemsAsync();
|
||||
systemId = systems[0]?.id ?? "";
|
||||
|
||||
const details = await integration.getSystemDetailsAsync(systemId);
|
||||
|
||||
expect(details.hostname).toBeDefined();
|
||||
expect(details.cpu).toBeDefined();
|
||||
expect(typeof details.cores).toBe("number");
|
||||
expect(details.cores).toBeGreaterThan(0);
|
||||
expect(typeof details.memory).toBe("number");
|
||||
expect(details.memory).toBeGreaterThan(0);
|
||||
expect(details.os_name).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSystemStatsAsync", () => {
|
||||
test("returns time-series stats records", async () => {
|
||||
const systems = await integration.getSystemsAsync();
|
||||
systemId = systems[0]?.id ?? "";
|
||||
|
||||
const stats = await integration.getSystemStatsAsync(systemId, "1m", 5);
|
||||
|
||||
expect(stats).toBeInstanceOf(Array);
|
||||
if (stats.length === 0) return;
|
||||
|
||||
const record = stats[0];
|
||||
expect(record?.stats).toBeDefined();
|
||||
expect(typeof record?.stats.cpu).toBe("number");
|
||||
expect(typeof record?.stats.mp).toBe("number");
|
||||
expect(typeof record?.stats.dp).toBe("number");
|
||||
expect(record?.created).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getContainersAsync", () => {
|
||||
test("returns container list", async () => {
|
||||
const systems = await integration.getSystemsAsync();
|
||||
systemId = systems[0]?.id ?? "";
|
||||
|
||||
const containers = await integration.getContainersAsync(systemId);
|
||||
expect(containers).toBeInstanceOf(Array);
|
||||
|
||||
if (containers.length > 0) {
|
||||
const container = containers[0];
|
||||
expect(container?.name).toBeDefined();
|
||||
expect(container?.image).toBeDefined();
|
||||
expect(container?.status).toBeDefined();
|
||||
expect(typeof container?.cpu).toBe("number");
|
||||
expect(typeof container?.memory).toBe("number");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("alert CRUD", () => {
|
||||
let alertId: string;
|
||||
|
||||
test("creates an alert", async () => {
|
||||
const systems = await integration.getSystemsAsync();
|
||||
systemId = systems[0]?.id ?? "";
|
||||
|
||||
const alert = await integration.createAlertAsync(systemId, {
|
||||
name: "CPU",
|
||||
value: 90,
|
||||
min: 1,
|
||||
});
|
||||
|
||||
expect(alert.id).toBeDefined();
|
||||
expect(alert.name).toBe("CPU");
|
||||
expect(alert.value).toBe(90);
|
||||
expect(alert.system).toBe(systemId);
|
||||
alertId = alert.id;
|
||||
});
|
||||
|
||||
test("lists alerts including the created one", async () => {
|
||||
const alerts = await integration.getAlertsAsync(systemId);
|
||||
const found = alerts.find((a) => a.id === alertId);
|
||||
expect(found).toBeDefined();
|
||||
expect(found?.name).toBe("CPU");
|
||||
});
|
||||
|
||||
test("updates alert threshold", async () => {
|
||||
const updated = await integration.updateAlertAsync(alertId, { value: 95 });
|
||||
expect(updated.value).toBe(95);
|
||||
});
|
||||
|
||||
test("deletes alert", async () => {
|
||||
await integration.deleteAlertAsync(alertId);
|
||||
const alerts = await integration.getAlertsAsync(systemId);
|
||||
const found = alerts.find((a) => a.id === alertId);
|
||||
expect(found).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("system actions", () => {
|
||||
test("pauses and resumes a system", async () => {
|
||||
const systems = await integration.getSystemsAsync();
|
||||
systemId = systems[0]?.id ?? "";
|
||||
|
||||
await integration.pauseSystemAsync(systemId);
|
||||
const afterPause = await integration.getSystemsAsync();
|
||||
const paused = afterPause.find((s) => s.id === systemId);
|
||||
expect(paused?.status).toBe("paused");
|
||||
|
||||
await integration.resumeSystemAsync(systemId);
|
||||
const afterResume = await integration.getSystemsAsync();
|
||||
const resumed = afterResume.find((s) => s.id === systemId);
|
||||
expect(resumed?.status).toMatch(/^(up|pending)$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("authentication", () => {
|
||||
test("authenticates with valid credentials", async () => {
|
||||
const systems = await integration.getSystemsAsync();
|
||||
expect(systems.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("fails with invalid credentials", async () => {
|
||||
const badIntegration = new BeszelIntegration({
|
||||
id: "test-bad",
|
||||
name: "Bad Beszel",
|
||||
url: BESZEL_URL,
|
||||
externalUrl: null,
|
||||
decryptedSecrets: [
|
||||
{ kind: "username", value: "wrong@example.com" },
|
||||
{ kind: "password", value: "wrongpassword" },
|
||||
],
|
||||
});
|
||||
|
||||
await expect(badIntegration.getSystemsAsync()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,343 @@
|
||||
import type {
|
||||
BeszelAlert,
|
||||
BeszelAlertHistory,
|
||||
BeszelContainerStats,
|
||||
BeszelContainerStatsRecord,
|
||||
BeszelSystem,
|
||||
BeszelSystemDetails,
|
||||
BeszelSystemStats,
|
||||
BeszelSystemStatsRecord,
|
||||
} from "../../beszel/beszel-types";
|
||||
|
||||
const GB = 1024 * 1024 * 1024;
|
||||
const MB = 1024 * 1024;
|
||||
const KB = 1024;
|
||||
|
||||
interface MockSystemDef {
|
||||
name: string;
|
||||
host: string;
|
||||
os: string;
|
||||
cpu: string;
|
||||
cores: number;
|
||||
mem: number;
|
||||
arch: string;
|
||||
baseCpu: number;
|
||||
baseMem: number;
|
||||
baseDisk: number;
|
||||
containers: number[];
|
||||
}
|
||||
|
||||
const mockSystems: MockSystemDef[] = [
|
||||
{
|
||||
name: "web-server",
|
||||
host: "192.168.1.10",
|
||||
os: "Ubuntu 24.04",
|
||||
cpu: "AMD Ryzen 9 7950X",
|
||||
cores: 16,
|
||||
mem: 64 * GB,
|
||||
arch: "amd64",
|
||||
baseCpu: 35,
|
||||
baseMem: 55,
|
||||
baseDisk: 42,
|
||||
containers: [0, 1, 2, 3, 5],
|
||||
},
|
||||
{
|
||||
name: "nas-01",
|
||||
host: "192.168.1.20",
|
||||
os: "Debian 12",
|
||||
cpu: "Intel Xeon E-2388G",
|
||||
cores: 8,
|
||||
mem: 32 * GB,
|
||||
arch: "amd64",
|
||||
baseCpu: 12,
|
||||
baseMem: 70,
|
||||
baseDisk: 68,
|
||||
containers: [1, 2, 6],
|
||||
},
|
||||
{
|
||||
name: "pi-cluster-1",
|
||||
host: "192.168.1.30",
|
||||
os: "Raspberry Pi OS",
|
||||
cpu: "ARM Cortex-A76",
|
||||
cores: 4,
|
||||
mem: 8 * GB,
|
||||
arch: "aarch64",
|
||||
baseCpu: 45,
|
||||
baseMem: 82,
|
||||
baseDisk: 35,
|
||||
containers: [0, 2, 7],
|
||||
},
|
||||
{
|
||||
name: "media-server",
|
||||
host: "192.168.1.40",
|
||||
os: "Proxmox VE 8.2",
|
||||
cpu: "Intel i7-13700K",
|
||||
cores: 16,
|
||||
mem: 128 * GB,
|
||||
arch: "amd64",
|
||||
baseCpu: 22,
|
||||
baseMem: 48,
|
||||
baseDisk: 55,
|
||||
containers: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||
},
|
||||
{
|
||||
name: "backup-node",
|
||||
host: "192.168.1.50",
|
||||
os: "TrueNAS SCALE",
|
||||
cpu: "Intel Xeon E5-2680",
|
||||
cores: 12,
|
||||
mem: 48 * GB,
|
||||
arch: "amd64",
|
||||
baseCpu: 8,
|
||||
baseMem: 30,
|
||||
baseDisk: 78,
|
||||
containers: [1, 6],
|
||||
},
|
||||
];
|
||||
|
||||
const containerProfiles: { name: string; cpuBase: number; memMB: number; netKB: number }[] = [
|
||||
{ name: "nginx", cpuBase: 2, memMB: 120, netKB: 800 },
|
||||
{ name: "postgres", cpuBase: 8, memMB: 450, netKB: 200 },
|
||||
{ name: "redis", cpuBase: 3, memMB: 180, netKB: 500 },
|
||||
{ name: "grafana", cpuBase: 5, memMB: 280, netKB: 150 },
|
||||
{ name: "prometheus", cpuBase: 6, memMB: 350, netKB: 300 },
|
||||
{ name: "traefik", cpuBase: 4, memMB: 90, netKB: 1200 },
|
||||
{ name: "minio", cpuBase: 3, memMB: 200, netKB: 600 },
|
||||
{ name: "gitea", cpuBase: 4, memMB: 250, netKB: 100 },
|
||||
];
|
||||
|
||||
const rand = (min: number, max: number) => min + Math.random() * (max - min);
|
||||
const randInt = (min: number, max: number) => Math.floor(rand(min, max));
|
||||
const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));
|
||||
|
||||
function smoothWalk(base: number, amplitude: number, steps: number, lo: number, hi: number): number[] {
|
||||
const values: number[] = [];
|
||||
let current = base + rand(-amplitude * 0.3, amplitude * 0.3);
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const spike = Math.random() < 0.08 ? rand(-amplitude * 2, amplitude * 3) : 0;
|
||||
current += rand(-amplitude, amplitude) + spike;
|
||||
current = clamp(current, lo, hi);
|
||||
values.push(current);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function resolveSystem(systemId: string): MockSystemDef {
|
||||
const idx = Number(systemId.replace("mock-sys-", "")) - 1;
|
||||
return mockSystems[idx] ?? mockSystems[0] ?? (mockSystems.find(Boolean) as MockSystemDef);
|
||||
}
|
||||
|
||||
const typeToMinutes: Record<string, number> = {
|
||||
"1m": 1,
|
||||
"10m": 10,
|
||||
"20m": 20,
|
||||
"120m": 120,
|
||||
"480m": 480,
|
||||
};
|
||||
|
||||
function generateSystemStatsSeries(count: number, sys: MockSystemDef): BeszelSystemStats[] {
|
||||
const cpuWalk = smoothWalk(sys.baseCpu, 3, count, 1, 98);
|
||||
const memPctWalk = smoothWalk(sys.baseMem, 1.5, count, 10, 95);
|
||||
const diskPctWalk = smoothWalk(sys.baseDisk, 0.2, count, 5, 95);
|
||||
const diskReadWalk = smoothWalk(5, 3, count, 0, 80);
|
||||
const diskWriteWalk = smoothWalk(3, 2, count, 0, 50);
|
||||
const netSendWalk = smoothWalk(500, 200, count, 10, 8000);
|
||||
const netRecvWalk = smoothWalk(800, 300, count, 10, 12000);
|
||||
|
||||
const memTotal = sys.mem;
|
||||
const diskTotal = rand(200, 2000) * GB;
|
||||
|
||||
return cpuWalk.map((cpu, i) => {
|
||||
const memPct = memPctWalk[i] ?? sys.baseMem;
|
||||
const diskPct = diskPctWalk[i] ?? sys.baseDisk;
|
||||
return {
|
||||
cpu,
|
||||
la: [cpu / 20 + rand(0, 0.5), cpu / 25 + rand(0, 0.3), cpu / 30 + rand(0, 0.2)] as [number, number, number],
|
||||
m: memTotal,
|
||||
mu: (memTotal * memPct) / 100,
|
||||
mp: memPct,
|
||||
mb: rand(0.5, 4) * GB,
|
||||
s: 4 * GB,
|
||||
su: rand(0, 2) * GB,
|
||||
d: diskTotal,
|
||||
du: (diskTotal * diskPct) / 100,
|
||||
dp: diskPct,
|
||||
dr: (diskReadWalk[i] ?? 5) * MB,
|
||||
dw: (diskWriteWalk[i] ?? 3) * MB,
|
||||
ns: (netSendWalk[i] ?? 500) * KB,
|
||||
nr: (netRecvWalk[i] ?? 800) * KB,
|
||||
b: [(netSendWalk[i] ?? 500) * KB * 0.7, (netRecvWalk[i] ?? 800) * KB * 0.7] as [number, number],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function generateContainerStatsSeries(count: number, profiles: typeof containerProfiles): BeszelContainerStats[][] {
|
||||
const walks = profiles.map((p) => ({
|
||||
profile: p,
|
||||
cpuWalk: smoothWalk(p.cpuBase, 1.5, count, 0, 50),
|
||||
memWalk: smoothWalk(p.memMB, p.memMB * 0.1, count, p.memMB * 0.5, p.memMB * 2),
|
||||
netWalk: smoothWalk(p.netKB, p.netKB * 0.3, count, 0, p.netKB * 4),
|
||||
}));
|
||||
|
||||
return Array.from({ length: count }, (_, i) =>
|
||||
walks.map(({ profile, cpuWalk, memWalk, netWalk }) => ({
|
||||
n: profile.name,
|
||||
c: cpuWalk[i] ?? profile.cpuBase,
|
||||
m: (memWalk[i] ?? profile.memMB) * MB,
|
||||
b: [(netWalk[i] ?? profile.netKB) * KB * 0.4, (netWalk[i] ?? profile.netKB) * KB * 0.6] as [number, number],
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
function buildTimeSeries<T>(
|
||||
systemId: string,
|
||||
count: number,
|
||||
intervalMinutes: number,
|
||||
type: string,
|
||||
dataPoints: T[],
|
||||
): (T & { id: string; system: string; type: string; created: string; updated: string })[] {
|
||||
const now = Date.now();
|
||||
const items = dataPoints.slice(0, count).map((data, i) => {
|
||||
const ts = new Date(now - (count - i) * intervalMinutes * 60_000).toISOString();
|
||||
return { id: `mock-stat-${i}`, system: systemId, type, created: ts, updated: ts, ...data };
|
||||
});
|
||||
return items.toReversed();
|
||||
}
|
||||
|
||||
const statusByIndex: Record<number, "up" | "paused" | "down"> = { 2: "paused", 4: "down" };
|
||||
|
||||
export class BeszelMockService {
|
||||
public async getSystemsAsync(): Promise<BeszelSystem[]> {
|
||||
return mockSystems.map((sys, i) => ({
|
||||
id: `mock-sys-${i + 1}`,
|
||||
name: sys.name,
|
||||
host: sys.host,
|
||||
port: "45876",
|
||||
status: statusByIndex[i] ?? "up",
|
||||
info: {
|
||||
h: sys.name,
|
||||
cpu: clamp(sys.baseCpu + rand(-5, 10), 0, 100),
|
||||
c: sys.cores,
|
||||
ct: sys.cores * 2,
|
||||
m: sys.cpu,
|
||||
la: [sys.baseCpu / 20, sys.baseCpu / 25, sys.baseCpu / 30] as [number, number, number],
|
||||
o: sys.os,
|
||||
u: randInt(86400, 2_000_000),
|
||||
mp: clamp(sys.baseMem + rand(-5, 5), 0, 100),
|
||||
dp: clamp(sys.baseDisk + rand(-2, 2), 0, 100),
|
||||
bb: rand(200, 3000) * KB,
|
||||
v: "0.8.2",
|
||||
g: i === 3 ? rand(15, 45) : 0,
|
||||
dt: rand(32, 58),
|
||||
sv: [randInt(8, 25), randInt(25, 40)] as [number, number],
|
||||
},
|
||||
users: [],
|
||||
created: new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
}));
|
||||
}
|
||||
|
||||
public async getSystemDetailsAsync(systemId: string): Promise<BeszelSystemDetails> {
|
||||
const sys = resolveSystem(systemId);
|
||||
return {
|
||||
id: systemId,
|
||||
system: systemId,
|
||||
hostname: sys.name,
|
||||
kernel: "6.8.0-40-generic",
|
||||
cores: sys.cores,
|
||||
threads: sys.cores * 2,
|
||||
cpu: sys.cpu,
|
||||
os: 1,
|
||||
os_name: sys.os,
|
||||
arch: sys.arch,
|
||||
memory: sys.mem,
|
||||
podman: false,
|
||||
updated: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
public async getSystemStatsAsync(
|
||||
systemId: string,
|
||||
type: string,
|
||||
perPage: number,
|
||||
): Promise<BeszelSystemStatsRecord[]> {
|
||||
const sys = resolveSystem(systemId);
|
||||
const intervalMinutes = typeToMinutes[type] ?? 1;
|
||||
const statsSeries = generateSystemStatsSeries(perPage, sys);
|
||||
return buildTimeSeries(
|
||||
systemId,
|
||||
perPage,
|
||||
intervalMinutes,
|
||||
type,
|
||||
statsSeries.map((stats) => ({ stats })),
|
||||
);
|
||||
}
|
||||
|
||||
public async getContainerStatsAsync(
|
||||
systemId: string,
|
||||
type: string,
|
||||
perPage: number,
|
||||
): Promise<BeszelContainerStatsRecord[]> {
|
||||
const sys = resolveSystem(systemId);
|
||||
const intervalMinutes = typeToMinutes[type] ?? 1;
|
||||
const selectedProfiles = sys.containers
|
||||
.map((i) => containerProfiles[i])
|
||||
.filter(Boolean) as typeof containerProfiles;
|
||||
const series = generateContainerStatsSeries(perPage, selectedProfiles);
|
||||
return buildTimeSeries(
|
||||
systemId,
|
||||
perPage,
|
||||
intervalMinutes,
|
||||
type,
|
||||
series.map((stats) => ({ stats })),
|
||||
);
|
||||
}
|
||||
|
||||
public async getAlertsAsync(): Promise<BeszelAlert[]> {
|
||||
return [
|
||||
{ id: "mock-alert-1", user: "mock-user", system: "mock-sys-1", name: "CPU", triggered: true, value: 90, min: 5 },
|
||||
{
|
||||
id: "mock-alert-2",
|
||||
user: "mock-user",
|
||||
system: "mock-sys-2",
|
||||
name: "Memory",
|
||||
triggered: false,
|
||||
value: 80,
|
||||
min: 10,
|
||||
},
|
||||
{
|
||||
id: "mock-alert-3",
|
||||
user: "mock-user",
|
||||
system: "mock-sys-1",
|
||||
name: "Disk",
|
||||
triggered: false,
|
||||
value: 85,
|
||||
min: 15,
|
||||
},
|
||||
{
|
||||
id: "mock-alert-4",
|
||||
user: "mock-user",
|
||||
system: "mock-sys-4",
|
||||
name: "Temperature",
|
||||
triggered: true,
|
||||
value: 75,
|
||||
min: 3,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public async getAlertHistoryAsync(_systemId?: string, perPage = 10): Promise<BeszelAlertHistory[]> {
|
||||
const now = Date.now();
|
||||
const names = ["CPU", "Memory", "Disk", "Temperature", "Bandwidth"];
|
||||
return Array.from({ length: Math.min(perPage, 5) }, (_, i) => ({
|
||||
id: `mock-history-${i}`,
|
||||
alert: `mock-alert-${(i % 4) + 1}`,
|
||||
user: "mock-user",
|
||||
system: `mock-sys-${(i % 5) + 1}`,
|
||||
name: names[i % names.length] ?? "CPU",
|
||||
val: rand(60, 95),
|
||||
created: new Date(now - (i + 1) * 3_600_000).toISOString(),
|
||||
resolved: i % 2 === 0 ? new Date(now - i * 1_800_000).toISOString() : null,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import { NetworkControllerSummaryMockService } from "./data/network-controller-s
|
||||
import { NotificationsMockService } from "./data/notifications";
|
||||
import { SmartHomeMockService } from "./data/smart-home";
|
||||
import { SystemHealthMonitoringMockService } from "./data/system-health-monitoring";
|
||||
import { BeszelMockService } from "./data/beszel";
|
||||
import { UpsSummaryMockService } from "./data/ups-summary";
|
||||
|
||||
export class MockIntegration
|
||||
@@ -61,6 +62,7 @@ export class MockIntegration
|
||||
private static readonly networkController = new NetworkControllerSummaryMockService();
|
||||
private static readonly notifications = new NotificationsMockService();
|
||||
private static readonly smartHome = new SmartHomeMockService();
|
||||
private static readonly beszel = new BeszelMockService();
|
||||
private static readonly upsSummary = new UpsSummaryMockService();
|
||||
|
||||
protected async testingAsync(_: IntegrationTestingInput): Promise<TestingResult> {
|
||||
@@ -130,4 +132,12 @@ export class MockIntegration
|
||||
|
||||
// UpsSummaryIntegration
|
||||
getUpsSummariesAsync = MockIntegration.upsSummary.getUpsSummariesAsync.bind(MockIntegration.upsSummary);
|
||||
|
||||
// BeszelIntegration
|
||||
getSystemsAsync = MockIntegration.beszel.getSystemsAsync.bind(MockIntegration.beszel);
|
||||
getSystemDetailsAsync = MockIntegration.beszel.getSystemDetailsAsync.bind(MockIntegration.beszel);
|
||||
getSystemStatsAsync = MockIntegration.beszel.getSystemStatsAsync.bind(MockIntegration.beszel);
|
||||
getContainerStatsAsync = MockIntegration.beszel.getContainerStatsAsync.bind(MockIntegration.beszel);
|
||||
getAlertsAsync = MockIntegration.beszel.getAlertsAsync.bind(MockIntegration.beszel);
|
||||
getAlertHistoryAsync = MockIntegration.beszel.getAlertHistoryAsync.bind(MockIntegration.beszel);
|
||||
}
|
||||
@@ -18,3 +18,4 @@ export * from "./interfaces/timetable/timetable-types";
|
||||
export * from "./tracearr/tracearr-types";
|
||||
export * from "./speedtest-tracker/speedtest-tracker-types";
|
||||
export * from "./umami/umami-types";
|
||||
export * from "./beszel/beszel-types";
|
||||
Reference in new issue
Block a user