Scaffold: fork of Chambers architecture, renamed to Boardroom Map

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jonathan Kirkwood
2026-07-06 13:10:25 -05:00
co-authored by Claude Fable 5
commit 1dde915540
48 changed files with 4025 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
"""Boardroom Map orchestrator web app.
Serves the control-panel UI and a small JSON API, and starts the background job
runner (see jobs.py) that actually convenes the panel. Most configuration happens
through the StartOS *actions* (Configure Sparks / Models / Reviewers / Review);
this UI is for dropping documents, triggering a review, watching it run, and
reading reports.
"""
from __future__ import annotations
import os
import threading
from fastapi import FastAPI, HTTPException, UploadFile, File
from fastapi.responses import HTMLResponse, PlainTextResponse
from fastapi.templating import Jinja2Templates
from starlette.requests import Request
import bm_config
import extraction
import reviewers as rev_mod
import serving
from jobs import runner, INBOX, REPORTS_DIR
DATA_DIR = os.environ.get("BM_DATA_DIR", "/data")
app = FastAPI(title="Boardroom Map Orchestrator")
templates = Jinja2Templates(directory=os.path.join(os.path.dirname(__file__), "templates"))
@app.on_event("startup")
def _startup():
runner.start()
# ----------------------------------------------------------------------- UI
@app.get("/", response_class=HTMLResponse)
def index(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.get("/healthz")
def healthz():
return {"ok": True}
# ----------------------------------------------------------------------- status
@app.get("/api/status")
def status():
cfg = bm_config.load()
catalog = {m["alias"] for m in (cfg.get("models") or [])}
return {
"configured": {
"sparks": bool(cfg.get("primarySparkHost")),
"models": len(cfg.get("models") or []),
"reviewers": len(cfg.get("reviewers") or []),
},
"networkMode": cfg.get("networkMode"),
"synthesis": bool(cfg.get("synthesisEnabled")),
"wipeRemoteDocs": bool(cfg.get("wipeRemoteDocs")),
"autoRunOnDrop": bool(cfg.get("autoRunOnDrop")),
"models": [{"alias": m["alias"], "hfModel": m["hfModel"], "spark": m.get("spark", "primary")}
for m in (cfg.get("models") or [])],
"panel": [{"name": r.get("name"), "model": r.get("model"),
"persona": bool((r.get("persona") or "").strip()),
"known": (r.get("model") in catalog)}
for r in (cfg.get("reviewers") or [])],
"inbox": _inbox_list(),
"runtime": runner.snapshot(),
}
@app.get("/api/events")
def events():
return {"events": runner.events()}
def _inbox_list() -> list[dict]:
if not os.path.isdir(INBOX):
return []
out = []
for fn in sorted(os.listdir(INBOX)):
p = os.path.join(INBOX, fn)
if os.path.isfile(p):
ext = os.path.splitext(fn)[1].lower()
out.append({"name": fn, "bytes": os.path.getsize(p),
"supported": ext in extraction.SUPPORTED})
return out
@app.get("/api/inbox")
def inbox():
return {"inbox": _inbox_list()}
# ----------------------------------------------------------------------- documents
@app.post("/api/upload")
async def upload(files: list[UploadFile] = File(...)):
os.makedirs(INBOX, exist_ok=True)
saved = []
for f in files:
name = os.path.basename(f.filename or "document")
dest = os.path.join(INBOX, name)
with open(dest, "wb") as out:
while chunk := await f.read(1 << 20):
out.write(chunk)
saved.append(name)
return {"ok": True, "saved": saved}
@app.post("/api/inbox/clear")
def inbox_clear():
if os.path.isdir(INBOX):
for fn in os.listdir(INBOX):
p = os.path.join(INBOX, fn)
if os.path.isfile(p):
os.remove(p)
return {"ok": True}
# ----------------------------------------------------------------------- run
@app.post("/api/run")
def run_now():
cfg = bm_config.load()
if not cfg.get("primarySparkHost"):
raise HTTPException(400, "No Spark configured (Configure Sparks).")
if not (cfg.get("models") and cfg.get("reviewers")):
raise HTTPException(400, "Configure at least one model and one reviewer first.")
if not _inbox_list():
raise HTTPException(400, "Inbox is empty — upload documents first.")
runner.request_run()
return {"ok": True, "message": "Review requested — watch the activity log."}
@app.get("/api/serving")
def serving_status():
cfg = bm_config.load()
if not cfg.get("primarySparkHost"):
raise HTTPException(400, "No Spark configured.")
return {"serving": serving.health(cfg)}
@app.post("/api/reviewer/build-image")
def build_reviewer_image():
cfg = bm_config.load()
if not cfg.get("primarySparkHost"):
raise HTTPException(400, "No Spark configured.")
threading.Thread(target=lambda: _safe_build(cfg), daemon=True).start()
return {"ok": True, "message": "Building reviewer image on the head Spark — watch the activity log."}
def _safe_build(cfg: dict):
try:
rev_mod.ensure_reviewer_image(cfg, runner.log)
except Exception as e:
runner.log(f"[reviewers] image build failed: {e}")
@app.post("/api/stop")
def stop():
cfg = bm_config.load()
if not cfg.get("primarySparkHost"):
raise HTTPException(400, "No Spark configured.")
serving.tear_down_all(cfg, runner.log)
runner.phase = "idle"
runner._persist()
return {"ok": True}
# ----------------------------------------------------------------------- reports
@app.get("/api/reports")
def list_reports():
if not os.path.isdir(REPORTS_DIR):
return {"reports": []}
jobs = sorted((d for d in os.listdir(REPORTS_DIR)
if os.path.isdir(os.path.join(REPORTS_DIR, d))), reverse=True)
return {"reports": jobs}
@app.get("/api/report", response_class=PlainTextResponse)
def latest_report():
path = os.path.join(REPORTS_DIR, "latest.md")
if not os.path.exists(path):
return "(no report yet — drop documents in the inbox and run a review)"
return open(path, errors="replace").read().strip() or "(empty report)"
@app.get("/api/reports/{job}", response_class=PlainTextResponse)
def get_report(job: str):
job = os.path.basename(job)
path = os.path.join(REPORTS_DIR, job, "report.md")
if not os.path.exists(path):
raise HTTPException(404, "no such report")
return open(path, errors="replace").read()