From 8551a6f8930a5c86e12e4f7ce969edea6ca641ca Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 21 Sep 2026 11:48:56 +0200 Subject: [PATCH] feat(admin): background markdown editor with character cap Phase 8c Task 5. Server Action caps at 40000 chars on write, owner- only via requireOwner(). Client component: one textarea, live counter, explicit Save button (no auto-save), repo URL rendered as a 'see also' link rather than a tool call. --- src/app/actions/project-background.ts | 44 ++++++ src/components/admin/background-md-editor.tsx | 128 ++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 src/app/actions/project-background.ts create mode 100644 src/components/admin/background-md-editor.tsx diff --git a/src/app/actions/project-background.ts b/src/app/actions/project-background.ts new file mode 100644 index 0000000..c1fd8b9 --- /dev/null +++ b/src/app/actions/project-background.ts @@ -0,0 +1,44 @@ +"use server"; + +import { revalidatePath } from "next/cache"; + +import { getAdminPb } from "@/lib/pb/admin"; +import { requireSession } from "@/lib/auth/session"; +import type { ProjectRecord } from "@/lib/pb/types"; + +const FIELD_CAP = 40_000; + +/** Owner-only entry point. Same pattern as log-work.ts. */ +async function requireOwner() { + const session = await requireSession(); + if (session.role !== "owner") throw new Error("Not found"); + return session; +} + +/** + * Save the project's background markdown. + * + * The 40000 field cap is the trip-wire: at that point the doc is too big + * for whole-doc prompting and we should revisit section-aware retrieval. + * Until then, the cap is the only validation we do here — anything the + * developer writes is the developer's responsibility. + */ +export async function setProjectBackgroundMdAction(input: { + projectId: string; + backgroundMd: string; +}): Promise { + await requireOwner(); + + const text = (input.backgroundMd ?? "").slice(0, FIELD_CAP); + + const pb = await getAdminPb(); + const project = await pb + .collection("projects") + .getOne(input.projectId); + + await pb.collection("projects").update(project.id, { + background_md: text, + }); + + revalidatePath(`/admin/projects/${input.projectId}`); +} diff --git a/src/components/admin/background-md-editor.tsx b/src/components/admin/background-md-editor.tsx new file mode 100644 index 0000000..f79c981 --- /dev/null +++ b/src/components/admin/background-md-editor.tsx @@ -0,0 +1,128 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { Save } from "lucide-react"; + +import { setProjectBackgroundMdAction } from "@/app/actions/project-background"; + +const FIELD_CAP = 40_000; + +/** + * The per-project background markdown editor. + * + * One textarea, live character counter, no preview pane. A preview pane + * invites half-maintained doc; the editor is the source of truth and the + * wizard is the consumer. Save is its own button — auto-save on every + * keystroke would hammer the admin pb with no upside. + * + * The helper text mentions the GitHub link as a "see also" — the URL is + * not turned into a tool call; the developer is expected to embed file + * paths in the markdown itself. + */ +export function BackgroundMdEditor({ + projectId, + initial, + repoUrl, +}: { + projectId: string; + initial: string; + repoUrl: string; +}) { + const [value, setValue] = useState(initial); + const [pending, startTransition] = useTransition(); + const [saved, setSaved] = useState(initial.length > 0); + const [error, setError] = useState(null); + + const dirty = value !== initial; + const over = value.length > FIELD_CAP; + + function save() { + setError(null); + startTransition(async () => { + try { + await setProjectBackgroundMdAction({ + projectId, + backgroundMd: value, + }); + setSaved(true); + } catch (e) { + setError(e instanceof Error ? e.message : "Could not save"); + } + }); + } + + const field = + "w-full rounded-lg border border-border-subtle bg-surface px-3 py-2 font-mono text-xs text-text-secondary outline-none focus:border-brand"; + + return ( +
+
+

+ Background notes for the assistant +

+

+ Plain markdown describing the app: what each section does today, + where it is allowed to go, what you would push back on. Sent into + every wizard prompt so the assistant answers clients as if it + knew the codebase. +

+
+ +