Stop maps to Proxmox's hard power-off (/status/stop) — fine for a
crashed guest, but risky for anything with a filesystem that'd rather
flush cleanly first. Proxmox exposes a separate /status/shutdown
endpoint that asks the guest to power itself down (ACPI event for a
VM, SIGTERM-then-wait for a container), so add it as its own action
rather than overloading Stop.
- New shutdownGuest() on the Proxmox adapter, calling /status/shutdown.
- The existing generic action route/loop already dispatches by
${action}Guest, so adding "shutdown" to that list was enough on the
server side — no new route needed.
- New Shutdown button next to Restart/Stop on both the Integrations
page's guest table and a Proxmox-linked server's detail page.
Stop's confirm prompt now explicitly points at Shutdown as the
gentler alternative.
Verified end-to-end against a mock Proxmox server: the shutdown call
hits /status/shutdown (never /status/stop) and is audit-logged as
shutdown_guest.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
668 lines
21 KiB
TypeScript
668 lines
21 KiB
TypeScript
export type UserRole = "admin" | "operator" | "viewer";
|
|
|
|
export interface CurrentUser {
|
|
id: number;
|
|
sub: string;
|
|
email?: string;
|
|
name?: string;
|
|
role: UserRole;
|
|
}
|
|
|
|
export interface UserRecord {
|
|
id: number;
|
|
oidcSub: string;
|
|
email: string | null;
|
|
name: string | null;
|
|
role: UserRole;
|
|
createdAt: string;
|
|
lastLoginAt: string | null;
|
|
}
|
|
|
|
export interface AuditLogEntry {
|
|
id: number;
|
|
actorUserId: number | null;
|
|
actorLabel: string | null;
|
|
category: string;
|
|
action: string;
|
|
targetType: string | null;
|
|
targetId: string | null;
|
|
detail: string | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
export type SecretType = "api_token" | "ssl_certificate" | "password" | "generic";
|
|
export type SecretStatus = "ok" | "expiring" | "expired";
|
|
|
|
export interface SecretRecord {
|
|
id: number;
|
|
name: string;
|
|
type: SecretType;
|
|
description: string | null;
|
|
expiryDate: string;
|
|
warnDays: number;
|
|
notes: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
status: SecretStatus;
|
|
daysLeft: number;
|
|
}
|
|
|
|
export interface SecretInput {
|
|
name: string;
|
|
type: SecretType;
|
|
description?: string;
|
|
expiryDate: string;
|
|
warnDays: number;
|
|
notes?: string;
|
|
}
|
|
|
|
export interface IpamEntry {
|
|
id: number;
|
|
ipAddress: string;
|
|
label: string | null;
|
|
vendor: string | null;
|
|
location: string | null;
|
|
notes: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
matchingDnsRecords: string[];
|
|
}
|
|
|
|
export interface IpamInput {
|
|
ipAddress: string;
|
|
label?: string;
|
|
vendor?: string;
|
|
location?: string;
|
|
notes?: string;
|
|
}
|
|
|
|
export type DnsProviderType = "cloudflare" | "loopia" | "pihole" | "azure" | "cpanel" | "technitium";
|
|
|
|
export interface GotifySettings {
|
|
enabled: boolean;
|
|
url: string;
|
|
token: string;
|
|
priority: number;
|
|
}
|
|
|
|
export interface NtfySettings {
|
|
enabled: boolean;
|
|
url: string;
|
|
topic: string;
|
|
token: string;
|
|
priority: number;
|
|
}
|
|
|
|
export interface SmtpSettings {
|
|
enabled: boolean;
|
|
host: string;
|
|
port: number;
|
|
secure: boolean;
|
|
username: string;
|
|
password: string;
|
|
from: string;
|
|
to: string;
|
|
}
|
|
|
|
export interface WebhookSettings {
|
|
enabled: boolean;
|
|
url: string;
|
|
secret: string;
|
|
}
|
|
|
|
export interface NotificationEvents {
|
|
dnsAdd: boolean;
|
|
dnsUpdate: boolean;
|
|
dnsDelete: boolean;
|
|
secretCheck: boolean;
|
|
secretCheckTime: string;
|
|
timezone: string;
|
|
}
|
|
|
|
export interface AppSettings {
|
|
gotify: GotifySettings;
|
|
ntfy: NtfySettings;
|
|
smtp: SmtpSettings;
|
|
webhook: WebhookSettings;
|
|
notifications: NotificationEvents;
|
|
providerColors: Record<string, string>;
|
|
}
|
|
|
|
export type AppSettingsPatch = { [K in keyof AppSettings]?: Partial<AppSettings[K]> };
|
|
|
|
export interface DnsProviderField {
|
|
key: string;
|
|
label: string;
|
|
secret: boolean;
|
|
type?: "text" | "password" | "checkbox";
|
|
placeholder?: string;
|
|
}
|
|
|
|
export interface DnsProviderSummary {
|
|
id: number;
|
|
providerType: DnsProviderType;
|
|
name: string;
|
|
enabled: boolean;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface DnsZone {
|
|
id: string;
|
|
name: string;
|
|
syncedAt: string | null;
|
|
recordCount: number;
|
|
}
|
|
|
|
export interface DnsRecord {
|
|
id: string;
|
|
type: string;
|
|
name: string;
|
|
content: string;
|
|
ttl: number | null;
|
|
priority: number | null;
|
|
proxied?: boolean | null;
|
|
}
|
|
|
|
export interface DnsRecordInput {
|
|
type: string;
|
|
name: string;
|
|
content: string;
|
|
ttl?: number;
|
|
priority?: number;
|
|
proxied?: boolean;
|
|
}
|
|
|
|
export interface ServerRecord {
|
|
id: number;
|
|
name: string;
|
|
hostname: string | null;
|
|
osType: string;
|
|
description: string | null;
|
|
apiTokenPrefix: string;
|
|
createdAt: string;
|
|
lastSeenAt: string | null;
|
|
proxmoxIntegrationId: number | null;
|
|
proxmoxNode: string | null;
|
|
proxmoxGuestType: "qemu" | "lxc" | null;
|
|
proxmoxVmid: number | null;
|
|
}
|
|
|
|
export interface ServerHardware {
|
|
source: "proxmox" | "agent" | "none";
|
|
error?: string;
|
|
integrationId?: number;
|
|
integrationName?: string;
|
|
status?: string;
|
|
cpuModel?: string;
|
|
cpuCores?: number | null;
|
|
cpuUsagePercent?: number | null;
|
|
cpuLoadPercent?: number | null;
|
|
memTotalBytes?: number | null;
|
|
memUsedBytes?: number | null;
|
|
diskBytes?: number | null;
|
|
disks?: { mount: string; sizeBytes: number; usedBytes: number }[];
|
|
uptime?: number | null;
|
|
guestAgentAvailable?: boolean;
|
|
}
|
|
|
|
export interface DnsMatch {
|
|
recordName: string;
|
|
recordType: string;
|
|
ip: string;
|
|
zoneName: string | null;
|
|
providerName: string;
|
|
providerType: string;
|
|
}
|
|
|
|
export interface ServerDetail {
|
|
server: ServerRecord;
|
|
ipAddresses: string[];
|
|
hardware: ServerHardware;
|
|
dnsMatches: DnsMatch[];
|
|
}
|
|
|
|
export interface ServerUpdateInput {
|
|
name?: string;
|
|
hostname?: string;
|
|
description?: string;
|
|
proxmoxIntegrationId?: number | null;
|
|
proxmoxNode?: string | null;
|
|
proxmoxGuestType?: "qemu" | "lxc" | null;
|
|
proxmoxVmid?: number | null;
|
|
}
|
|
|
|
export type ScheduleType = "cron" | "systemd_timer" | "docker" | "backup" | "update" | "n8n_workflow" | "manual";
|
|
|
|
export interface TaskRecord {
|
|
id: number;
|
|
serverId: number;
|
|
serverName: string;
|
|
scheduleType: ScheduleType;
|
|
origin: "agent" | "manual";
|
|
name: string;
|
|
command: string | null;
|
|
scheduleExpression: string | null;
|
|
source: string | null;
|
|
enabled: boolean;
|
|
nextRunAt: string | null;
|
|
isStale: boolean;
|
|
firstSeenAt: string;
|
|
lastSeenAt: string;
|
|
}
|
|
|
|
export interface ManualTaskInput {
|
|
serverId: number;
|
|
scheduleType: ScheduleType;
|
|
name: string;
|
|
command?: string;
|
|
scheduleExpression?: string;
|
|
nextRunAt?: string;
|
|
enabled?: boolean;
|
|
}
|
|
|
|
export type IntegrationType = "proxmox" | "synology" | "semaphore" | "tailscale" | "gitea" | "dockhand";
|
|
|
|
export interface IntegrationField {
|
|
key: string;
|
|
label: string;
|
|
secret: boolean;
|
|
type?: "text" | "password" | "checkbox";
|
|
placeholder?: string;
|
|
}
|
|
|
|
export interface IntegrationSummary {
|
|
id: number;
|
|
type: IntegrationType;
|
|
name: string;
|
|
baseUrl: string;
|
|
enabled: boolean;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface TailscaleDevice {
|
|
id: string;
|
|
nodeId: string;
|
|
hostname: string;
|
|
label: string;
|
|
addresses: string[];
|
|
primaryAddress: string;
|
|
os: string;
|
|
lastSeen: string | null;
|
|
isExitNode: boolean;
|
|
authorized: boolean;
|
|
online: boolean;
|
|
}
|
|
|
|
export interface TailscaleDevicesResponse {
|
|
devices: TailscaleDevice[];
|
|
summary: { total: number; online: number; unauthorized: number };
|
|
}
|
|
|
|
export interface GiteaWorkflowRun {
|
|
id: number;
|
|
displayTitle: string;
|
|
status: string;
|
|
conclusion: string | null;
|
|
headBranch: string;
|
|
event: string;
|
|
runNumber: number;
|
|
htmlUrl: string;
|
|
startedAt: string | null;
|
|
completedAt: string | null;
|
|
}
|
|
|
|
export interface GiteaRepo {
|
|
owner: string;
|
|
name: string;
|
|
fullName: string;
|
|
htmlUrl: string;
|
|
private: boolean;
|
|
hasActions: boolean;
|
|
updatedAt: string;
|
|
latestRun: GiteaWorkflowRun | null;
|
|
}
|
|
|
|
export interface DockhandContainer {
|
|
id: string;
|
|
name: string;
|
|
image: string;
|
|
state: string;
|
|
status: string;
|
|
environmentId: number;
|
|
environmentName: string;
|
|
}
|
|
|
|
export interface DockhandContainersResponse {
|
|
containers: DockhandContainer[];
|
|
summary: { total: number; running: number };
|
|
}
|
|
|
|
export type SemaphoreTaskStatus =
|
|
| "waiting"
|
|
| "starting"
|
|
| "waiting_confirmation"
|
|
| "confirmed"
|
|
| "rejected"
|
|
| "running"
|
|
| "stopping"
|
|
| "stopped"
|
|
| "success"
|
|
| "error";
|
|
|
|
export interface SemaphoreTask {
|
|
id: number;
|
|
status: SemaphoreTaskStatus;
|
|
created: string | null;
|
|
start: string | null;
|
|
end: string | null;
|
|
}
|
|
|
|
export interface SemaphoreTemplate {
|
|
id: number;
|
|
projectId: number;
|
|
projectName: string;
|
|
name: string;
|
|
playbook: string;
|
|
lastTask: SemaphoreTask | null;
|
|
}
|
|
|
|
export interface SemaphoreTemplatesResponse {
|
|
templates: SemaphoreTemplate[];
|
|
summary: { total: number; failing: number };
|
|
}
|
|
|
|
export type ProxmoxGuestType = "qemu" | "lxc";
|
|
|
|
export interface ProxmoxGuest {
|
|
vmid: number;
|
|
name: string;
|
|
node: string;
|
|
type: ProxmoxGuestType;
|
|
status: string;
|
|
cpu: number | null;
|
|
maxmem: number | null;
|
|
mem: number | null;
|
|
uptime: number | null;
|
|
}
|
|
|
|
export interface ProxmoxGuestsResponse {
|
|
guests: ProxmoxGuest[];
|
|
summary: { total: number; running: number };
|
|
}
|
|
|
|
export interface SynologyVolume {
|
|
id: string;
|
|
status: string;
|
|
deviceType: string;
|
|
sizeTotal: number | null;
|
|
sizeUsed: number | null;
|
|
}
|
|
|
|
export interface SynologyDisk {
|
|
id: string;
|
|
name: string;
|
|
device: string;
|
|
status: string;
|
|
smartStatus: string;
|
|
temp: number | null;
|
|
exceedBadSectorThreshold: boolean;
|
|
belowRemainLifeThreshold: boolean;
|
|
}
|
|
|
|
export interface SynologyStorageResponse {
|
|
volumes: SynologyVolume[];
|
|
disks: SynologyDisk[];
|
|
summary: { volumeCount: number; volumesNotNormal: number; diskCount: number; disksNotNormal: number };
|
|
}
|
|
|
|
export class UnauthorizedError extends Error {}
|
|
export class ForbiddenError extends Error {}
|
|
|
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const res = await fetch(path, {
|
|
...init,
|
|
headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) },
|
|
credentials: "same-origin",
|
|
});
|
|
if (res.status === 401) {
|
|
throw new UnauthorizedError("unauthorized");
|
|
}
|
|
if (res.status === 403) {
|
|
throw new ForbiddenError("forbidden");
|
|
}
|
|
if (!res.ok) {
|
|
const body = await res.text().catch(() => "");
|
|
throw new Error(`Request failed (${res.status}): ${body}`);
|
|
}
|
|
if (res.status === 204) return undefined as T;
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
export const api = {
|
|
me: () => request<{ user: CurrentUser }>("/api/me"),
|
|
users: {
|
|
list: () => request<{ users: UserRecord[] }>("/api/users"),
|
|
updateRole: (id: number, role: UserRole) =>
|
|
request<{ user: UserRecord }>(`/api/users/${id}/role`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ role }),
|
|
}),
|
|
},
|
|
auditLog: {
|
|
list: (limit = 200) => request<{ entries: AuditLogEntry[] }>(`/api/audit-log?limit=${limit}`),
|
|
},
|
|
secrets: {
|
|
list: () => request<{ secrets: SecretRecord[] }>("/api/secrets"),
|
|
create: (data: SecretInput) =>
|
|
request<{ secret: SecretRecord }>("/api/secrets", { method: "POST", body: JSON.stringify(data) }),
|
|
update: (id: number, data: Partial<SecretInput>) =>
|
|
request<{ secret: SecretRecord }>(`/api/secrets/${id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(data),
|
|
}),
|
|
remove: (id: number) => request<void>(`/api/secrets/${id}`, { method: "DELETE" }),
|
|
},
|
|
ipam: {
|
|
list: () => request<{ entries: IpamEntry[] }>("/api/ipam"),
|
|
create: (data: IpamInput) =>
|
|
request<{ entry: IpamEntry }>("/api/ipam", { method: "POST", body: JSON.stringify(data) }),
|
|
update: (id: number, data: Partial<Omit<IpamInput, "ipAddress">>) =>
|
|
request<{ entry: IpamEntry }>(`/api/ipam/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
|
|
remove: (id: number) => request<void>(`/api/ipam/${id}`, { method: "DELETE" }),
|
|
},
|
|
dns: {
|
|
providerFields: () =>
|
|
request<{ fields: Record<DnsProviderType, DnsProviderField[]> }>("/api/dns/provider-fields"),
|
|
providers: {
|
|
list: () => request<{ providers: DnsProviderSummary[] }>("/api/dns/providers"),
|
|
create: (data: { providerType: DnsProviderType; name: string; config: Record<string, string | boolean> }) =>
|
|
request<{ provider: DnsProviderSummary }>("/api/dns/providers", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
}),
|
|
update: (id: number, data: { name?: string; enabled?: boolean; config?: Record<string, string | boolean> }) =>
|
|
request<{ provider: DnsProviderSummary }>(`/api/dns/providers/${id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(data),
|
|
}),
|
|
remove: (id: number) => request<void>(`/api/dns/providers/${id}`, { method: "DELETE" }),
|
|
test: (data: { providerType: DnsProviderType; config: Record<string, string | boolean> }) =>
|
|
request<{ ok: boolean; zoneCount?: number; error?: string }>("/api/dns/providers/test", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
}),
|
|
},
|
|
zones: {
|
|
list: (providerId: number) => request<{ zones: DnsZone[] }>(`/api/dns/providers/${providerId}/zones`),
|
|
},
|
|
records: {
|
|
list: (providerId: number, zoneId: string) =>
|
|
request<{ records: DnsRecord[]; syncedAt: string | null }>(
|
|
`/api/dns/providers/${providerId}/zones/${encodeURIComponent(zoneId)}/records`,
|
|
),
|
|
sync: (providerId: number, zoneId: string, zoneName?: string) =>
|
|
request<{ records: DnsRecord[]; syncedAt: string }>(
|
|
`/api/dns/providers/${providerId}/zones/${encodeURIComponent(zoneId)}/sync`,
|
|
{ method: "POST", body: JSON.stringify({ zoneName }) },
|
|
),
|
|
create: (providerId: number, zoneId: string, data: DnsRecordInput) =>
|
|
request<{ record: DnsRecord }>(
|
|
`/api/dns/providers/${providerId}/zones/${encodeURIComponent(zoneId)}/records`,
|
|
{ method: "POST", body: JSON.stringify(data) },
|
|
),
|
|
update: (providerId: number, zoneId: string, recordId: string, data: DnsRecordInput) =>
|
|
request<{ record: DnsRecord }>(
|
|
`/api/dns/providers/${providerId}/zones/${encodeURIComponent(zoneId)}/records/${encodeURIComponent(recordId)}`,
|
|
{ method: "PUT", body: JSON.stringify(data) },
|
|
),
|
|
remove: (providerId: number, zoneId: string, recordId: string) =>
|
|
request<void>(
|
|
`/api/dns/providers/${providerId}/zones/${encodeURIComponent(zoneId)}/records/${encodeURIComponent(recordId)}`,
|
|
{ method: "DELETE" },
|
|
),
|
|
},
|
|
clearCache: () => request<{ ok: true }>("/api/dns/cache/clear", { method: "POST" }),
|
|
},
|
|
servers: {
|
|
list: () => request<{ servers: ServerRecord[] }>("/api/servers"),
|
|
create: (data: { name: string; hostname?: string; description?: string }) =>
|
|
request<{ server: ServerRecord; token: string }>("/api/servers", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
}),
|
|
rotateToken: (id: number) =>
|
|
request<{ server: ServerRecord; token: string }>(`/api/servers/${id}/rotate-token`, {
|
|
method: "POST",
|
|
}),
|
|
update: (id: number, data: ServerUpdateInput) =>
|
|
request<{ server: ServerRecord }>(`/api/servers/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
|
|
detail: (id: number) => request<ServerDetail>(`/api/servers/${id}/detail`),
|
|
remove: (id: number) => request<void>(`/api/servers/${id}`, { method: "DELETE" }),
|
|
},
|
|
tasks: {
|
|
list: (
|
|
params: { serverId?: number; scheduleType?: string; search?: string; includeStale?: boolean } = {},
|
|
) => {
|
|
const qs = new URLSearchParams();
|
|
if (params.serverId) qs.set("serverId", String(params.serverId));
|
|
if (params.scheduleType) qs.set("scheduleType", params.scheduleType);
|
|
if (params.search) qs.set("search", params.search);
|
|
if (params.includeStale) qs.set("includeStale", "true");
|
|
const query = qs.toString();
|
|
return request<{ tasks: TaskRecord[] }>(`/api/tasks${query ? `?${query}` : ""}`);
|
|
},
|
|
create: (data: ManualTaskInput) =>
|
|
request<{ task: TaskRecord }>("/api/tasks", { method: "POST", body: JSON.stringify(data) }),
|
|
update: (id: number, data: Partial<Omit<ManualTaskInput, "serverId">>) =>
|
|
request<{ task: TaskRecord }>(`/api/tasks/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
|
|
remove: (id: number) => request<void>(`/api/tasks/${id}`, { method: "DELETE" }),
|
|
},
|
|
integrations: {
|
|
fields: () => request<{ fields: Partial<Record<IntegrationType, IntegrationField[]>> }>("/api/integrations/fields"),
|
|
list: () => request<{ integrations: IntegrationSummary[] }>("/api/integrations"),
|
|
create: (data: { type: IntegrationType; name: string; config: Record<string, string | boolean> }) =>
|
|
request<{ integration: IntegrationSummary }>("/api/integrations", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
}),
|
|
update: (id: number, data: { name?: string; enabled?: boolean; config?: Record<string, string | boolean> }) =>
|
|
request<{ integration: IntegrationSummary }>(`/api/integrations/${id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(data),
|
|
}),
|
|
remove: (id: number) => request<void>(`/api/integrations/${id}`, { method: "DELETE" }),
|
|
test: (data: { type: IntegrationType; config: Record<string, string | boolean> }) =>
|
|
request<{ ok: boolean; latencyMs?: number; error?: string }>("/api/integrations/test", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
}),
|
|
getConfig: (id: number) =>
|
|
request<{
|
|
integration: { id: number; type: IntegrationType; name: string; enabled: boolean };
|
|
config: Record<string, string | boolean>;
|
|
}>(`/api/integrations/${id}/config`),
|
|
testExisting: (id: number, config: Record<string, string | boolean>) =>
|
|
request<{ ok: boolean; latencyMs?: number; error?: string }>(`/api/integrations/${id}/test`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ config }),
|
|
}),
|
|
tailscale: {
|
|
devices: (integrationId: number) =>
|
|
request<TailscaleDevicesResponse>(`/api/integrations/${integrationId}/tailscale/devices`),
|
|
setAuthorized: (integrationId: number, deviceId: string, authorized: boolean) =>
|
|
request<void>(`/api/integrations/${integrationId}/tailscale/devices/${encodeURIComponent(deviceId)}/authorize`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ authorized }),
|
|
}),
|
|
remove: (integrationId: number, deviceId: string) =>
|
|
request<void>(`/api/integrations/${integrationId}/tailscale/devices/${encodeURIComponent(deviceId)}`, {
|
|
method: "DELETE",
|
|
}),
|
|
},
|
|
gitea: {
|
|
repos: (integrationId: number) => request<{ repos: GiteaRepo[] }>(`/api/integrations/${integrationId}/gitea/repos`),
|
|
rerunFailed: (integrationId: number, owner: string, repo: string, runId: number) =>
|
|
request<void>(
|
|
`/api/integrations/${integrationId}/gitea/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/runs/${runId}/rerun-failed`,
|
|
{ method: "POST" },
|
|
),
|
|
},
|
|
dockhand: {
|
|
containers: (integrationId: number) =>
|
|
request<DockhandContainersResponse>(`/api/integrations/${integrationId}/dockhand/containers`),
|
|
start: (integrationId: number, envId: number, containerId: string) =>
|
|
request<void>(
|
|
`/api/integrations/${integrationId}/dockhand/environments/${envId}/containers/${encodeURIComponent(containerId)}/start`,
|
|
{ method: "POST" },
|
|
),
|
|
stop: (integrationId: number, envId: number, containerId: string) =>
|
|
request<void>(
|
|
`/api/integrations/${integrationId}/dockhand/environments/${envId}/containers/${encodeURIComponent(containerId)}/stop`,
|
|
{ method: "POST" },
|
|
),
|
|
restart: (integrationId: number, envId: number, containerId: string) =>
|
|
request<void>(
|
|
`/api/integrations/${integrationId}/dockhand/environments/${envId}/containers/${encodeURIComponent(containerId)}/restart`,
|
|
{ method: "POST" },
|
|
),
|
|
},
|
|
semaphore: {
|
|
templates: (integrationId: number) =>
|
|
request<SemaphoreTemplatesResponse>(`/api/integrations/${integrationId}/semaphore/templates`),
|
|
run: (integrationId: number, projectId: number, templateId: number) =>
|
|
request<{ task: SemaphoreTask }>(
|
|
`/api/integrations/${integrationId}/semaphore/projects/${projectId}/templates/${templateId}/run`,
|
|
{ method: "POST" },
|
|
),
|
|
},
|
|
proxmox: {
|
|
guests: (integrationId: number) => request<ProxmoxGuestsResponse>(`/api/integrations/${integrationId}/proxmox/guests`),
|
|
start: (integrationId: number, node: string, type: ProxmoxGuestType, vmid: number) =>
|
|
request<void>(`/api/integrations/${integrationId}/proxmox/nodes/${node}/${type}/${vmid}/start`, { method: "POST" }),
|
|
stop: (integrationId: number, node: string, type: ProxmoxGuestType, vmid: number) =>
|
|
request<void>(`/api/integrations/${integrationId}/proxmox/nodes/${node}/${type}/${vmid}/stop`, { method: "POST" }),
|
|
restart: (integrationId: number, node: string, type: ProxmoxGuestType, vmid: number) =>
|
|
request<void>(`/api/integrations/${integrationId}/proxmox/nodes/${node}/${type}/${vmid}/restart`, { method: "POST" }),
|
|
shutdown: (integrationId: number, node: string, type: ProxmoxGuestType, vmid: number) =>
|
|
request<void>(`/api/integrations/${integrationId}/proxmox/nodes/${node}/${type}/${vmid}/shutdown`, { method: "POST" }),
|
|
},
|
|
synology: {
|
|
storage: (integrationId: number) => request<SynologyStorageResponse>(`/api/integrations/${integrationId}/synology/storage`),
|
|
},
|
|
},
|
|
settings: {
|
|
get: () => request<{ settings: AppSettings }>("/api/settings"),
|
|
providerColors: () => request<{ providerColors: Record<string, string> }>("/api/settings/provider-colors"),
|
|
update: (data: AppSettingsPatch) =>
|
|
request<{ settings: AppSettings }>("/api/settings", { method: "PUT", body: JSON.stringify(data) }),
|
|
testGotify: (data: { url: string; token: string; priority?: number }) =>
|
|
request<{ ok: true }>("/api/settings/test-gotify", { method: "POST", body: JSON.stringify(data) }),
|
|
testNtfy: (data: { url: string; topic: string; token?: string; priority?: number }) =>
|
|
request<{ ok: true }>("/api/settings/test-ntfy", { method: "POST", body: JSON.stringify(data) }),
|
|
testSmtp: (data: { host: string; port?: number; secure?: boolean; username?: string; password?: string; from: string; to: string }) =>
|
|
request<{ ok: true }>("/api/settings/test-smtp", { method: "POST", body: JSON.stringify(data) }),
|
|
testWebhook: (data: { url: string; secret?: string }) =>
|
|
request<{ ok: true }>("/api/settings/test-webhook", { method: "POST", body: JSON.stringify(data) }),
|
|
},
|
|
};
|