Add Servers & Tasks module ported from Schedule Task Manager
Ports cron/systemd task tracking across Debian/Raspbian servers, including the Linux push agent (install/report/uninstall scripts, rebranded from "schedule-task-manager-agent" to "homelab-manager-agent") and the manual-task-entry flow for things an agent can't see (Docker jobs, backups). The schema (servers/scheduled_tasks tables) was already in place from the foundation pass, so this is mostly a straight port of the original's services/routes. Deliberate change from the original: server/token management (which mints agent credentials) is now admin-only rather than open to any logged-in user, and manual task CRUD is gated to operator+ — consistent with how Secrets, IPAM, and DNS already split "configure credentials" from "everyday edits" across roles. All mutations are audit-logged. - server/src/services/tokens.ts, taskSync.ts: ported near-verbatim (agent token hashing, the agent-sync-marks-missing-as-stale-not-deleted logic). - server/src/routes/servers.ts, tasks.ts, agentReport.ts: same contract as the original (agent auth is a per-server bearer token, independent of the session-based requireAuth used everywhere else). - web: a single Servers & Tasks page (filter bar, task table grouped by server/schedule type, manual task form, and an admin-only server management panel with token reveal + copyable install/uninstall commands), replacing the original's two separate pages/apps. Verified: full build passes; a scripted HTTP test against a running server covers unauthenticated access, role gating at each tier (admin-only server mgmt, operator+ task mgmt), agent bearer-token auth (valid/invalid/rotated), manual-vs-agent task edit protection, stale-marking on re-sync, and cascade delete — 22/22 checks passing. Real agent installation on an actual Debian/Raspbian host still needs to be tried on the user's network. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ac3feb935d
commit
9712d611a6
17 files changed
+1616
-1
No files matched your search
@@ -120,6 +120,46 @@ export interface DnsRecordInput {
|
||||
proxied?: boolean;
|
||||
}
|
||||
|
||||
export interface ServerRecord {
|
||||
id: number;
|
||||
name: string;
|
||||
hostname: string | null;
|
||||
osType: string;
|
||||
description: string | null;
|
||||
apiTokenPrefix: string;
|
||||
createdAt: string;
|
||||
lastSeenAt: string | 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 class UnauthorizedError extends Error {}
|
||||
export class ForbiddenError extends Error {}
|
||||
|
||||
@@ -227,4 +267,35 @@ export const api = {
|
||||
),
|
||||
},
|
||||
},
|
||||
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",
|
||||
}),
|
||||
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" }),
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user