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 argparse
import json import json
import mimetypes import mimetypes
import re
import sqlite3 import sqlite3
import threading import threading
import time import time
@@ -13,7 +14,7 @@ from datetime import datetime, timezone
from http.server import HTTPServer, SimpleHTTPRequestHandler from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path from pathlib import Path
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
from urllib.parse import unquote, urlparse from urllib.parse import parse_qs, unquote, urlparse
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer): class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
@@ -28,7 +29,48 @@ _db_conn = None
_db_lock = threading.Lock() _db_lock = threading.Lock()
_db_path = "" _db_path = ""
_mortality_dirs: list[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: def _calc_cycle_info(target_date_str: str) -> dict:
@@ -93,12 +135,31 @@ class DashboardHandler(SimpleHTTPRequestHandler):
def log_message(self, format, *args): def log_message(self, format, *args):
pass 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): def do_GET(self):
try: try:
self._handle() self._handle()
except (BrokenPipeError, ConnectionResetError): except (BrokenPipeError, ConnectionResetError):
pass 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): def _handle(self):
parsed = urlparse(self.path) parsed = urlparse(self.path)
path = unquote(parsed.path) path = unquote(parsed.path)
@@ -119,6 +180,35 @@ class DashboardHandler(SimpleHTTPRequestHandler):
self._send_json({"cameras": _discover_cameras(self.shm_dir)}) self._send_json({"cameras": _discover_cameras(self.shm_dir)})
return 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/"): if path.startswith("/api/db/"):
self._handle_db(path) self._handle_db(path)
return return
@@ -133,6 +223,35 @@ class DashboardHandler(SimpleHTTPRequestHandler):
self._send_error(404) 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): def _handle_status(self):
cams = _discover_cameras(self.shm_dir) cams = _discover_cameras(self.shm_dir)
now = time.time() now = time.time()
@@ -407,11 +526,13 @@ class DashboardHandler(SimpleHTTPRequestHandler):
self.end_headers() self.end_headers()
self.wfile.write(data) self.wfile.write(data)
def _send_json(self, obj): def _send_json(self, obj, status_code=200):
data = json.dumps(obj).encode("utf-8") 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("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*") 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.send_header("Content-Length", str(len(data)))
self.end_headers() self.end_headers()
self.wfile.write(data) self.wfile.write(data)