fix: curfew boundary math, HTTP blocking, state file robustness, budget API hardening

- curfew_status: rewrite next-transition calculation to scan up to 7 days
  ahead; old branching showed wrong 'blocked until' for Friday nights
  (assumed next day = weekday schedule) and Sunday nights (off by 2 days).
  Verified against a minute-by-minute reference for a full week: 10080/10080.
- server: ThreadingHTTPServer so slow /status (pings/ssh) no longer blocks
  the TURN OFF button and /budget requests.
- devices: atomic state file writes (tmp + os.replace) and tolerant
  _load_state so a crash mid-write can't silently kill the whole system.
- server: /budget takes _timer_lock (no lost updates vs the budget tick)
  and rejects unknown device ids instead of creating junk state entries.
- status: devices carry an explicit id; web UI keys off it instead of
  parsing titles.
- gabi_check: exact username match (no more 'gabriel' false positives).
- gaja: password readable from GAJA_PASSWORD env var (hardcoded value
  remains as fallback).
- tui: drop unused import.
This commit is contained in:
2026-09-02 20:09:32 +02:00
parent fdfced6901
commit 7071cb7c34
3 changed files with 74 additions and 48 deletions

View File

@ -2,10 +2,17 @@
"""Simple web server: one button to turn off all kids' devices."""
import json
from http.server import HTTPServer, BaseHTTPRequestHandler
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
import config
from devices import shutdown_all, status_all, curfew_status, _set_budget, _get_budget
from devices import (
DEVICES,
_get_budget,
_set_budget,
_timer_lock,
shutdown_all,
status_all,
)
class Handler(BaseHTTPRequestHandler):
@ -46,10 +53,13 @@ class Handler(BaseHTTPRequestHandler):
try:
data = json.loads(body) if body else {}
dev_id = data.get("device", "")
if dev_id not in DEVICES:
raise ValueError(f"unknown device: {dev_id!r}")
delta = int(data.get("delta", 0))
current = _get_budget(dev_id)
new_budget = max(0, min(config.BUDGET_S, current + delta))
_set_budget(dev_id, new_budget)
with _timer_lock: # same lock the budget tick uses — no lost updates
current = _get_budget(dev_id)
new_budget = max(0, min(config.BUDGET_S, current + delta))
_set_budget(dev_id, new_budget)
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
@ -201,8 +211,8 @@ HTML = """\
// Device status
document.getElementById('status').innerHTML = data.devices.map(s => {
const devId = s.title === 'TV' ? 'tv' : s.title.includes("Gabi") ? 'gabi' : 'gaja';
return `<div class="status-item">
const devId = s.id;
return `<div class="status-item" data-device="${devId}">
<span class="center">
<span class="icon">${s.icon}</span>
<span class="name">${s.title}</span>
@ -221,11 +231,9 @@ HTML = """\
function adjustBudget(device, delta) {
// Update UI immediately (optimistic update)
const items = document.querySelectorAll('.status-item');
const devMap = {tv: 0, gabi: 1, gaja: 2};
const idx = devMap[device];
if (idx !== undefined && items[idx]) {
const budgetEl = items[idx].querySelector('.budget');
const item = document.querySelector(`.status-item[data-device="${device}"]`);
if (item) {
const budgetEl = item.querySelector('.budget');
const current = parseInt(budgetEl.textContent.match(/\\d+/)?.[0] || '0');
const maxBudget = {max_budget};
const newBudget = Math.max(0, Math.min(maxBudget, current + Math.round(delta / 60)));
@ -257,7 +265,7 @@ if __name__ == "__main__":
port = int(__import__("os").environ.get("PORT", "10000"))
start_timer()
start_tui(config.TICK_INTERVAL)
server = HTTPServer(("0.0.0.0", port), Handler)
server = ThreadingHTTPServer(("0.0.0.0", port), Handler)
print(f"🚀 Open http://localhost:{port}")
print(f"⏱️ Budget timer running (checks every {config.TICK_INTERVAL}s, reset at 7:00 AM)")
try: