feat: add full cycle_start_date REST API, YAML persistence, and CORS support to dashboard.py
This commit is contained in:
1 parent
ec7226d172
commit
6723701424
1 file changed
+125
-4
+125
-4
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user