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;
}