Archive: also cancel open Paperclip issues to clear agent widget
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 32s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 32s
When a case is archived, the legal-ai UI's AgentStatusWidget kept showing
"agents started working, waiting for first report" because related
Paperclip issues remained in 'todo' / 'in_progress' status. Concrete
example: case 1130-25 had two open issues (CMP-15 ניתוח תכנוני, CMP-21
כתיבת החלטה) that lingered after the case was finalized; 1194-25 had
two more (CMP-37, CMP-44).
Extended pc_archive_project to also UPDATE issues SET status='cancelled',
cancelled_at=now() WHERE project_id matches AND status IN
('backlog','todo','in_progress','blocked','in_review'). Returns the list
of cancelled issues so the toast can announce the count.
Updated cases.ts ArchiveResult.paperclip.issues_cancelled type and the
toast message in case-archive-action to surface "(N משימות פתוחות בוטלו)"
when relevant.
Restore is intentionally unchanged — we don't auto-recreate cancelled
issues; if work needs to resume, a fresh issue should be created.
Stale issues for 1130-25 / 1194-25 cancelled directly in DB as a one-off
cleanup (CMP-15, CMP-21, CMP-37, CMP-44).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,11 @@ import { useArchiveCase, useRestoreCase, type ArchiveResult } from "@/lib/api/ca
|
|||||||
function paperclipMessage(res: ArchiveResult, action: "archive" | "restore"): string {
|
function paperclipMessage(res: ArchiveResult, action: "archive" | "restore"): string {
|
||||||
const verb = action === "archive" ? "אורכן" : "שוחזר";
|
const verb = action === "archive" ? "אורכן" : "שוחזר";
|
||||||
switch (res.paperclip?.status) {
|
switch (res.paperclip?.status) {
|
||||||
case "archived":
|
case "archived": {
|
||||||
|
const cancelled = res.paperclip.issues_cancelled?.length ?? 0;
|
||||||
|
const issuesNote = cancelled > 0 ? ` (${cancelled} משימות פתוחות בוטלו)` : "";
|
||||||
|
return `התיק ${verb}. גם הפרויקט ב-Paperclip${issuesNote}.`;
|
||||||
|
}
|
||||||
case "restored":
|
case "restored":
|
||||||
return `התיק ${verb}. גם הפרויקט ב-Paperclip ${verb}.`;
|
return `התיק ${verb}. גם הפרויקט ב-Paperclip ${verb}.`;
|
||||||
case "not_found":
|
case "not_found":
|
||||||
|
|||||||
@@ -100,7 +100,13 @@ export type ArchiveResult = {
|
|||||||
status: string;
|
status: string;
|
||||||
case_number: string;
|
case_number: string;
|
||||||
archived_at?: string | null;
|
archived_at?: string | null;
|
||||||
paperclip?: { status: string; project_id?: string; archived_at?: string | null; message?: string };
|
paperclip?: {
|
||||||
|
status: string;
|
||||||
|
project_id?: string;
|
||||||
|
archived_at?: string | null;
|
||||||
|
message?: string;
|
||||||
|
issues_cancelled?: Array<{ identifier: string; title: string }>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useArchiveCase(caseNumber: string | undefined) {
|
export function useArchiveCase(caseNumber: string | undefined) {
|
||||||
|
|||||||
@@ -139,12 +139,18 @@ async def create_project(
|
|||||||
|
|
||||||
|
|
||||||
async def archive_project(case_number: str) -> dict:
|
async def archive_project(case_number: str) -> dict:
|
||||||
"""Set archived_at on the Paperclip project matching this case number.
|
"""Set archived_at on the Paperclip project matching this case number,
|
||||||
|
and cancel any open issues so the legal-ai UI's agent widget stops
|
||||||
|
reporting "agents are working" on a closed case.
|
||||||
|
|
||||||
The project is identified by `name LIKE '%{case_number}%'` (consistent with
|
The project is identified by `name LIKE '%{case_number}%'` (consistent with
|
||||||
`create_project`'s lookup). Idempotent — if already archived, returns the
|
`create_project`'s lookup). Idempotent — re-archiving a project that's
|
||||||
existing timestamp.
|
already archived returns the existing timestamp without re-cancelling
|
||||||
|
issues that have already been completed.
|
||||||
"""
|
"""
|
||||||
|
# Issue statuses considered "open" — anything not done/cancelled.
|
||||||
|
OPEN_STATUSES = ("backlog", "todo", "in_progress", "blocked", "in_review")
|
||||||
|
|
||||||
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
|
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
|
||||||
try:
|
try:
|
||||||
row = await conn.fetchrow(
|
row = await conn.fetchrow(
|
||||||
@@ -156,11 +162,26 @@ async def archive_project(case_number: str) -> dict:
|
|||||||
)
|
)
|
||||||
if not row:
|
if not row:
|
||||||
return {"status": "not_found", "case_number": case_number}
|
return {"status": "not_found", "case_number": case_number}
|
||||||
|
|
||||||
|
cancelled = await conn.fetch(
|
||||||
|
"""UPDATE issues
|
||||||
|
SET status = 'cancelled',
|
||||||
|
cancelled_at = now(),
|
||||||
|
updated_at = now()
|
||||||
|
WHERE project_id = $1 AND status = ANY($2::text[])
|
||||||
|
RETURNING identifier, title""",
|
||||||
|
row["id"], list(OPEN_STATUSES),
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "archived",
|
"status": "archived",
|
||||||
"project_id": str(row["id"]),
|
"project_id": str(row["id"]),
|
||||||
"name": row["name"],
|
"name": row["name"],
|
||||||
"archived_at": row["archived_at"].isoformat() if row["archived_at"] else None,
|
"archived_at": row["archived_at"].isoformat() if row["archived_at"] else None,
|
||||||
|
"issues_cancelled": [
|
||||||
|
{"identifier": r["identifier"], "title": r["title"]}
|
||||||
|
for r in cancelled
|
||||||
|
],
|
||||||
}
|
}
|
||||||
finally:
|
finally:
|
||||||
await conn.close()
|
await conn.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user