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:
2026-08-21 09:08:08 +00:00
co-authored by Claude Opus 5
parent 09d9837dec
commit 0867221a3c
3 changed files with 329 additions and 230 deletions
+28 -4
View File
@@ -205,12 +205,30 @@ erreichbar ist und die Durchwahl als Kennung nutzt.
### Bedienung ### Bedienung
``` ```
http://SERVER:8080/zentrale - Durchwahl eingeben und Zentrale uebernehmen http://SERVER:8080/zentrale - Durchwahl eingeben
http://SERVER:8080/zentrale/{durchwahl} - Direktlink pro Person, z.B. als QR-Code http://SERVER:8080/zentrale/{durchwahl} - Zustandsseite pro Person, als Lesezeichen
oder QR-Code
``` ```
Der Direktlink zeigt nur den Stand an und aendert nichts - das Umstellen passiert Die Bedienung laeuft in zwei Schritten. Auf `/zentrale` gibt man seine Durchwahl ein
erst per Knopfdruck. und landet auf der eigenen Zustandsseite `/zentrale/{durchwahl}`. Dort steht, wer
gerade auf der Zentrale sitzt und welche Knoepfe zur Verfuegung stehen - geaendert
wird erst per Klick.
Die Zustandsseite ist die eigentliche Adresse fuer den Alltag und als Lesezeichen
gedacht. Sie zeigt immer den echten Zustand, passt die Knoepfe daran an und ist
gefahrlos neu ladbar:
| Lage | Angebot |
|------|---------|
| Zentrale frei | **Zentrale besetzen** |
| jemand anderes sitzt darauf | **Zentrale uebernehmen**, mit Hinweis was mit dieser Person passiert |
| man sitzt selbst darauf | **Zurueck auf mein Tuerschild** |
| es ist der eigene Stammplatz | kein Knopf, nur der Hinweis dass nichts zu tun ist |
Jede Aktion leitet auf diese Seite zurueck (POST/Redirect/GET). Man landet also nie
auf einer Formular-Antwort, die beim Neuladen die Aktion wiederholen wuerde, und sieht
nach jedem Klick den neuen Zustand samt Bestaetigung.
### Verhalten ### Verhalten
@@ -353,6 +371,12 @@ busylight/
- Unter Tuerschilder den Status pruefen ("Alle jetzt pruefen") - Unter Tuerschilder den Status pruefen ("Alle jetzt pruefen")
- Netzwerkverbindung pruefen: `curl http://HOSTNAME/json/info` - Netzwerkverbindung pruefen: `curl http://HOSTNAME/json/info`
### Auf /zentrale fehlt der Knopf zum Zurueckgeben
Der ist absichtlich erst auf der Zustandsseite `/zentrale/{durchwahl}` zu sehen, denn
vorher weiss die Anwendung nicht, wer zurueckgeben will. Durchwahl eingeben, **Weiter**
klicken - dann erscheint der passende Knopf.
### Zentrale-Seite sagt "noch nicht eingerichtet" ### Zentrale-Seite sagt "noch nicht eingerichtet"
Unter **Einstellungen > Zentrale (Self-Service)** ist kein Busylight ausgewaehlt. Unter **Einstellungen > Zentrale (Self-Service)** ist kein Busylight ausgewaehlt.
+109 -115
View File
@@ -10,6 +10,7 @@ import shutil
import sys import sys
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import datetime from datetime import datetime
from urllib.parse import quote
from fastapi import FastAPI, Request, Form, UploadFile, File, BackgroundTasks from fastapi import FastAPI, Request, Form, UploadFile, File, BackgroundTasks
from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse, JSONResponse from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse, JSONResponse
@@ -559,43 +560,6 @@ def _user_by_extension(extension, conn):
""", (ext,)).fetchone() """, (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): def _zentrale_owner_row(conn, target):
"""Heimateintrag, der auf das Zentral-Segment zeigt - der Stamm-Besetzer.""" """Heimateintrag, der auf das Zentral-Segment zeigt - der Stamm-Besetzer."""
if not target: if not target:
@@ -634,45 +598,122 @@ def _restore_zentrale_owner(conn, target):
return owner 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) @app.get("/zentrale", response_class=HTMLResponse)
async def zentrale_page(request: Request): async def zentrale_page(request: Request):
"""Self-Service-Seite fuer den Empfangsbereich. Absichtlich ohne Admin-Login.""" """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() conn = database.get_db()
try: try:
return _zentrale_view(request, conn, _selfservice_target(conn)) return _zentrale_page(request, conn, _selfservice_target(conn),
done=request.query_params.get("done"))
finally: finally:
conn.close() conn.close()
@app.get("/zentrale/{extension}", response_class=HTMLResponse) @app.get("/zentrale/{extension}", response_class=HTMLResponse)
async def zentrale_page_for_extension(request: Request, extension: str): 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() conn = database.get_db()
try: try:
target = _selfservice_target(conn) target = _selfservice_target(conn)
user = _user_by_extension(extension, conn) user = _user_by_extension(extension, conn)
error = None if user else f"Durchwahl {extension} ist keinem aktiven Benutzer zugeordnet." done = request.query_params.get("done") if user else "err_unbekannt"
return _zentrale_view(request, conn, target, extension=extension, user=user, error=error) return _zentrale_page(request, conn, target, extension=extension, user=user, done=done)
finally: finally:
conn.close() conn.close()
@app.post("/api/zentrale/uebernehmen", response_class=HTMLResponse) @app.post("/api/zentrale/uebernehmen")
async def zentrale_takeover(request: Request, extension: str = Form(...)): async def zentrale_takeover(extension: str = Form(...)):
"""Mitarbeiter setzt sich selbst auf das Busylight der Zentrale.""" """Mitarbeiter setzt sich selbst auf das Busylight der Zentrale."""
conn = database.get_db() conn = database.get_db()
try: try:
target = _selfservice_target(conn) target = _selfservice_target(conn)
if not target: if not target:
return _zentrale_view(request, conn, target, extension=extension, return _zentrale_redirect(extension, "err_nicht_eingerichtet")
error="Die Zentrale ist noch nicht eingerichtet. Bitte den Administrator informieren.")
user = _user_by_extension(extension, conn) user = _user_by_extension(extension, conn)
if not user: if not user:
return _zentrale_view(request, conn, target, extension=extension, return _zentrale_redirect(extension, "err_unbekannt")
error=f"Durchwahl {extension} ist keinem aktiven Benutzer zugeordnet.")
# Sitzt der Benutzer schon dort? Dann nur das Licht nachziehen.
already = conn.execute(""" already = conn.execute("""
SELECT 1 FROM assignments SELECT 1 FROM assignments
WHERE user_azure_id = ? AND wled_device_id = ? AND segment = ? 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: if already:
scheduler.push_user_light(user["azure_id"], user["display_name"], scheduler.push_user_light(user["azure_id"], user["display_name"],
target["hostname"], target["segment"], conn) target["hostname"], target["segment"], conn)
return _zentrale_view(request, conn, target, extension=extension, user=user, return _zentrale_redirect(extension, "bereits")
message=f"{user['display_name']} ist bereits auf der Zentrale eingetragen.")
# Bisheriger Platz des Benutzers wird als Heimat-Tuerschild gemerkt # Bisheriger Platz des Benutzers wird als Heimat-Tuerschild gemerkt
own = conn.execute(""" 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 # er erst selbst "zurueck" klicken, bevor die Uebernahme fuer ihn stimmt - und
# bis dahin haette er gar kein Licht. # bis dahin haette er gar kein Licht.
pred_back = None pred_back = None
pred_blocked = None
pred_waiting = False
if predecessor and predecessor["azure_id"] != user["azure_id"]: if predecessor and predecessor["azure_id"] != user["azure_id"]:
pred_home = conn.execute(""" pred_home = conn.execute("""
SELECT h.wled_device_id, h.segment, d.hostname, d.name 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. # Sein Stammplatz IST die Zentrale, die jetzt der Nachfolger hat.
# Der Heimateintrag bleibt stehen, damit er zurueckkommt, sobald # Der Heimateintrag bleibt stehen, damit er zurueckkommt, sobald
# die Zentrale wieder frei wird. # die Zentrale wieder frei wird.
pred_waiting = True pass
elif pred_home: elif pred_home:
taken = conn.execute(""" taken = conn.execute("""
SELECT u.display_name FROM assignments a JOIN users u ON u.azure_id = a.user_azure_id SELECT 1 FROM assignments
WHERE a.wled_device_id = ? AND a.segment = ? WHERE wled_device_id = ? AND segment = ?
""", (pred_home["wled_device_id"], pred_home["segment"])).fetchone() """, (pred_home["wled_device_id"], pred_home["segment"])).fetchone()
if taken: if not taken:
pred_blocked = taken["display_name"]
else:
conn.execute(""" conn.execute("""
INSERT INTO assignments (user_azure_id, wled_device_id, segment) VALUES (?, ?, ?) INSERT INTO assignments (user_azure_id, wled_device_id, segment) VALUES (?, ?, ?)
""", (predecessor["azure_id"], pred_home["wled_device_id"], pred_home["segment"])) """, (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) INSERT OR REPLACE INTO home_assignments (user_azure_id, wled_device_id, segment)
VALUES (?, ?, ?) VALUES (?, ?, ?)
""", (predecessor["azure_id"], target["id"], target["segment"])) """, (predecessor["azure_id"], target["id"], target["segment"]))
pred_waiting = True
conn.commit() 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"], scheduler.push_user_light(predecessor["azure_id"], predecessor["display_name"],
pred_back["hostname"], pred_back["segment"], conn) 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, user["display_name"], extension,
predecessor["display_name"] if predecessor else "-", predecessor["display_name"] if predecessor else "-")
"zurueck auf eigenem Tuerschild" if pred_back return _zentrale_redirect(extension, "uebernommen")
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)
finally: finally:
conn.close() conn.close()
@app.post("/api/zentrale/verlassen", response_class=HTMLResponse) @app.post("/api/zentrale/verlassen")
async def zentrale_release(request: Request, extension: str = Form(...)): async def zentrale_release(extension: str = Form(...)):
"""Mitarbeiter verlaesst die Zentrale und geht auf sein Heimat-Tuerschild zurueck.""" """Mitarbeiter verlaesst die Zentrale und geht auf sein Heimat-Tuerschild zurueck."""
conn = database.get_db() conn = database.get_db()
try: try:
target = _selfservice_target(conn) target = _selfservice_target(conn)
user = _user_by_extension(extension, conn) user = _user_by_extension(extension, conn)
if not user: if not user:
return _zentrale_view(request, conn, target, extension=extension, return _zentrale_redirect(extension, "err_unbekannt")
error=f"Durchwahl {extension} ist keinem aktiven Benutzer zugeordnet.")
was_here = bool(target) and conn.execute(""" was_here = bool(target) and conn.execute("""
SELECT 1 FROM assignments SELECT 1 FROM assignments
@@ -829,8 +843,7 @@ async def zentrale_release(request: Request, extension: str = Form(...)):
if not home: if not home:
if not was_here: if not was_here:
return _zentrale_view(request, conn, target, extension=extension, user=user, return _zentrale_redirect(extension, "err_nichts_zu_tun")
error="Es ist kein Heimat-Tuerschild gespeichert und du sitzt nicht auf der Zentrale.")
# Nur von der Zentrale abmelden, es gibt keinen Platz zum Zurueckgehen # Nur von der Zentrale abmelden, es gibt keinen Platz zum Zurueckgehen
conn.execute("DELETE FROM assignments WHERE user_azure_id = ?", (user["azure_id"],)) conn.execute("DELETE FROM assignments WHERE user_azure_id = ?", (user["azure_id"],))
returning = _restore_zentrale_owner(conn, target) returning = _restore_zentrale_owner(conn, target)
@@ -840,35 +853,21 @@ async def zentrale_release(request: Request, extension: str = Form(...)):
target["hostname"], target["segment"], conn) target["hostname"], target["segment"], conn)
else: else:
scheduler.clear_segment(target["hostname"], target["segment"], conn) 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 "-") 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." return _zentrale_redirect(extension, "abgemeldet")
if returning:
msg += f" {returning['display_name']} sitzt wieder auf der Zentrale."
return _zentrale_view(request, conn, target, extension=extension, user=user, message=msg)
# Der Stamm-Besetzer der Zentrale hat keinen anderen Platz, auf den er # Der Stamm-Besetzer hat keinen anderen Platz - sein Heimatplatz IST die Zentrale
# zurueckgehen koennte - sein Heimatplatz IST die Zentrale.
if target and (home["wled_device_id"], home["segment"]) == (target["id"], target["segment"]): if target and (home["wled_device_id"], home["segment"]) == (target["id"], target["segment"]):
if was_here: return _zentrale_redirect(extension, "stammplatz_hier" if was_here else "err_stammplatz_fremd")
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."))
# Ist der Heimatplatz inzwischen von jemand anderem belegt? # Ist der Heimatplatz inzwischen von jemand anderem belegt?
blocker = conn.execute(""" blocker = conn.execute("""
SELECT u.display_name SELECT 1 FROM assignments
FROM assignments a JOIN users u ON u.azure_id = a.user_azure_id WHERE wled_device_id = ? AND segment = ? AND user_azure_id != ?
WHERE a.wled_device_id = ? AND a.segment = ? AND a.user_azure_id != ?
""", (home["wled_device_id"], home["segment"], user["azure_id"])).fetchone() """, (home["wled_device_id"], home["segment"], user["azure_id"])).fetchone()
if blocker: if blocker:
return _zentrale_view(request, conn, target, extension=extension, user=user, return _zentrale_redirect(extension, "err_heimat_belegt")
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."))
conn.execute("DELETE FROM assignments WHERE user_azure_id = ?", (user["azure_id"],)) conn.execute("DELETE FROM assignments WHERE user_azure_id = ?", (user["azure_id"],))
conn.execute(""" 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", logger.info("Zentrale verlassen von %s, zurueck auf %s Segment %s; Zentrale nun: %s",
user["display_name"], home["hostname"], home["segment"], user["display_name"], home["hostname"], home["segment"],
returning["display_name"] if returning else "frei") returning["display_name"] if returning else "frei")
return _zentrale_redirect(extension, "zurueck")
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)
finally: finally:
conn.close() conn.close()
+192 -111
View File
@@ -8,118 +8,199 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css" rel="stylesheet">
</head> </head>
<body class="bg-light"> <body class="bg-light">
<div class="container"> <div class="container">
<div class="row justify-content-center mt-4 mb-5"> <div class="row justify-content-center mt-4 mb-5">
<div class="col-md-5"> <div class="col-md-5">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body p-4"> <div class="card-body p-4">
<div class="text-center mb-4">
<img src="{{ logo_url() }}" alt="Logo" style="height: 40px;" class="mb-2">
<h5 class="mb-0">Zentrale besetzen</h5>
</div>
{% if message %} <div class="text-center mb-4">
<div class="alert alert-success py-2"> <img src="{{ logo_url() }}" alt="Logo" style="height: 40px;" class="mb-2">
<small><i class="bi bi-check-circle"></i> {{ message }}</small> <h5 class="mb-0">Zentrale besetzen</h5>
</div>
{% endif %}
{% if error %}
<div class="alert alert-danger py-2">
<small><i class="bi bi-exclamation-triangle"></i> {{ error }}</small>
</div>
{% endif %}
{% if not target %}
<div class="alert alert-warning py-2 mb-0">
<small>
<i class="bi bi-gear"></i>
Die Zentrale ist noch nicht eingerichtet. Unter
<strong>Einstellungen &gt; Zentrale</strong> muss ein Busylight
und ein Segment ausgewaehlt werden.
</small>
</div>
{% else %}
<div class="border rounded p-3 mb-4 bg-white">
<div class="d-flex justify-content-between align-items-center">
<div>
<small class="text-muted d-block">Busylight der Zentrale</small>
<strong>{{ target.name }}</strong>
<small class="text-muted">(Segment {{ target.segment }})</small>
</div>
</div>
<hr class="my-2">
<small class="text-muted d-block">Aktuell besetzt von</small>
{% if occupant %}
<strong class="text-success"><i class="bi bi-person-fill"></i> {{ occupant }}</strong>
{% else %}
<span class="text-muted"><i class="bi bi-person-dash"></i> niemand</span>
{% endif %}
</div>
{% if user %}
<div class="text-center mb-3">
<small class="text-muted d-block">Durchwahl {{ user.phone_extension }}</small>
<strong>{{ user.display_name }}</strong>
</div>
{% if is_here %}
<div class="alert alert-info py-2">
<small><i class="bi bi-info-circle"></i> Du sitzt derzeit auf der Zentrale.</small>
</div>
{% endif %}
<form method="post" action="/api/zentrale/uebernehmen" class="mb-2">
<input type="hidden" name="extension" value="{{ user.phone_extension }}">
<button type="submit" class="btn btn-primary w-100" {% if is_here %}disabled{% endif %}>
<i class="bi bi-box-arrow-in-right"></i> Zentrale uebernehmen
</button>
</form>
<form method="post" action="/api/zentrale/verlassen">
<input type="hidden" name="extension" value="{{ user.phone_extension }}">
<button type="submit" class="btn btn-outline-secondary w-100">
<i class="bi bi-box-arrow-left"></i> Zurueck auf mein Tuerschild
</button>
</form>
{% if home %}
<div class="text-center mt-3">
<small class="text-muted">
Gemerktes Tuerschild: {{ home.name or home.hostname }} (Segment {{ home.segment }})
</small>
</div>
{% endif %}
<div class="text-center mt-3">
<a href="/zentrale" class="text-muted small">Andere Durchwahl</a>
</div>
{% else %}
<form method="post" action="/api/zentrale/uebernehmen">
<div class="mb-3">
<label class="form-label">Eigene Telefon-Durchwahl</label>
<input type="text" name="extension" class="form-control form-control-lg text-center"
inputmode="numeric" autocomplete="off" autofocus required
value="{{ extension }}" placeholder="z.B. 123">
<small class="text-muted">
Damit wird die eigene Person auf das Busylight der Zentrale gesetzt.
</small>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-box-arrow-in-right"></i> Zentrale uebernehmen
</button>
</form>
{% endif %}
{% endif %}
<div class="text-center mt-4">
<small class="text-secondary" style="font-size:0.65rem">&copy; cynfo GmbH</small>
</div>
</div>
</div>
</div>
</div>
</div> </div>
{# ── Rueckmeldung der letzten Aktion ───────────────────────────────── #}
{% if done == 'uebernommen' %}
<div class="alert alert-success py-2"><small><i class="bi bi-check-circle"></i>
Du sitzt jetzt auf der Zentrale.</small></div>
{% elif done == 'bereits' %}
<div class="alert alert-info py-2"><small><i class="bi bi-info-circle"></i>
Du warst schon auf der Zentrale eingetragen. Das Licht wurde neu gesetzt.</small></div>
{% elif done == 'zurueck' %}
<div class="alert alert-success py-2"><small><i class="bi bi-check-circle"></i>
Zurueck auf dem eigenen Tuerschild.</small></div>
{% elif done == 'abgemeldet' %}
<div class="alert alert-success py-2"><small><i class="bi bi-check-circle"></i>
Von der Zentrale abgemeldet.</small></div>
{% elif done == 'stammplatz_hier' %}
<div class="alert alert-info py-2"><small><i class="bi bi-info-circle"></i>
Die Zentrale ist dein eigener Platz - es gibt nichts zurueckzusetzen.</small></div>
{% elif done == 'err_unbekannt' %}
<div class="alert alert-danger py-2"><small><i class="bi bi-x-circle"></i>
Die Durchwahl {{ extension }} ist keinem aktiven Benutzer zugeordnet.</small></div>
{% elif done == 'err_nicht_eingerichtet' %}
<div class="alert alert-warning py-2"><small><i class="bi bi-gear"></i>
Die Zentrale ist noch nicht eingerichtet.</small></div>
{% elif done == 'err_heimat_belegt' %}
<div class="alert alert-danger py-2"><small><i class="bi bi-x-circle"></i>
Dein eigenes Tuerschild ist inzwischen
{%- if home_blocker %} {{ home_blocker }}{% else %} jemand anderem{% endif %} zugeordnet.
Es wurde nichts geaendert - bitte den Administrator ansprechen.</small></div>
{% elif done == 'err_nichts_zu_tun' %}
<div class="alert alert-danger py-2"><small><i class="bi bi-x-circle"></i>
Du sitzt nicht auf der Zentrale und es ist kein eigenes Tuerschild gemerkt.</small></div>
{% elif done == 'err_stammplatz_fremd' %}
<div class="alert alert-warning py-2"><small><i class="bi bi-exclamation-triangle"></i>
Die Zentrale ist dein eigener Platz, aktuell hat sie jemand anderes.
Mit "Zentrale uebernehmen" holst du sie zurueck.</small></div>
{% endif %}
{% if not target %}
<div class="alert alert-warning py-2 mb-0">
<small><i class="bi bi-gear"></i>
Die Zentrale ist noch nicht eingerichtet. Unter
<strong>Einstellungen &gt; Zentrale</strong> muss ein Busylight und ein
Segment ausgewaehlt werden.</small>
</div>
{% else %}
{# ── Zustand der Zentrale, immer sichtbar ──────────────────────────── #}
<div class="border rounded p-3 mb-3 bg-white">
<small class="text-muted d-block">Busylight der Zentrale</small>
<strong>{{ target.name }}</strong>
<small class="text-muted">(Segment {{ target.segment }})</small>
<hr class="my-2">
{% if occupant %}
<small class="text-muted d-block">Aktuell besetzt von</small>
<strong class="{% if is_here %}text-primary{% else %}text-success{% endif %}">
<i class="bi bi-person-fill"></i> {{ occupant.display_name }}
{%- if is_here %} <span class="badge bg-primary">das bist du</span>{% endif %}
</strong>
{% if occupant_is_owner %}
<div><small class="text-muted">Das ist der Stammplatz dieser Person.</small></div>
{% endif %}
{% else %}
<small class="text-muted d-block">Aktuell besetzt von</small>
<span class="text-muted"><i class="bi bi-person-dash"></i> niemand - die Zentrale ist frei</span>
{% endif %}
{% if owner_waiting %}
<div class="mt-2"><small class="text-muted">
<i class="bi bi-hourglass-split"></i>
{{ owner_waiting }} gehoert auf die Zentrale und kommt automatisch zurueck,
sobald sie freigegeben wird.
</small></div>
{% endif %}
</div>
{% if not user %}
{# ── Schritt 1: Wer bist du? ──────────────────────────────────── #}
<form method="get" action="/zentrale">
<div class="mb-3">
<label class="form-label">Eigene Telefon-Durchwahl</label>
<input type="text" name="durchwahl" class="form-control form-control-lg text-center"
inputmode="numeric" autocomplete="off" autofocus required
value="{{ extension }}" placeholder="z.B. 123">
<small class="text-muted">
Im naechsten Schritt siehst du, was du tun kannst - es wird noch nichts geaendert.
</small>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-arrow-right-circle"></i> Weiter
</button>
</form>
{% else %}
{# ── Schritt 2: Zustand und passende Aktionen ──────────────────── #}
<div class="text-center mb-3">
<small class="text-muted d-block">Durchwahl {{ user.phone_extension }}</small>
<strong>{{ user.display_name }}</strong>
</div>
{% if is_owner and is_here %}
<div class="alert alert-info py-2">
<small><i class="bi bi-house-check"></i>
Die Zentrale ist dein eigener Platz und du sitzt darauf.
Hier ist nichts zu tun - jemand anderes kann sie jederzeit uebernehmen,
danach kommst du automatisch zurueck.</small>
</div>
{% elif is_here %}
<div class="alert alert-primary py-2">
<small><i class="bi bi-person-check"></i>
Du sitzt aktuell auf der Zentrale.
{% if home %}
Dein eigenes Tuerschild ({{ home.name or home.hostname }}, Segment {{ home.segment }})
ist bis zur Rueckkehr ausgeschaltet.
{% endif %}
</small>
</div>
<form method="post" action="/api/zentrale/verlassen" class="mb-2">
<input type="hidden" name="extension" value="{{ user.phone_extension }}">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-box-arrow-left"></i> Zurueck auf mein Tuerschild
</button>
</form>
{% if home_blocker %}
<div class="alert alert-warning py-2 mb-0">
<small><i class="bi bi-exclamation-triangle"></i>
Achtung: dein Tuerschild ist derzeit {{ home_blocker }} zugeordnet.
Die Rueckkehr wird abgelehnt, solange das so ist.</small>
</div>
{% endif %}
{% else %}
<form method="post" action="/api/zentrale/uebernehmen" class="mb-2">
<input type="hidden" name="extension" value="{{ user.phone_extension }}">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-box-arrow-in-right"></i>
{% if occupant %}Zentrale uebernehmen{% else %}Zentrale besetzen{% endif %}
</button>
<small class="text-muted">
{% if occupant and occupant_is_owner %}
{{ occupant.display_name }} gehoert auf die Zentrale und kommt automatisch
zurueck, sobald du sie wieder freigibst.
{% elif occupant %}
{{ occupant.display_name }} wird dabei auf das eigene Tuerschild zurueckgesetzt.
{% else %}
Die Zentrale ist frei.
{% endif %}
</small>
</form>
{% if home %}
<form method="post" action="/api/zentrale/verlassen">
<button type="submit" class="btn btn-outline-secondary w-100 btn-sm">
<i class="bi bi-box-arrow-left"></i> Zurueck auf mein Tuerschild
</button>
<input type="hidden" name="extension" value="{{ user.phone_extension }}">
<small class="text-muted">
Gemerkt: {{ home.name or home.hostname }}, Segment {{ home.segment }}.
</small>
</form>
{% endif %}
{% endif %}
<div class="text-center mt-3">
<a href="/zentrale/{{ user.phone_extension }}" class="text-muted small me-3">
<i class="bi bi-arrow-clockwise"></i> Aktualisieren
</a>
<a href="/zentrale" class="text-muted small">Andere Durchwahl</a>
</div>
<div class="text-center mt-2">
<small class="text-secondary" style="font-size:0.7rem">
Diese Seite als Lesezeichen: <code>/zentrale/{{ user.phone_extension }}</code>
</small>
</div>
{% endif %}
{% endif %}
<div class="text-center mt-4">
<small class="text-secondary" style="font-size:0.65rem">&copy; cynfo GmbH</small>
</div>
</div>
</div>
</div>
</div>
</div>
</body> </body>
</html> </html>