Add allowlisted diagnostic jobs and MG4 ADB plan

This commit is contained in:
2026-07-31 19:00:04 +02:00
parent e48b034678
commit 4970fda1b5
9 changed files with 578 additions and 9 deletions
+44 -1
View File
@@ -8,6 +8,7 @@ import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest }
import { z } from "zod";
import type { AppConfig } from "./config.js";
import { openDatabase, type CompanionDatabase } from "./database.js";
import { JobRunner } from "./jobs.js";
import {
clientAddress,
CSRF_COOKIE,
@@ -96,13 +97,18 @@ export async function buildApp(config: AppConfig): Promise<FastifyInstance> {
}
});
const database = openDatabase(config.databasePath);
const jobRunner = new JobRunner(database);
app.decorate("database", database);
app.decorate("jobRunner", jobRunner);
app.decorateRequest("authUser", null);
await app.register(cookie);
await app.register(rateLimit, { global: false });
app.addHook("onClose", async () => database.close());
app.addHook("onClose", async () => {
jobRunner.close();
database.close();
});
app.addHook("onSend", async (_request, reply) => {
reply.header("X-Content-Type-Options", "nosniff");
reply.header("Referrer-Policy", "no-referrer");
@@ -272,6 +278,39 @@ export async function buildApp(config: AppConfig): Promise<FastifyInstance> {
return collectSystemStatus();
});
app.get("/api/jobs", async (request, reply) => {
if (!requireUser(request, reply)) return;
return { jobs: jobRunner.listJobs() };
});
app.get("/api/job-runs", async (request, reply) => {
if (!requireUser(request, reply)) return;
return { runs: jobRunner.listRuns() };
});
app.get<{ Params: { id: string } }>("/api/job-runs/:id", async (request, reply) => {
if (!requireUser(request, reply)) return;
const run = jobRunner.getRun(request.params.id);
if (!run) return reply.code(404).send({ error: { code: "RUN_NOT_FOUND", message: "Job run not found" } });
return run;
});
app.post<{ Params: { jobId: string } }>("/api/jobs/:jobId/runs", async (request, reply) => {
if (!requireUser(request, reply)) return;
if (!jobRunner.has(request.params.jobId)) {
return reply.code(404).send({ error: { code: "JOB_NOT_FOUND", message: "Job is not available" } });
}
const run = jobRunner.enqueue(request.params.jobId, request.authUser!.id);
audit(database, {
actorUserId: request.authUser!.id,
action: "job.enqueue",
result: "success",
ipAddress: clientAddress(request),
details: { jobId: request.params.jobId, runId: run.id }
});
return reply.code(202).send(run);
});
app.get("/api/system/update", async (request, reply) => {
if (!requireUser(request, reply)) return;
return readUpdateStatus(config);
@@ -338,9 +377,13 @@ export async function buildApp(config: AppConfig): Promise<FastifyInstance> {
};
await sendStatus();
const interval = setInterval(() => void sendStatus(), 10_000);
const stopJobEvents = jobRunner.onRun((run) => {
if (!closed) reply.raw.write(`event: job-run\ndata: ${JSON.stringify(run)}\n\n`);
});
request.raw.on("close", () => {
closed = true;
clearInterval(interval);
stopJobEvents();
});
});