diff --git a/agent/Dockerfile b/agent/Dockerfile
index d50a94d3..f1ca08b6 100644
--- a/agent/Dockerfile
+++ b/agent/Dockerfile
@@ -8,6 +8,7 @@ ARG VERSION=dev
RUN CGO_ENABLED=0 go build -ldflags="-s -w -X github.com/TerrifiedBug/vectorflow/agent/internal/agent.Version=${VERSION}" -o /vf-agent .
# Stage 2: Download Vector
+# Keep this default in sync with src/lib/vector-version.ts (canonical source).
FROM alpine:3.21 AS vector
ARG VECTOR_VERSION=0.54.0
RUN apk add --no-cache curl && \
diff --git a/agent/install.sh b/agent/install.sh
index f5507b18..1b740acb 100755
--- a/agent/install.sh
+++ b/agent/install.sh
@@ -11,6 +11,7 @@ VECTOR_DATA_DIR="/var/lib/vector"
CONFIG_DIR="/etc/vectorflow"
ENV_FILE="${CONFIG_DIR}/agent.env"
SERVICE_NAME="vf-agent"
+# Keep this in sync with src/lib/vector-version.ts (canonical source).
VECTOR_VERSION="0.54.0"
# Defaults
diff --git a/docker/server/Dockerfile b/docker/server/Dockerfile
index 880fcc4e..e31c905d 100644
--- a/docker/server/Dockerfile
+++ b/docker/server/Dockerfile
@@ -11,8 +11,9 @@ RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile
# ---- Stage 2: Download Vector binary (cached unless VECTOR_VERSION changes) ----
+# Keep this default in sync with src/lib/vector-version.ts (canonical source).
FROM alpine:3.21 AS vector
-ARG VECTOR_VERSION=0.53.0
+ARG VECTOR_VERSION=0.54.0
RUN apk add --no-cache curl && \
curl -sSfL -o /tmp/vector.tar.gz \
"https://packages.timber.io/vector/${VECTOR_VERSION}/vector-${VECTOR_VERSION}-x86_64-unknown-linux-musl.tar.gz" && \
diff --git a/package.json b/package.json
index 14dec53a..b24ca9d3 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
+ "date-fns": "^4.1.0",
"diff": "^8.0.3",
"dotenv": "^17.3.1",
"ioredis": "^5.10.1",
@@ -53,6 +54,7 @@
"qrcode": "^1.5.4",
"radix-ui": "^1.4.3",
"react": "19.2.3",
+ "react-day-picker": "^9.14.0",
"react-dom": "19.2.3",
"react-grid-layout": "^2.2.2",
"react-hook-form": "^7.71.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 85f3af51..eb5ea512 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -78,6 +78,9 @@ importers:
cmdk:
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ date-fns:
+ specifier: ^4.1.0
+ version: 4.1.0
diff:
specifier: ^8.0.3
version: 8.0.3
@@ -129,6 +132,9 @@ importers:
react:
specifier: 19.2.3
version: 19.2.3
+ react-day-picker:
+ specifier: ^9.14.0
+ version: 9.14.0(react@19.2.3)
react-dom:
specifier: 19.2.3
version: 19.2.3(react@19.2.3)
@@ -661,6 +667,9 @@ packages:
'@dagrejs/graphlib@3.0.4':
resolution: {integrity: sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg==}
+ '@date-fns/tz@1.4.1':
+ resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
+
'@discoveryjs/json-ext@0.5.7':
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
engines: {node: '>=10.0.0'}
@@ -2956,6 +2965,10 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
+ '@tabby_ai/hijri-converter@1.0.5':
+ resolution: {integrity: sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==}
+ engines: {node: '>=16.0.0'}
+
'@tailwindcss/node@4.2.1':
resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==}
@@ -4012,6 +4025,12 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'}
+ date-fns-jalali@4.1.0-0:
+ resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==}
+
+ date-fns@4.1.0:
+ resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
+
debounce@1.2.1:
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
@@ -5963,6 +5982,12 @@ packages:
rc9@2.1.2:
resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==}
+ react-day-picker@9.14.0:
+ resolution: {integrity: sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ react: '>=16.8.0'
+
react-dom@19.2.3:
resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==}
peerDependencies:
@@ -7770,6 +7795,8 @@ snapshots:
'@dagrejs/graphlib@3.0.4': {}
+ '@date-fns/tz@1.4.1': {}
+
'@discoveryjs/json-ext@0.5.7': {}
'@dotenvx/dotenvx@1.52.0':
@@ -10190,6 +10217,8 @@ snapshots:
dependencies:
tslib: 2.8.1
+ '@tabby_ai/hijri-converter@1.0.5': {}
+
'@tailwindcss/node@4.2.1':
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -11289,6 +11318,10 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.2
+ date-fns-jalali@4.1.0-0: {}
+
+ date-fns@4.1.0: {}
+
debounce@1.2.1: {}
debug@3.2.7:
@@ -13339,6 +13372,14 @@ snapshots:
defu: 6.1.6
destr: 2.0.5
+ react-day-picker@9.14.0(react@19.2.3):
+ dependencies:
+ '@date-fns/tz': 1.4.1
+ '@tabby_ai/hijri-converter': 1.0.5
+ date-fns: 4.1.0
+ date-fns-jalali: 4.1.0-0
+ react: 19.2.3
+
react-dom@19.2.3(react@19.2.3):
dependencies:
react: 19.2.3
diff --git a/src/app/(dashboard)/alerts/_components/outbound-webhooks-section.tsx b/src/app/(dashboard)/alerts/_components/outbound-webhooks-section.tsx
new file mode 100644
index 00000000..711f8526
--- /dev/null
+++ b/src/app/(dashboard)/alerts/_components/outbound-webhooks-section.tsx
@@ -0,0 +1,890 @@
+"use client";
+
+import { useState } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { useTRPC } from "@/trpc/client";
+import { useTeamStore } from "@/stores/team-store";
+import { copyToClipboard } from "@/lib/utils";
+import { toast } from "sonner";
+import {
+ Plus,
+ Loader2,
+ Copy,
+ Trash2,
+ Webhook,
+ ShieldCheck,
+ Clock,
+ ChevronDown,
+ ChevronRight,
+ Play,
+ CheckCircle,
+ XCircle,
+ AlertCircle,
+ Pencil,
+ ToggleLeft,
+ ToggleRight,
+} from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Checkbox } from "@/components/ui/checkbox";
+import { EmptyState } from "@/components/empty-state";
+import { QueryError } from "@/components/query-error";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { ConfirmDialog } from "@/components/confirm-dialog";
+import type { AlertMetric } from "@/generated/prisma";
+
+// ─── Constants ──────────────────────────────────────────────────────────────
+
+/**
+ * Supported webhook event types with human-readable labels.
+ * Only the outbound-webhook-relevant subset of AlertMetric.
+ */
+const WEBHOOK_EVENT_TYPES: { value: AlertMetric; label: string; description: string }[] = [
+ {
+ value: "deploy_completed" as AlertMetric,
+ label: "Deploy Completed",
+ description: "A pipeline was successfully deployed",
+ },
+ {
+ value: "pipeline_crashed" as AlertMetric,
+ label: "Pipeline Crashed",
+ description: "A running pipeline process exited unexpectedly",
+ },
+ {
+ value: "node_unreachable" as AlertMetric,
+ label: "Node Unreachable",
+ description: "A fleet node stopped sending heartbeats",
+ },
+ {
+ value: "node_joined" as AlertMetric,
+ label: "Node Joined",
+ description: "A new fleet node enrolled",
+ },
+ {
+ value: "node_left" as AlertMetric,
+ label: "Node Left",
+ description: "A fleet node was removed",
+ },
+ {
+ value: "deploy_rejected" as AlertMetric,
+ label: "Deploy Rejected",
+ description: "A deployment request was rejected",
+ },
+ {
+ value: "deploy_cancelled" as AlertMetric,
+ label: "Deploy Cancelled",
+ description: "A pending deployment was cancelled",
+ },
+ {
+ value: "promotion_completed" as AlertMetric,
+ label: "Promotion Completed",
+ description: "A pipeline was promoted to another environment",
+ },
+];
+
+// ─── Helpers ─────────────────────────────────────────────────────────────────
+
+function formatRelativeTime(date: Date | string | null | undefined): string {
+ if (!date) return "Never";
+ const d = typeof date === "string" ? new Date(date) : date;
+ const diffMs = Date.now() - d.getTime();
+ const diffSec = Math.floor(diffMs / 1000);
+ if (diffSec < 60) return "Just now";
+ const diffMin = Math.floor(diffSec / 60);
+ if (diffMin < 60) return `${diffMin}m ago`;
+ const diffHr = Math.floor(diffMin / 60);
+ if (diffHr < 24) return `${diffHr}h ago`;
+ return `${Math.floor(diffHr / 24)}d ago`;
+}
+
+function deliveryStatusBadge(status: string) {
+ switch (status) {
+ case "success":
+ return (
+
+ Send HMAC-signed event notifications to external systems +
+ +