diff --git a/frontend/src/pages/service-tabs/JobsTab.tsx b/frontend/src/pages/service-tabs/JobsTab.tsx
new file mode 100644
index 0000000..e33f525
--- /dev/null
+++ b/frontend/src/pages/service-tabs/JobsTab.tsx
@@ -0,0 +1,87 @@
+/**
+ * JobsTab — operational content for the backups service page.
+ *
+ * Lifted from the old top-level `components/BackupsPage.tsx`. The three
+ * sub-tables (Jobs / Runs / Alerts) and their hooks are preserved verbatim.
+ *
+ * NOTE: the backup hooks currently query globally (no service_id filter).
+ * The backend gained `service_id` attribution in Slice 3, but the hooks don't
+ * yet accept a serviceId param. This tab shows ALL backups data for now;
+ * per-instance scoping by `instance.id` is a follow-up once the hooks gain the
+ * parameter.
+ */
+import { useState } from "react";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import {
+ useAcknowledgeAlert,
+ useBackupAlerts,
+ useBackupJobs,
+ useBackupRuns,
+} from "../../hooks/useBackups";
+import BackupAlertsTable from "../../components/BackupAlertsTable";
+import BackupJobsTable from "../../components/BackupJobsTable";
+import BackupRunsTable from "../../components/BackupRunsTable";
+import type { ServiceInstance } from "../../types";
+
+export function JobsTab({ instance }: { instance: ServiceInstance }) {
+ // instance.id is not yet used — backup hooks query globally (see file
+ // docstring). Per-instance scoping is a follow-up.
+ void instance;
+ const [tab, setTab] = useState("jobs");
+ const { data: jobsData, isLoading: jobsLoading } = useBackupJobs();
+ const { data: runsData, isLoading: runsLoading } = useBackupRuns();
+ const { data: alertsData, isLoading: alertsLoading } = useBackupAlerts(
+ undefined,
+ false,
+ );
+ const acknowledgeMutation = useAcknowledgeAlert();
+
+ // Build a map of latest runs per job
+ const latestRuns = new Map();
+ if (runsData) {
+ for (const run of runsData) {
+ const existing = latestRuns.get(run.job_id);
+ if (!existing || run.started_at > existing.started_at) {
+ latestRuns.set(run.job_id, run);
+ }
+ }
+ }
+
+ const alertsLabel = alertsData ? `Alerts (${alertsData.length})` : "Alerts";
+
+ return (
+
+
+
+ Jobs
+ Runs
+ {alertsLabel}
+
+
+ {jobsLoading ? (
+ Loading jobs…
+ ) : (
+
+ )}
+
+
+ {runsLoading ? (
+ Loading runs…
+ ) : (
+
+ )}
+
+
+ {alertsLoading ? (
+ Loading alerts…
+ ) : (
+ acknowledgeMutation.mutate(id)}
+ />
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/pages/service-tabs/__tests__/JobsTab.test.tsx b/frontend/src/pages/service-tabs/__tests__/JobsTab.test.tsx
new file mode 100644
index 0000000..dec3b38
--- /dev/null
+++ b/frontend/src/pages/service-tabs/__tests__/JobsTab.test.tsx
@@ -0,0 +1,58 @@
+import { describe, it, expect, vi } from "vitest";
+import { render, screen } from "@testing-library/react";
+import { JobsTab } from "../JobsTab";
+import type { ServiceInstance } from "../../../types";
+
+const instance: ServiceInstance = {
+ id: "bkp-1",
+ service_type: "backups",
+ name: "Main Backups",
+ config: { ingestion_label: "default" },
+ secrets_set: {},
+ enabled: true,
+ created_at: 1_700_000_000,
+ updated_at: 1_700_000_000,
+};
+
+vi.mock("../../../hooks/useBackups", () => ({
+ useBackupJobs: () => ({
+ data: [
+ {
+ id: "job-1",
+ name: "nightly",
+ source: "/data",
+ target: "s3://bucket",
+ schedule_interval_seconds: 86400,
+ created_at: 1_700_000_000,
+ },
+ ],
+ isLoading: false,
+ }),
+ useBackupRuns: () => ({
+ data: [],
+ isLoading: false,
+ }),
+ useBackupAlerts: () => ({
+ data: [],
+ isLoading: false,
+ }),
+ useAcknowledgeAlert: () => ({ mutate: vi.fn() }),
+}));
+
+function renderTab() {
+ return render();
+}
+
+describe("JobsTab", () => {
+ it("renders the Jobs, Runs, and Alerts sub-tabs", () => {
+ renderTab();
+ expect(screen.getByRole("tab", { name: "Jobs" })).toBeInTheDocument();
+ expect(screen.getByRole("tab", { name: "Runs" })).toBeInTheDocument();
+ expect(screen.getByRole("tab", { name: /Alerts/ })).toBeInTheDocument();
+ });
+
+ it("renders the backup job name in the Jobs tab", () => {
+ renderTab();
+ expect(screen.getByText("nightly")).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/pages/service-tabs/index.ts b/frontend/src/pages/service-tabs/index.ts
index 3b35b01..882ad1a 100644
--- a/frontend/src/pages/service-tabs/index.ts
+++ b/frontend/src/pages/service-tabs/index.ts
@@ -8,7 +8,6 @@ import type { ComponentType } from "react";
import type { ServiceInstance } from "../../types";
import {
AlertsTab,
- JobsTab,
LinksTab,
MessagingTab,
MetricsTab,
@@ -19,6 +18,7 @@ import { MediaTab } from "./MediaTab";
import { RequestsTab } from "./RequestsTab";
import { FilesTab } from "./FilesTab";
import { ActionsTab } from "./ActionsTab";
+import { JobsTab } from "./JobsTab";
export type ServiceTabComponent = ComponentType<{ instance: ServiceInstance }>;
diff --git a/frontend/src/pages/service-tabs/stubs.tsx b/frontend/src/pages/service-tabs/stubs.tsx
index d16fe35..0a111d4 100644
--- a/frontend/src/pages/service-tabs/stubs.tsx
+++ b/frontend/src/pages/service-tabs/stubs.tsx
@@ -28,10 +28,6 @@ export function OverviewTab({ instance }: { instance: ServiceInstance }) {
return ;
}
-export function JobsTab({ instance }: { instance: ServiceInstance }) {
- return ;
-}
-
export function UsersTab({ instance }: { instance: ServiceInstance }) {
return ;
}