From 6b7786b48509b6df953738c350772c3805b038a7 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sat, 7 Mar 2026 11:08:45 +0000 Subject: [PATCH 01/10] feat: add maintenanceMode fields to VectorNode schema --- .../20260307000000_add_node_maintenance_mode/migration.sql | 3 +++ prisma/schema.prisma | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 prisma/migrations/20260307000000_add_node_maintenance_mode/migration.sql diff --git a/prisma/migrations/20260307000000_add_node_maintenance_mode/migration.sql b/prisma/migrations/20260307000000_add_node_maintenance_mode/migration.sql new file mode 100644 index 00000000..a7ad5937 --- /dev/null +++ b/prisma/migrations/20260307000000_add_node_maintenance_mode/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "VectorNode" ADD COLUMN "maintenanceMode" BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE "VectorNode" ADD COLUMN "maintenanceModeAt" TIMESTAMP(3); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 74048c4f..f0db424c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -105,6 +105,8 @@ model VectorNode { os String? deploymentMode DeploymentMode @default(UNKNOWN) pendingAction Json? + maintenanceMode Boolean @default(false) + maintenanceModeAt DateTime? pipelineStatuses NodePipelineStatus[] nodeMetrics NodeMetric[] pipelineLogs PipelineLog[] From 96ebb6f49f1b6c78d80505417f0affd460ed001e Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sat, 7 Mar 2026 11:09:47 +0000 Subject: [PATCH 02/10] feat: add setMaintenanceMode mutation to fleet router --- src/server/routers/fleet.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/server/routers/fleet.ts b/src/server/routers/fleet.ts index ce038870..14e9160b 100644 --- a/src/server/routers/fleet.ts +++ b/src/server/routers/fleet.ts @@ -264,6 +264,31 @@ export const fleetRouter = router({ }); }), + setMaintenanceMode: protectedProcedure + .input( + z.object({ + nodeId: z.string(), + enabled: z.boolean(), + }), + ) + .use(withTeamAccess("ADMIN")) + .use(withAudit("node.maintenance_toggled", "VectorNode")) + .mutation(async ({ input }) => { + const node = await prisma.vectorNode.findUnique({ + where: { id: input.nodeId }, + }); + if (!node) { + throw new TRPCError({ code: "NOT_FOUND", message: "Node not found" }); + } + return prisma.vectorNode.update({ + where: { id: input.nodeId }, + data: { + maintenanceMode: input.enabled, + maintenanceModeAt: input.enabled ? new Date() : null, + }, + }); + }), + listWithPipelineStatus: protectedProcedure .input(z.object({ environmentId: z.string() })) .use(withTeamAccess("VIEWER")) From 69a355ad8cb86a36ec41003a031e46e71c80a5d3 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sat, 7 Mar 2026 11:10:57 +0000 Subject: [PATCH 03/10] feat: return empty pipeline list for nodes in maintenance mode --- src/app/api/agent/config/route.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/app/api/agent/config/route.ts b/src/app/api/agent/config/route.ts index 7dec76ed..14543ea6 100644 --- a/src/app/api/agent/config/route.ts +++ b/src/app/api/agent/config/route.ts @@ -16,9 +16,26 @@ export async function GET(request: Request) { // Fetch the node to check for pending actions (e.g., self-update) const node = await prisma.vectorNode.findUnique({ where: { id: agent.nodeId }, - select: { pendingAction: true }, + select: { pendingAction: true, maintenanceMode: true }, }); + if (node?.maintenanceMode) { + const environment = await prisma.environment.findUnique({ + where: { id: agent.environmentId }, + select: { secretBackend: true, secretBackendConfig: true }, + }); + const settings = await prisma.systemSettings.findUnique({ + where: { id: "singleton" }, + select: { fleetPollIntervalMs: true }, + }); + return NextResponse.json({ + pipelines: [], + pollIntervalMs: settings?.fleetPollIntervalMs ?? 15_000, + secretBackend: environment?.secretBackend ?? "BUILTIN", + pendingAction: node.pendingAction ?? undefined, + }); + } + const environment = await prisma.environment.findUnique({ where: { id: agent.environmentId }, select: { From f1ab2dc1efe8d7356d11facaf0030e40680cd1e3 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sat, 7 Mar 2026 11:11:16 +0000 Subject: [PATCH 04/10] feat: dim deployment matrix columns for nodes in maintenance mode --- src/components/fleet/deployment-matrix.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/fleet/deployment-matrix.tsx b/src/components/fleet/deployment-matrix.tsx index b10da0a7..1fc0b58e 100644 --- a/src/components/fleet/deployment-matrix.tsx +++ b/src/components/fleet/deployment-matrix.tsx @@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query"; import { useTRPC } from "@/trpc/client"; import { Badge } from "@/components/ui/badge"; -import { Minus } from "lucide-react"; +import { Minus, Wrench } from "lucide-react"; import Link from "next/link"; import { StatusDot } from "@/components/ui/status-dot"; import { pipelineStatusVariant, pipelineStatusLabel } from "@/lib/status"; @@ -51,10 +51,18 @@ export function DeploymentMatrix({ environmentId }: DeploymentMatrixProps) { {nodes.map((node) => (
{node.name}
{node.host}
+ {node.maintenanceMode && ( +
+ + Maintenance +
+ )} ))} @@ -79,7 +87,7 @@ export function DeploymentMatrix({ environmentId }: DeploymentMatrixProps) { if (!ps) { return ( - +
@@ -90,7 +98,7 @@ export function DeploymentMatrix({ environmentId }: DeploymentMatrixProps) { const isOutdated = ps.version < pipeline.latestVersion; return ( - +
{isOutdated ? (
Date: Sat, 7 Mar 2026 11:12:53 +0000 Subject: [PATCH 05/10] feat: add maintenance mode toggle to fleet table --- src/app/(dashboard)/fleet/page.tsx | 106 ++++++++++++++++++++--------- 1 file changed, 72 insertions(+), 34 deletions(-) diff --git a/src/app/(dashboard)/fleet/page.tsx b/src/app/(dashboard)/fleet/page.tsx index e74d6483..5ac1440c 100644 --- a/src/app/(dashboard)/fleet/page.tsx +++ b/src/app/(dashboard)/fleet/page.tsx @@ -23,6 +23,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { Skeleton } from "@/components/ui/skeleton"; +import { Wrench } from "lucide-react"; import { DeploymentMatrix } from "@/components/fleet/deployment-matrix"; import { formatLastSeen } from "@/lib/format"; import { nodeStatusVariant, nodeStatusLabel } from "@/lib/status"; @@ -87,6 +88,14 @@ export default function FleetPage() { }), ); + const setMaintenance = useMutation( + trpc.fleet.setMaintenanceMode.mutationOptions({ + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: trpc.fleet.list.queryKey() }); + }, + }), + ); + return (
{isLoading ? ( @@ -161,54 +170,83 @@ export default function FleetPage() {
- - {nodeStatusLabel(node.status)} - + {node.maintenanceMode ? ( + + + Maintenance + + ) : ( + + {nodeStatusLabel(node.status)} + + )} {formatLastSeen(node.lastSeen)} - {node.pendingAction ? ( - - Update pending... - - ) : node.deploymentMode === "DOCKER" ? ( - getNodeLatest(node).version && - node.agentVersion && - isVersionOlder(node.agentVersion, getNodeLatest(node).version ?? "") ? ( - - - - - - - Update via Docker image pull - - ) : null - ) : getNodeLatest(node).version && - node.agentVersion && - isVersionOlder(node.agentVersion, getNodeLatest(node).version ?? "") ? ( +
- ) : null} + {node.pendingAction ? ( + + Update pending... + + ) : node.deploymentMode === "DOCKER" ? ( + getNodeLatest(node).version && + node.agentVersion && + isVersionOlder(node.agentVersion, getNodeLatest(node).version ?? "") ? ( + + + + + + + Update via Docker image pull + + ) : null + ) : getNodeLatest(node).version && + node.agentVersion && + isVersionOlder(node.agentVersion, getNodeLatest(node).version ?? "") ? ( + + ) : null} +
))} From 244bf508dae636baa364dd70198a2c010f09202f Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sat, 7 Mar 2026 11:14:21 +0000 Subject: [PATCH 06/10] feat: add maintenance mode toggle and banner to agent detail page --- src/app/(dashboard)/fleet/[nodeId]/page.tsx | 61 ++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/src/app/(dashboard)/fleet/[nodeId]/page.tsx b/src/app/(dashboard)/fleet/[nodeId]/page.tsx index 04e8ac8d..7bc57636 100644 --- a/src/app/(dashboard)/fleet/[nodeId]/page.tsx +++ b/src/app/(dashboard)/fleet/[nodeId]/page.tsx @@ -3,7 +3,7 @@ import { useParams, useRouter } from "next/navigation"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useTRPC } from "@/trpc/client"; -import { ArrowLeft, ShieldOff, Trash2, Activity, Terminal, Server, Pencil, Check, X } from "lucide-react"; +import { ArrowLeft, ShieldOff, Trash2, Activity, Terminal, Server, Pencil, Check, X, Wrench } from "lucide-react"; import { NodeLogs } from "@/components/fleet/node-logs"; import { toast } from "sonner"; import { useState } from "react"; @@ -131,6 +131,30 @@ export default function NodeDetailPage() { }) ); + const maintenanceMutation = useMutation( + trpc.fleet.setMaintenanceMode.mutationOptions({ + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: trpc.fleet.get.queryKey() }); + }, + }), + ); + + function handleMaintenanceToggle() { + if (!node) return; + if (!node.maintenanceMode) { + const runningCount = node.pipelineStatuses.filter( + (s) => s.status === "RUNNING" + ).length; + if (!confirm( + `Enter maintenance mode for "${node.name}"?\n\nThis will stop ${runningCount} running pipeline(s) on this node. Pipelines will automatically resume when maintenance mode is turned off.` + )) return; + } + maintenanceMutation.mutate({ + nodeId: node.id, + enabled: !node.maintenanceMode, + }); + } + function handleRevoke() { if (!node) return; if (!confirm(`Revoke token for "${node.name}"? The agent will no longer be able to connect.`)) { @@ -217,6 +241,19 @@ export default function NodeDetailPage() {
+ {node.nodeTokenHash && (
+ {node.maintenanceMode && ( +
+ +
+

+ This node is in maintenance mode +

+

+ All pipelines are stopped. They will automatically resume when maintenance mode is turned off. +

+
+ +
+ )} +
{/* Node Details */} From 62feccafbb70d61f21694e794e2c98d51797223e Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sat, 7 Mar 2026 11:16:52 +0000 Subject: [PATCH 07/10] fix: pass nodeId to queryKey in maintenance mutation invalidation --- src/app/(dashboard)/fleet/[nodeId]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(dashboard)/fleet/[nodeId]/page.tsx b/src/app/(dashboard)/fleet/[nodeId]/page.tsx index 7bc57636..89c78364 100644 --- a/src/app/(dashboard)/fleet/[nodeId]/page.tsx +++ b/src/app/(dashboard)/fleet/[nodeId]/page.tsx @@ -134,7 +134,7 @@ export default function NodeDetailPage() { const maintenanceMutation = useMutation( trpc.fleet.setMaintenanceMode.mutationOptions({ onSuccess: () => { - queryClient.invalidateQueries({ queryKey: trpc.fleet.get.queryKey() }); + queryClient.invalidateQueries({ queryKey: trpc.fleet.get.queryKey({ id: params.nodeId }) }); }, }), ); From c81f68581cc008804361cf7593e1bef986963195 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sat, 7 Mar 2026 11:22:09 +0000 Subject: [PATCH 08/10] =?UTF-8?q?fix:=20review=20cleanup=20=E2=80=94=20dro?= =?UTF-8?q?p=20unused=20select=20field,=20invalidate=20deployment=20matrix?= =?UTF-8?q?=20on=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(dashboard)/fleet/page.tsx | 1 + src/app/api/agent/config/route.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/(dashboard)/fleet/page.tsx b/src/app/(dashboard)/fleet/page.tsx index 5ac1440c..2296f0bc 100644 --- a/src/app/(dashboard)/fleet/page.tsx +++ b/src/app/(dashboard)/fleet/page.tsx @@ -92,6 +92,7 @@ export default function FleetPage() { trpc.fleet.setMaintenanceMode.mutationOptions({ onSuccess: () => { queryClient.invalidateQueries({ queryKey: trpc.fleet.list.queryKey() }); + queryClient.invalidateQueries({ queryKey: trpc.fleet.listWithPipelineStatus.queryKey() }); }, }), ); diff --git a/src/app/api/agent/config/route.ts b/src/app/api/agent/config/route.ts index 14543ea6..4f3beded 100644 --- a/src/app/api/agent/config/route.ts +++ b/src/app/api/agent/config/route.ts @@ -22,7 +22,7 @@ export async function GET(request: Request) { if (node?.maintenanceMode) { const environment = await prisma.environment.findUnique({ where: { id: agent.environmentId }, - select: { secretBackend: true, secretBackendConfig: true }, + select: { secretBackend: true }, }); const settings = await prisma.systemSettings.findUnique({ where: { id: "singleton" }, From 75836efc46c30c6fad373f37c32863a9d06bc40c Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sat, 7 Mar 2026 11:32:47 +0000 Subject: [PATCH 09/10] fix: scope maintenance button disabled state to targeted node --- src/app/(dashboard)/fleet/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(dashboard)/fleet/page.tsx b/src/app/(dashboard)/fleet/page.tsx index 2296f0bc..b20644e7 100644 --- a/src/app/(dashboard)/fleet/page.tsx +++ b/src/app/(dashboard)/fleet/page.tsx @@ -190,7 +190,7 @@ export default function FleetPage() {