diff --git a/docker/Dockerfile.server b/docker/Dockerfile.server index e2ee4bc73..e908161ed 100644 --- a/docker/Dockerfile.server +++ b/docker/Dockerfile.server @@ -43,7 +43,8 @@ ENV MIGRATIONS_DIR=/app/db/migrations ENV PORT=8080 # curl is only needed by the API healthcheck below; workers don't use it. -RUN apk add --no-cache curl +# git is needed by cron tasks that clone the seeds repo (BMS table sync, backsync, etc.). +RUN apk add --no-cache curl git WORKDIR /app COPY --from=deps /app . diff --git a/typescript/client/src/app/pages/dashboard/DashboardPage.tsx b/typescript/client/src/app/pages/dashboard/DashboardPage.tsx index 7eab6fcc4..fab15c81d 100644 --- a/typescript/client/src/app/pages/dashboard/DashboardPage.tsx +++ b/typescript/client/src/app/pages/dashboard/DashboardPage.tsx @@ -11,7 +11,6 @@ import GoalLink from "#components/util/GoalLink"; import LinkButton from "#components/util/LinkButton"; import Loading from "#components/util/Loading"; import useApiQuery from "#components/util/query/useApiQuery"; -import WorkerVisualizer from "#components/workers/WorkerVisualizer"; import { UserContext } from "#context/UserContext"; import { UserSettingsContext } from "#context/UserSettingsContext"; import { WindowContext } from "#context/WindowContext"; @@ -72,8 +71,6 @@ function DashboardLoggedIn({ user }: { user: UserDocument }) { - -
diff --git a/typescript/server/src/lib/admin/admin-queries.test.ts b/typescript/server/src/lib/admin/admin-queries.test.ts new file mode 100644 index 000000000..6b118a269 --- /dev/null +++ b/typescript/server/src/lib/admin/admin-queries.test.ts @@ -0,0 +1,54 @@ +import DB from "#services/pg/db"; +import { afterEach, describe, expect, it } from "vitest"; + +import { GetCronTaskExecutions } from "./admin-queries"; + +const TEST_CHATTY_TASK = "test-cron-chatty-task"; +const TEST_DAILY_TASK = "test-cron-daily-task"; + +async function cleanupTestExecutions() { + await DB.deleteFrom("cron_task_execution") + .where("task_id", "in", [TEST_CHATTY_TASK, TEST_DAILY_TASK]) + .execute(); +} + +describe("GetCronTaskExecutions", () => { + afterEach(cleanupTestExecutions); + + it("caps results per task so a high-frequency task cannot crowd out other tasks", async () => { + const now = Date.now(); + const isoAt = (offsetMs: number) => new Date(now - offsetMs).toISOString(); + + // Insert 25 executions for a chatty minutely task — exceeds the 20-per-task cap. + const chattyValues = Array.from({ length: 25 }, (_, i) => ({ + task_id: TEST_CHATTY_TASK, + scheduled_at: isoAt(i * 60_000), + status: "success" as const, + completed_at: isoAt(i * 60_000 - 1_000), + output: null, + error: null, + })); + + // Insert 3 executions for a daily task. These must not be crowded out. + const dailyValues = Array.from({ length: 3 }, (_, i) => ({ + task_id: TEST_DAILY_TASK, + scheduled_at: isoAt(i * 24 * 60 * 60_000), + status: "success" as const, + completed_at: isoAt(i * 24 * 60 * 60_000 - 1_000), + output: null, + error: null, + })); + + await DB.insertInto("cron_task_execution") + .values([...chattyValues, ...dailyValues]) + .execute(); + + const results = await GetCronTaskExecutions(); + + const chatty = results.filter((r) => r.task_id === TEST_CHATTY_TASK); + const daily = results.filter((r) => r.task_id === TEST_DAILY_TASK); + + expect(chatty).toHaveLength(20); + expect(daily).toHaveLength(3); + }); +}); diff --git a/typescript/server/src/lib/admin/admin-queries.ts b/typescript/server/src/lib/admin/admin-queries.ts index c5b749dc3..ca229c941 100644 --- a/typescript/server/src/lib/admin/admin-queries.ts +++ b/typescript/server/src/lib/admin/admin-queries.ts @@ -1,12 +1,9 @@ import type { Action, CronTask, CronTaskExecution, JobQueue } from "tachi-db"; import { ONE_HOUR } from "#lib/constants/time"; -import { - SELECT_CRON_TASK, - SELECT_CRON_TASK_EXECUTION, - SELECT_JOB_QUEUE, -} from "#lib/db-formats/admin-jobs"; +import { SELECT_CRON_TASK, SELECT_JOB_QUEUE } from "#lib/db-formats/admin-jobs"; import DB from "#services/pg/db"; +import { sql } from "kysely"; export const ADMIN_PAGE_SIZE = 50; @@ -161,11 +158,22 @@ export function GetCronTasks(): Promise> { .execute(); } -export function GetCronTaskExecutions(): Promise> { - return DB.selectFrom("cron_task_execution") - .select(SELECT_CRON_TASK_EXECUTION) - .where("cron_task_execution.scheduled_at", ">=", adminRecentSinceIso(24 * 7)) - .orderBy("cron_task_execution.scheduled_at", "desc") - .limit(100) - .execute(); +/** Most recent executions per task, capped at 20 per task_id to prevent high-frequency tasks from crowding out others. */ +export async function GetCronTaskExecutions(): Promise> { + const since = adminRecentSinceIso(24 * 7); + + const rows = await sql` + SELECT id, task_id, scheduled_at, started_at, completed_at, status, output, error + FROM ( + SELECT + id, task_id, scheduled_at, started_at, completed_at, status, output, error, + ROW_NUMBER() OVER (PARTITION BY task_id ORDER BY scheduled_at DESC) AS rn + FROM cron_task_execution + WHERE scheduled_at >= ${since} + ) ranked + WHERE rn <= 20 + ORDER BY scheduled_at DESC + `.execute(DB); + + return rows.rows; }