From 8c60c1dc1b5091e842f9c697f2bee78fc83d5dad Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 16 Sep 2026 11:49:31 +0200 Subject: [PATCH] feat(admin): repository linking, webhook setup and backfill --- src/app/admin/projects/[projectId]/page.tsx | 57 ++++++++ src/app/admin/projects/page.tsx | 8 +- src/components/admin/ingest-panel.tsx | 142 ++++++++++++++++++++ 3 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 src/app/admin/projects/[projectId]/page.tsx create mode 100644 src/components/admin/ingest-panel.tsx diff --git a/src/app/admin/projects/[projectId]/page.tsx b/src/app/admin/projects/[projectId]/page.tsx new file mode 100644 index 0000000..0065de6 --- /dev/null +++ b/src/app/admin/projects/[projectId]/page.tsx @@ -0,0 +1,57 @@ +import { notFound } from "next/navigation"; + +import { AppShell } from "@/components/shell/app-shell"; +import { TopBar } from "@/components/shell/top-bar"; +import { IngestPanel } from "@/components/admin/ingest-panel"; +import { getAdminPb } from "@/lib/pb/admin"; +import type { ProjectRecord } from "@/lib/pb/types"; + +/** + * The admin layout has already refused anyone who is not the owner. + * + * `webhook_secret` is read here but only its presence crosses into the client + * component — never the value. PocketBase rules are record-level and cannot + * hide one field, so stripping it is this page's job. + */ +export default async function ProjectAdminPage({ + params, +}: { + params: Promise<{ projectId: string }>; +}) { + const { projectId } = await params; + + const pb = await getAdminPb(); + + let project: ProjectRecord; + try { + project = await pb.collection("projects").getOne(projectId); + } catch { + notFound(); + } + + const commits = await pb + .collection("commits") + .getList(1, 1, { filter: `project = "${projectId}"`, fields: "id" }); + + const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"; + + return ( + + + +
+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/admin/projects/page.tsx b/src/app/admin/projects/page.tsx index 9bf3cc2..a6fc905 100644 --- a/src/app/admin/projects/page.tsx +++ b/src/app/admin/projects/page.tsx @@ -28,11 +28,17 @@ export default async function AdminProjectsPage() { {project.name} + + Board + {client.name} diff --git a/src/components/admin/ingest-panel.tsx b/src/components/admin/ingest-panel.tsx new file mode 100644 index 0000000..3751606 --- /dev/null +++ b/src/components/admin/ingest-panel.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { Copy, RefreshCw, Download } from "lucide-react"; + +import { backfillProject, rotateWebhookSecret } from "@/app/actions/ingest"; + +export function IngestPanel({ + projectId, + repoOwner, + repoName, + hasSecret, + webhookUrl, + lastIngestAt, + commitCount, +}: { + projectId: string; + repoOwner: string; + repoName: string; + hasSecret: boolean; + webhookUrl: string; + lastIngestAt: string; + commitCount: number; +}) { + const [pending, startTransition] = useTransition(); + const [secret, setSecret] = useState(null); + const [status, setStatus] = useState(null); + const [error, setError] = useState(null); + + const linked = Boolean(repoOwner && repoName); + + const field = + "w-full rounded-lg border border-border-subtle bg-surface px-3 py-2 text-xs text-text-secondary"; + + return ( +
+
+

+ Commit ingestion +

+

+ {linked ? ( + <> + Linked to {repoOwner}/{repoName} ·{" "} + {commitCount} commits stored + {lastIngestAt && <> · last synced {lastIngestAt.slice(0, 16).replace("T", " ")}} + + ) : ( + "No repository linked. Set the owner and name on this project first." + )} +

+
+ + {linked && ( + <> +
+ Webhook URL +
+ + +
+

+ In Forgejo: Settings → Webhooks → Add Webhook → Gitea. Method POST, + content type application/json, trigger on Push. +

+
+ +
+ + Webhook secret {hasSecret && !secret && "(set)"} + +
+ {secret ? ( + + ) : ( + + {hasSecret + ? "A secret is configured. Rotating it requires updating Forgejo." + : "No secret yet. The webhook will reject every delivery until one is set."} + + )} + +
+
+ +
+ + + {status && {status}} + {error && ( + + {error} + + )} +
+ + )} +
+ ); +} \ No newline at end of file