Case archive/restore with Paperclip sync
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m27s
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:
71
web/app.py
71
web/app.py
@@ -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 ─────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user