From b02e6702e4f6137ece6817cbe4f8cfe6e5788ba6 Mon Sep 17 00:00:00 2001 From: Christian Mueller Date: Fri, 21 Aug 2026 08:47:21 +0000 Subject: [PATCH] fix: Stamm-Besetzer der Zentrale kommt bei Freigabe zurueck "Zurueck auf mein Tuerschild" hat die Zentrale nur ausgeschaltet, statt ihren eigentlichen Besetzer wieder darauf zu setzen. Wer normalerweise am Empfang sitzt, blieb nach einer Vertretung ohne Zuordnung. Die Zentrale hat jetzt einen Stamm-Besetzer, ohne dass es dafuer eine eigene Einstellung braucht: wer beim ersten Verdraengen keinen anderen Platz gemerkt hat, bekommt das Zentral-Segment als Heimatplatz vermerkt. Wird die Zentrale wieder frei, kommt er automatisch zurueck - egal wie viele Vertretungen sich zwischenzeitlich abgewechselt haben. Der Vermerk des Stamm-Besetzers bleibt dabei dauerhaft stehen, anders als bei einer Vertretung: die Zentrale ist sein normaler Platz, nicht eine Station, aus der er zurueckkehrt. Klickt er selbst "zurueck auf mein Tuerschild", aendert sich deshalb nichts und die Seite erklaert es. Damit gilt in beide Richtungen dieselbe Regel: wird ein Platz frei, kommt zurueck, fuer wen er gemerkt ist. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 18 ++++++--- app.py | 114 +++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 117 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index cff80d3..0a37d80 100644 --- a/README.md +++ b/README.md @@ -205,11 +205,19 @@ erst per Knopfdruck. Segment ausgeschaltet. - **Zurueck auf mein Tuerschild**: Der gemerkte Platz wird wiederhergestellt und die Zentrale freigegeben (Farbe `abwesend`, Standard: aus). -- **Vorgaenger**: Wer die Zentrale abgibt, wird dabei automatisch auf sein eigenes - Tuerschild zurueckgesetzt. Er muss also nicht erst selbst "zurueck" klicken, damit - der Nachfolger uebernehmen kann. Ist sein Tuerschild inzwischen jemand anderem - zugeordnet oder war keines gemerkt, bleibt er ohne Zuordnung - die Meldung auf der - Seite sagt, welcher der beiden Faelle vorliegt. +- **Vorgaenger mit eigenem Tuerschild**: Wer die Zentrale abgibt, wird dabei + automatisch auf sein eigenes Tuerschild zurueckgesetzt. Er muss also nicht erst + selbst "zurueck" klicken, damit der Nachfolger uebernehmen kann. Ist sein Tuerschild + inzwischen jemand anderem zugeordnet, bleibt er ohne Zuordnung - die Meldung nennt + dann, wer dort sitzt. +- **Stamm-Besetzer der Zentrale**: Wer normalerweise auf der Zentrale sitzt und kein + eigenes Tuerschild hat, wird beim ersten Verdraengen als Stamm-Besetzer vermerkt. + Er kommt automatisch zurueck, sobald die Zentrale wieder freigegeben wird - egal + wie viele Vertretungen sich zwischenzeitlich abgewechselt haben. Sein Vermerk bleibt + dauerhaft stehen, denn die Zentrale ist sein normaler Platz und keine Vertretung. +- **Der Stamm-Besetzer selbst** braucht "Zurueck auf mein Tuerschild" nicht: sein + Platz ist die Zentrale. Klickt er es trotzdem, aendert sich nichts und die Seite + sagt es ihm. - **Belegter Heimatplatz**: Ist das eigene Tuerschild inzwischen jemand anderem zugeordnet, wird die Rueckkehr abgelehnt und nichts geaendert. Dann muss der Administrator die Zuordnung aufloesen. diff --git a/app.py b/app.py index f14b789..c839c09 100644 --- a/app.py +++ b/app.py @@ -569,6 +569,44 @@ def _zentrale_view(request, conn, target, extension="", user=None, error=None, m }) +def _zentrale_owner_row(conn, target): + """Heimateintrag, der auf das Zentral-Segment zeigt - der Stamm-Besetzer.""" + if not target: + return None + return conn.execute(""" + SELECT h.user_azure_id, u.display_name + FROM home_assignments h JOIN users u ON u.azure_id = h.user_azure_id + WHERE h.wled_device_id = ? AND h.segment = ? AND u.active = 1 + AND NOT EXISTS (SELECT 1 FROM assignments a WHERE a.user_azure_id = h.user_azure_id) + ORDER BY h.created_at, h.user_azure_id + LIMIT 1 + """, (target["id"], target["segment"])).fetchone() + + +def _restore_zentrale_owner(conn, target): + """Stamm-Besetzer zurueckholen, sobald das Zentral-Segment frei ist. + + Als Stamm-Besetzer gilt, fuer wen das Zentral-Segment als Heimatplatz + gemerkt ist. Sein Eintrag bleibt dabei absichtlich stehen: die Zentrale + ist sein normaler Platz, nicht eine Vertretung, aus der er zurueckkehrt. + """ + if not target: + return None + busy = conn.execute( + "SELECT 1 FROM assignments WHERE wled_device_id = ? AND segment = ?", + (target["id"], target["segment"]), + ).fetchone() + if busy: + return None + owner = _zentrale_owner_row(conn, target) + if not owner: + return None + conn.execute(""" + INSERT INTO assignments (user_azure_id, wled_device_id, segment) VALUES (?, ?, ?) + """, (owner["user_azure_id"], target["id"], target["segment"])) + return owner + + @app.get("/zentrale", response_class=HTMLResponse) async def zentrale_page(request: Request): """Self-Service-Seite fuer den Empfangsbereich. Absichtlich ohne Admin-Login.""" @@ -651,13 +689,23 @@ async def zentrale_takeover(request: Request, extension: str = Form(...)): # 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 FROM home_assignments h JOIN wled_devices d ON d.id = h.wled_device_id WHERE h.user_azure_id = ? """, (predecessor["azure_id"],)).fetchone() - if pred_home: + + is_owner = pred_home and (pred_home["wled_device_id"], pred_home["segment"]) \ + == (target["id"], target["segment"]) + + if is_owner: + # 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 + 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 = ? @@ -671,6 +719,19 @@ async def zentrale_takeover(request: Request, extension: str = Form(...)): conn.execute("DELETE FROM home_assignments WHERE user_azure_id = ?", (predecessor["azure_id"],)) pred_back = pred_home + else: + # Kein anderer Platz gemerkt: die Zentrale ist sein Stammplatz. + # Merken, damit er automatisch zurueckkommt, wenn sie frei wird. + claimed = conn.execute(""" + SELECT 1 FROM home_assignments + WHERE wled_device_id = ? AND segment = ? AND user_azure_id != ? + """, (target["id"], target["segment"], predecessor["azure_id"])).fetchone() + if not claimed: + conn.execute(""" + 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() @@ -694,6 +755,7 @@ async def zentrale_takeover(request: Request, extension: str = Form(...)): 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']})." @@ -705,6 +767,9 @@ async def zentrale_takeover(request: Request, extension: str = Form(...)): 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.") @@ -741,11 +806,30 @@ async def zentrale_release(request: Request, extension: str = Form(...)): 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"],)) + returning = _restore_zentrale_owner(conn, target) conn.commit() - scheduler.clear_segment(target["hostname"], target["segment"], conn) - logger.info("Zentrale verlassen von %s (kein Heimat-Tuerschild)", user["display_name"]) + if returning: + scheduler.push_user_light(returning["user_azure_id"], returning["display_name"], + 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", + 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) + + # Der Stamm-Besetzer der Zentrale hat keinen anderen Platz, auf den er + # zurueckgehen koennte - 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, - message=f"{user['display_name']} ist von der Zentrale abgemeldet. Ein eigenes Tuerschild war nicht gespeichert.") + 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? blocker = conn.execute(""" @@ -764,19 +848,29 @@ async def zentrale_release(request: Request, extension: str = Form(...)): 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"],)) + + # Die Zentrale ist jetzt frei - ihr Stamm-Besetzer kommt zurueck. + returning = _restore_zentrale_owner(conn, target) if was_here else None 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) + if returning: + scheduler.push_user_light(returning["user_azure_id"], returning["display_name"], + target["hostname"], target["segment"], conn) + else: + 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"]) + 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") - 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']}).")) + 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: conn.close()