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

@@ -138,6 +138,59 @@ async def create_project(
await conn.close()
async def archive_project(case_number: str) -> dict:
"""Set archived_at on the Paperclip project matching this case number.
The project is identified by `name LIKE '%{case_number}%'` (consistent with
`create_project`'s lookup). Idempotent — if already archived, returns the
existing timestamp.
"""
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
try:
row = await conn.fetchrow(
"""UPDATE projects
SET archived_at = COALESCE(archived_at, now()),
updated_at = now()
WHERE name LIKE $1 RETURNING id, name, archived_at""",
f"%{case_number}%",
)
if not row:
return {"status": "not_found", "case_number": case_number}
return {
"status": "archived",
"project_id": str(row["id"]),
"name": row["name"],
"archived_at": row["archived_at"].isoformat() if row["archived_at"] else None,
}
finally:
await conn.close()
async def restore_project(case_number: str) -> dict:
"""Clear archived_at on the Paperclip project matching this case number.
Idempotent — if already active, returns success without changes.
"""
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
try:
row = await conn.fetchrow(
"""UPDATE projects
SET archived_at = NULL,
updated_at = now()
WHERE name LIKE $1 RETURNING id, name""",
f"%{case_number}%",
)
if not row:
return {"status": "not_found", "case_number": case_number}
return {
"status": "restored",
"project_id": str(row["id"]),
"name": row["name"],
}
finally:
await conn.close()
async def _create_issue(
conn: asyncpg.Connection,
company_id: str,