mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: load plugin frontends at runtime and remove plugin ids from the shell
This commit is contained in:
1 parent
49f0286a14
commit
e88d5a52e3
721 files changed
+54265
-44416
No files matched your search
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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 }> {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user