diff --git a/backend/prisma/migrations/20251130120000_add_matching_runs_audit/migration.sql b/backend/prisma/migrations/20251130120000_add_matching_runs_audit/migration.sql
new file mode 100644
index 0000000..929cd1a
--- /dev/null
+++ b/backend/prisma/migrations/20251130120000_add_matching_runs_audit/migration.sql
@@ -0,0 +1,21 @@
+-- CreateTable
+CREATE TABLE "matching_runs" (
+ "id" SERIAL NOT NULL,
+ "event_id" INTEGER NOT NULL,
+ "trigger" VARCHAR(20) NOT NULL,
+ "status" VARCHAR(20) NOT NULL DEFAULT 'running',
+ "started_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "ended_at" TIMESTAMP(3),
+ "matched_count" INTEGER NOT NULL DEFAULT 0,
+ "not_found_count" INTEGER NOT NULL DEFAULT 0,
+ "error" TEXT,
+
+ CONSTRAINT "matching_runs_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "matching_runs_event_id_started_at_idx" ON "matching_runs"("event_id", "started_at");
+
+-- AddForeignKey
+ALTER TABLE "matching_runs" ADD CONSTRAINT "matching_runs_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "events"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
diff --git a/frontend/src/components/events/MatchingRunsSection.jsx b/frontend/src/components/events/MatchingRunsSection.jsx
new file mode 100644
index 0000000..cbfcd43
--- /dev/null
+++ b/frontend/src/components/events/MatchingRunsSection.jsx
@@ -0,0 +1,125 @@
+import { useEffect, useState } from 'react';
+import { adminAPI } from '../../services/api';
+import { RefreshCcw, Play } from 'lucide-react';
+
+export default function MatchingRunsSection({ slug }) {
+ const [runs, setRuns] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const [running, setRunning] = useState(false);
+
+ const loadRuns = async () => {
+ try {
+ setLoading(true);
+ setError('');
+ const res = await adminAPI.getMatchingRuns(slug, 20);
+ setRuns(res.data || []);
+ } catch (err) {
+ console.error('Failed to load matching runs', err);
+ setError(err.message || 'Failed to load matching runs');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadRuns();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [slug]);
+
+ const handleRunNow = async () => {
+ try {
+ setRunning(true);
+ setError('');
+ await adminAPI.runMatchingNow(slug);
+ await loadRuns();
+ } catch (err) {
+ console.error('Failed to run matching now', err);
+ setError(err.message || 'Failed to run matching');
+ } finally {
+ setRunning(false);
+ }
+ };
+
+ const formatDateTime = (dt) => dt ? new Date(dt).toLocaleString() : '-';
+
+ return (
+ Matching Runs
+
+
+
+
+
+
+
+ {loading ? (
+ Started
+ Ended
+ Trigger
+ Status
+ Matched
+ Not found
+
+
+ ) : runs.length === 0 ? (
+ Loading...
+
+
+ ) : (
+ runs.map((run) => (
+ No runs yet
+
+
+ ))
+ )}
+
+ {formatDateTime(run.startedAt)}
+ {formatDateTime(run.endedAt)}
+ {run.trigger}
+
+
+ {run.status}
+
+
+ {run.matchedCount}
+ {run.notFoundCount}
+