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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user