from __future__ import annotations import argparse from http.server import BaseHTTPRequestHandler, HTTPServer import json from pathlib import Path from .config import load_config from .review_bridge import ReviewWorkspaceBridge from .workspace_manager import WorkspaceManager def json_response(handler: BaseHTTPRequestHandler, status: int, payload: dict) -> None: body = json.dumps(payload, indent=2).encode("utf-8") handler.send_response(status) handler.send_header("Content-Type", "application/json") handler.send_header("Content-Length", str(len(body))) handler.send_header("Access-Control-Allow-Origin", "*") handler.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS") handler.send_header("Access-Control-Allow-Headers", "Content-Type") handler.end_headers() handler.wfile.write(body) class ReviewBridgeHandler(BaseHTTPRequestHandler): reviewer: str = "Unknown Reviewer" workspace_manager: WorkspaceManager = None # type: ignore active_bridge: ReviewWorkspaceBridge | None = None active_workspace_id: str | None = None @classmethod def set_active_workspace(cls, workspace_id: str) -> bool: meta = cls.workspace_manager.touch_recent(workspace_id) if meta is None: return False cls.active_workspace_id = workspace_id cls.active_bridge = ReviewWorkspaceBridge(meta.path, reviewer=cls.reviewer) return True def do_OPTIONS(self): json_response(self, 200, {"ok": True}) def do_GET(self): if self.path == "/api/workspaces": reg = self.workspace_manager.list_workspaces() json_response(self, 200, reg.model_dump()) return if self.path == "/api/load": if self.active_bridge is None: json_response(self, 400, {"error": "no active workspace"}) return session = self.active_bridge.load_session() json_response(self, 200, {"workspace_id": self.active_workspace_id, "session": session.model_dump()}) return json_response(self, 404, {"error": "not found"}) def do_POST(self): length = int(self.headers.get("Content-Length", "0")) raw = self.rfile.read(length) if length else b"{}" payload = json.loads(raw.decode("utf-8") or "{}") if self.path == "/api/workspaces/create": meta = self.workspace_manager.create_workspace( workspace_id=payload["workspace_id"], title=payload["title"], notes=payload.get("notes", "") ) self.set_active_workspace(meta.workspace_id) json_response(self, 200, {"ok": True, "workspace": meta.model_dump()}) return if self.path == "/api/workspaces/open": ok = self.set_active_workspace(payload["workspace_id"]) if not ok: json_response(self, 404, {"error": "workspace not found"}) return json_response(self, 200, {"ok": True, "workspace_id": self.active_workspace_id}) return if self.path == "/api/workspaces/import-preview": preview = self.workspace_manager.preview_import( source_dir=payload["source_dir"], workspace_id=payload["workspace_id"] ) json_response(self, 200, preview.model_dump()) return if self.path == "/api/workspaces/import": try: meta = self.workspace_manager.import_draft_pack( source_dir=payload["source_dir"], workspace_id=payload["workspace_id"], title=payload.get("title"), notes=payload.get("notes", ""), allow_overwrite=bool(payload.get("allow_overwrite", False)), ) except FileNotFoundError as exc: json_response(self, 404, {"ok": False, "error": str(exc)}) return except FileExistsError as exc: json_response(self, 409, {"ok": False, "error": str(exc)}) return except ValueError as exc: json_response(self, 400, {"ok": False, "error": str(exc)}) return self.set_active_workspace(meta.workspace_id) json_response(self, 200, {"ok": True, "workspace": meta.model_dump()}) return if self.active_bridge is None: json_response(self, 400, {"error": "no active workspace"}) return if self.path == "/api/save": session = self.active_bridge.apply_actions(payload.get("actions", [])) json_response(self, 200, {"ok": True, "workspace_id": self.active_workspace_id, "session": session.model_dump()}) return if self.path == "/api/export": session = self.active_bridge.export_promoted() json_response(self, 200, {"ok": True, "promoted_pack_dir": str(self.active_bridge.promoted_pack_dir), "workspace_id": self.active_workspace_id, "session": session.model_dump()}) return json_response(self, 404, {"error": "not found"}) def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Didactopus local review bridge server with semantic QA") parser.add_argument("--config", default="configs/config.example.yaml") return parser def main() -> None: args = build_parser().parse_args() config = load_config(Path(args.config)) ReviewBridgeHandler.reviewer = config.review.default_reviewer ReviewBridgeHandler.workspace_manager = WorkspaceManager( registry_path=config.bridge.registry_path, default_workspace_root=config.bridge.default_workspace_root ) server = HTTPServer((config.bridge.host, config.bridge.port), ReviewBridgeHandler) print(f"Didactopus review bridge listening on http://{config.bridge.host}:{config.bridge.port}") server.serve_forever() if __name__ == "__main__": main()