diff --git a/src/app/actions/narrative.ts b/src/app/actions/narrative.ts
new file mode 100644
index 0000000..d79f6ae
--- /dev/null
+++ b/src/app/actions/narrative.ts
@@ -0,0 +1,31 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+
+import { requireSession } from "@/lib/auth/session";
+import { narrateProject } from "@/lib/ai/narrate";
+
+/** See the security note at the top of src/app/actions/cards.ts. */
+export async function narrateProjectAction(input: {
+ projectId: string;
+}): Promise<{ written: number; message: string }> {
+ const session = await requireSession();
+ if (session.role !== "owner") throw new Error("Not found");
+
+ const result = await narrateProject(input.projectId);
+ revalidatePath(`/admin/projects/${input.projectId}`);
+
+ if (result.skipped) {
+ return { written: 0, message: result.error ?? "No AI provider is configured." };
+ }
+ if (result.considered === 0) {
+ return { written: 0, message: "Every narrative is up to date." };
+ }
+
+ return {
+ written: result.written,
+ message: `${result.written} of ${result.considered} narratives written${
+ result.error ? ` · ${result.error}` : ""
+ }`,
+ };
+}
diff --git a/src/components/admin/summary-panel.tsx b/src/components/admin/summary-panel.tsx
index d1a161d..e05cc41 100644
--- a/src/components/admin/summary-panel.tsx
+++ b/src/components/admin/summary-panel.tsx
@@ -1,12 +1,13 @@
"use client";
import { useState, useTransition } from "react";
-import { Sparkles, RotateCcw } from "lucide-react";
+import { Sparkles, RotateCcw, BookText } from "lucide-react";
import {
retryFailedSummariesAction,
summariseProjectAction,
} from "@/app/actions/summarise";
+import { narrateProjectAction } from "@/app/actions/narrative";
export function SummaryPanel({
projectId,
@@ -83,6 +84,22 @@ export function SummaryPanel({
)}
+
+
{message && {message}}