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:
@@ -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.
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
+136
-55
@@ -13,109 +13,190 @@
|
|||||||
<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">
|
<div class="text-center mb-4">
|
||||||
<img src="{{ logo_url() }}" alt="Logo" style="height: 40px;" class="mb-2">
|
<img src="{{ logo_url() }}" alt="Logo" style="height: 40px;" class="mb-2">
|
||||||
<h5 class="mb-0">Zentrale besetzen</h5>
|
<h5 class="mb-0">Zentrale besetzen</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if message %}
|
{# ── Rueckmeldung der letzten Aktion ───────────────────────────────── #}
|
||||||
<div class="alert alert-success py-2">
|
{% if done == 'uebernommen' %}
|
||||||
<small><i class="bi bi-check-circle"></i> {{ message }}</small>
|
<div class="alert alert-success py-2"><small><i class="bi bi-check-circle"></i>
|
||||||
</div>
|
Du sitzt jetzt auf der Zentrale.</small></div>
|
||||||
{% endif %}
|
{% elif done == 'bereits' %}
|
||||||
|
<div class="alert alert-info py-2"><small><i class="bi bi-info-circle"></i>
|
||||||
{% if error %}
|
Du warst schon auf der Zentrale eingetragen. Das Licht wurde neu gesetzt.</small></div>
|
||||||
<div class="alert alert-danger py-2">
|
{% elif done == 'zurueck' %}
|
||||||
<small><i class="bi bi-exclamation-triangle"></i> {{ error }}</small>
|
<div class="alert alert-success py-2"><small><i class="bi bi-check-circle"></i>
|
||||||
</div>
|
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 %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not target %}
|
{% if not target %}
|
||||||
<div class="alert alert-warning py-2 mb-0">
|
<div class="alert alert-warning py-2 mb-0">
|
||||||
<small>
|
<small><i class="bi bi-gear"></i>
|
||||||
<i class="bi bi-gear"></i>
|
|
||||||
Die Zentrale ist noch nicht eingerichtet. Unter
|
Die Zentrale ist noch nicht eingerichtet. Unter
|
||||||
<strong>Einstellungen > Zentrale</strong> muss ein Busylight
|
<strong>Einstellungen > Zentrale</strong> muss ein Busylight und ein
|
||||||
und ein Segment ausgewaehlt werden.
|
Segment ausgewaehlt werden.</small>
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
<div class="border rounded p-3 mb-4 bg-white">
|
{# ── Zustand der Zentrale, immer sichtbar ──────────────────────────── #}
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="border rounded p-3 mb-3 bg-white">
|
||||||
<div>
|
|
||||||
<small class="text-muted d-block">Busylight der Zentrale</small>
|
<small class="text-muted d-block">Busylight der Zentrale</small>
|
||||||
<strong>{{ target.name }}</strong>
|
<strong>{{ target.name }}</strong>
|
||||||
<small class="text-muted">(Segment {{ target.segment }})</small>
|
<small class="text-muted">(Segment {{ target.segment }})</small>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr class="my-2">
|
<hr class="my-2">
|
||||||
<small class="text-muted d-block">Aktuell besetzt von</small>
|
|
||||||
{% if occupant %}
|
{% if occupant %}
|
||||||
<strong class="text-success"><i class="bi bi-person-fill"></i> {{ occupant }}</strong>
|
<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 %}
|
{% else %}
|
||||||
<span class="text-muted"><i class="bi bi-person-dash"></i> niemand</span>
|
<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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if user %}
|
{% 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">
|
<div class="text-center mb-3">
|
||||||
<small class="text-muted d-block">Durchwahl {{ user.phone_extension }}</small>
|
<small class="text-muted d-block">Durchwahl {{ user.phone_extension }}</small>
|
||||||
<strong>{{ user.display_name }}</strong>
|
<strong>{{ user.display_name }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_here %}
|
{% if is_owner and is_here %}
|
||||||
<div class="alert alert-info py-2">
|
<div class="alert alert-info py-2">
|
||||||
<small><i class="bi bi-info-circle"></i> Du sitzt derzeit auf der Zentrale.</small>
|
<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>
|
</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 %}
|
{% endif %}
|
||||||
|
</small>
|
||||||
<form method="post" action="/api/zentrale/uebernehmen" class="mb-2">
|
</div>
|
||||||
|
<form method="post" action="/api/zentrale/verlassen" class="mb-2">
|
||||||
<input type="hidden" name="extension" value="{{ user.phone_extension }}">
|
<input type="hidden" name="extension" value="{{ user.phone_extension }}">
|
||||||
<button type="submit" class="btn btn-primary w-100" {% if is_here %}disabled{% endif %}>
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
<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
|
<i class="bi bi-box-arrow-left"></i> Zurueck auf mein Tuerschild
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% if home_blocker %}
|
||||||
{% if home %}
|
<div class="alert alert-warning py-2 mb-0">
|
||||||
<div class="text-center mt-3">
|
<small><i class="bi bi-exclamation-triangle"></i>
|
||||||
<small class="text-muted">
|
Achtung: dein Tuerschild ist derzeit {{ home_blocker }} zugeordnet.
|
||||||
Gemerktes Tuerschild: {{ home.name or home.hostname }} (Segment {{ home.segment }})
|
Die Rueckkehr wird abgelehnt, solange das so ist.</small>
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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">
|
<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>
|
<a href="/zentrale" class="text-muted small">Andere Durchwahl</a>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
<div class="text-center mt-2">
|
||||||
<form method="post" action="/api/zentrale/uebernehmen">
|
<small class="text-secondary" style="font-size:0.7rem">
|
||||||
<div class="mb-3">
|
Diese Seite als Lesezeichen: <code>/zentrale/{{ user.phone_extension }}</code>
|
||||||
<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>
|
</small>
|
||||||
</div>
|
</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 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="text-center mt-4">
|
<div class="text-center mt-4">
|
||||||
<small class="text-secondary" style="font-size:0.65rem">© cynfo GmbH</small>
|
<small class="text-secondary" style="font-size:0.65rem">© cynfo GmbH</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user