fix: Zentrale-Seite zeigt den Zustand und den Weg zurueck
Zwei Handhabungsprobleme, die zusammengehoeren.
Auf /zentrale gab es ueberhaupt keinen Knopf zum Zurueckgeben - den fand
man nur ueber den Direktlink, den man nach einer Uebernahme in der Hand
hatte. Wer stattdessen /zentrale aufrief, konnte nur weiter uebernehmen
und sah nicht, dass erst zurueckgesetzt werden muss.
Ausserdem rendern die POST-Endpunkte ihr Ergebnis direkt. Man blieb also
auf /api/zentrale/uebernehmen stehen: eine Adresse, die beim Neuladen die
Aktion wiederholt und den Zustand nicht widerspiegelt.
Neu:
- Zwei Schritte. /zentrale fragt nur die Durchwahl ab und leitet auf
/zentrale/{durchwahl} um. Diese Zustandsseite ist die Adresse fuer den
Alltag, als Lesezeichen oder QR-Code, und ist gefahrlos neu ladbar.
- Die Knoepfe richten sich nach der Lage: frei -> besetzen, fremd besetzt
-> uebernehmen samt Hinweis was mit dieser Person passiert, selbst
besetzt -> zurueck auf mein Tuerschild, eigener Stammplatz -> kein
Knopf, nur der Hinweis dass nichts zu tun ist.
- Alle Aktionen antworten mit POST/Redirect/GET auf die Zustandsseite und
uebergeben nur einen kurzen Code fuer die Bestaetigung. Die Einzelheiten
- wer sitzt drauf, wer wartet, wer blockiert den Heimatplatz - leitet
die Seite bei jedem Aufruf frisch aus dem echten Zustand ab.
- Die allgemeine Seite nennt den aktuellen Besetzer und, falls vorhanden,
wer bei einer Freigabe automatisch zurueckkommt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import shutil
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import FastAPI, Request, Form, UploadFile, File, BackgroundTasks
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse, JSONResponse
|
||||
@@ -559,43 +560,6 @@ def _user_by_extension(extension, conn):
|
||||
""", (ext,)).fetchone()
|
||||
|
||||
|
||||
def _zentrale_view(request, conn, target, extension="", user=None, error=None, message=None):
|
||||
"""Kontext fuer die Self-Service-Seite zusammenbauen."""
|
||||
occupant = None
|
||||
if target:
|
||||
occupant = conn.execute("""
|
||||
SELECT u.display_name
|
||||
FROM assignments a JOIN users u ON u.azure_id = a.user_azure_id
|
||||
WHERE a.wled_device_id = ? AND a.segment = ?
|
||||
""", (target["id"], target["segment"])).fetchone()
|
||||
|
||||
home = None
|
||||
is_here = False
|
||||
if user:
|
||||
home = conn.execute("""
|
||||
SELECT h.segment, d.name, d.hostname
|
||||
FROM home_assignments h JOIN wled_devices d ON d.id = h.wled_device_id
|
||||
WHERE h.user_azure_id = ?
|
||||
""", (user["azure_id"],)).fetchone()
|
||||
if target:
|
||||
is_here = conn.execute("""
|
||||
SELECT 1 FROM assignments
|
||||
WHERE user_azure_id = ? AND wled_device_id = ? AND segment = ?
|
||||
""", (user["azure_id"], target["id"], target["segment"])).fetchone() is not None
|
||||
|
||||
return templates.TemplateResponse(request, "zentrale.html", {
|
||||
"page": "zentrale",
|
||||
"target": target,
|
||||
"occupant": occupant["display_name"] if occupant else None,
|
||||
"extension": extension,
|
||||
"user": user,
|
||||
"home": home,
|
||||
"is_here": is_here,
|
||||
"error": error,
|
||||
"message": message,
|
||||
})
|
||||
|
||||
|
||||
def _zentrale_owner_row(conn, target):
|
||||
"""Heimateintrag, der auf das Zentral-Segment zeigt - der Stamm-Besetzer."""
|
||||
if not target:
|
||||
@@ -634,45 +598,122 @@ def _restore_zentrale_owner(conn, target):
|
||||
return owner
|
||||
|
||||
|
||||
def _zentrale_state(conn, target, user):
|
||||
"""Alles, was die Self-Service-Seite ueber die aktuelle Lage wissen muss.
|
||||
|
||||
Wird bei jedem GET frisch berechnet. Die Aktionen leiten deshalb nach dem
|
||||
Absenden auf /zentrale/{durchwahl} um: die Seite zeigt dann den echten
|
||||
Zustand und nicht das Ergebnis eines abgeschickten Formulars.
|
||||
"""
|
||||
st = {
|
||||
"occupant": None, "is_here": False, "home": None,
|
||||
"home_blocker": None, "is_owner": False, "owner_waiting": None,
|
||||
"occupant_is_owner": False,
|
||||
}
|
||||
if not target:
|
||||
return st
|
||||
|
||||
st["occupant"] = conn.execute("""
|
||||
SELECT u.azure_id, u.display_name
|
||||
FROM assignments a JOIN users u ON u.azure_id = a.user_azure_id
|
||||
WHERE a.wled_device_id = ? AND a.segment = ?
|
||||
""", (target["id"], target["segment"])).fetchone()
|
||||
|
||||
owner = _zentrale_owner_row(conn, target)
|
||||
if owner and (not st["occupant"] or owner["user_azure_id"] != st["occupant"]["azure_id"]):
|
||||
st["owner_waiting"] = owner["display_name"]
|
||||
|
||||
# Sitzt der aktuelle Besetzer auf seinem Stammplatz? Dann kommt er nach einer
|
||||
# Uebernahme von allein zurueck - das gehoert auf den Knopf geschrieben.
|
||||
if st["occupant"]:
|
||||
st["occupant_is_owner"] = conn.execute("""
|
||||
SELECT 1 FROM home_assignments
|
||||
WHERE user_azure_id = ? AND wled_device_id = ? AND segment = ?
|
||||
""", (st["occupant"]["azure_id"], target["id"], target["segment"])).fetchone() is not None
|
||||
|
||||
if not user:
|
||||
return st
|
||||
|
||||
st["is_here"] = bool(st["occupant"] and st["occupant"]["azure_id"] == user["azure_id"])
|
||||
|
||||
st["home"] = conn.execute("""
|
||||
SELECT h.wled_device_id, h.segment, d.name, d.hostname
|
||||
FROM home_assignments h JOIN wled_devices d ON d.id = h.wled_device_id
|
||||
WHERE h.user_azure_id = ?
|
||||
""", (user["azure_id"],)).fetchone()
|
||||
|
||||
if st["home"]:
|
||||
st["is_owner"] = (st["home"]["wled_device_id"], st["home"]["segment"]) \
|
||||
== (target["id"], target["segment"])
|
||||
if not st["is_owner"]:
|
||||
blocker = conn.execute("""
|
||||
SELECT u.display_name FROM assignments a JOIN users u ON u.azure_id = a.user_azure_id
|
||||
WHERE a.wled_device_id = ? AND a.segment = ? AND a.user_azure_id != ?
|
||||
""", (st["home"]["wled_device_id"], st["home"]["segment"], user["azure_id"])).fetchone()
|
||||
st["home_blocker"] = blocker["display_name"] if blocker else None
|
||||
return st
|
||||
|
||||
|
||||
def _zentrale_page(request, conn, target, extension="", user=None, done=None):
|
||||
"""Self-Service-Seite rendern."""
|
||||
st = _zentrale_state(conn, target, user)
|
||||
return templates.TemplateResponse(request, "zentrale.html", {
|
||||
"page": "zentrale", "target": target, "extension": extension,
|
||||
"user": user, "done": done, **st,
|
||||
})
|
||||
|
||||
|
||||
def _zentrale_redirect(extension, done):
|
||||
"""Nach jeder Aktion zurueck auf die Zustandsseite (POST/Redirect/GET)."""
|
||||
ext = quote((extension or "").strip(), safe="")
|
||||
if not ext:
|
||||
return RedirectResponse(f"/zentrale?done={done}", status_code=303)
|
||||
return RedirectResponse(f"/zentrale/{ext}?done={done}", status_code=303)
|
||||
|
||||
|
||||
@app.get("/zentrale", response_class=HTMLResponse)
|
||||
async def zentrale_page(request: Request):
|
||||
"""Self-Service-Seite fuer den Empfangsbereich. Absichtlich ohne Admin-Login."""
|
||||
# Das Formular schickt die Durchwahl per GET - auf die Personenseite umleiten,
|
||||
# damit die Adresse merkbar und neu ladbar ist.
|
||||
typed = (request.query_params.get("durchwahl") or "").strip()
|
||||
if typed:
|
||||
return RedirectResponse(f"/zentrale/{quote(typed, safe='')}", status_code=303)
|
||||
|
||||
conn = database.get_db()
|
||||
try:
|
||||
return _zentrale_view(request, conn, _selfservice_target(conn))
|
||||
return _zentrale_page(request, conn, _selfservice_target(conn),
|
||||
done=request.query_params.get("done"))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.get("/zentrale/{extension}", response_class=HTMLResponse)
|
||||
async def zentrale_page_for_extension(request: Request, extension: str):
|
||||
"""Direktlink pro Mitarbeiter (z.B. als QR-Code). Zeigt nur an, aendert nichts."""
|
||||
"""Zustandsseite pro Mitarbeiter. Zeigt nur an, geaendert wird per Knopf."""
|
||||
conn = database.get_db()
|
||||
try:
|
||||
target = _selfservice_target(conn)
|
||||
user = _user_by_extension(extension, conn)
|
||||
error = None if user else f"Durchwahl {extension} ist keinem aktiven Benutzer zugeordnet."
|
||||
return _zentrale_view(request, conn, target, extension=extension, user=user, error=error)
|
||||
done = request.query_params.get("done") if user else "err_unbekannt"
|
||||
return _zentrale_page(request, conn, target, extension=extension, user=user, done=done)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.post("/api/zentrale/uebernehmen", response_class=HTMLResponse)
|
||||
async def zentrale_takeover(request: Request, extension: str = Form(...)):
|
||||
@app.post("/api/zentrale/uebernehmen")
|
||||
async def zentrale_takeover(extension: str = Form(...)):
|
||||
"""Mitarbeiter setzt sich selbst auf das Busylight der Zentrale."""
|
||||
conn = database.get_db()
|
||||
try:
|
||||
target = _selfservice_target(conn)
|
||||
if not target:
|
||||
return _zentrale_view(request, conn, target, extension=extension,
|
||||
error="Die Zentrale ist noch nicht eingerichtet. Bitte den Administrator informieren.")
|
||||
return _zentrale_redirect(extension, "err_nicht_eingerichtet")
|
||||
|
||||
user = _user_by_extension(extension, conn)
|
||||
if not user:
|
||||
return _zentrale_view(request, conn, target, extension=extension,
|
||||
error=f"Durchwahl {extension} ist keinem aktiven Benutzer zugeordnet.")
|
||||
return _zentrale_redirect(extension, "err_unbekannt")
|
||||
|
||||
# Sitzt der Benutzer schon dort? Dann nur das Licht nachziehen.
|
||||
already = conn.execute("""
|
||||
SELECT 1 FROM assignments
|
||||
WHERE user_azure_id = ? AND wled_device_id = ? AND segment = ?
|
||||
@@ -680,8 +721,7 @@ async def zentrale_takeover(request: Request, extension: str = Form(...)):
|
||||
if already:
|
||||
scheduler.push_user_light(user["azure_id"], user["display_name"],
|
||||
target["hostname"], target["segment"], conn)
|
||||
return _zentrale_view(request, conn, target, extension=extension, user=user,
|
||||
message=f"{user['display_name']} ist bereits auf der Zentrale eingetragen.")
|
||||
return _zentrale_redirect(extension, "bereits")
|
||||
|
||||
# Bisheriger Platz des Benutzers wird als Heimat-Tuerschild gemerkt
|
||||
own = conn.execute("""
|
||||
@@ -715,8 +755,6 @@ async def zentrale_takeover(request: Request, extension: str = Form(...)):
|
||||
# er erst selbst "zurueck" klicken, bevor die Uebernahme fuer ihn stimmt - und
|
||||
# bis dahin haette er gar kein Licht.
|
||||
pred_back = None
|
||||
pred_blocked = None
|
||||
pred_waiting = False
|
||||
if predecessor and predecessor["azure_id"] != user["azure_id"]:
|
||||
pred_home = conn.execute("""
|
||||
SELECT h.wled_device_id, h.segment, d.hostname, d.name
|
||||
@@ -731,15 +769,13 @@ async def zentrale_takeover(request: Request, extension: str = Form(...)):
|
||||
# Sein Stammplatz IST die Zentrale, die jetzt der Nachfolger hat.
|
||||
# Der Heimateintrag bleibt stehen, damit er zurueckkommt, sobald
|
||||
# die Zentrale wieder frei wird.
|
||||
pred_waiting = True
|
||||
pass
|
||||
elif pred_home:
|
||||
taken = conn.execute("""
|
||||
SELECT u.display_name FROM assignments a JOIN users u ON u.azure_id = a.user_azure_id
|
||||
WHERE a.wled_device_id = ? AND a.segment = ?
|
||||
SELECT 1 FROM assignments
|
||||
WHERE wled_device_id = ? AND segment = ?
|
||||
""", (pred_home["wled_device_id"], pred_home["segment"])).fetchone()
|
||||
if taken:
|
||||
pred_blocked = taken["display_name"]
|
||||
else:
|
||||
if not taken:
|
||||
conn.execute("""
|
||||
INSERT INTO assignments (user_azure_id, wled_device_id, segment) VALUES (?, ?, ?)
|
||||
""", (predecessor["azure_id"], pred_home["wled_device_id"], pred_home["segment"]))
|
||||
@@ -758,7 +794,6 @@ async def zentrale_takeover(request: Request, extension: str = Form(...)):
|
||||
INSERT OR REPLACE INTO home_assignments (user_azure_id, wled_device_id, segment)
|
||||
VALUES (?, ?, ?)
|
||||
""", (predecessor["azure_id"], target["id"], target["segment"]))
|
||||
pred_waiting = True
|
||||
|
||||
conn.commit()
|
||||
|
||||
@@ -777,44 +812,23 @@ async def zentrale_takeover(request: Request, extension: str = Form(...)):
|
||||
scheduler.push_user_light(predecessor["azure_id"], predecessor["display_name"],
|
||||
pred_back["hostname"], pred_back["segment"], conn)
|
||||
|
||||
logger.info("Zentrale uebernommen von %s (Durchwahl %s), Vorgaenger: %s (%s)",
|
||||
logger.info("Zentrale uebernommen von %s (Durchwahl %s), Vorgaenger: %s",
|
||||
user["display_name"], extension,
|
||||
predecessor["display_name"] if predecessor else "-",
|
||||
"zurueck auf eigenem Tuerschild" if pred_back
|
||||
else f"Tuerschild belegt von {pred_blocked}" if pred_blocked
|
||||
else "wartet auf die Zentrale" if pred_waiting
|
||||
else "kein Tuerschild gemerkt")
|
||||
|
||||
msg = f"{user['display_name']} sitzt jetzt auf der Zentrale ({target['name']})."
|
||||
if own:
|
||||
msg += f" Das eigene Tuerschild ({own['name'] or own['hostname']}) ist bis zur Rueckkehr ausgeschaltet."
|
||||
if pred_back:
|
||||
msg += (f" {predecessor['display_name']} wurde auf das eigene Tuerschild "
|
||||
f"({pred_back['name'] or pred_back['hostname']}) zurueckgesetzt.")
|
||||
elif pred_blocked:
|
||||
msg += (f" {predecessor['display_name']} konnte nicht zurueckgesetzt werden, "
|
||||
f"weil das eigene Tuerschild inzwischen {pred_blocked} zugeordnet ist.")
|
||||
elif pred_waiting:
|
||||
msg += (f" {predecessor['display_name']} gehoert auf die Zentrale und kommt automatisch "
|
||||
"zurueck, sobald sie wieder freigegeben wird.")
|
||||
elif predecessor and predecessor["azure_id"] != user["azure_id"]:
|
||||
msg += (f" Fuer {predecessor['display_name']} war kein eigenes Tuerschild gemerkt, "
|
||||
"er ist jetzt ohne Zuordnung.")
|
||||
return _zentrale_view(request, conn, target, extension=extension, user=user, message=msg)
|
||||
predecessor["display_name"] if predecessor else "-")
|
||||
return _zentrale_redirect(extension, "uebernommen")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.post("/api/zentrale/verlassen", response_class=HTMLResponse)
|
||||
async def zentrale_release(request: Request, extension: str = Form(...)):
|
||||
@app.post("/api/zentrale/verlassen")
|
||||
async def zentrale_release(extension: str = Form(...)):
|
||||
"""Mitarbeiter verlaesst die Zentrale und geht auf sein Heimat-Tuerschild zurueck."""
|
||||
conn = database.get_db()
|
||||
try:
|
||||
target = _selfservice_target(conn)
|
||||
user = _user_by_extension(extension, conn)
|
||||
if not user:
|
||||
return _zentrale_view(request, conn, target, extension=extension,
|
||||
error=f"Durchwahl {extension} ist keinem aktiven Benutzer zugeordnet.")
|
||||
return _zentrale_redirect(extension, "err_unbekannt")
|
||||
|
||||
was_here = bool(target) and conn.execute("""
|
||||
SELECT 1 FROM assignments
|
||||
@@ -829,8 +843,7 @@ async def zentrale_release(request: Request, extension: str = Form(...)):
|
||||
|
||||
if not home:
|
||||
if not was_here:
|
||||
return _zentrale_view(request, conn, target, extension=extension, user=user,
|
||||
error="Es ist kein Heimat-Tuerschild gespeichert und du sitzt nicht auf der Zentrale.")
|
||||
return _zentrale_redirect(extension, "err_nichts_zu_tun")
|
||||
# Nur von der Zentrale abmelden, es gibt keinen Platz zum Zurueckgehen
|
||||
conn.execute("DELETE FROM assignments WHERE user_azure_id = ?", (user["azure_id"],))
|
||||
returning = _restore_zentrale_owner(conn, target)
|
||||
@@ -840,35 +853,21 @@ async def zentrale_release(request: Request, extension: str = Form(...)):
|
||||
target["hostname"], target["segment"], conn)
|
||||
else:
|
||||
scheduler.clear_segment(target["hostname"], target["segment"], conn)
|
||||
logger.info("Zentrale verlassen von %s (kein Heimat-Tuerschild), zurueck auf der Zentrale: %s",
|
||||
logger.info("Zentrale verlassen von %s (kein Heimat-Tuerschild), zurueck: %s",
|
||||
user["display_name"], returning["display_name"] if returning else "-")
|
||||
msg = f"{user['display_name']} ist von der Zentrale abgemeldet. Ein eigenes Tuerschild war nicht gespeichert."
|
||||
if returning:
|
||||
msg += f" {returning['display_name']} sitzt wieder auf der Zentrale."
|
||||
return _zentrale_view(request, conn, target, extension=extension, user=user, message=msg)
|
||||
return _zentrale_redirect(extension, "abgemeldet")
|
||||
|
||||
# Der Stamm-Besetzer der Zentrale hat keinen anderen Platz, auf den er
|
||||
# zurueckgehen koennte - sein Heimatplatz IST die Zentrale.
|
||||
# Der Stamm-Besetzer hat keinen anderen Platz - sein Heimatplatz IST die Zentrale
|
||||
if target and (home["wled_device_id"], home["segment"]) == (target["id"], target["segment"]):
|
||||
if was_here:
|
||||
return _zentrale_view(request, conn, target, extension=extension, user=user,
|
||||
message=("Die Zentrale ist dein eigener Platz - du sitzt schon dort. "
|
||||
"Es gibt nichts zurueckzusetzen."))
|
||||
return _zentrale_view(request, conn, target, extension=extension, user=user,
|
||||
error=("Die Zentrale ist dein eigener Platz, aktuell hat sie jemand anderes. "
|
||||
"Mit \"Zentrale uebernehmen\" holst du sie zurueck."))
|
||||
return _zentrale_redirect(extension, "stammplatz_hier" if was_here else "err_stammplatz_fremd")
|
||||
|
||||
# Ist der Heimatplatz inzwischen von jemand anderem belegt?
|
||||
blocker = conn.execute("""
|
||||
SELECT u.display_name
|
||||
FROM assignments a JOIN users u ON u.azure_id = a.user_azure_id
|
||||
WHERE a.wled_device_id = ? AND a.segment = ? AND a.user_azure_id != ?
|
||||
SELECT 1 FROM assignments
|
||||
WHERE wled_device_id = ? AND segment = ? AND user_azure_id != ?
|
||||
""", (home["wled_device_id"], home["segment"], user["azure_id"])).fetchone()
|
||||
if blocker:
|
||||
return _zentrale_view(request, conn, target, extension=extension, user=user,
|
||||
error=(f"Das eigene Tuerschild ({home['name'] or home['hostname']}, Segment {home['segment']}) "
|
||||
f"ist inzwischen {blocker['display_name']} zugeordnet. "
|
||||
"Bitte den Administrator ansprechen - es wurde nichts geaendert."))
|
||||
return _zentrale_redirect(extension, "err_heimat_belegt")
|
||||
|
||||
conn.execute("DELETE FROM assignments WHERE user_azure_id = ?", (user["azure_id"],))
|
||||
conn.execute("""
|
||||
@@ -892,12 +891,7 @@ async def zentrale_release(request: Request, extension: str = Form(...)):
|
||||
logger.info("Zentrale verlassen von %s, zurueck auf %s Segment %s; Zentrale nun: %s",
|
||||
user["display_name"], home["hostname"], home["segment"],
|
||||
returning["display_name"] if returning else "frei")
|
||||
|
||||
msg = (f"{user['display_name']} ist zurueck auf dem eigenen Tuerschild "
|
||||
f"({home['name'] or home['hostname']}, Segment {home['segment']}).")
|
||||
if returning:
|
||||
msg += f" {returning['display_name']} sitzt wieder auf der Zentrale."
|
||||
return _zentrale_view(request, conn, target, extension=extension, user=user, message=msg)
|
||||
return _zentrale_redirect(extension, "zurueck")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user