First version -- working.

This commit is contained in:
2026-06-06 15:54:28 +02:00
commit c337a0d5f1
2 changed files with 242 additions and 0 deletions

204
server.py Executable file
View File

@ -0,0 +1,204 @@
#!/usr/bin/env python3
"""Simple web server: one button to turn off all kids' devices."""
import json
import re
import subprocess
from pathlib import Path
from http.server import HTTPServer, BaseHTTPRequestHandler
SCRIPT = Path(__file__).parent / "off.sh"
def parse_output(text: str) -> list[dict]:
"""Parse off.sh output into structured action items."""
actions = []
lines = text.strip().splitlines()
for line in lines:
line = line.strip()
if not line:
continue
# Time window messages
m = re.match(r"PRE:\s*(.+)seconds?\s*to\s*go\.", line)
if m:
actions.append({"icon": "", "title": "Time Window", "detail": f"Before active hours — {m.group(1)}s until start"})
continue
m = re.match(r"POST:\s*(.+)seconds?\s*passed\.", line)
if m:
actions.append({"icon": "", "title": "Time Window", "detail": f"After active hours — {m.group(1)}s past end"})
continue
m = re.match(r"ACTIVE:\s*(.+)", line)
if m:
actions.append({"icon": "🟢", "title": "Active Hours", "detail": m.group(1)})
continue
# TV checks
if "TV is unreachable" in line:
actions.append({"icon": "", "title": "TV", "detail": "Unreachable (ping failed)"})
continue
if "TV is turning OFF" in line:
actions.append({"icon": "📺", "title": "TV", "detail": "Already off — no action needed"})
continue
if "mWakefulness=Awake" in line:
actions.append({"icon": "📺", "title": "TV", "detail": "Screen turned off (power key sent)"})
continue
# User checks
if "Gabi is not online" in line:
actions.append({"icon": "👤", "title": "Gabi's PC", "detail": "Not logged in — no action needed"})
continue
if "terminate-user gabi" in line or "terminated" in line.lower():
actions.append({"icon": "💻", "title": "Gabi's PC", "detail": "Session terminated"})
continue
# If no structured items found, fallback to raw
if not actions:
actions.append({"icon": "📜", "title": "Output", "detail": text.strip() or "(empty)"})
return actions
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/" or self.path == "/index.html":
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(HTML.encode())
else:
self.send_error(404)
def do_POST(self):
if self.path == "/run":
try:
result = subprocess.run(
["bash", str(SCRIPT)],
capture_output=True,
text=True,
timeout=30,
)
raw_output = (result.stdout + result.stderr).strip() or "Script executed (no output)."
status = "ok" if result.returncode == 0 else f"exit {result.returncode}"
except FileNotFoundError:
raw_output = f"Script not found at {SCRIPT}"
status = "error"
except subprocess.TimeoutExpired:
raw_output = "Script timed out after 30 seconds."
status = "error"
except Exception as e:
raw_output = str(e)
status = "error"
actions = parse_output(raw_output) if status == "ok" else [{"icon": "", "title": "Error", "detail": raw_output}]
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"status": status, "actions": actions}).encode())
else:
self.send_error(404)
def log_message(self, format, *args):
pass
HTML = """\
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kids Devices — OFF</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, sans-serif;
display: flex; justify-content: center; align-items: center;
min-height: 100vh; background: #1a1a2e; color: #eee;
}
.card {
text-align: center; padding: 3rem; border-radius: 16px;
background: #16213e; box-shadow: 0 8px 32px rgba(0,0,0,.4);
max-width: 420px; width: 90%;
}
h1 { font-size: 1.5rem; margin-bottom: .5rem; }
p { color: #aaa; margin-bottom: 2rem; font-size: .9rem; }
button {
background: #e94560; color: #fff; border: none;
padding: 1rem 2.5rem; font-size: 1.2rem; font-weight: 700;
border-radius: 8px; cursor: pointer; transition: .2s;
}
button:hover { background: #c73650; }
button:disabled { opacity: .5; cursor: wait; }
#output {
margin-top: 1.5rem; text-align: left; display: none;
}
.action-item {
display: flex; align-items: flex-start; gap: .75rem;
padding: .75rem 1rem; margin-bottom: .5rem;
border-radius: 8px; background: #0f3460;
}
.action-item .icon { font-size: 1.3rem; flex-shrink: 0; }
.action-item .info { flex: 1; }
.action-item .title { font-weight: 600; font-size: .95rem; }
.action-item .detail { color: #aaa; font-size: .82rem; margin-top: .15rem; }
</style>
</head>
<body>
<div class="card">
<h1>📺 Kids Devices</h1>
<p>TV + Gabi's computer</p>
<button id="btn" onclick="run()">TURN OFF</button>
<div id="output"></div>
</div>
<script>
async function run() {
const btn = document.getElementById('btn');
const out = document.getElementById('output');
btn.disabled = true;
btn.textContent = '';
out.style.display = 'block';
out.innerHTML = '<div class="action-item"><span class="icon">⏳</span><div class="info"><div class="title">Running…</div></div></div>';
try {
const res = await fetch('https://noom.cc/off/run', { method: 'POST' });
const data = await res.json();
if (data.actions && data.actions.length) {
out.innerHTML = data.actions.map(a =>
`<div class="action-item">
<span class="icon">${a.icon}</span>
<div class="info">
<div class="title">${a.title}</div>
<div class="detail">${a.detail}</div>
</div>
</div>`
).join('');
} else {
out.textContent = 'No actions taken.';
}
} catch(e) {
out.innerHTML = `<div class="action-item">
<span class="icon">❌</span>
<div class="info"><div class="title">Error</div><div class="detail">${e.message}</div></div>
</div>`;
}
btn.disabled = false;
btn.textContent = 'TURN OFF';
}
</script>
</body>
</html>
"""
if __name__ == "__main__":
port = int(__import__("os").environ.get("PORT", "10000"))
server = HTTPServer(("0.0.0.0", port), Handler)
print(f"🚀 Open http://localhost:{port}")
server.serve_forever()