165 lines
4.9 KiB
Python
165 lines
4.9 KiB
Python
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
|