#!/usr/bin/env python3
"""
TP6 β Klick-Konsole fuer den Call-Durchlauf.
Zwei Aufgaben:
1. FUEHREN: die Seite sagt jederzeit, was gerade laeuft, wie lange es noch dauert und
was der Mensch am Telefon jetzt tun (und lassen) soll. Am Ende sagt sie unmissver-
staendlich, dass die Messung abgeschlossen ist.
2. MITSCHREIBEN: jeder Klick landet mit Zeitstempel in derselben Zeitachse wie der
Core-Sampler. Der Not-Aus killt zusaetzlich sofort die Saettigungs-Flows.
π΄ Zwei Dinge, die hier absichtlich so gebaut sind:
- Die Buttons sind server-seitig gerenderte
'
'
'
% (r.replace(" ", "%20"), r, " hier" if r == raum else "", r) for r in ROOMS)
raumzeile = ('
Wo bist du gerade?
'
'
%s
%s
' % (aktuell, kacheln))
# Die Call-Buttons (Start / Not-Aus fuer die Saettigungs-Flows) gehoeren zu TP6 und haben
# ohne Treiber keine Wirkung -- ein Start-Klick haette dort NICHTS gemessen. Sichtbar
# also nur, wenn wirklich ein Treiber laeuft.
btns = "\n".join(
'
'
'
' % (n, n, c, l)
for n, l, c, modes in BUTTONS if mode in modes
) if st.get("driver") else ""
notaus = ('
'
'
'
) if st.get("driver") else ""
# πΆ Gehen/Stehen braucht keinen Treiber -- das sind reine Zeitmarken fuer die Auswertung.
btns = ('
{buttons}
{notaus}
"""
class Handler(BaseHTTPRequestHandler):
def _send(self, code, body, ctype="application/json"):
raw = body.encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", ctype + "; charset=utf-8")
self.send_header("Content-Length", str(len(raw)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(raw)
def _back(self):
self.send_response(303)
self.send_header("Location", "/")
self.send_header("Content-Length", "0")
self.end_headers()
def do_GET(self):
if self.path.startswith("/status"):
st = read_state()
st["sampler"] = sampler_tail()
st["last"] = last_marker()
self._send(200, json.dumps(st))
elif self.path in ("/", "/index.html"):
self._send(200, render(read_state()), "text/html")
else:
self._send(404, "{}")
def do_POST(self):
if self.path.startswith("/jserr"):
with open(_p("jserrors.log"), "a") as fh:
fh.write("%s %s\n" % (time.strftime("%H:%M:%S"), self.path.split("msg=", 1)[-1]))
self._send(200, "{}")
return
if self.path.startswith("/auto"):
_auto["an"] = not _auto["an"]
_auto["aufgegeben"] = ""
_auto["seit_weg"] = time.time()
write_marker("AUTO=%s" % ("an" if _auto["an"] else "aus"))
if _auto["an"]:
entscheide()
if "text/html" in (self.headers.get("Accept") or ""):
self._back()
else:
self._send(200, json.dumps({"ok": True, "auto": _auto["an"]}))
return
if self.path.startswith("/pin?"):
ap = urllib.parse.unquote(self.path.split("ap=", 1)[1].split("&")[0])
if ap not in ("ap1", "ap2", "off"):
self._send(400, json.dumps({"error": "ap muss ap1, ap2 oder off sein"}))
return
if _pin["busy"]:
self._send(409, json.dumps({"error": "laeuft schon"}))
return
pin_setzen(ap)
if "text/html" in (self.headers.get("Accept") or ""):
self._back()
else:
self._send(200, json.dumps({"ok": True, "ziel": ap}))
return
if self.path.startswith("/m?"):
# π΄ Erst dekodieren, dann NICHT hochschreiben: "RAUM%3DDiele%20OG" wurde sonst zu
# "RAUM%3DDIELE%20OG" β der Marker war weder lesbar noch als Raum erkennbar.
name = urllib.parse.unquote(self.path.split("type=", 1)[1].split("&")[0])
if not name.startswith("RAUM="):
name = name.upper()
if name == "ABORT":
write_marker("ABORT_killed=%d" % kill_flows())
else:
if name == "CALL_START":
open(_p("go.flag"), "w").close()
write_marker(name)
if "text/html" in (self.headers.get("Accept") or ""):
self._back()
else:
self._send(200, json.dumps({"ok": True}))
else:
self._send(404, "{}")
def log_message(self, *_a):
pass
if __name__ == "__main__":
# π΄ Hier stand eine HARTKODIERTE IP (10.40.10.183) -- die des MacBook Air. Auf dem Mac
# mini ist es 10.40.10.82, und am 2026-08-22 wurde die falsche Adresse ans Telefon
# weitergegeben: "geht nicht" bei einem Server, der einwandfrei auf 0.0.0.0 lauschte.
# Eine Adresse, die man ansagt, ohne sie gelesen zu haben, ist keine Adresse.
# Deshalb kommt sie jetzt vom Interface -- und zwar ALLE, mit Kennzeichnung.
print("Konsole lokal: http://localhost:%d/" % PORT)
for name, ip in lokale_adressen():
print(" vom Telefon: http://%s:%d/ (%s)" % (ip, PORT, name))
if not lokale_adressen():
print(" β οΈ keine LAN-Adresse gefunden -- vom Telefon nicht erreichbar")
print("Marker -> %s" % markers_pfad())
for iface, wo in sorted(ap_map().items()):
print(" %-14s %s" % (iface, wo))
threading.Thread(target=beobachter, daemon=True).start()
# π΄ Threading ist Pflicht, seit der Pin dazugekommen ist: das Umschalten redet ueber SSH
# mit dem Manager und braucht ~8 s. Mit dem alten HTTPServer (single-threaded) haette
# das die ganze Konsole blockiert -- inklusive Not-Aus.
ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()