feat: expand scheduling, notifications, and queue runtime
- add scheduled task plumbing across backend core, CLI, and frontend surfaces - add frontend notification UI and keep service pages using the repaired shared stylesheet - refactor code queue runtime and update baidu netdisk/service integration docs
This commit is contained in:
@@ -301,6 +301,7 @@ const LAYOUT_OVERFLOW_PAGE_TEST_IDS: Record<string, string> = {
|
||||
"/nodes/gateway/": "gateway-version-page",
|
||||
"/tasks/pending/": "pending-task-page",
|
||||
"/tasks/history/": "task-history-page",
|
||||
"/tasks/scheduled/": "scheduled-task-page",
|
||||
"/app/catalog/": "microservice-catalog-page",
|
||||
"/app/todo-note/": "todo-note-page",
|
||||
"/app/findjob/": "findjob-page",
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { type UniDeskConfig } from "./config";
|
||||
import { coreInternalFetch } from "./microservices";
|
||||
|
||||
function stringOption(args: string[], name: string): string | undefined {
|
||||
const index = args.indexOf(name);
|
||||
if (index === -1) return undefined;
|
||||
const raw = args[index + 1];
|
||||
if (raw === undefined || raw.length === 0) throw new Error(`${name} requires a non-empty value`);
|
||||
return raw;
|
||||
}
|
||||
|
||||
function numberOption(args: string[], name: string, defaultValue: number): number {
|
||||
const raw = stringOption(args, name);
|
||||
if (raw === undefined) return defaultValue;
|
||||
const value = Number(raw);
|
||||
if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function booleanOption(args: string[], name: string, defaultValue: boolean): boolean {
|
||||
const raw = stringOption(args, name);
|
||||
if (raw === undefined) return defaultValue;
|
||||
if (["1", "true", "yes", "on"].includes(raw.toLowerCase())) return true;
|
||||
if (["0", "false", "no", "off"].includes(raw.toLowerCase())) return false;
|
||||
throw new Error(`${name} must be true or false`);
|
||||
}
|
||||
|
||||
function responseBody(response: unknown): Record<string, unknown> {
|
||||
if (typeof response !== "object" || response === null || Array.isArray(response)) return {};
|
||||
const body = (response as { body?: unknown }).body;
|
||||
return typeof body === "object" && body !== null && !Array.isArray(body) ? body as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
function terminalRunStatus(status: unknown): boolean {
|
||||
return ["succeeded", "failed", "skipped"].includes(String(status || ""));
|
||||
}
|
||||
|
||||
async function waitForScheduleRun(scheduleId: string, runId: string, timeoutMs: number): Promise<unknown> {
|
||||
const started = Date.now();
|
||||
let latest: unknown = null;
|
||||
while (Date.now() - started < timeoutMs) {
|
||||
latest = coreInternalFetch(`/api/schedules/${encodeURIComponent(scheduleId)}/runs?limit=20`);
|
||||
const runs = responseBody(latest).runs;
|
||||
if (Array.isArray(runs)) {
|
||||
const run = runs.find((item) => typeof item === "object" && item !== null && (item as { id?: unknown }).id === runId);
|
||||
if (run !== undefined && terminalRunStatus((run as { status?: unknown }).status)) return { ok: true, run };
|
||||
}
|
||||
await Bun.sleep(2000);
|
||||
}
|
||||
return { ok: false, timeoutMs, latest };
|
||||
}
|
||||
|
||||
function pgdataBackupScheduleBody(config: UniDeskConfig, args: string[]): Record<string, unknown> {
|
||||
const id = stringOption(args, "--id") ?? "unidesk-pgdata-baidu-daily";
|
||||
const timeOfDay = stringOption(args, "--time") ?? "03:30";
|
||||
const remoteBaseDir = stringOption(args, "--remote-base") ?? "/SERVER_DATA/UNIDESK_PG_DATA";
|
||||
const stagingSubdir = stringOption(args, "--staging-subdir") ?? "server-data/unidesk-pg-data";
|
||||
const enabled = args.includes("--disabled") ? false : booleanOption(args, "--enabled", true);
|
||||
const timeoutMs = numberOption(args, "--timeout-ms", 60 * 60_000);
|
||||
return {
|
||||
id,
|
||||
name: "PGDATA daily Baidu Netdisk backup",
|
||||
description: "Daily PostgreSQL physical base backup uploaded to Baidu Netdisk /SERVER_DATA with monthly rotation.",
|
||||
enabled,
|
||||
concurrencyPolicy: "skip",
|
||||
schedule: { type: "daily", timeOfDay, timezone: config.project.timezone || "Etc/UTC" },
|
||||
action: {
|
||||
type: "pgdata_backup",
|
||||
volumeName: config.database.volume,
|
||||
remoteBaseDir,
|
||||
stagingSubdir,
|
||||
baiduBaseUrl: "http://baidu-netdisk:4244",
|
||||
timeoutMs,
|
||||
cleanupLocal: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function runScheduleCommand(config: UniDeskConfig, args: string[]): Promise<unknown> {
|
||||
const [action = "list", idArg] = args;
|
||||
if (action === "list") return coreInternalFetch("/api/schedules?limit=200");
|
||||
if (action === "get") {
|
||||
if (!idArg) throw new Error("schedule get requires schedule id");
|
||||
return coreInternalFetch(`/api/schedules/${encodeURIComponent(idArg)}`);
|
||||
}
|
||||
if (action === "runs") {
|
||||
const limit = numberOption(args, "--limit", 50);
|
||||
return idArg
|
||||
? coreInternalFetch(`/api/schedules/${encodeURIComponent(idArg)}/runs?limit=${limit}`)
|
||||
: coreInternalFetch(`/api/schedules/runs?limit=${limit}`);
|
||||
}
|
||||
if (action === "delete") {
|
||||
if (!idArg) throw new Error("schedule delete requires schedule id");
|
||||
return coreInternalFetch(`/api/schedules/${encodeURIComponent(idArg)}`, { method: "DELETE" });
|
||||
}
|
||||
if (action === "run") {
|
||||
if (!idArg) throw new Error("schedule run requires schedule id");
|
||||
const response = coreInternalFetch(`/api/schedules/${encodeURIComponent(idArg)}/run`, { method: "POST", body: {} });
|
||||
const run = responseBody(response).run as { id?: unknown } | undefined;
|
||||
const waitMs = numberOption(args, "--wait-ms", 0);
|
||||
const wait = waitMs > 0 && typeof run?.id === "string" ? await waitForScheduleRun(idArg, run.id, waitMs) : null;
|
||||
return { trigger: response, wait };
|
||||
}
|
||||
if (action === "upsert-pgdata-backup" || action === "pgdata-backup") {
|
||||
const body = pgdataBackupScheduleBody(config, args);
|
||||
const id = String(body.id || "unidesk-pgdata-baidu-daily");
|
||||
return coreInternalFetch(`/api/schedules/${encodeURIComponent(id)}`, { method: "PUT", body });
|
||||
}
|
||||
throw new Error("schedule command must be one of: list, get, runs, run, delete, upsert-pgdata-backup");
|
||||
}
|
||||
Reference in New Issue
Block a user