diff --git a/README.md b/README.md index d591902..90d196a 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Canonical repo: `https://gitea.ten31.ai/Ten31AI/boardroom-map`. ## Status -**v0.1.6 — live in production.** Deployed on a StartOS box driving a DGX Spark +**v0.1.7 — live in production.** Deployed on a StartOS box driving a DGX Spark in single-spark air-gapped mode (gemma-4-31B panel: munger-lens / girdley-operator / buffett-owner). First full grading run completed 2026-07-29: a three-deck company history graded end-to-end into a running @@ -135,7 +135,10 @@ timeouts for ~3.6 tok/s local generation; v0.1.5 fixed the dashboard viewer (stays open across background refresh) and added report/JSON/scorecard downloads; v0.1.6 made reports human-friendly — rendered markdown in the dashboard, an at-a-glance strip per deck report, and a concise "At a glance" -summary table atop every generated DECK_REPORT.md. +summary table atop every generated DECK_REPORT.md; v0.1.7 added company +deletion from the dashboard (wipes the graded history, optionally restores +the graded deck files to the inbox) so an evaluation can be re-run from +scratch with a different model panel. Known optimization not yet done: the wave is torn down per deck, so the 31B reloads from disk (~6 min) between decks even when the model set is unchanged. diff --git a/orchestrator/app.py b/orchestrator/app.py index d010f32..9d75508 100644 --- a/orchestrator/app.py +++ b/orchestrator/app.py @@ -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) diff --git a/orchestrator/templates/index.html b/orchestrator/templates/index.html index ac2c7a5..2c8c9ee 100644 --- a/orchestrator/templates/index.html +++ b/orchestrator/templates/index.html @@ -34,6 +34,8 @@ padding:8px 12px;font-size:13px;cursor:pointer} button:hover{background:#2c3650} button.primary{background:#3a2f12;border-color:#6b551f;color:#ffdf9a} button.mini{padding:2px 8px;font-size:11px;border-radius:7px} + button.danger{background:#2c1416;border-color:#552222;color:var(--bad)} + button.danger:hover{background:#3b1414} a.mini{display:inline-block;background:#222a3a;border:1px solid var(--edge);border-radius:7px; padding:2px 8px;font-size:11px;color:var(--ink);text-decoration:none;cursor:pointer} a.mini:hover{background:#2c3650} @@ -127,7 +129,8 @@

Company — - + +

@@ -471,6 +474,25 @@ function renderDetail(d,changed){ document.getElementById('scBtn').textContent='View SCORECARD.md'; } } +async function deleteCompany(){ + if(!currentSlug) return; + const name=(companiesCache.find(c=>c.slug===currentSlug)||{}).name||currentSlug; + if(!confirm(`Delete "${name}"?\n\nThis wipes its graded history: all deck records, `+ + `the scorecard, and report copies. A company registered in Configure Companies `+ + `reappears as an empty row (aliases and pinned targets kept), ready to re-grade.`)) return; + const restore=confirm('Move its already-graded deck files back into the inbox '+ + 'so they can be re-graded?\n\nOK = yes, put decks back · Cancel = no, just delete'); + try{ + const r=await fetch('/api/companies/'+encodeURIComponent(currentSlug)+ + (restore?'?restore_decks=true':''),{method:'DELETE'}); + const t=await r.text(); + if(!r.ok){ alert('Delete failed: '+t.slice(0,300)); return; } + const d=JSON.parse(t); + alert(`Deleted ${name}.`+(d.restored_decks?` ${d.restored_decks} deck file(s) `+ + `restored to the inbox — hit "Grade decks" to re-run.`:'')); + closeCompany(); refresh(); loadCompanies(); + }catch(e){ alert('Delete failed: '+(e.message||e)); } +} function deckBase(deckId){ return '/api/companies/'+encodeURIComponent(currentSlug)+'/decks/'+encodeURIComponent(deckId); } diff --git a/startos/versions/index.ts b/startos/versions/index.ts index 357527b..5685e72 100644 --- a/startos/versions/index.ts +++ b/startos/versions/index.ts @@ -6,9 +6,10 @@ import { v_0_1_3 } from './v_0_1_3' import { v_0_1_4 } from './v_0_1_4' import { v_0_1_5 } from './v_0_1_5' import { v_0_1_6 } from './v_0_1_6' +import { v_0_1_7 } from './v_0_1_7' /** The current version MUST be the first argument (`current`). */ export const versions = VersionGraph.of({ - current: v_0_1_6, - other: [v_0_1_5, v_0_1_4, v_0_1_3, v_0_1_2, v_0_1_1, v_0_1_0], + current: v_0_1_7, + other: [v_0_1_6, v_0_1_5, v_0_1_4, v_0_1_3, v_0_1_2, v_0_1_1, v_0_1_0], }) diff --git a/startos/versions/v_0_1_7.ts b/startos/versions/v_0_1_7.ts new file mode 100644 index 0000000..0fad6f0 --- /dev/null +++ b/startos/versions/v_0_1_7.ts @@ -0,0 +1,16 @@ +import { VersionInfo } from '@start9labs/start-sdk' + +/** Company deletion for re-evaluation. ExVer form `:`. */ +export const v_0_1_7 = VersionInfo.of({ + version: '0.1.7:0', + releaseNotes: + 'Companies can now be deleted from the dashboard (delete button on the ' + + 'company card) so an evaluation can be re-run from scratch with different ' + + 'models or graders. Deletion wipes the graded history — deck records, ' + + 'scorecard, and per-job report copies — and optionally moves the ' + + 'already-graded deck files from /data/processed back into the inbox, ' + + 'ready for a fresh Grade Decks run. Companies registered in Configure ' + + 'Companies reappear as empty rows with their aliases and pinned targets ' + + 'kept; deletion is blocked while a grading job is running.', + migrations: {}, +})