Case archive/restore with Paperclip sync
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m27s

Adds a comprehensive archive flow for closed cases — separate /archive
screen in the UI, archive/restore actions on the case detail page, and
automatic two-way sync with Paperclip.

Backend (web/app.py + mcp-server/services/db.py):
- New SCHEMA_V6 migration: cases.archived_at TIMESTAMPTZ + partial index
- list_cases gains include_archived/archived_only flags; default excludes
  archived rows so the main /api/cases list hides closed cases
- archive_case / restore_case helpers in db.py
- POST /api/cases/{n}/archive sets archived_at and calls
  pc_archive_project (sets Paperclip projects.archived_at via direct DB)
- POST /api/cases/{n}/restore clears archived_at and calls
  pc_restore_project (clears Paperclip archived_at)
- archive_project / restore_project in paperclip_client.py — name-based
  match consistent with create_project's lookup

Frontend (web-ui):
- cases.ts: scope param ("active"|"archived"|"all") on useCases;
  useArchiveCase / useRestoreCase mutations
- /archive page (new): table of archived cases with restore button +
  search, sort, empty state matching the editorial aesthetic of /
- case-archive-action.tsx: button on case detail header. Active case →
  confirm dialog → archive. Archived case → restore (no confirm).
  Toast announces both legal-ai and Paperclip outcomes (synced, not
  found in pc, error)
- case-header shows "בארכיון" badge when archived_at is set
- Nav: ארכיון link added to AppShell after בית

Tested end-to-end against the live DB:
- 1130-25 archive → list_cases(include_archived=False) excludes it,
  list_cases(archived_only=True) includes it, restore reverses
- pc archive/restore on 1194-25 verified via direct DB lookup
- TypeScript compiles clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 18:54:52 +00:00
parent 8b816c8b61
commit 2b7f291928
8 changed files with 629 additions and 21 deletions

View File

@@ -36,6 +36,7 @@ _web_dir = Path(__file__).resolve().parent
sys.path.insert(0, str(_web_dir.parent))
from web.gitea_client import create_repo, setup_remote_and_push
from web.paperclip_client import (
archive_project as pc_archive_project,
create_project as pc_create_project,
create_workflow_issue as pc_create_workflow_issue,
get_agents_for_case as pc_get_agents_for_case,
@@ -44,6 +45,7 @@ from web.paperclip_client import (
get_issue_comments as pc_get_issue_comments,
get_project_url,
post_comment as pc_post_comment,
restore_project as pc_restore_project,
wake_ceo_agent as pc_wake_ceo,
)
@@ -1021,12 +1023,25 @@ async def health():
@app.get("/api/cases")
async def list_cases(detail: bool = False):
"""List existing cases. With detail=true, includes doc counts and integration URLs."""
cases = await db.list_cases()
async def list_cases(
detail: bool = False,
include_archived: bool = False,
archived_only: bool = False,
):
"""List existing cases. By default excludes archived (use include_archived=true
or archived_only=true to see them). With detail=true, includes doc counts."""
cases = await db.list_cases(
include_archived=include_archived,
archived_only=archived_only,
)
if not detail:
return [
{"case_number": c["case_number"], "title": c["title"], "status": c["status"]}
{
"case_number": c["case_number"],
"title": c["title"],
"status": c["status"],
"archived_at": c["archived_at"].isoformat() if c.get("archived_at") else None,
}
for c in cases
]
# Enhanced listing with document counts
@@ -1049,6 +1064,7 @@ async def list_cases(detail: bool = False):
"expected_outcome": c.get("expected_outcome", ""),
"committee_type": c.get("committee_type", ""),
"hearing_date": str(c["hearing_date"]) if c.get("hearing_date") else "",
"archived_at": c["archived_at"].isoformat() if c.get("archived_at") else None,
"document_count": doc_count,
"processing_count": processing_count,
"gitea_url": f"https://gitea.nautilus.marcusgroup.org/cases/{c['case_number']}",
@@ -1056,6 +1072,53 @@ async def list_cases(detail: bool = False):
return result
@app.post("/api/cases/{case_number}/archive")
async def api_archive_case(case_number: str):
"""Move a case to the archive. Also archives the matching Paperclip project."""
case = await db.get_case_by_number(case_number)
if not case:
raise HTTPException(404, f"Case {case_number} not found")
updated = await db.archive_case(UUID(case["id"]))
paperclip_result: dict = {"status": "skipped"}
try:
paperclip_result = await pc_archive_project(case_number)
except Exception as e:
logger.warning("paperclip archive sync failed for %s: %s", case_number, e)
paperclip_result = {"status": "error", "message": str(e)}
return {
"status": "archived",
"case_number": case_number,
"archived_at": updated["archived_at"].isoformat() if updated and updated.get("archived_at") else None,
"paperclip": paperclip_result,
}
@app.post("/api/cases/{case_number}/restore")
async def api_restore_case(case_number: str):
"""Restore an archived case. Also restores the matching Paperclip project."""
case = await db.get_case_by_number(case_number)
if not case:
raise HTTPException(404, f"Case {case_number} not found")
await db.restore_case(UUID(case["id"]))
paperclip_result: dict = {"status": "skipped"}
try:
paperclip_result = await pc_restore_project(case_number)
except Exception as e:
logger.warning("paperclip restore sync failed for %s: %s", case_number, e)
paperclip_result = {"status": "error", "message": str(e)}
return {
"status": "restored",
"case_number": case_number,
"paperclip": paperclip_result,
}
# ── Paperclip Integration API ─────────────────────────────────────