v0.1.7: delete companies for re-evaluation

- DELETE /api/companies/{slug}: wipes the ledger (deck records, scorecard,
  report md), sweeps per-job report and processed-deck copies; optional
  restore_decks moves the graded originals from /data/processed back into
  the inbox for a fresh Grade Decks run; 409 while a job is running;
  config-registered companies reappear as empty rows (aliases + pinned
  targets kept)
- Dashboard: "delete company…" danger button on the company card with a
  two-step confirm (delete, then restore-decks choice)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jonathan Kirkwood
2026-07-30 14:25:44 -05:00
co-authored by Claude Fable 5
parent 32d5d7f373
commit 7ec5222834
5 changed files with 105 additions and 5 deletions
+58
View File
@@ -29,6 +29,7 @@ LEDGER_DIR = os.path.join(DATA_DIR, "ledger")
runner = jobs.runner
INBOX = getattr(jobs, "INBOX", os.path.join(DATA_DIR, "inbox"))
REPORTS_DIR = getattr(jobs, "REPORTS_DIR", os.path.join(DATA_DIR, "reports"))
PROCESSED = getattr(jobs, "PROCESSED", os.path.join(DATA_DIR, "processed"))
app = FastAPI(title="Boardroom Map Orchestrator")
templates = Jinja2Templates(directory=os.path.join(os.path.dirname(__file__), "templates"))
@@ -348,6 +349,63 @@ def company_detail(slug: str):
}
@app.delete("/api/companies/{slug}")
def delete_company(slug: str, restore_decks: bool = False):
"""Wipe a company's graded data so it can be re-evaluated from scratch.
Removes the ledger (deck records, scorecard, report markdown) plus the
per-job report and processed-deck copies. With restore_decks, the original
deck files graded earlier are moved from /data/processed back into the
company's inbox folder first, ready for a fresh Grade Decks run. A
config-registered company reappears as an empty row (name, aliases and
pinned targets kept); an auto-created one disappears entirely.
"""
import shutil
slug = os.path.basename(slug)
if runner.phase in jobs.RUNNING_PHASES:
raise HTTPException(409, "A grading job is running — wait for it to finish.")
if not os.path.isdir(os.path.join(LEDGER_DIR, slug)) \
and not os.path.isdir(os.path.join(INBOX, slug)):
raise HTTPException(404, "no such company")
restored = 0
if restore_decks and os.path.isdir(PROCESSED):
dest_dir = os.path.join(INBOX, slug)
for job_dir in sorted(os.listdir(PROCESSED)):
src_dir = os.path.join(PROCESSED, job_dir, slug)
if not os.path.isdir(src_dir):
continue
os.makedirs(dest_dir, exist_ok=True)
for fn in sorted(os.listdir(src_dir)):
src = os.path.join(src_dir, fn)
dest = os.path.join(dest_dir, fn)
if os.path.isfile(src) and not os.path.exists(dest):
try:
shutil.move(src, dest)
restored += 1
except OSError:
pass
removed = {"ledger": False, "report_dirs": 0, "processed_dirs": 0}
led_dir = os.path.join(LEDGER_DIR, slug)
if os.path.isdir(led_dir):
shutil.rmtree(led_dir, ignore_errors=True)
removed["ledger"] = True
for base, key in ((REPORTS_DIR, "report_dirs"), (PROCESSED, "processed_dirs")):
if not os.path.isdir(base):
continue
for job_dir in os.listdir(base):
d = os.path.join(base, job_dir, slug)
if os.path.isdir(d):
shutil.rmtree(d, ignore_errors=True)
removed[key] += 1
runner.log(f"[api] company '{slug}' deleted "
f"(ledger={removed['ledger']}, report dirs={removed['report_dirs']}, "
f"processed dirs={removed['processed_dirs']}, "
f"decks restored to inbox={restored})")
return {"ok": True, "removed": removed, "restored_decks": restored}
@app.get("/api/companies/{slug}/scorecard", response_class=PlainTextResponse)
def company_scorecard(slug: str):
slug = os.path.basename(slug)