Files
kandev/apps/web/lib/api/domains/system-api.ts
T
José Almeida 041a19f060 feat: service-managed UI self-update (#1210)
* feat: add service-managed self-update

* chore: add temporary self-update test scripts

* chore: simplify temporary script markdown fences

* chore: run frontend in temporary self-update scripts

* fix: show self-update job progress after apply

* chore: add real npm self-update test script

* fix: isolate npm self-update prefix

* fix: detect npm service installs from bin shim

* chore: reset service before real npm update test

* chore: expand real npm status diagnostics

* fix: preserve service home during self-update

* fix(cli): tee self-update helper output to a log file

* fix(cli): avoid launchd bootstrap race when reloading the macOS service

* fix(backend): run macOS self-update helper once via transient launchd plist

* fix(web): show self-update progress and lock the apply button until the update lands

* fix(cli): reinstall via the kandev wrapper on Homebrew self-update

* chore: remove temp self-update test scripts

* fix(web): drop unused apply path from useUpdates hook

* fix(backend): snapshot install state once in apply preflight

* docs: align self-update docs with transient LaunchAgent flow

* chore(release): run cli bundle from apps workspace root

* fix(backend): guard against concurrent self-update applies

* fix(backend): check scheme and helper paths in self-update apply

* fix(backend): drop duplicate binary name from npx manual command

* fix(cli): allow --help for service self-update without --intent

* fix(cli): harden self-update dir perms and darwin uid resolution

* fix(web): set required POST fields last in applyUpdate

* docs: use JSON body notation for the apply endpoint

* fix(backend): expire self-update guard so a stuck helper cannot wedge apply

* refactor(cli): collapse redundant writeSync branch in self-update log
2026-06-01 17:49:15 +01:00

171 lines
5.8 KiB
TypeScript

import { fetchJson, type ApiRequestOptions } from "../client";
import { getBackendConfig } from "@/lib/config";
import type {
SystemInfo,
DiskUsageResponse,
DatabaseStats,
SnapshotInfo,
LogFileInfo,
LogTailResponse,
UpdatesResponse,
JobAcceptResponse,
} from "@/lib/types/system";
const SYSTEM_BASE = "/api/v1/system";
// --- Info ---------------------------------------------------------------
export function fetchSystemInfo(options?: ApiRequestOptions): Promise<SystemInfo> {
return fetchJson<SystemInfo>(`${SYSTEM_BASE}/info`, options);
}
// --- Disk usage ---------------------------------------------------------
export function fetchDiskUsage(options?: ApiRequestOptions): Promise<DiskUsageResponse> {
return fetchJson<DiskUsageResponse>(`${SYSTEM_BASE}/disk-usage`, options);
}
export function refreshDiskUsage(options?: ApiRequestOptions): Promise<JobAcceptResponse> {
return fetchJson<JobAcceptResponse>(`${SYSTEM_BASE}/disk-usage/refresh`, {
...options,
init: { method: "POST", ...(options?.init ?? {}) },
});
}
export function openDataFolder(options?: ApiRequestOptions): Promise<{ path: string }> {
return fetchJson<{ path: string }>(`${SYSTEM_BASE}/disk-usage/open`, {
...options,
init: { method: "POST", ...(options?.init ?? {}) },
});
}
// --- Database -----------------------------------------------------------
export function fetchDatabaseStats(options?: ApiRequestOptions): Promise<DatabaseStats> {
return fetchJson<DatabaseStats>(`${SYSTEM_BASE}/database`, options);
}
export function vacuumDatabase(options?: ApiRequestOptions): Promise<JobAcceptResponse> {
return fetchJson<JobAcceptResponse>(`${SYSTEM_BASE}/database/vacuum`, {
...options,
init: { method: "POST", ...(options?.init ?? {}) },
});
}
export function optimizeDatabase(options?: ApiRequestOptions): Promise<JobAcceptResponse> {
return fetchJson<JobAcceptResponse>(`${SYSTEM_BASE}/database/optimize`, {
...options,
init: { method: "POST", ...(options?.init ?? {}) },
});
}
export function resetDatabase(
confirm: string,
options?: ApiRequestOptions,
): Promise<JobAcceptResponse> {
return fetchJson<JobAcceptResponse>(`${SYSTEM_BASE}/database/reset`, {
...options,
init: { method: "POST", body: JSON.stringify({ confirm }), ...(options?.init ?? {}) },
});
}
// --- Backups ------------------------------------------------------------
export async function fetchBackups(options?: ApiRequestOptions): Promise<SnapshotInfo[]> {
const res = await fetchJson<{ snapshots: SnapshotInfo[] }>(`${SYSTEM_BASE}/backups`, options);
return res.snapshots ?? [];
}
export function createBackup(options?: ApiRequestOptions): Promise<JobAcceptResponse> {
return fetchJson<JobAcceptResponse>(`${SYSTEM_BASE}/backups`, {
...options,
init: { method: "POST", ...(options?.init ?? {}) },
});
}
export function restoreBackup(
name: string,
confirm: string,
options?: ApiRequestOptions,
): Promise<JobAcceptResponse> {
return fetchJson<JobAcceptResponse>(
`${SYSTEM_BASE}/backups/${encodeURIComponent(name)}/restore`,
{
...options,
init: { method: "POST", body: JSON.stringify({ confirm }), ...(options?.init ?? {}) },
},
);
}
export function deleteBackup(name: string, options?: ApiRequestOptions): Promise<void> {
return fetchJson<void>(`${SYSTEM_BASE}/backups/${encodeURIComponent(name)}`, {
...options,
init: { method: "DELETE", ...(options?.init ?? {}) },
});
}
/**
* Builds an absolute URL for downloading a snapshot. Snapshot downloads stream
* binary data, so they cannot reuse fetchJson — they are typically wired into
* <a href={...} download> or window.open().
*/
export function buildBackupDownloadUrl(name: string, baseUrl?: string): string {
const root = baseUrl ?? getBackendConfig().apiBaseUrl;
return `${root}${SYSTEM_BASE}/backups/${encodeURIComponent(name)}/download`;
}
// --- Logs ---------------------------------------------------------------
export async function fetchLogFiles(options?: ApiRequestOptions): Promise<LogFileInfo[]> {
const res = await fetchJson<{ files: LogFileInfo[] }>(`${SYSTEM_BASE}/logs`, options);
return res.files ?? [];
}
export function fetchLogTail(n = 1000, options?: ApiRequestOptions): Promise<LogTailResponse> {
return fetchJson<LogTailResponse>(`${SYSTEM_BASE}/logs/tail?n=${encodeURIComponent(String(n))}`, {
cache: "no-store",
...options,
});
}
export function buildLogDownloadUrl(name: string, baseUrl?: string): string {
const root = baseUrl ?? getBackendConfig().apiBaseUrl;
return `${root}${SYSTEM_BASE}/logs/${encodeURIComponent(name)}/download`;
}
// --- Jobs ---------------------------------------------------------------
export function fetchSystemJob(
jobId: string,
options?: ApiRequestOptions,
): Promise<import("@/lib/types/system").SystemJob> {
return fetchJson<import("@/lib/types/system").SystemJob>(
`${SYSTEM_BASE}/jobs/${encodeURIComponent(jobId)}`,
{ cache: "no-store", ...options },
);
}
// --- Updates ------------------------------------------------------------
export function fetchUpdates(options?: ApiRequestOptions): Promise<UpdatesResponse> {
return fetchJson<UpdatesResponse>(`${SYSTEM_BASE}/updates`, options);
}
export function checkUpdates(options?: ApiRequestOptions): Promise<UpdatesResponse> {
return fetchJson<UpdatesResponse>(`${SYSTEM_BASE}/updates/check`, {
...options,
init: { method: "POST", ...(options?.init ?? {}) },
});
}
export function applyUpdate(
confirm = "UPDATE",
options?: ApiRequestOptions,
): Promise<JobAcceptResponse> {
return fetchJson<JobAcceptResponse>(`${SYSTEM_BASE}/updates/apply`, {
...options,
// Spread caller init first so the required method/body can't be overridden.
init: { ...(options?.init ?? {}), method: "POST", body: JSON.stringify({ confirm }) },
});
}