Wichtel-Werkstatt: Secret-Santa-App mit Räumen, Ausschlüssen und Recovery-Links
- FastAPI + SQLite, serverseitig gerenderte Templates, mobil-zuerst - Admin-Passwort, einseitige Ausschlüsse, eingefrorene Auslosung - Teilnehmer sehen Ergebnis per Cookie; Einmal-Recovery-Links bei Verlust - Docker/podman-tauglich (Entrypoint mit Privilegien-Drop, SELinux-:z) - Unit-Tests für Auslosung, E2E-Testskript (30 Checks)
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Admin – {{ room.name }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="admin-head">
|
||||
<h1 class="room-title">{{ room.name }}</h1>
|
||||
{% if drawn %}
|
||||
<span class="tag tag-gold">✨ Ausgelost</span>
|
||||
{% else %}
|
||||
<span class="tag">Wartet auf Auslosung</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if message %}
|
||||
<div class="alert {{ 'alert-error' if message_kind == 'error' else 'alert-ok' }}" role="alert">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if recovery_link %}
|
||||
<section class="card card-highlight">
|
||||
<h2>🔑 Wiederherstellungslink für {{ recovery_for }}</h2>
|
||||
<p>Schicke diesen Link direkt an <strong>{{ recovery_for }}</strong>.
|
||||
Er funktioniert <strong>ein einziges Mal</strong> und stellt dort das
|
||||
Ergebnis wieder her:</p>
|
||||
<div class="copy-row">
|
||||
<input type="text" readonly value="{{ recovery_link }}" id="recovery-link">
|
||||
<button type="button" class="btn btn-secondary" data-copy="#recovery-link">Kopieren</button>
|
||||
</div>
|
||||
<p class="hint">Ein eventuell älterer, unbenutzter Link für {{ recovery_for }}
|
||||
wurde dadurch ersetzt.</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="card">
|
||||
<h2>Einladung teilen</h2>
|
||||
<p>Mit diesem Link melden sich die Teilnehmer an:</p>
|
||||
<div class="copy-row">
|
||||
<input type="text" readonly value="{{ share_link }}" id="share-link">
|
||||
<button type="button" class="btn btn-secondary" data-copy="#share-link">Kopieren</button>
|
||||
</div>
|
||||
<p class="code-line">Raumcode: <span class="code">{{ room.code }}</span></p>
|
||||
</section>
|
||||
|
||||
{% if not drawn %}
|
||||
<section class="card">
|
||||
<h2>Selbst mitwichteln 🎅</h2>
|
||||
{% if admin_participant %}
|
||||
<p>Du bist als <strong>{{ admin_participant.name }}</strong> mit dabei. ✅
|
||||
Deine Teilnehmeransicht – und später dein Ergebnis – erreichst du über
|
||||
den Einladungslink.</p>
|
||||
{% else %}
|
||||
<p>Auch der Admin braucht ein Los: Melde dich selbst als Teilnehmer an.</p>
|
||||
<form method="post" action="/r/{{ room.code }}/admin/self-register" class="stack">
|
||||
<label>
|
||||
Dein Name
|
||||
<input type="text" name="name" maxlength="50" required
|
||||
autocomplete="name" placeholder="z. B. Nico">
|
||||
</label>
|
||||
<button type="submit" class="btn btn-secondary">Mich anmelden 🎁</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="card">
|
||||
<h2>Teilnehmer ({{ participants|length }})</h2>
|
||||
{% if participants %}
|
||||
<ul class="list">
|
||||
{% for p in participants %}
|
||||
<li class="list-item">
|
||||
<span class="list-name">{{ p.name }}</span>
|
||||
{% if not drawn %}
|
||||
<form method="post" action="/r/{{ room.code }}/participants/{{ p.id }}/delete"
|
||||
data-confirm="»{{ p.name }}« wirklich aus dem Raum entfernen?">
|
||||
<button type="submit" class="btn btn-small btn-danger" aria-label="{{ p.name }} entfernen">✕</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="hint">Noch niemand dabei – teile den Einladungslink!</p>
|
||||
{% endif %}
|
||||
{% if drawn %}<p class="hint">Nach der Auslosung ist die Teilnehmerliste eingefroren.</p>{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Ausschlüsse</h2>
|
||||
<p class="hint">Wer darf <em>wen nicht</em> ziehen? (z. B. Ehepartner gegenseitig
|
||||
ausschließen – das sind zwei Einträge)</p>
|
||||
{% if not drawn %}
|
||||
<form method="post" action="/r/{{ room.code }}/exclusions/add" class="exclusion-form">
|
||||
<select name="from_id" required aria-label="Wer darf nicht ziehen?">
|
||||
<option value="" disabled selected>Wer darf nicht…</option>
|
||||
{% for p in participants %}<option value="{{ p.id }}">{{ p.name }}</option>{% endfor %}
|
||||
</select>
|
||||
<select name="to_id" required aria-label="Wen nicht?">
|
||||
<option value="" disabled selected>…wen ziehen?</option>
|
||||
{% for p in participants %}<option value="{{ p.id }}">{{ p.name }}</option>{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-secondary" {% if participants|length < 2 %}disabled{% endif %}>
|
||||
Ausschließen
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if exclusions %}
|
||||
<ul class="list">
|
||||
{% for e in exclusions %}
|
||||
<li class="list-item">
|
||||
<span class="list-name">{{ e.from_name }} ➜ darf nicht ➜ {{ e.to_name }}</span>
|
||||
{% if not drawn %}
|
||||
<form method="post" action="/r/{{ room.code }}/exclusions/{{ e.from_id }}/{{ e.to_id }}/delete">
|
||||
<button type="submit" class="btn btn-small btn-danger" aria-label="Ausschluss entfernen">✕</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% elif drawn %}
|
||||
<p class="hint">Keine Ausschlüsse festgelegt.</p>
|
||||
{% endif %}
|
||||
{% if drawn %}<p class="hint">Nach der Auslosung sind die Ausschlüsse eingefroren.</p>{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Auslosung</h2>
|
||||
{% if drawn %}
|
||||
<div class="alert alert-ok">🎉 Es wurde ausgelost! Wer wen beschenkt, sehen nur die
|
||||
Teilnehmer selbst auf ihrer Seite – hier im Admin-Bereich bleibt das geheim.</div>
|
||||
{% else %}
|
||||
<p>Jede Person zieht genau eine andere Person – niemand zieht sich selbst,
|
||||
Ausschlüsse werden respektiert.</p>
|
||||
<form method="post" action="/r/{{ room.code }}/draw"
|
||||
data-confirm="Wirklich jetzt auslosen? Danach kann nichts mehr geändert werden.">
|
||||
<button type="submit" class="btn btn-primary btn-big" {% if not can_draw %}disabled{% endif %}>
|
||||
🎲 Jetzt auslosen
|
||||
</button>
|
||||
</form>
|
||||
{% if not can_draw %}
|
||||
<p class="hint">Mindestens {{ min_draw }} Teilnehmer nötig
|
||||
(aktuell {{ participants|length }}).</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Zugriff wiederherstellen 🔑</h2>
|
||||
<p class="hint">Hat jemand das Cookie gelöscht oder ein neues Gerät?
|
||||
Erstelle hier einen Einmal-Link, mit dem die Person ihr Ergebnis wiedersehen kann –
|
||||
auch nach der Auslosung. Du erfährst dabei weiterhin nicht, wer wen gezogen hat.</p>
|
||||
<ul class="list">
|
||||
{% for p in participants %}
|
||||
<li class="list-item">
|
||||
<span class="list-name">{{ p.name }}</span>
|
||||
<form method="post" action="/r/{{ room.code }}/recovery/{{ p.id }}">
|
||||
<button type="submit" class="btn btn-small btn-secondary">Link erstellen</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if not participants %}<p class="hint">Noch keine Teilnehmer im Raum.</p>{% endif %}
|
||||
</section>
|
||||
|
||||
<form method="post" action="/r/{{ room.code }}/admin/logout" class="logout-row">
|
||||
<button type="submit" class="btn btn-ghost">Abmelden</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user