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) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 08:47:21 +00:00
co-authored by Claude Opus 5
parent 11c8042de0
commit b02e6702e4
2 changed files with 117 additions and 15 deletions
+13 -5
View File
@@ -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.
+104 -10
View File
@@ -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()