feat: add full cycle_start_date REST API, YAML persistence, and CORS support to dashboard.py

This commit is contained in:
proitlab committed 2026-08-19 10:32:35 +07:00
1 parent ec7226d172
commit 6723701424
1 file changed
+125 -4
+125 -4
View File
@@ -6,6 +6,7 @@ from __future__ import annotations
import argparse
import json
import mimetypes
import re
import sqlite3
import threading
import time
@@ -13,7 +14,7 @@ from datetime import datetime, timezone
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
from socketserver import ThreadingMixIn
from urllib.parse import unquote, urlparse
from urllib.parse import parse_qs, unquote, urlparse
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
@@ -28,7 +29,48 @@ _db_conn = None
_db_lock = threading.Lock()
_db_path = ""
_mortality_dirs: list[Path] = []
_cycle_start_date: str = "2026-05-22"
def _load_initial_cycle_start_date() -> str:
"""Read cycle_start_date from configs/cycle7_batch_optimized.yaml if available."""
cfg_path = Path(__file__).resolve().parent / "configs" / "cycle7_batch_optimized.yaml"
if cfg_path.exists():
content = cfg_path.read_text(encoding="utf-8")
match = re.search(r"^\s*cycle_start_date:\s*['\"]?([^'\"\s#]+)['\"]?", content, re.MULTILINE)
if match:
return match.group(1).strip()
return "2026-05-22"
_cycle_start_date: str = _load_initial_cycle_start_date()
def _persist_cycle_start_date(new_date: str) -> bool:
"""Update in-memory cycle_start_date and save to config YAML files."""
global _cycle_start_date
_cycle_start_date = new_date
updated_any = False
for cfg_name in ("cycle7_batch_optimized.yaml", "cycle7_batch.yaml"):
cfg_path = Path(__file__).resolve().parent / "configs" / cfg_name
if cfg_path.exists():
content = cfg_path.read_text(encoding="utf-8")
pattern = r"^([ \t]*cycle_start_date:[ \t]*)(?:['\"]?)([^'\"\r\n#]+)(?:['\"]?)([ \t]*(?:#.*)?)$"
def replacer(match: re.Match) -> str:
prefix = match.group(1)
comment = match.group(3) or ""
if comment and not comment.startswith(" "):
comment = f" {comment.lstrip()}"
if not comment.startswith(" "):
comment = f" {comment}"
return f'{prefix}"{new_date}"{comment}'
new_content, count = re.subn(pattern, replacer, content, count=1, flags=re.MULTILINE)
if count > 0:
cfg_path.write_text(new_content, encoding="utf-8")
updated_any = True
print(f"[dashboard] 📅 Updated cycle_start_date to: {new_date} (persisted in configs: {updated_any})")
return updated_any
def _calc_cycle_info(target_date_str: str) -> dict:
@@ -93,12 +135,31 @@ class DashboardHandler(SimpleHTTPRequestHandler):
def log_message(self, format, *args):
pass
def do_OPTIONS(self):
self.send_response(200)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
self.send_header("Content-Length", "0")
self.end_headers()
def do_GET(self):
try:
self._handle()
except (BrokenPipeError, ConnectionResetError):
pass
def do_POST(self):
try:
parsed = urlparse(self.path)
path = unquote(parsed.path)
if path == "/api/config/cycle_start_date":
self._handle_set_cycle_start_date()
return
self._send_error(404)
except (BrokenPipeError, ConnectionResetError):
pass
def _handle(self):
parsed = urlparse(self.path)
path = unquote(parsed.path)
@@ -119,6 +180,35 @@ class DashboardHandler(SimpleHTTPRequestHandler):
self._send_json({"cameras": _discover_cameras(self.shm_dir)})
return
if path == "/api/config/cycle_start_date":
query = parse_qs(parsed.query)
if "set" in query and query["set"]:
new_date = query["set"][0].strip()
try:
from datetime import date as date_type
date_type.fromisoformat(new_date)
persisted = _persist_cycle_start_date(new_date)
self._send_json({
"status": "ok",
"message": f"Cycle start date successfully set to {new_date}",
"cycle_start_date": _cycle_start_date,
"persisted": persisted,
})
return
except ValueError as err:
self._send_json({"error": f"Invalid date format (expected YYYY-MM-DD): {err}"}, status_code=400)
return
self._send_json({"cycle_start_date": _cycle_start_date})
return
if path == "/api/config":
self._send_json({
"cycle_start_date": _cycle_start_date,
"shm_dir": self.shm_dir,
"db_path": _db_path,
})
return
if path.startswith("/api/db/"):
self._handle_db(path)
return
@@ -133,6 +223,35 @@ class DashboardHandler(SimpleHTTPRequestHandler):
self._send_error(404)
def _handle_set_cycle_start_date(self):
content_length = int(self.headers.get("Content-Length", 0))
if content_length > 0:
body = self.rfile.read(content_length).decode("utf-8")
try:
data = json.loads(body)
new_date = str(data.get("cycle_start_date", "")).strip()
if new_date:
from datetime import date as date_type
date_type.fromisoformat(new_date)
persisted = _persist_cycle_start_date(new_date)
self._send_json({
"status": "ok",
"message": f"Cycle start date successfully set to {new_date}",
"cycle_start_date": _cycle_start_date,
"persisted": persisted,
})
return
else:
self._send_json({"error": "Missing 'cycle_start_date' in request body"}, status_code=400)
return
except ValueError as err:
self._send_json({"error": f"Invalid date format (expected YYYY-MM-DD): {err}"}, status_code=400)
return
except Exception as err:
self._send_json({"error": str(err)}, status_code=400)
return
self._send_json({"error": "Empty request body"}, status_code=400)
def _handle_status(self):
cams = _discover_cameras(self.shm_dir)
now = time.time()
@@ -407,11 +526,13 @@ class DashboardHandler(SimpleHTTPRequestHandler):
self.end_headers()
self.wfile.write(data)
def _send_json(self, obj):
def _send_json(self, obj, status_code=200):
data = json.dumps(obj).encode("utf-8")
self.send_response(200)
self.send_response(status_code)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)