forked from zakaria/chicken-counting-sukawarna-det
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 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)
|
||||
|
||||
Reference in new issue
Block a user