feat: Zentrale per Durchwahl selbst besetzen
Das Busylight im Empfangsbereich wird wechselnd besetzt. Mitarbeiter
koennen sich jetzt selbst darauf setzen, ohne Admin-Passwort.
- Neue Seite /zentrale (Durchwahl eingeben) und /zentrale/{durchwahl}
als Direktlink fuer QR-Codes. Der Direktlink zeigt nur an und aendert
nichts, damit Link-Vorschau und Prefetch niemanden versetzen.
- Neue Tabelle home_assignments merkt das eigene Tuerschild, solange der
Mitarbeiter in der Zentrale sitzt. Beim Verlassen wird es
wiederhergestellt, das Zentral-Segment wird freigegeben.
- Der Vorgaenger bleibt ohne Zuordnung - er ist nicht da. Sein gemerkter
Heimatplatz bleibt erhalten, er kann jederzeit zurueckkehren.
- Ist der Heimatplatz inzwischen belegt, wird die Rueckkehr abgelehnt
und nichts geaendert.
- Eine ausdrueckliche Admin-Zuordnung verwirft den gemerkten Heimatplatz.
Ein reines Loeschen der Zuordnung nicht, sonst kaeme niemand mehr von
der Zentrale zurueck.
- Farbe wird sofort ans Geraet geschickt (scheduler.push_user_light).
Das Polling allein reicht nicht: _process_user sendet nur bei einer
Statusaenderung, und beim Platzwechsel bleibt der Status gleich.
- Neue Settings selfservice_device_id und selfservice_segment,
einstellbar unter Einstellungen.
- Jeder Wechsel landet im Protokoll (zentrale / zentrale_frei).
Die Durchwahl ist eine Kennung, kein Geheimnis - im README dokumentiert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -469,6 +469,10 @@ async def create_assignment(
|
||||
"INSERT OR REPLACE INTO assignments (user_azure_id, wled_device_id, segment) VALUES (?, ?, ?)",
|
||||
(user_azure_id, wled_device_id, segment),
|
||||
)
|
||||
# Eine ausdrueckliche Admin-Zuordnung setzt den fuer die Zentrale gemerkten
|
||||
# Heimatplatz ausser Kraft, sonst wuerde die Rueckkehr auf einen veralteten
|
||||
# Platz zeigen.
|
||||
conn.execute("DELETE FROM home_assignments WHERE user_azure_id=?", (user_azure_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return RedirectResponse("/assignments", status_code=303)
|
||||
@@ -477,12 +481,258 @@ async def create_assignment(
|
||||
@app.post("/api/assignments/{assignment_id}/delete")
|
||||
async def delete_assignment(assignment_id: int):
|
||||
conn = database.get_db()
|
||||
# Der fuer die Zentrale gemerkte Heimatplatz bleibt hier absichtlich stehen:
|
||||
# wer von der Zentrale entfernt wird, soll noch zurueckkehren koennen.
|
||||
conn.execute("DELETE FROM assignments WHERE id=?", (assignment_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return RedirectResponse("/assignments", status_code=303)
|
||||
|
||||
|
||||
# ── Zentrale (Self-Service per Durchwahl) ──────────────────────
|
||||
|
||||
def _selfservice_target(conn):
|
||||
"""Konfiguriertes Zentral-Geraet + Segment, oder None wenn nicht eingerichtet."""
|
||||
raw_id = database.get_setting("selfservice_device_id", "")
|
||||
if not raw_id:
|
||||
return None
|
||||
try:
|
||||
device_id = int(raw_id)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
dev = conn.execute(
|
||||
"SELECT id, hostname, name FROM wled_devices WHERE id = ?", (device_id,)
|
||||
).fetchone()
|
||||
if not dev:
|
||||
return None
|
||||
|
||||
try:
|
||||
segment = int(database.get_setting("selfservice_segment", "0"))
|
||||
except (TypeError, ValueError):
|
||||
segment = 0
|
||||
|
||||
return {
|
||||
"id": dev["id"],
|
||||
"hostname": dev["hostname"],
|
||||
"name": dev["name"] or dev["hostname"],
|
||||
"segment": segment,
|
||||
}
|
||||
|
||||
|
||||
def _user_by_extension(extension, conn):
|
||||
"""Aktiven Benutzer anhand der Telefon-Durchwahl finden."""
|
||||
ext = (extension or "").strip()
|
||||
if not ext:
|
||||
return None
|
||||
return conn.execute("""
|
||||
SELECT azure_id, display_name, phone_extension
|
||||
FROM users
|
||||
WHERE active = 1 AND TRIM(phone_extension) = ?
|
||||
""", (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,
|
||||
})
|
||||
|
||||
|
||||
@app.get("/zentrale", response_class=HTMLResponse)
|
||||
async def zentrale_page(request: Request):
|
||||
"""Self-Service-Seite fuer den Empfangsbereich. Absichtlich ohne Admin-Login."""
|
||||
conn = database.get_db()
|
||||
try:
|
||||
return _zentrale_view(request, conn, _selfservice_target(conn))
|
||||
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."""
|
||||
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)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.post("/api/zentrale/uebernehmen", response_class=HTMLResponse)
|
||||
async def zentrale_takeover(request: Request, 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.")
|
||||
|
||||
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.")
|
||||
|
||||
# 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 = ?
|
||||
""", (user["azure_id"], target["id"], target["segment"])).fetchone()
|
||||
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.")
|
||||
|
||||
# Bisheriger Platz des Benutzers wird als Heimat-Tuerschild gemerkt
|
||||
own = conn.execute("""
|
||||
SELECT a.wled_device_id, a.segment, d.hostname, d.name
|
||||
FROM assignments a JOIN wled_devices d ON d.id = a.wled_device_id
|
||||
WHERE a.user_azure_id = ?
|
||||
""", (user["azure_id"],)).fetchone()
|
||||
|
||||
predecessor = 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()
|
||||
|
||||
if own:
|
||||
conn.execute("""
|
||||
INSERT OR REPLACE INTO home_assignments (user_azure_id, wled_device_id, segment)
|
||||
VALUES (?, ?, ?)
|
||||
""", (user["azure_id"], own["wled_device_id"], own["segment"]))
|
||||
|
||||
# Zentral-Segment freimachen, eigene Zuordnung loesen, neu eintragen.
|
||||
# Beide DELETEs sind noetig: UNIQUE(wled_device_id, segment) und UNIQUE(user_azure_id).
|
||||
conn.execute("DELETE FROM assignments WHERE wled_device_id = ? AND segment = ?",
|
||||
(target["id"], target["segment"]))
|
||||
conn.execute("DELETE FROM assignments WHERE user_azure_id = ?", (user["azure_id"],))
|
||||
conn.execute("""
|
||||
INSERT INTO assignments (user_azure_id, wled_device_id, segment) VALUES (?, ?, ?)
|
||||
""", (user["azure_id"], target["id"], target["segment"]))
|
||||
conn.commit()
|
||||
|
||||
# Licht sofort setzen - das Polling wuerde bei unveraendertem Status nichts senden
|
||||
scheduler.push_user_light(user["azure_id"], user["display_name"],
|
||||
target["hostname"], target["segment"], conn)
|
||||
if own:
|
||||
scheduler.clear_segment(own["hostname"], own["segment"], conn)
|
||||
|
||||
logger.info("Zentrale uebernommen von %s (Durchwahl %s), Vorgaenger: %s",
|
||||
user["display_name"], extension,
|
||||
predecessor["display_name"] if predecessor else "-")
|
||||
|
||||
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."
|
||||
return _zentrale_view(request, conn, target, extension=extension, user=user, message=msg)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.post("/api/zentrale/verlassen", response_class=HTMLResponse)
|
||||
async def zentrale_release(request: Request, 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.")
|
||||
|
||||
was_here = bool(target) and 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
|
||||
|
||||
home = conn.execute("""
|
||||
SELECT h.wled_device_id, h.segment, d.hostname, d.name
|
||||
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 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.")
|
||||
# Nur von der Zentrale abmelden, es gibt keinen Platz zum Zurueckgehen
|
||||
conn.execute("DELETE FROM assignments WHERE user_azure_id = ?", (user["azure_id"],))
|
||||
conn.commit()
|
||||
scheduler.clear_segment(target["hostname"], target["segment"], conn)
|
||||
logger.info("Zentrale verlassen von %s (kein Heimat-Tuerschild)", user["display_name"])
|
||||
return _zentrale_view(request, conn, target, extension=extension, user=user,
|
||||
message=f"{user['display_name']} ist von der Zentrale abgemeldet. Ein eigenes Tuerschild war nicht gespeichert.")
|
||||
|
||||
# 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 != ?
|
||||
""", (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."))
|
||||
|
||||
conn.execute("DELETE FROM assignments WHERE user_azure_id = ?", (user["azure_id"],))
|
||||
conn.execute("""
|
||||
INSERT INTO assignments (user_azure_id, wled_device_id, segment) VALUES (?, ?, ?)
|
||||
""", (user["azure_id"], home["wled_device_id"], home["segment"]))
|
||||
conn.execute("DELETE FROM home_assignments WHERE user_azure_id = ?", (user["azure_id"],))
|
||||
conn.commit()
|
||||
|
||||
scheduler.push_user_light(user["azure_id"], user["display_name"],
|
||||
home["hostname"], home["segment"], conn)
|
||||
if was_here:
|
||||
scheduler.clear_segment(target["hostname"], target["segment"], conn)
|
||||
|
||||
logger.info("Zentrale verlassen von %s, zurueck auf %s Segment %s",
|
||||
user["display_name"], home["hostname"], home["segment"])
|
||||
|
||||
return _zentrale_view(request, conn, target, extension=extension, user=user,
|
||||
message=(f"{user['display_name']} ist zurueck auf dem eigenen Tuerschild "
|
||||
f"({home['name'] or home['hostname']}, Segment {home['segment']})."))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ── Color Rules ────────────────────────────────────────────────────────────
|
||||
|
||||
@app.get("/colors", response_class=HTMLResponse)
|
||||
@@ -548,7 +798,12 @@ async def settings_page(request: Request):
|
||||
redir = _require_login(request)
|
||||
if redir: return redir
|
||||
settings = database.get_all_settings()
|
||||
return templates.TemplateResponse(*_ctx(request, "settings", settings=settings))
|
||||
conn = database.get_db()
|
||||
devices = conn.execute(
|
||||
"SELECT id, hostname, name, segment_count FROM wled_devices ORDER BY name"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return templates.TemplateResponse(*_ctx(request, "settings", settings=settings, devices=devices))
|
||||
|
||||
|
||||
@app.post("/api/settings")
|
||||
@@ -559,6 +814,7 @@ async def update_settings(request: Request):
|
||||
"azure_auth_tenant", "azure_scopes", "poll_interval",
|
||||
"attendance_csv_path", "attendance_status_anwesend", "attendance_status_abwesend",
|
||||
"attendance_status_homeoffice",
|
||||
"selfservice_device_id", "selfservice_segment",
|
||||
]
|
||||
for key in keys:
|
||||
value = form.get(key, "")
|
||||
|
||||
Reference in New Issue
Block a user