From 226f8576f20ac0d9654cb8bd97b01ae2bda0cfe6 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 17 Sep 2026 09:08:02 +0200 Subject: [PATCH] fix(board): pin the DndContext id to stop a hydration mismatch dnd-kit derives the id of its accessibility description element from an internal counter that starts fresh on the server and is already advanced on the client. Every sortable card rendered aria-describedby="DndDescribedBy-0" server-side and "DndDescribedBy-3" client-side, and React reported a hydration mismatch across the whole board. A stable id makes both sides agree. Co-Authored-By: Claude Opus 5 --- runsum.mts | 119 +++++++++++++++++++++++++++++++++ src/components/board/board.tsx | 10 +++ 2 files changed, 129 insertions(+) create mode 100644 runsum.mts diff --git a/runsum.mts b/runsum.mts new file mode 100644 index 0000000..5eaf338 --- /dev/null +++ b/runsum.mts @@ -0,0 +1,119 @@ +/** + * One-off: summarise a project's commits from the command line. + * + * Imports the same PURE modules the app uses — batch.ts, preamble.ts, + * prompt.ts — so the prompts, the batching and the index-matched parsing are + * byte-identical to what the "Summarise" button does. Only the surrounding + * plumbing (which carries `server-only`) is reimplemented here. + */ +import PocketBase from "pocketbase"; + +import { BATCH_SIZE, buildBatchPrompt, chunk, parseBatchResponse } from "./src/lib/ai/batch"; +import { PROMPT_VERSION } from "./src/lib/ai/preamble"; +import { buildSystemPrompt } from "./src/lib/ai/prompt"; +import { stripThinking } from "./src/lib/ai/think-strip"; + +async function main() { + const pb = new PocketBase(process.env.NEXT_PUBLIC_PB_URL!); + pb.autoCancellation(false); + await pb + .collection("_superusers") + .authWithPassword(process.env.PB_SUPERUSER_EMAIL!, process.env.PB_SUPERUSER_PASSWORD!); + + const settings = await pb.collection("settings").getFirstListItem("id != ''"); + const key: string = settings.ai_api_key ?? ""; + const model: string = settings.ai_model || "MiniMax-M2"; + if (!key) throw new Error("No API key configured — paste it in Settings first."); + + const system = buildSystemPrompt(settings.ai_system_prompt_extra ?? ""); + + const pending = await pb.collection("commits").getFullList({ + filter: [ + "is_merge = false", + `(summary = "" || summary_version < ${PROMPT_VERSION})`, + "summary_attempts < 3", + ].join(" && "), + sort: "-authored_at", + fields: "id,subject,type,scope,summary_attempts", + }); + + console.log(`provider=${settings.ai_provider} model=${model}`); + console.log(`pending commits: ${pending.length}`); + if (pending.length === 0) return; + + const batches = chunk( + pending.map((c) => ({ + id: c.id, + subject: (c.subject as string) || "", + type: (c.type as string) || "other", + scope: (c.scope as string) || "", + })), + BATCH_SIZE + ); + + let summarised = 0; + let missing = 0; + let inTok = 0; + let outTok = 0; + + for (const [index, batch] of batches.entries()) { + process.stdout.write(` batch ${index + 1}/${batches.length} … `); + + let text = ""; + try { + const res = await fetch("https://api.minimax.io/v1/chat/completions", { + method: "POST", + headers: { "content-type": "application/json", authorization: `Bearer ${key}` }, + body: JSON.stringify({ + model, + max_tokens: 4096, + n: 1, + messages: [ + { role: "system", content: system }, + { role: "user", content: buildBatchPrompt(batch) }, + ], + }), + }); + + if (!res.ok) { + console.log(`HTTP ${res.status} — ${(await res.text()).slice(0, 120)}`); + continue; + } + + const json = await res.json(); + text = stripThinking(json.choices?.[0]?.message?.content ?? ""); + inTok += json.usage?.prompt_tokens ?? 0; + outTok += json.usage?.completion_tokens ?? 0; + } catch (error) { + console.log(`failed — ${(error as Error).message}`); + continue; + } + + const parsed = parseBatchResponse(text, batch); + + for (const commit of batch) { + const summary = parsed.get(commit.id); + if (!summary) { + missing += 1; + continue; + } + await pb.collection("commits").update(commit.id, { + summary, + summary_version: PROMPT_VERSION, + summary_attempts: 0, + }); + summarised += 1; + } + + console.log(`${parsed.size}/${batch.length}`); + } + + console.log(`\nsummarised: ${summarised}`); + console.log(`not returned by the model: ${missing}`); + console.log(`tokens: ${inTok} in, ${outTok} out`); +} + +main().catch((e) => { + console.error("FAILED:", e?.message ?? e); + process.exit(1); +}); diff --git a/src/components/board/board.tsx b/src/components/board/board.tsx index 927edae..6f3b45c 100644 --- a/src/components/board/board.tsx +++ b/src/components/board/board.tsx @@ -292,6 +292,16 @@ export function Board({ return (