from __future__ import annotations import json import os from datetime import datetime, timezone from pathlib import Path from fastapi import APIRouter, HTTPException, Request _BASE_DIR = Path(__file__).resolve().parent _STATUS_FILE = _BASE_DIR / "project_status.json" _HISTORY_FILE = _BASE_DIR / "project_status_history.jsonl" _ALLOWED_KEYS = frozenset(("phase", "current_step", "last_completed_step", "next_step", "last_update", "notes")) try: _HISTORY_FILE.parent.mkdir(parents=True, exist_ok=True) except Exception: pass router = APIRouter(tags=["project"]) def _read_expected_token() -> str: expected = os.environ.get("MEDWORK_API_TOKEN", "").strip() if expected: return expected try: token_path = _BASE_DIR / "backend_token.txt" with open(token_path, "r", encoding="utf-8") as f: return (f.readline() or "").strip() except Exception: return "" def _check_token(request: Request) -> bool: expected = _read_expected_token() if not expected: return False token = (request.headers.get("X-API-Token", "") or "").strip() if not token: auth = (request.headers.get("Authorization", "") or "").strip() if auth.startswith("Bearer "): token = auth[len("Bearer "):].strip() return token == expected def _get_device_id(request: Request) -> str: return (request.headers.get("X-Device-Id", "") or "").strip() or "" def _append_history_entry(ts: str, device_id: str, action: str, status: dict) -> None: try: _HISTORY_FILE.parent.mkdir(parents=True, exist_ok=True) entry = {"ts": ts, "device_id": device_id, "action": action, "status": status} with open(_HISTORY_FILE, "a", encoding="utf-8") as hf: hf.write(json.dumps(entry, ensure_ascii=False) + "\n") hf.flush() os.fsync(hf.fileno()) except Exception: pass def _atomic_write_status(data: dict) -> bool: tmp = _STATUS_FILE.with_suffix(".json.tmp") try: text = json.dumps(data, indent=2, ensure_ascii=False) + "\n" with open(tmp, "w", encoding="utf-8") as f: f.write(text) f.flush() os.fsync(f.fileno()) os.replace(tmp, _STATUS_FILE) return True except Exception: try: tmp.unlink(missing_ok=True) except Exception: pass return False @router.get("/api/project/status") def get_project_status(request: Request): if not _check_token(request): raise HTTPException(status_code=401, detail="Unauthorized") if not _STATUS_FILE.is_file(): raise HTTPException(status_code=500, detail="project_status.json missing") try: with open(_STATUS_FILE, "r", encoding="utf-8") as f: data = json.load(f) except Exception: raise HTTPException(status_code=500, detail="project_status.json missing") data["updated_at"] = datetime.now(timezone.utc).isoformat() try: _append_history_entry( data["updated_at"], _get_device_id(request), "read", {k: data.get(k) for k in ("phase", "current_step", "last_completed_step", "next_step", "last_update", "notes")}, ) except Exception: pass return data @router.post("/api/project/status") async def post_project_status(request: Request): if not _check_token(request): raise HTTPException(status_code=401, detail="Unauthorized") try: body = await request.json() except Exception: body = {} if not isinstance(body, dict): body = {} data = {} if _STATUS_FILE.is_file(): try: with open(_STATUS_FILE, "r", encoding="utf-8") as f: data = json.load(f) except Exception: pass if not data: data = { "project": "AZA Medical AI Assistant", "phase": "", "current_step": 0, "last_completed_step": 0, "next_step": 0, "last_update": "", "notes": "", "todos": [], "roadmap": [], } for key in _ALLOWED_KEYS: if key in body: val = body[key] if key in ("current_step", "last_completed_step", "next_step"): try: data[key] = int(val) if val is not None and str(val).strip() != "" else 0 except (ValueError, TypeError): data[key] = data.get(key, 0) else: data[key] = str(val) if val is not None else "" data["updated_at"] = datetime.now(timezone.utc).isoformat() if not _atomic_write_status(data): raise HTTPException(status_code=500, detail="Failed to write project_status.json") try: _append_history_entry( data["updated_at"], _get_device_id(request), "update", dict(data), ) except Exception: pass return data