diff --git a/backend/app/main.py b/backend/app/main.py
index c0967cc..95d4d95 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -686,6 +686,32 @@ async def film_holen(movie_id: int):
return movie
+# ── Info: Bezahlen und Regeln ───────────────────────────────────────────
+INFO_MAX = 8000
+
+
+@app.get("/api/info")
+def info():
+ """Ein Markdown-Text des Hosts: PayPal, IBAN, Regeln, was man wissen
+ muss. Solange keiner da ist, steht die Vorlage mit den Punkteregeln."""
+ return {"text": db.get_meta("info_text") or punkte.vorlage()}
+
+
+@app.put("/api/info")
+def info_setzen(
+ response: Response,
+ text: str = Body(..., embed=True),
+ filmabend_sid: str | None = Cookie(None),
+):
+ sid = sitzung(response, filmabend_sid)
+ if not users.ist_host(sid):
+ raise HTTPException(status_code=403, detail="Nur für den Host")
+ with db.connect() as conn:
+ db.set_meta(conn, "info_text", text.strip()[:INFO_MAX])
+ events.senden("info")
+ return {"text": db.get_meta("info_text")}
+
+
@app.get("/api/features")
def wuensche(response: Response, filmabend_sid: str | None = Cookie(None)):
sid = sitzung(response, filmabend_sid)
diff --git a/backend/app/punkte.py b/backend/app/punkte.py
index 749d40c..5b21028 100644
--- a/backend/app/punkte.py
+++ b/backend/app/punkte.py
@@ -20,6 +20,15 @@ KOMMENTAR = 5
ANTWORT = 2
HERZ = 1
+# Für den Info-Reiter: was zählt, mit der Zahl — aus denselben Konstanten
+REGELN = (
+ ("Beim Filmabend dabei", SCHAUEN),
+ ("Film mit Sternen bewertet", BEWERTEN),
+ ("Kommentar geschrieben", KOMMENTAR),
+ ("Auf einen Kommentar geantwortet", ANTWORT),
+ ("Herz vergeben", HERZ),
+)
+
ZAEHLUNGEN = (
(SCHAUEN, "SELECT user_id, COUNT(*) AS n FROM watched_dabei GROUP BY user_id"),
(
@@ -46,6 +55,17 @@ ZAEHLUNGEN = (
)
+def vorlage():
+ """Der Info-Text, solange der Host keinen eigenen geschrieben hat —
+ die Regeln mit ihren Zahlen und die ersten Levelstufen, als Markdown."""
+ zeilen = ["## Erfahrung", ""]
+ zeilen += [f"- {was}: **+{p}**" for was, p in REGELN]
+ zeilen += ["", "Je Film zählt jede Art nur einmal.", "", "## Level", ""]
+ stufen = ", ".join(f"Level {stufe} ab {stufe * (stufe - 1) // 2}" for stufe in range(2, 8))
+ zeilen.append(f"{stufen} … Jedes weitere Level kostet einen Punkt mehr als das davor.")
+ return "\n".join(zeilen)
+
+
def level(punkte):
"""Größtes L mit L·(L−1)/2 ≤ Punkte. Null Punkte sind Level 1."""
stufe = (1 + isqrt(1 + 8 * punkte)) // 2
diff --git a/backend/tests/test_info.py b/backend/tests/test_info.py
new file mode 100644
index 0000000..7e166bb
--- /dev/null
+++ b/backend/tests/test_info.py
@@ -0,0 +1,22 @@
+"""Der Info-Reiter: ein Markdown-Text des Hosts, vorbelegt mit den Regeln."""
+
+from app import punkte
+
+
+def test_vorlage_traegt_die_regeln_aus_dem_code(client):
+ text = client.get("/api/info").json()["text"]
+ assert text == punkte.vorlage()
+ assert f"- Beim Filmabend dabei: **+{punkte.SCHAUEN}**" in text
+ assert f"- Herz vergeben: **+{punkte.HERZ}**" in text
+ # Dreieckszahlen: Level 2 ab 1, Level 3 ab 3, Level 4 ab 6
+ assert "Level 2 ab 1, Level 3 ab 3, Level 4 ab 6" in text
+
+
+def test_text_schreibt_nur_der_host(client, zuschauer, hostmodus):
+ assert client.put("/api/info", json={"text": "PayPal: x"}).status_code == 403
+ hostmodus(zuschauer)
+ assert zuschauer.put("/api/info", json={"text": " PayPal: x "}).json()["text"] == "PayPal: x"
+ assert client.get("/api/info").json()["text"] == "PayPal: x"
+ # Leer gespeichert: wieder die Vorlage
+ zuschauer.put("/api/info", json={"text": ""})
+ assert client.get("/api/info").json()["text"] == punkte.vorlage()
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 1e403ef..eabf933 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -13,6 +13,7 @@ import NamensWahl from './components/NamensWahl.vue'
import FilmLogin from './components/FilmLogin.vue'
import VerwaltungTab from './components/VerwaltungTab.vue'
import WuenscheTab from './components/WuenscheTab.vue'
+import InfoTab from './components/InfoTab.vue'
import SearchTab from './components/SearchTab.vue'
import StreamTab from './components/StreamTab.vue'
import WatchedTab from './components/WatchedTab.vue'
@@ -150,6 +151,7 @@ const tabs = computed(() => [
label: 'Wünsche',
count: wuensche.value.filter((w) => !w.done_at).length,
},
+ { id: 'info', label: 'Info', count: null },
...(host.value || angemeldet.value
? [{ id: 'verwaltung', label: 'Verwaltung', count: null }]
: []),
@@ -430,6 +432,8 @@ onMounted(load)
@changed="ladeWuensche"
/>
+
+
diff --git a/frontend/src/components/InfoTab.vue b/frontend/src/components/InfoTab.vue
new file mode 100644
index 0000000..81c1799
--- /dev/null
+++ b/frontend/src/components/InfoTab.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
Lade …
+
{{ fehler }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/composables/useEinstellungen.js b/frontend/src/composables/useEinstellungen.js
index fa84e37..200887e 100644
--- a/frontend/src/composables/useEinstellungen.js
+++ b/frontend/src/composables/useEinstellungen.js
@@ -35,6 +35,7 @@ const TAB_IDS = [
'merkliste',
'watched',
'wuensche',
+ 'info',
'verwaltung',
'stream',
'clips',