041a19f060
* 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
171 lines
5.8 KiB
TypeScript
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 }) },
|
|
});
|
|
}
|