feat: load plugin frontends at runtime and remove plugin ids from the shell

This commit is contained in:
LukeGus committed 2026-09-22 20:15:52 -05:00
1 parent 49f0286a14
commit e88d5a52e3
721 files changed
+54265 -44416

No files matched your search

-308
View File
@@ -1,308 +0,0 @@
import { authApi, handleApiError } from "@/main-axios";
export type AiProviderType =
"ollama" | "anthropic" | "openai" | "gemini" | "openai_compatible";
export interface AiProvider {
id: number;
providerType: AiProviderType;
label: string;
baseUrl: string | null;
/** The first few characters only; the key itself never leaves the server. */
apiKeyPrefix: string | null;
defaultModel: string | null;
enabled: boolean;
createdAt: string;
}
export interface AiConversation {
id: number;
title: string | null;
providerId: number | null;
model: string | null;
createdAt: string;
updatedAt: string;
}
export interface AiMessage {
id: number;
conversationId: number;
role: "user" | "assistant" | "tool";
content: string;
toolCalls: string | null;
createdAt: string;
}
export interface AiProposal {
id: number;
conversationId: number;
kind: string;
summary: string | null;
payload: string;
status: "pending" | "applied" | "rejected" | "expired";
resultSummary: string | null;
createdAt: string;
}
export interface AiStatus {
globallyEnabled: boolean;
enabled: boolean;
allowReadOnlyCommands: boolean;
}
export async function getAiStatus(): Promise<AiStatus> {
try {
return (await authApi.get("/plugin-api/ai/status")).data;
} catch (error) {
throw handleApiError(error, "get AI status");
}
}
export async function getAiProviders(): Promise<AiProvider[]> {
try {
return (await authApi.get("/plugin-api/ai/providers")).data.providers;
} catch (error) {
throw handleApiError(error, "list AI providers");
}
}
export async function createAiProvider(input: {
providerType: AiProviderType;
label: string;
baseUrl?: string | null;
apiKey?: string | null;
defaultModel?: string | null;
}): Promise<AiProvider> {
try {
return (await authApi.post("/plugin-api/ai/providers", input)).data
.provider;
} catch (error) {
throw handleApiError(error, "create AI provider");
}
}
export async function updateAiProvider(
id: number,
input: Partial<{
label: string;
baseUrl: string | null;
apiKey: string | null;
defaultModel: string | null;
enabled: boolean;
}>,
): Promise<AiProvider> {
try {
return (await authApi.patch(`/plugin-api/ai/providers/${id}`, input)).data
.provider;
} catch (error) {
throw handleApiError(error, "update AI provider");
}
}
export async function deleteAiProvider(id: number): Promise<void> {
try {
await authApi.delete(`/plugin-api/ai/providers/${id}`);
} catch (error) {
throw handleApiError(error, "delete AI provider");
}
}
/**
* Model list for a provider that may not be saved yet, so the add form can
* fill its picker before anything is persisted.
*/
export async function probeAiModels(input: {
providerType: AiProviderType;
baseUrl?: string | null;
apiKey?: string | null;
providerId?: number | null;
}): Promise<{
models: string[];
source: "live" | "fallback" | "none";
warning?: string;
}> {
try {
return (await authApi.post("/plugin-api/ai/probe-models", input)).data;
} catch (error) {
throw handleApiError(error, "detect models");
}
}
export async function getAiProviderModels(id: number): Promise<string[]> {
try {
return (await authApi.get(`/plugin-api/ai/providers/${id}/models`)).data
.models;
} catch (error) {
throw handleApiError(error, "list provider models");
}
}
export async function getAiConversations(): Promise<AiConversation[]> {
try {
return (await authApi.get("/plugin-api/ai/conversations")).data
.conversations;
} catch (error) {
throw handleApiError(error, "list AI conversations");
}
}
export async function getAiConversation(id: number): Promise<{
conversation: AiConversation;
messages: AiMessage[];
proposals: AiProposal[];
}> {
try {
return (await authApi.get(`/plugin-api/ai/conversations/${id}`)).data;
} catch (error) {
throw handleApiError(error, "load AI conversation");
}
}
export async function deleteAiConversation(id: number): Promise<void> {
try {
await authApi.delete(`/plugin-api/ai/conversations/${id}`);
} catch (error) {
throw handleApiError(error, "delete AI conversation");
}
}
export async function applyAiProposal(
id: number,
): Promise<{ success: boolean; summary: string }> {
try {
return (await authApi.post(`/plugin-api/ai/proposals/${id}/apply`)).data;
} catch (error) {
throw handleApiError(error, "apply AI proposal");
}
}
export async function rejectAiProposal(id: number): Promise<void> {
try {
await authApi.post(`/plugin-api/ai/proposals/${id}/reject`);
} catch (error) {
throw handleApiError(error, "reject AI proposal");
}
}
/**
* For the terminal-docked assistant: records that a run_command proposal was
* typed into the user's open terminal instead of run over a pooled connection.
*/
export async function markAiProposalRunInTerminal(
id: number,
hostId: number,
summary?: string,
): Promise<{ success: boolean; summary: string }> {
try {
return (
await authApi.post(
`/plugin-api/ai/proposals/${id}/mark-run-in-terminal`,
{
hostId,
summary,
},
)
).data;
} catch (error) {
throw handleApiError(error, "mark AI proposal run in terminal");
}
}
// --- admin ---
export async function getAiGloballyEnabled(): Promise<boolean> {
try {
return (await authApi.get("/plugin-api/ai/enabled")).data.enabled;
} catch (error) {
throw handleApiError(error, "get AI enabled setting");
}
}
export async function setAiGloballyEnabled(enabled: boolean): Promise<boolean> {
try {
return (await authApi.patch("/plugin-api/ai/enabled", { enabled })).data
.enabled;
} catch (error) {
throw handleApiError(error, "update AI enabled setting");
}
}
export async function getAiPrivateEndpoints(): Promise<string[]> {
try {
return (await authApi.get("/plugin-api/ai/private-endpoints")).data.hosts;
} catch (error) {
throw handleApiError(error, "get AI endpoint allowlist");
}
}
export async function setAiPrivateEndpoints(
hosts: string[],
): Promise<string[]> {
try {
return (await authApi.patch("/plugin-api/ai/private-endpoints", { hosts }))
.data.hosts;
} catch (error) {
throw handleApiError(error, "update AI endpoint allowlist");
}
}
export async function getNotificationPrivateEndpoints(): Promise<string[]> {
try {
return (await authApi.get("/users/notification-private-endpoints")).data
.hosts;
} catch (error) {
throw handleApiError(error, "get notification endpoint allowlist");
}
}
export async function getStepCaPrivateEndpoints(): Promise<string[]> {
try {
return (await authApi.get("/users/step-ca-private-endpoints")).data.hosts;
} catch (error) {
throw handleApiError(error, "get Step CA endpoint allowlist");
}
}
export async function setStepCaPrivateEndpoints(
hosts: string[],
): Promise<string[]> {
try {
return (await authApi.patch("/users/step-ca-private-endpoints", { hosts }))
.data.hosts;
} catch (error) {
throw handleApiError(error, "update Step CA endpoint allowlist");
}
}
export async function getSecretSourcePrivateEndpoints(): Promise<string[]> {
try {
return (await authApi.get("/users/secret-source-private-endpoints")).data
.hosts;
} catch (error) {
throw handleApiError(error, "get secret source endpoint allowlist");
}
}
export async function setSecretSourcePrivateEndpoints(
hosts: string[],
): Promise<string[]> {
try {
return (
await authApi.patch("/users/secret-source-private-endpoints", { hosts })
).data.hosts;
} catch (error) {
throw handleApiError(error, "update secret source endpoint allowlist");
}
}
export async function setNotificationPrivateEndpoints(
hosts: string[],
): Promise<string[]> {
try {
return (
await authApi.patch("/users/notification-private-endpoints", { hosts })
).data.hosts;
} catch (error) {
throw handleApiError(error, "update notification endpoint allowlist");
}
}
+3 -18
View File
@@ -13,15 +13,8 @@ export interface UptimeInfo {
export interface RecentActivityItem {
id: number;
userId: string;
type:
| "terminal"
| "file_manager"
| "server_stats"
| "tunnel"
| "docker"
| "telnet"
| "vnc"
| "rdp";
/** Core records file_manager and tunnel; plugin tabs record their own. */
type: string;
hostId: number;
hostName: string;
timestamp: string;
@@ -50,15 +43,7 @@ export async function getRecentActivity(
}
export async function logActivity(
type:
| "terminal"
| "file_manager"
| "server_stats"
| "tunnel"
| "docker"
| "rdp"
| "vnc"
| "telnet",
type: string,
hostId: number,
hostName: string,
): Promise<{ message: string; id: number | string }> {
-345
View File
@@ -1,345 +0,0 @@
import axios from "axios";
import { dockerApi, handleApiError } from "@/main-axios";
import type {
DockerContainer,
DockerLogOptions,
DockerStats,
DockerValidation,
} from "@/types/index";
type ApiConnectionLog = {
type: "info" | "success" | "warning" | "error";
stage: string;
message: string;
details?: Record<string, unknown>;
};
type ConnectErrorResponse = {
error?: string;
message?: string;
connectionLogs?: ApiConnectionLog[];
requires_totp?: boolean;
requires_warpgate?: boolean;
sessionId?: string;
prompt?: string;
url?: string;
securityKey?: string;
status?: string;
reason?: string;
};
export async function connectDockerSession(
sessionId: string,
hostId: number,
config?: {
userProvidedPassword?: string;
userProvidedSshKey?: string;
userProvidedKeyPassword?: string;
forceKeyboardInteractive?: boolean;
useSocks5?: boolean;
socks5Host?: string;
socks5Port?: number;
socks5Username?: string;
socks5Password?: string;
socks5ProxyChain?: unknown;
},
): Promise<{
success?: boolean;
message?: string;
requires_totp?: boolean;
prompt?: string;
isPassword?: boolean;
status?: string;
reason?: string;
connectionLogs?: ApiConnectionLog[];
requires_warpgate?: boolean;
url?: string;
securityKey?: string;
}> {
try {
const response = await dockerApi.post("/ssh/connect", {
sessionId,
hostId,
...config,
});
return response.data;
} catch (error: unknown) {
if (
axios.isAxiosError<ConnectErrorResponse>(error) &&
error.response?.data?.status === "auth_required"
) {
return error.response.data;
}
if (
axios.isAxiosError<ConnectErrorResponse>(error) &&
error.response?.data?.requires_totp
) {
return error.response.data;
}
if (
axios.isAxiosError<ConnectErrorResponse>(error) &&
error.response?.data?.requires_warpgate
) {
return error.response.data;
}
if (
axios.isAxiosError<ConnectErrorResponse>(error) &&
error.response?.data?.connectionLogs
) {
const data = error.response.data;
const errorWithLogs = new Error(
data.error || data.message || error.message,
);
Object.assign(errorWithLogs, {
connectionLogs: data.connectionLogs,
});
throw errorWithLogs;
}
throw handleApiError(error, "connect to Docker SSH session");
}
}
export async function verifyDockerTOTP(
sessionId: string,
totpCode: string,
): Promise<{ status: string; message: string }> {
try {
const response = await dockerApi.post("/ssh/connect-totp", {
sessionId,
totpCode,
});
return response.data;
} catch (error) {
throw handleApiError(error, "verify Docker TOTP");
}
}
export async function verifyDockerWarpgate(
sessionId: string,
): Promise<{ status: string; message: string }> {
try {
const response = await dockerApi.post("/ssh/connect-warpgate", {
sessionId,
});
return response.data;
} catch (error) {
throw handleApiError(error, "verify Docker Warpgate");
}
}
export async function disconnectDockerSession(
sessionId: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await dockerApi.post("/ssh/disconnect", {
sessionId,
});
return response.data;
} catch (error) {
throw handleApiError(error, "disconnect from Docker SSH session");
}
}
export async function keepaliveDockerSession(
sessionId: string,
): Promise<{ success: boolean }> {
try {
const response = await dockerApi.post("/ssh/keepalive", {
sessionId,
});
return response.data;
} catch (error) {
throw handleApiError(error, "keepalive Docker SSH session");
}
}
export async function getDockerSessionStatus(
sessionId: string,
): Promise<{ success: boolean; connected: boolean }> {
try {
const response = await dockerApi.get("/ssh/status", {
params: { sessionId },
});
return response.data;
} catch (error) {
throw handleApiError(error, "get Docker session status");
}
}
export async function validateDockerAvailability(
sessionId: string,
): Promise<DockerValidation> {
try {
const response = await dockerApi.get(`/validate/${sessionId}`);
return response.data;
} catch (error) {
throw handleApiError(error, "validate Docker availability");
}
}
export async function listDockerContainers(
sessionId: string,
all: boolean = true,
): Promise<DockerContainer[]> {
try {
const response = await dockerApi.get(`/containers/${sessionId}`, {
params: { all },
});
return response.data;
} catch (error) {
throw handleApiError(error, "list Docker containers");
}
}
export async function getDockerContainerDetails(
sessionId: string,
containerId: string,
): Promise<DockerContainer> {
try {
const response = await dockerApi.get(
`/containers/${sessionId}/${containerId}`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "get Docker container details");
}
}
export async function startDockerContainer(
sessionId: string,
containerId: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await dockerApi.post(
`/containers/${sessionId}/${containerId}/start`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "start Docker container");
}
}
export async function stopDockerContainer(
sessionId: string,
containerId: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await dockerApi.post(
`/containers/${sessionId}/${containerId}/stop`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "stop Docker container");
}
}
export async function restartDockerContainer(
sessionId: string,
containerId: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await dockerApi.post(
`/containers/${sessionId}/${containerId}/restart`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "restart Docker container");
}
}
export async function pauseDockerContainer(
sessionId: string,
containerId: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await dockerApi.post(
`/containers/${sessionId}/${containerId}/pause`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "pause Docker container");
}
}
export async function unpauseDockerContainer(
sessionId: string,
containerId: string,
): Promise<{ success: boolean; message: string }> {
try {
const response = await dockerApi.post(
`/containers/${sessionId}/${containerId}/unpause`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "unpause Docker container");
}
}
export async function removeDockerContainer(
sessionId: string,
containerId: string,
force: boolean = false,
): Promise<{ success: boolean; message: string }> {
try {
const response = await dockerApi.delete(
`/containers/${sessionId}/${containerId}/remove`,
{
params: { force },
},
);
return response.data;
} catch (error) {
throw handleApiError(error, "remove Docker container");
}
}
export async function getContainerLogs(
sessionId: string,
containerId: string,
options?: DockerLogOptions,
): Promise<{ logs: string }> {
try {
const response = await dockerApi.get(
`/containers/${sessionId}/${containerId}/logs`,
{
params: options,
},
);
return response.data;
} catch (error) {
throw handleApiError(error, "get container logs");
}
}
export async function downloadContainerLogs(
sessionId: string,
containerId: string,
options?: DockerLogOptions,
): Promise<Blob> {
try {
const response = await dockerApi.get(
`/containers/${sessionId}/${containerId}/logs`,
{
params: { ...options, download: true },
responseType: "blob",
},
);
return response.data;
} catch (error) {
throw handleApiError(error, "download container logs");
}
}
export async function getContainerStats(
sessionId: string,
containerId: string,
): Promise<DockerStats> {
try {
const response = await dockerApi.get(
`/containers/${sessionId}/${containerId}/stats`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "get container stats");
}
}
-300
View File
@@ -1,300 +0,0 @@
import {
authApi,
handleApiError,
type SharePermissionLevel,
type ShareTarget,
} from "@/main-axios";
export interface FleetRow {
id: number;
userId: string;
name: string;
description: string | null;
color: string | null;
icon: string | null;
tagRules: string[];
syncId: string | null;
createdAt: string;
updatedAt: string;
memberCount: number;
}
export interface FleetMemberRow {
id: number;
name: string;
ip: string;
tags: string[];
static: boolean;
permissionLevel: "connect" | "view" | "edit" | "manage" | null;
}
export interface FleetHostResult {
hostId: number;
hostName: string;
success: boolean;
output?: string;
error?: string;
}
export interface FleetInventoryRecord {
id: number;
hostId: number;
userId: string;
osPrettyName: string | null;
kernel: string | null;
architecture: string | null;
hostname: string | null;
uptimeSeconds: number | null;
ip: string | null;
packageManager: string | null;
collectedAt: string;
}
export interface FleetInventoryEntry {
hostId: number;
hostName: string;
inventory: FleetInventoryRecord | null;
}
export type FleetPackageAction = "install" | "remove" | "upgrade-all";
export async function listFleets(): Promise<FleetRow[]> {
try {
const response = await authApi.get("/plugin-api/fleets");
return response.data;
} catch (error) {
throw handleApiError(error, "fetch fleets");
}
}
export async function createFleet(fleetData: {
name: string;
description?: string | null;
color?: string | null;
icon?: string | null;
tagRules?: string[];
}): Promise<FleetRow> {
try {
const response = await authApi.post("/plugin-api/fleets", fleetData);
return response.data;
} catch (error) {
throw handleApiError(error, "create fleet");
}
}
export async function updateFleet(
fleetId: number,
fleetData: {
name?: string;
description?: string | null;
color?: string | null;
icon?: string | null;
tagRules?: string[];
},
): Promise<FleetRow> {
try {
const response = await authApi.patch(
`/plugin-api/fleets/${fleetId}`,
fleetData,
);
return response.data;
} catch (error) {
throw handleApiError(error, "update fleet");
}
}
export async function deleteFleet(
fleetId: number,
): Promise<{ success: boolean }> {
try {
const response = await authApi.delete(`/plugin-api/fleets/${fleetId}`);
return response.data;
} catch (error) {
throw handleApiError(error, "delete fleet");
}
}
export async function getFleetMembers(
fleetId: number,
): Promise<FleetMemberRow[]> {
try {
const response = await authApi.get(`/plugin-api/fleets/${fleetId}/members`);
return response.data;
} catch (error) {
throw handleApiError(error, "fetch fleet members");
}
}
export async function addFleetMember(
fleetId: number,
hostId: number,
): Promise<{ success: boolean }> {
try {
const response = await authApi.post(
`/plugin-api/fleets/${fleetId}/members`,
{
hostId,
},
);
return response.data;
} catch (error) {
throw handleApiError(error, "add fleet member");
}
}
export async function removeFleetMember(
fleetId: number,
hostId: number,
): Promise<{ success: boolean }> {
try {
const response = await authApi.delete(
`/plugin-api/fleets/${fleetId}/members/${hostId}`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "remove fleet member");
}
}
export async function runFleetCommand(
fleetId: number,
command: string,
inputValues?: Record<string, string>,
): Promise<{ results: FleetHostResult[] }> {
try {
const response = await authApi.post(
`/plugin-api/fleets/${fleetId}/execute`,
{
command,
...(inputValues ? { inputValues } : {}),
},
);
return response.data;
} catch (error) {
throw handleApiError(error, "run fleet command");
}
}
export async function pushFleetFile(
fleetId: number,
file: File,
remotePath: string,
): Promise<{ results: FleetHostResult[] }> {
try {
const form = new FormData();
form.append("file", file);
form.append("remotePath", remotePath);
const response = await authApi.post(
`/plugin-api/fleets/${fleetId}/transfer/push`,
form,
{ headers: { "Content-Type": "multipart/form-data" } },
);
return response.data;
} catch (error) {
throw handleApiError(error, "push file to fleet");
}
}
export async function pullFleetFile(
fleetId: number,
remotePath: string,
): Promise<{ results: FleetHostResult[]; blob: Blob; fileName: string }> {
try {
const response = await authApi.post(
`/plugin-api/fleets/${fleetId}/transfer/pull`,
{ remotePath },
{ responseType: "blob" },
);
const resultsHeader = response.headers["x-fleet-transfer-results"];
const results: FleetHostResult[] = resultsHeader
? JSON.parse(atob(resultsHeader))
: [];
const disposition = response.headers["content-disposition"] as
string | undefined;
const match = disposition?.match(/filename="([^"]+)"/);
const fileName = match?.[1] ?? "fleet-transfer.zip";
return { results, blob: response.data, fileName };
} catch (error) {
throw handleApiError(error, "pull file from fleet");
}
}
export async function getFleetInventory(
fleetId: number,
): Promise<FleetInventoryEntry[]> {
try {
const response = await authApi.get(
`/plugin-api/fleets/${fleetId}/inventory`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "fetch fleet inventory");
}
}
export async function refreshFleetInventory(
fleetId: number,
): Promise<{ results: FleetHostResult[] }> {
try {
const response = await authApi.post(
`/plugin-api/fleets/${fleetId}/inventory`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "refresh fleet inventory");
}
}
export async function runFleetPackageAction(
fleetId: number,
action: FleetPackageAction,
packageName?: string,
): Promise<{ results: FleetHostResult[] }> {
try {
const response = await authApi.post(
`/plugin-api/fleets/${fleetId}/packages`,
{
action,
...(packageName ? { package: packageName } : {}),
},
);
return response.data;
} catch (error) {
throw handleApiError(error, "run fleet package action");
}
}
export interface FleetShareHostResult {
hostId: number;
shared: boolean;
reason?: string;
}
export async function shareFleet(
fleetId: number,
shareData: {
targets: ShareTarget[];
permissionLevel: SharePermissionLevel;
durationHours?: number;
},
): Promise<{
success: boolean;
permissionLevel: SharePermissionLevel;
expiresAt: string | null;
hostsShared: number;
hostsTotal: number;
hostResults: FleetShareHostResult[];
}> {
try {
const response = await authApi.post(
`/plugin-api/fleets/${fleetId}/share`,
shareData,
);
return response.data;
} catch (error) {
throw handleApiError(error, "share fleet");
}
}
-146
View File
@@ -1,146 +0,0 @@
import { handleApiError, statsApi } from "@/main-axios";
import type { HostMetricsLayout } from "@/types/host-metrics";
// Every function below is keyed by a host's numeric database id, and the
// receiving backend must own that host in its own database -- a synced
// host has a different numeric id on each side (only its syncId matches
// across them). These calls always target the embedded local backend; see
// getAllServerStatuses in host-metrics-status-api.ts for the one metrics
// call that IS safely merged across local + remote (a process-local,
// in-memory aggregate keyed by whichever host ids that process happens to
// know about, not a per-host lookup).
export interface MetricsHistoryRow {
ts: string;
cpu_percent: number | null;
mem_percent: number | null;
disk_percent: number | null;
net_rx_bytes: number | null;
net_tx_bytes: number | null;
}
export interface MetricsHistoryResponse {
rows: MetricsHistoryRow[];
fromTs: string;
toTs: string;
}
export async function getMetricsHistory(
hostId: number,
opts: { range?: string; from?: string; to?: string },
): Promise<MetricsHistoryResponse> {
const res = await statsApi.get(`/metrics/history/${hostId}`, {
params: opts,
});
return res.data as MetricsHistoryResponse;
}
export async function getMetricsHistoryRetention(): Promise<number> {
const res = await statsApi.get("/global-settings/history");
return (res.data as { metricsHistoryRetentionDays: number })
.metricsHistoryRetentionDays;
}
export async function saveMetricsHistoryRetention(days: number): Promise<void> {
await statsApi.post("/global-settings/history", {
metricsHistoryRetentionDays: days,
});
}
/**
* Host Metrics layout persistence (server-synced per user/host) + the manager
* card endpoints. All routes live under the `/host-metrics/*` prefix on the
* stats app (port 30005).
*/
export async function getHostMetricsLayout(
hostId: number,
): Promise<HostMetricsLayout | null> {
try {
const res = await statsApi.get(`/host-metrics/preferences/${hostId}`, {
validateStatus: (status) => status === 200 || status === 404,
});
if (res.status === 404) return null;
return res.data?.layout ?? null;
} catch (error) {
handleApiError(error, "fetch host metrics layout");
throw error;
}
}
export async function saveHostMetricsLayout(
hostId: number,
layout: HostMetricsLayout,
): Promise<void> {
try {
await statsApi.post(`/host-metrics/preferences/${hostId}`, layout);
} catch (error) {
handleApiError(error, "save host metrics layout");
throw error;
}
}
// ─── Managers ───────────────────────────────────────────────────────────────
export interface PlatformInfo {
hasSystemd: boolean;
pkg: "apt" | "dnf" | "yum" | "pacman" | null;
hasCertbot: boolean;
hasAcmeSh: boolean;
hasDocker: boolean;
osPrettyName: string | null;
}
export async function getHostPlatform(hostId: number): Promise<PlatformInfo> {
const res = await statsApi.get(`/host-metrics/platform/${hostId}`);
return res.data;
}
/** GET a manager resource (read). */
export async function managerGet<T>(
hostId: number,
resource: string,
params?: Record<string, string | number>,
): Promise<T> {
const res = await statsApi.get(
`/host-metrics/managers/${resource}/${hostId}`,
{ params },
);
return res.data as T;
}
/**
* GET a manager sub-resource where the host id sits in the middle of the path,
* e.g. /managers/logs/{id}/files.
*/
export async function managerGetSub<T>(
hostId: number,
resource: string,
sub: string,
params?: Record<string, string | number>,
): Promise<T> {
const res = await statsApi.get(
`/host-metrics/managers/${resource}/${hostId}/${sub}`,
{ params },
);
return res.data as T;
}
/**
* POST a manager action. `resource` is the manager name; `action` is the
* optional sub-path (e.g. "action", "signal", "renew"). The host id always
* sits between them: /managers/{resource}/{id}[/{action}].
*/
export async function managerPost<T>(
hostId: number,
resource: string,
body: unknown,
action?: string,
): Promise<T> {
const suffix = action ? `/${action}` : "";
const res = await statsApi.post(
`/host-metrics/managers/${resource}/${hostId}${suffix}`,
body,
);
return res.data as T;
}
+22
View File
@@ -46,9 +46,21 @@ export interface PluginSettingsContribution {
host?: PluginHostSettingsContribution;
}
/** A panel or dashboard card, declared so its owner is known while it is off. */
export interface PluginViewContribution {
id: string;
titleKey: string;
icon?: string;
}
export interface PluginContributions {
tabs?: PluginTabContribution[];
panels?: PluginViewContribution[];
dashboardCards?: PluginViewContribution[];
/** The frontend also runs on anonymous guest pages. */
guest?: boolean;
settings?: PluginSettingsContribution;
permissions?: { name: string; titleKey: string; descriptionKey: string }[];
}
/** A secret as it arrives from the server. The value never leaves the server. */
@@ -173,6 +185,16 @@ export interface PluginSummary {
contributes: PluginContributions | null;
/** Lucide icon name from the manifest. */
icon?: string;
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
/** A built frontend bundle is served at /plugin-assets/<id>/frontend.js. */
frontend?: boolean;
/** A frontend.css sits beside the bundle. */
css?: boolean;
/** Cache key for the bundle; changes when it is rebuilt. */
assetVersion?: string | null;
/** "en" plus the xx_YY names of shipped translations. */
locales?: string[];
tier?: string;
source?: string;
+67
View File
@@ -0,0 +1,67 @@
import { authApi, handleApiError } from "@/main-axios";
/**
* Admin allowlists of private addresses that outbound requests may reach, one
* per core feature that makes them.
*/
export async function getNotificationPrivateEndpoints(): Promise<string[]> {
try {
return (await authApi.get("/users/notification-private-endpoints")).data
.hosts;
} catch (error) {
throw handleApiError(error, "get notification endpoint allowlist");
}
}
export async function getStepCaPrivateEndpoints(): Promise<string[]> {
try {
return (await authApi.get("/users/step-ca-private-endpoints")).data.hosts;
} catch (error) {
throw handleApiError(error, "get Step CA endpoint allowlist");
}
}
export async function setStepCaPrivateEndpoints(
hosts: string[],
): Promise<string[]> {
try {
return (await authApi.patch("/users/step-ca-private-endpoints", { hosts }))
.data.hosts;
} catch (error) {
throw handleApiError(error, "update Step CA endpoint allowlist");
}
}
export async function getSecretSourcePrivateEndpoints(): Promise<string[]> {
try {
return (await authApi.get("/users/secret-source-private-endpoints")).data
.hosts;
} catch (error) {
throw handleApiError(error, "get secret source endpoint allowlist");
}
}
export async function setSecretSourcePrivateEndpoints(
hosts: string[],
): Promise<string[]> {
try {
return (
await authApi.patch("/users/secret-source-private-endpoints", { hosts })
).data.hosts;
} catch (error) {
throw handleApiError(error, "update secret source endpoint allowlist");
}
}
export async function setNotificationPrivateEndpoints(
hosts: string[],
): Promise<string[]> {
try {
return (
await authApi.patch("/users/notification-private-endpoints", { hosts })
).data.hosts;
} catch (error) {
throw handleApiError(error, "update notification endpoint allowlist");
}
}
-93
View File
@@ -1,93 +0,0 @@
import axios from "axios";
import { handleApiError, statsApi } from "@/main-axios";
import type { ProxmoxStatsSnapshot } from "@/types/proxmox";
// Every function below is keyed by a host's numeric database id, and the
// receiving backend must own that host in its own database -- same caveat as
// host-metrics-api.ts / host-metrics-status-api.ts. All routes live under the
// `/proxmox-stats/*` prefix on the stats app (port 30005).
export async function getProxmoxStats(
hostId: number,
): Promise<ProxmoxStatsSnapshot | null> {
try {
const response = await statsApi.get(`/proxmox-stats/${hostId}`, {
// Treat 404 as an expected "no stats yet / disabled" signal rather than
// an error so we don't spam warn logs on the client.
validateStatus: (status) => status === 200 || status === 404,
});
if (response.status === 404) {
return null;
}
return response.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return null;
}
handleApiError(error, "fetch proxmox stats");
throw error;
}
}
export async function startProxmoxStatsPolling(hostId: number): Promise<{
success: boolean;
viewerSessionId?: string;
status?: string;
error?: string;
}> {
try {
const response = await statsApi.post(`/proxmox-stats/start/${hostId}`);
return response.data;
} catch (error) {
handleApiError(error, "start proxmox stats polling");
throw error;
}
}
export async function stopProxmoxStatsPolling(
hostId: number,
viewerSessionId?: string,
): Promise<void> {
try {
await statsApi.post(`/proxmox-stats/stop/${hostId}`, { viewerSessionId });
} catch (error) {
handleApiError(error, "stop proxmox stats polling");
throw error;
}
}
export async function sendProxmoxStatsHeartbeat(
viewerSessionId: string,
): Promise<void> {
try {
await statsApi.post("/proxmox-stats/heartbeat", { viewerSessionId });
} catch (error) {
handleApiError(error, "send proxmox stats heartbeat");
throw error;
}
}
export interface ProxmoxStatsHistoryRow {
ts: string;
cpu_percent: number | null;
mem_percent: number | null;
disk_percent: number | null;
net_rx_bytes: number | null;
net_tx_bytes: number | null;
}
export interface ProxmoxStatsHistoryResponse {
rows: ProxmoxStatsHistoryRow[];
fromTs: string;
toTs: string;
}
export async function getProxmoxStatsHistory(
hostId: number,
opts: { range?: string; from?: string; to?: string },
): Promise<ProxmoxStatsHistoryResponse> {
const res = await statsApi.get(`/proxmox-stats/history/${hostId}`, {
params: opts,
});
return res.data as ProxmoxStatsHistoryResponse;
}