diff --git a/src/app/actions/cards.ts b/src/app/actions/cards.ts index 53f4e48..988fc7d 100644 --- a/src/app/actions/cards.ts +++ b/src/app/actions/cards.ts @@ -7,6 +7,7 @@ import { requireSession } from "@/lib/auth/session"; import { logActivity } from "@/app/actions/activity"; import { positionBetween } from "@/lib/ordering/fractional"; import { nextCardNumber } from "@/lib/cards/number"; +import { relinkProject } from "@/lib/linking/engine"; import type { CardRecord, ColumnRecord } from "@/lib/pb/types"; /** @@ -109,6 +110,10 @@ export async function updateCard(input: { await pb.collection("cards").update(input.cardId, patch); + if (input.scopeRule !== undefined) { + await relinkProject(card.project); + } + // One row per field, matching how client edits are logged, so the timeline // reads consistently regardless of who made the change. for (const field of Object.keys(patch)) { @@ -172,6 +177,8 @@ export async function acceptCard(input: { position: positionBetween(last ? last.position : null, null), }); + await relinkProject(card.project); + await logActivity({ project: card.project, card: card.id, @@ -195,6 +202,8 @@ export async function archiveCard(input: { await pb.collection("cards").update(input.cardId, { archived: true }); + await relinkProject(card.project); + await logActivity({ project: card.project, card: card.id, @@ -217,6 +226,8 @@ export async function unarchiveCard(input: { await pb.collection("cards").update(input.cardId, { archived: false }); + await relinkProject(card.project); + await logActivity({ project: card.project, card: card.id, diff --git a/src/app/actions/linking.ts b/src/app/actions/linking.ts new file mode 100644 index 0000000..2abeee0 --- /dev/null +++ b/src/app/actions/linking.ts @@ -0,0 +1,54 @@ +"use server"; + +import { revalidatePath } from "next/cache"; + +import { requireSession } from "@/lib/auth/session"; +import { + linkCommitManually, + relinkProject, + unlinkCommit, +} from "@/lib/linking/engine"; + +/** See the security note at the top of src/app/actions/cards.ts. */ +async function requireOwner() { + const session = await requireSession(); + if (session.role !== "owner") throw new Error("Not found"); + return session; +} + +export async function relinkProjectAction(input: { + projectId: string; + clientSlug: string; + projectSlug: string; +}): Promise<{ linked: number; unlinked: number; preserved: number }> { + await requireOwner(); + const result = await relinkProject(input.projectId); + + revalidatePath(`/p/${input.clientSlug}/${input.projectSlug}/unassigned`); + return { + linked: result.linked, + unlinked: result.unlinked, + preserved: result.preserved, + }; +} + +export async function linkCommitAction(input: { + commitId: string; + cardId: string; + clientSlug: string; + projectSlug: string; +}): Promise { + await requireOwner(); + await linkCommitManually(input.commitId, input.cardId); + revalidatePath(`/p/${input.clientSlug}/${input.projectSlug}/unassigned`); +} + +export async function unlinkCommitAction(input: { + commitId: string; + clientSlug: string; + projectSlug: string; +}): Promise { + await requireOwner(); + await unlinkCommit(input.commitId); + revalidatePath(`/p/${input.clientSlug}/${input.projectSlug}/unassigned`); +} \ No newline at end of file diff --git a/src/lib/git/ingest.ts b/src/lib/git/ingest.ts index e37cae1..17f4bf8 100644 --- a/src/lib/git/ingest.ts +++ b/src/lib/git/ingest.ts @@ -168,6 +168,12 @@ export async function ingestProject( .collection("projects") .update(project.id, { last_ingest_at: new Date().toISOString() }); + // New commits mean new links. Doing this here rather than in each caller + // means the webhook, the poll and the backfill all get it, and none of them + // has to remember. + const { relinkProject } = await import("@/lib/linking/engine"); + await relinkProject(project.id); + return { created, skipped, pagesRead: page }; }