This commit is contained in:
team3
2026-09-30 15:15:00 +02:00
parent 817f39a42c
commit 09ae21c4b2
6 changed files with 66 additions and 10 deletions

View File

@@ -19,16 +19,26 @@ log = logging.getLogger("events")
PUFFER = 16
_abonnenten: set[asyncio.Queue] = set()
# Welche Sitzung hinter einer Verbindung steckt — daraus ergibt sich,
# wer gerade online ist: eine offene Seite mit gewähltem Namen
_sitzungen: dict[asyncio.Queue, str] = {}
def anmelden():
def anmelden(session_id=None):
queue = asyncio.Queue(maxsize=PUFFER)
_abonnenten.add(queue)
if session_id:
_sitzungen[queue] = session_id
return queue
def abmelden(queue):
_abonnenten.discard(queue)
_sitzungen.pop(queue, None)
def sitzungen_online():
return set(_sitzungen.values())
def anzahl():
@@ -60,3 +70,5 @@ async def stream(queue, request):
yield ": ping\n\n"
finally:
abmelden(queue)
# Die anderen Seiten sollen den grünen Punkt ausmachen
senden("users")

View File

@@ -564,8 +564,10 @@ letzter_dreh = {"ergebnis": None, "start": 0.0, "teilnehmer": []}
@app.get("/api/events")
async def ereignisse(request: Request):
queue = events.anmelden()
async def ereignisse(request: Request, filmabend_sid: str | None = Cookie(None)):
queue = events.anmelden(filmabend_sid)
# Die anderen Seiten sollen den grünen Punkt anmachen
events.senden("users")
return StreamingResponse(
events.stream(queue, request),
media_type="text/event-stream",
@@ -833,8 +835,11 @@ def runde(response: Response, filmabend_sid: str | None = Cookie(None)):
sid = sitzung(response, filmabend_sid)
staende = punkte.alle()
leer = punkte.stand(0)
online = users.online_ids(events.sitzungen_online())
return {
"items": [{**u, **staende.get(u["id"], leer)} for u in users.alle()],
"items": [
{**u, **staende.get(u["id"], leer), "online": u["id"] in online} for u in users.alle()
],
"mine": users.aktuell(sid),
"host": users.ist_host(sid),
}
@@ -995,6 +1000,7 @@ def name_waehlen(
sid = sitzung(response, filmabend_sid)
if user_id is None:
users.abwaehlen(sid)
events.senden("users")
return {"mine": None}
film = users.schutz_von(user_id)
@@ -1009,6 +1015,7 @@ def name_waehlen(
benutzer = users.waehlen(sid, user_id)
if not benutzer:
raise HTTPException(status_code=404, detail="Unbekannter Name")
events.senden("users")
return {"mine": benutzer}

View File

@@ -184,6 +184,11 @@ def host_setzen(session_id, an):
return an
def online_ids(session_ids):
"""Die Namen hinter offenen Verbindungen — wer gerade da ist."""
return {besitzer(sid) for sid in session_ids} - {None}
def besitzer(session_id):
"""Die user_id dieser Sitzung, oder None ohne gewählten Namen.

View File

@@ -46,3 +46,23 @@ def test_abwaehlen_macht_wieder_zum_zuschauer(client, film):
# Lesen geht weiter, Schreiben nicht mehr
assert client.get("/api/suggestions").json()["mine"] is None
assert client.post("/api/suggestions", json={"movie_id": film(2)}).status_code == 403
def test_online_ist_wer_eine_seite_offen_hat(client, anmelden):
from app import events
anmelden(client, "Anna")
sid = client.cookies.get("filmabend_sid")
ich = client.get("/api/users").json()["mine"]["id"]
def online():
return {u["id"]: u["online"] for u in client.get("/api/users").json()["items"]}
assert online()[ich] is False
queue = events.anmelden(sid)
assert online()[ich] is True
# Namenswechsel im selben Browser: der alte Name ist offline
anmelden(client, "Ben")
assert online()[ich] is False
events.abmelden(queue)
assert True not in online().values()

View File

@@ -141,7 +141,7 @@ const tabs = computed(() => [
count: quellen.value.find((q) => q.id === quelle.value)?.count ?? null,
},
{ id: 'suche', label: 'Suche', count: null },
...(host.value ? [] : [{ id: 'merkliste', label: 'Sammlungen', count: null }]),
{ id: 'merkliste', label: 'Sammlungen', count: null },
{ id: 'wheel', label: 'Rad', count: null },
{ id: 'stream', label: 'Stream', count: null },
...(angemeldet.value || host.value ? [{ id: 'clips', label: 'Clips', count: null }] : []),
@@ -571,6 +571,14 @@ onMounted(load)
gap: 0.35rem;
align-items: stretch;
min-width: 0;
/* Zehn Reiter passen nicht auf jeden Schirm — dann rollt die Leiste,
statt unter den Namen zu laufen */
overflow-x: auto;
scrollbar-width: none;
}
.tabs::-webkit-scrollbar {
display: none;
}
.rechts {

View File

@@ -153,7 +153,7 @@ onUnmounted(() => {
<button type="button" :class="{ aktiv: mine?.id === u.id }" @click="waehlen(u)">
<span class="zeile" :title="`Erfahrung: ${u.punkte - u.von} / ${u.bis - u.von}`">
<PersonName :name="u.name" :level="u.level" />
<span v-if="u.geschuetzt" class="schloss" title="Mit Anmeldefilm">&#9679;</span>
<span v-if="u.online" class="online" title="Gerade da"></span>
</span>
</button>
</li>
@@ -423,11 +423,15 @@ onUnmounted(() => {
}
/* Der Punkt zeigt: hier fragt die Seite nach dem Anmeldefilm */
.schloss {
margin-left: 0.3rem;
color: var(--muted);
font-size: 0.6rem;
/* Grüner Punkt: eine Seite mit diesem Namen ist gerade offen */
.online {
display: inline-block;
width: 0.5rem;
height: 0.5rem;
margin-left: 0.35rem;
vertical-align: middle;
background: #3ddc84;
border-radius: 50%;
}
@media (max-width: 640px), (pointer: coarse) {