Files
Homelab-manager/web/src/api/client.ts
T
bobbanandClaude Sonnet 5 10d123b18a Add automatic retention purging for the Diagnostic and Audit logs
The diagnostic log already rings-buffer to 500 rows, but the audit
log had no cap at all and would grow forever. Adds an opt-in
age-based purge under Settings -> Logs: keep entries for N days,
checked on a configurable interval (hourly through monthly), plus a
manual "Purge now" button. Reuses the existing node-schedule-style
reschedule-on-settings-change pattern from the secret/Tailscale
expiry checkers, but as a plain setInterval since "how often" here is
an interval rather than a specific daily time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 02:22:32 +02:00

806 lines
26 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 interface DiagLogEntry {
id: number;
source: string;
operation: string;
ok: boolean;
latencyMs: number;
error: string | null;
createdAt: string;
}
export interface DiagLogResponse {
total: number;
entries: DiagLogEntry[];
}
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;
source: string | null;
createdAt: string;
updatedAt: string;
matchingDnsRecords: string[];
}
export interface IpamSyncResult {
added: number;
updated: number;
skipped: number;
skippedIps: string[];
errors: 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;
tailscaleKeyCheck: boolean;
secretCheckTime: string;
timezone: string;
}
export type DateFormat = "ymd" | "dmy" | "mdy";
export type TimeFormat = "24h" | "12h";
export interface DisplaySettings {
dateFormat: DateFormat;
timeFormat: TimeFormat;
pageSize: number;
}
export interface LogRetentionSettings {
enabled: boolean;
retentionDays: number;
intervalHours: number;
}
export interface AppSettings {
gotify: GotifySettings;
ntfy: NtfySettings;
smtp: SmtpSettings;
webhook: WebhookSettings;
notifications: NotificationEvents;
providerColors: Record<string, string>;
integrationColors: Record<string, string>;
display: DisplaySettings;
logRetention: LogRetentionSettings;
}
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;
hideProxmoxLink: boolean;
}
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 ServerLink {
id: number;
label: string;
url: string;
}
export interface ServerDetail {
server: ServerRecord;
ipAddresses: string[];
hardware: ServerHardware;
dnsMatches: DnsMatch[];
links: ServerLink[];
}
export interface ServerUpdateInput {
name?: string;
hostname?: string;
description?: string;
proxmoxIntegrationId?: number | null;
proxmoxNode?: string | null;
proxmoxGuestType?: "qemu" | "lxc" | null;
proxmoxVmid?: number | null;
hideProxmoxLink?: boolean;
}
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;
keyExpiry: string | null;
keyExpiryDisabled: boolean;
}
export interface TailscaleDevicesResponse {
devices: TailscaleDevice[];
summary: { total: number; online: number; unauthorized: number; expiringSoon: 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;
updateAvailable: boolean | null;
newerVersion: string | null;
checkedAt: string | null;
}
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 ProxmoxStorage {
id: string;
type: string;
active: boolean;
shared: boolean;
totalBytes: number | null;
usedBytes: number | null;
availBytes: number | null;
}
export interface ProxmoxNodeStats {
node: string;
error: string | null;
uptime: number | null;
cpuUsagePercent: number | null;
cpuCores: number | null;
loadAverage: [number, number, number] | null;
memTotalBytes: number | null;
memUsedBytes: number | null;
swapTotalBytes: number | null;
swapUsedBytes: number | null;
rootfsTotalBytes: number | null;
rootfsUsedBytes: number | null;
pveVersion: string | null;
storages: ProxmoxStorage[];
}
export interface ProxmoxNodesResponse {
nodes: ProxmoxNodeStats[];
}
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 interface SynologySystemInfo {
hostname: string | null;
model: string | null;
serial: string | null;
firmwareVersion: string | null;
uptime: string | null;
ipAddresses: string[];
cpu: { cores: number | null; clockSpeedMHz: number | null; loadPercent: number | null };
memory: { totalBytes: number | null; usedBytes: number | null };
}
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}`),
},
diagLog: {
list: (params: { source?: string; ok?: boolean; limit?: number; offset?: number } = {}) => {
const qs = new URLSearchParams();
if (params.source) qs.set("source", params.source);
if (params.ok !== undefined) qs.set("ok", String(params.ok));
if (params.limit !== undefined) qs.set("limit", String(params.limit));
if (params.offset !== undefined) qs.set("offset", String(params.offset));
const query = qs.toString();
return request<DiagLogResponse>(`/api/diag-log${query ? `?${query}` : ""}`);
},
clear: () => request<void>("/api/diag-log", { method: "DELETE" }),
},
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" }),
syncTailscale: () => request<IpamSyncResult>("/api/ipam/sync-tailscale", { method: "POST" }),
syncProxmox: () => request<IpamSyncResult>("/api/ipam/sync-proxmox", { method: "POST" }),
},
dns: {
providerFields: () =>
request<{ fields: Record<DnsProviderType, DnsProviderField[]> }>("/api/dns/provider-fields"),
stats: () =>
request<{
totalProviders: number;
enabledProviders: number;
totalZones: number;
perProvider: { providerId: number; name: string; providerType: DnsProviderType; zoneCount: number; error: string | null }[];
totalRecords: number;
recordsByType: { type: string; count: number }[];
}>("/api/dns/stats"),
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" }),
addLink: (id: number, data: { label: string; url: string }) =>
request<{ link: ServerLink }>(`/api/servers/${id}/links`, { method: "POST", body: JSON.stringify(data) }),
updateLink: (id: number, linkId: number, data: { label?: string; url?: string }) =>
request<{ link: ServerLink }>(`/api/servers/${id}/links/${linkId}`, { method: "PATCH", body: JSON.stringify(data) }),
removeLink: (id: number, linkId: number) =>
request<void>(`/api/servers/${id}/links/${linkId}`, { 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" },
),
checkForUpdates: (integrationId: number) =>
request<{ total: number; updatesFound: number }>(`/api/integrations/${integrationId}/dockhand/check-updates`, {
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`),
nodes: (integrationId: number) => request<ProxmoxNodesResponse>(`/api/integrations/${integrationId}/proxmox/nodes`),
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`),
system: (integrationId: number) => request<SynologySystemInfo>(`/api/integrations/${integrationId}/synology/system`),
},
},
settings: {
get: () => request<{ settings: AppSettings }>("/api/settings"),
providerColors: () => request<{ providerColors: Record<string, string> }>("/api/settings/provider-colors"),
integrationColors: () => request<{ integrationColors: Record<string, string> }>("/api/settings/integration-colors"),
display: () => request<{ display: DisplaySettings }>("/api/settings/display"),
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) }),
purgeLogs: () =>
request<{ diagDeleted: number; auditDeleted: number }>("/api/settings/purge-logs", { method: "POST" }),
},
};