From b1712ec33b2a5b2ee9c3f8fbfc03bb3a00bd8b8c Mon Sep 17 00:00:00 2001 From: Dogfather Date: Thu, 30 Jul 2026 20:25:56 +0200 Subject: [PATCH] =?UTF-8?q?Rollenbasiertes=20Postfach,=20gro=C3=9Fe=20Star?= =?UTF-8?q?tseite,=20Casper-Fix,=20Streamplan/TikTok-Korrekturen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/js/main.js | 2 +- kasper.html => casper.html | 18 +-- cloudflare-worker/README.md | 24 +++- cloudflare-worker/src/worker.js | 159 +++++++++++++++++++++++--- dogfather.html | 2 +- hasidog.html | 4 +- index.html | 151 ++++++++++++++++++++++--- postfach.html | 195 ++++++++++++++++++++++++++++---- sitemap.xml | 2 +- streamplan.html | 21 ++-- 10 files changed, 496 insertions(+), 82 deletions(-) rename kasper.html => casper.html (81%) diff --git a/assets/js/main.js b/assets/js/main.js index 8cb0180..74edaaa 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -24,7 +24,7 @@ label: "HasiDog", children: [ { label: "HasiDog-Welt", href: "hasidog.html" }, - { label: "Kasper", href: "kasper.html" }, + { label: "Casper", href: "casper.html" }, { label: "Community", href: "community.html" }, { label: "Stimmen", href: "stimmen.html" }, ], diff --git a/kasper.html b/casper.html similarity index 81% rename from kasper.html rename to casper.html index 9db6c7a..f37e24b 100644 --- a/kasper.html +++ b/casper.html @@ -3,18 +3,18 @@ -Kasper — DOGFATHER UNIVERSE - +Casper — DOGFATHER UNIVERSE + - - + + - + - - + + @@ -29,7 +29,7 @@
Welt 2 — HasiDog -

Kasper 🐾

+

Casper 🐾

Der Husky an Dogfathers Seite — Namensgeber vieler Herzen in der Community.

@@ -40,7 +40,7 @@ Foto: Casper
Foto folgt (assets/img/casper-1.jpg — ggf. aus HEIC konvertieren)
-

Wer ist Kasper?

+

Wer ist Casper?

Husky Casper ist einer von Dogfathers treuen Begleitern und längst Teil der Marke geworden. Zusammen mit Kater Simba gehört er zur „Familie" hinter der Kamera.

Die gemeinsame Geschichte von Dogfather und Casper gibt es sogar als Buch: „Hasidog & Casper".

diff --git a/cloudflare-worker/README.md b/cloudflare-worker/README.md index c609de2..cf61d90 100644 --- a/cloudflare-worker/README.md +++ b/cloudflare-worker/README.md @@ -58,10 +58,26 @@ und `postfach.html` kann sie mit dem in Schritt 3 gesetzten Code anzeigen. ## Wer den Code kennen darf -Der Code aus Schritt 3 ist der einzige Schlüssel zum Postfach. Nur an Personen weitergeben, die -wirklich Zugriff auf Bewerbungen haben sollen. Code ändern: einfach `wrangler secret put -POSTFACH_CODE` erneut ausführen und neuen Wert eingeben — alte Bewerbungen bleiben erhalten, -nur der Zugangscode ändert sich. +Der Code aus Schritt 3 ist dein **Owner-Code** — nur du (Dogi) solltest ihn kennen. Damit kannst +du nicht nur Bewerbungen sehen, sondern auch Helfer-Codes für andere Personen vergeben (siehe +unten). Code ändern: einfach `wrangler secret put POSTFACH_CODE` erneut ausführen und neuen Wert +eingeben — alte Bewerbungen bleiben erhalten, nur der Zugangscode ändert sich. + +## Rollen für Rechte Hand, Linke Hand & Co. + +Auf `postfach.html` gibt es — nur sichtbar, wenn du dich mit deinem Owner-Code einloggst — einen +Bereich „Rollen verwalten". Dort kannst du: + +- **Neue Codes erzeugen**, mit Rolle (Rechte Hand / Linke Hand / frei) + Name (z.B. "VanVan"). + Der neue Code wird **einmalig** angezeigt — sofort weitergeben/notieren, danach ist er nicht + mehr abrufbar (nur als Hash gespeichert, aus Sicherheitsgründen). +- **Codes entziehen**, falls sich jemandes Rolle ändert oder der Zugang nicht mehr gebraucht wird. + +**Was Helfer-Codes können:** ausschließlich die eingegangenen Bewerbungen ansehen — dieselbe +Ansicht wie du. **Was sie NICHT können:** neue Codes erzeugen, andere Codes entziehen, oder +irgendetwas an der Website selbst verändern/löschen. Für Website-Änderungen gibt es in diesem +Worker schlicht keine einzige Funktion — das ist technisch gar nicht möglich, egal welchen Code +jemand hat. ## Wartung diff --git a/cloudflare-worker/src/worker.js b/cloudflare-worker/src/worker.js index 743765d..84d37db 100644 --- a/cloudflare-worker/src/worker.js +++ b/cloudflare-worker/src/worker.js @@ -1,20 +1,38 @@ /* ===================================================================== DOGFATHER UNIVERSE — Bewerbungs-Postfach Worker Nimmt Bewerbungen von bewerben.html entgegen, speichert sie in KV, - und gibt sie nur nach korrektem Code (serverseitig geprüft, per - Secret gespeichert) an postfach.html zurück. + und gibt sie nur nach korrektem Code (serverseitig geprüft) an + postfach.html zurück. + + Zugriffs-Modell (wichtig für's Verständnis): + - Owner-Code (POSTFACH_CODE, Secret) = Dogi selbst. Sieht alle + Bewerbungen UND kann Helfer-Codes mit Rollen (z.B. "Rechte Hand", + "Linke Hand") anlegen/entziehen. + - Helfer-Codes (von Dogi vergeben) = sehen NUR die Bewerbungen. + Sie können KEINE neuen Codes anlegen, KEINE Rollen verwalten, + und haben KEINERLEI Möglichkeit, irgendetwas an der Website + selbst zu verändern oder zu löschen — dafür gibt es in diesem + Worker schlicht keine Funktion. Er kann ausschließlich + Bewerbungen entgegennehmen und (mit gültigem Code) anzeigen. + - Alle Codes werden nur als SHA-256-Hash in KV gespeichert, nie im + Klartext. Ein neu erzeugter Code wird genau einmal zurückgegeben + (beim Erstellen) — danach ist er nicht mehr abrufbar. Endpoints: - POST /submit { type: "creator"|"scout"|"kooperation", data: {...} } - POST /postfach { code: "..." } -> Liste aller Bewerbungen - GET /health -> einfacher Check ob der Worker läuft + POST /submit { type, data } -> Bewerbung speichern + POST /postfach { code } -> Bewerbungen ansehen + POST /admin/roles/list { ownerCode } -> Helfer-Codes auflisten (nur Owner) + POST /admin/roles/create { ownerCode, role, label } -> neuen Helfer-Code erzeugen (nur Owner) + POST /admin/roles/revoke { ownerCode, keyId } -> Helfer-Code entziehen (nur Owner) + GET /health -> einfacher Check ob der Worker läuft Secrets/Bindings (siehe wrangler.toml + README.md): - KV Binding: BEWERBUNGEN (Cloudflare KV Namespace) + KV Binding: BEWERBUNGEN (Cloudflare KV Namespace, wird auch für Rollen genutzt) Secret: POSTFACH_CODE (mit `wrangler secret put POSTFACH_CODE` setzen) ===================================================================== */ const ALLOWED_TYPES = ["creator", "scout", "kooperation"]; +const ROLE_KEY_PREFIX = "role:"; function corsHeaders() { // TODO: sobald die Domain final steht, statt "*" die echte Domain eintragen @@ -34,6 +52,30 @@ function json(data, status = 200) { }); } +async function sha256Hex(text) { + const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)); + return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join(""); +} + +function generateFriendlyCode() { + // z.B. "K3F9-QX2L-7HRT" — leicht abzutippen/vorzulesen, ausreichend Entropie + const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // ohne verwechselbare Zeichen (I,O,0,1) + const bytes = crypto.getRandomValues(new Uint8Array(12)); + let raw = ""; + for (const b of bytes) raw += alphabet[b % alphabet.length]; + return `${raw.slice(0, 4)}-${raw.slice(4, 8)}-${raw.slice(8, 12)}`; +} + +async function isOwnerCode(code, env) { + return !!env.POSTFACH_CODE && code === env.POSTFACH_CODE; +} + +async function findHelperRole(code, env) { + const hash = await sha256Hex(code); + const raw = await env.BEWERBUNGEN.get(ROLE_KEY_PREFIX + hash); + return raw ? { keyId: ROLE_KEY_PREFIX + hash, ...JSON.parse(raw) } : null; +} + async function handleSubmit(request, env) { let body; try { @@ -47,7 +89,6 @@ async function handleSubmit(request, env) { return json({ ok: false, error: "Ungültige Bewerbungsdaten." }, 400); } - // simple Server-seitige Validierung: keine leeren Kernfelder const values = Object.values(data).map((v) => String(v || "").trim()); if (values.every((v) => v === "")) { return json({ ok: false, error: "Leere Bewerbung." }, 400); @@ -57,10 +98,7 @@ async function handleSubmit(request, env) { const timestamp = new Date().toISOString(); const key = `sub:${timestamp}:${id}`; - await env.BEWERBUNGEN.put( - key, - JSON.stringify({ id, type, data, timestamp }) - ); + await env.BEWERBUNGEN.put(key, JSON.stringify({ id, type, data, timestamp })); return json({ ok: true }); } @@ -74,7 +112,14 @@ async function handlePostfach(request, env) { } const code = (body && body.code) || ""; - if (!env.POSTFACH_CODE || code !== env.POSTFACH_CODE) { + let owner = await isOwnerCode(code, env); + let helperRole = null; + + if (!owner) { + helperRole = await findHelperRole(code, env); + } + + if (!owner && !helperRole) { // bewusst generische Fehlermeldung, kein Hinweis ob Code existiert return json({ ok: false, error: "Falscher Code." }, 401); } @@ -91,7 +136,83 @@ async function handlePostfach(request, env) { .filter(Boolean) .sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)); // neueste zuerst - return json({ ok: true, bewerbungen }); + return json({ + ok: true, + isOwner: owner, + roleLabel: owner ? "Dogi (Owner)" : helperRole.label, + bewerbungen, + }); +} + +async function requireOwner(body, env) { + const ownerCode = (body && body.ownerCode) || ""; + return isOwnerCode(ownerCode, env); +} + +async function handleRolesList(request, env) { + let body; + try { + body = await request.json(); + } catch { + return json({ ok: false, error: "Ungültiges JSON." }, 400); + } + if (!(await requireOwner(body, env))) { + return json({ ok: false, error: "Nur Dogi darf Rollen verwalten." }, 401); + } + + const list = await env.BEWERBUNGEN.list({ prefix: ROLE_KEY_PREFIX, limit: 200 }); + const roles = await Promise.all( + list.keys.map(async (k) => { + const raw = await env.BEWERBUNGEN.get(k.name); + return raw ? { keyId: k.name, ...JSON.parse(raw) } : null; + }) + ); + + return json({ ok: true, roles: roles.filter(Boolean) }); +} + +async function handleRolesCreate(request, env) { + let body; + try { + body = await request.json(); + } catch { + return json({ ok: false, error: "Ungültiges JSON." }, 400); + } + if (!(await requireOwner(body, env))) { + return json({ ok: false, error: "Nur Dogi darf Rollen verwalten." }, 401); + } + + const role = String((body && body.role) || "").slice(0, 60).trim() || "Helfer"; + const label = String((body && body.label) || "").slice(0, 80).trim() || role; + + const newCode = generateFriendlyCode(); + const hash = await sha256Hex(newCode); + const record = { role, label, createdAt: new Date().toISOString() }; + + await env.BEWERBUNGEN.put(ROLE_KEY_PREFIX + hash, JSON.stringify(record)); + + // Der Klartext-Code wird NUR jetzt einmalig zurückgegeben — danach nie wieder. + return json({ ok: true, code: newCode, ...record }); +} + +async function handleRolesRevoke(request, env) { + let body; + try { + body = await request.json(); + } catch { + return json({ ok: false, error: "Ungültiges JSON." }, 400); + } + if (!(await requireOwner(body, env))) { + return json({ ok: false, error: "Nur Dogi darf Rollen verwalten." }, 401); + } + + const keyId = String((body && body.keyId) || ""); + if (!keyId.startsWith(ROLE_KEY_PREFIX)) { + return json({ ok: false, error: "Ungültige keyId." }, 400); + } + + await env.BEWERBUNGEN.delete(keyId); + return json({ ok: true }); } export default { @@ -114,6 +235,18 @@ export default { return handlePostfach(request, env); } + if (url.pathname === "/admin/roles/list" && request.method === "POST") { + return handleRolesList(request, env); + } + + if (url.pathname === "/admin/roles/create" && request.method === "POST") { + return handleRolesCreate(request, env); + } + + if (url.pathname === "/admin/roles/revoke" && request.method === "POST") { + return handleRolesRevoke(request, env); + } + return json({ ok: false, error: "Not found." }, 404); }, }; diff --git a/dogfather.html b/dogfather.html index 6056715..df80bb9 100644 --- a/dogfather.html +++ b/dogfather.html @@ -42,7 +42,7 @@

Wer ist Dogfather?

Content Creator, Streamer auf TikTok (@dogfather0804) und das Herz einer wachsenden Community.

-

An seiner Seite: Husky Casper und Kater Simba — treue Begleiter, die längst Teil der Marke geworden sind. Mit seinem Core-Team baut Dogfather jeden Tag daran, dass sich Menschen in seiner Community nicht allein fühlen.

+

An seiner Seite: Husky Casper und Kater Simba — treue Begleiter, die längst Teil der Marke geworden sind. Mit seinem Core-Team baut Dogfather jeden Tag daran, dass sich Menschen in seiner Community nicht allein fühlen.

Hinweis zur Privatsphäre: Auf dieser Website wird ausschließlich der Persona-Name „Dogfather / Dogi" veröffentlicht — keine echten bürgerlichen Daten (kein Nachname, kein genaues Alter, keine genaue Herkunft).
diff --git a/hasidog.html b/hasidog.html index 91c8354..1091c97 100644 --- a/hasidog.html +++ b/hasidog.html @@ -43,7 +43,7 @@

Was ist HasiDog?

HasiDog ist mehr als ein Spitzname — es ist die verspielte, familiäre Marke rund um Dogfathers Community. Unter dem Namen „Hasidog" erscheint auch die eigene Musik auf Spotify.

@@ -55,7 +55,7 @@

Die HasiDog-Familie

🎵 Musik

Songs unter dem Namen „Hasidog" auf Spotify.

-

🐕 Casper

Der Husky, der zum Gesicht der HasiDog-Welt wurde — eigene Seite: Kasper.

+

🐕 Casper

Der Husky, der zum Gesicht der HasiDog-Welt wurde — eigene Seite: Casper.

📖 Buch

„Hasidog & Casper" — die Geschichte in Buchform.

diff --git a/index.html b/index.html index ec1c1f2..4a02733 100644 --- a/index.html +++ b/index.html @@ -26,11 +26,18 @@
+
🩵 DOGFATHER UNIVERSE

Streamer. Community. HasiDog. Manager.
Gemeinsam stark.

Drei Welten, eine Familie: Team Dogi.

+ +
+ 🔴 Live-Status +

Dogfather geht jeden Tag ab 20 Uhr live — oft schon tagsüber, zu wechselnden Zeiten. Streamplan ansehen →

+
+
Alle Links 🩵 Teil vom Team werden @@ -56,39 +63,147 @@
+
-
-
-

🐾 Team Dogi ist Familie

-

Unsere Mission: Jeder soll lächeln und sich nicht allein fühlen.

-
-
-

🎥 TikTok @dogfather0804

-

Halligalli, Eindeutig Zweideutig & Hasencontent — Content mit Herz und Humor.

-
-
-

🎵 Musik: Hasidog & Van-Van

-

Eigene Musikprojekte auf Spotify — von Dogfather und VanVan.

-
+
+

Menschen im Team Dogi

+

Täglich

live ab 20 Uhr

+

3

Welten, eine Familie

+

🩵

Team Dogi ist Familie

+ +
+ +
+ + +
+
+

Team Dogi 🩵

+

Von VanVan bis zu den Scouts — die Menschen, die DOGFATHER UNIVERSE tragen.

+
+ +
+
+ + +
+
+

Was gerade läuft

+
+ +
+
+ + +
+
+

Impressionen

+
+
Foto folgt
+
Foto folgt
+
Foto folgt
+
Foto folgt
+
+ +
+
+ +
- Neu in der Community? -

Schau dir an, wer Team Dogi ausmacht

-

Von den Scouts über die Modis bis zu VanVan — lerne die Menschen hinter Dogfather kennen.

+ Was andere sagen +
+

„Platzhalter-Zitat — echtes Testimonial folgt."

+

— Community-Mitglied

+
+
+
+ + +
+
+ 🩵 Bereit? +

Werde Teil von DOGFATHER UNIVERSE

+

Als Zuschauer:in, Creator, Scout oder Kooperationspartner — hier ist für jeden Platz.

+
+ + + + diff --git a/postfach.html b/postfach.html index 089cff0..be7321f 100644 --- a/postfach.html +++ b/postfach.html @@ -7,17 +7,6 @@ Postfach — DOGFATHER UNIVERSE - - - - - - - - - - - @@ -36,7 +25,7 @@
-
+
@@ -56,6 +45,15 @@
+ + + + + + + @@ -75,12 +107,21 @@ const API_BASE_URL = ""; document.addEventListener("DOMContentLoaded", () => { + const gateContainer = document.getElementById("gate-container"); const form = document.getElementById("code-gate"); const status = document.getElementById("gate-status"); + const sessionBar = document.getElementById("session-bar"); + const roleBadge = document.getElementById("role-badge"); + const logoutBtn = document.getElementById("logout-btn"); const inboxSection = document.getElementById("inbox-section"); const inboxList = document.getElementById("inbox-list"); const refreshBtn = document.getElementById("refresh-btn"); const notConfigured = document.getElementById("not-configured-note"); + const adminSection = document.getElementById("admin-section"); + const roleCreateForm = document.getElementById("role-create-form"); + const roleCreateStatus = document.getElementById("role-create-status"); + const newCodeReveal = document.getElementById("new-code-reveal"); + const rolesList = document.getElementById("roles-list"); if (!API_BASE_URL) { notConfigured.style.display = "block"; @@ -88,7 +129,7 @@ document.addEventListener("DOMContentLoaded", () => { notConfigured.style.display = "none"; } - let lastCode = ""; + let session = { code: "", isOwner: false, roleLabel: "" }; function renderInbox(bewerbungen) { if (!bewerbungen.length) { @@ -111,44 +152,148 @@ document.addEventListener("DOMContentLoaded", () => { .join(""); } - async function loadInbox(code) { + async function loadInbox() { status.className = "form-status show"; status.textContent = "Lade Postfach …"; try { const res = await fetch(`${API_BASE_URL}/postfach`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ code }), + body: JSON.stringify({ code: session.code }), }); - const json = await res.json(); - if (!res.ok || !json.ok) { + const data = await res.json(); + if (!res.ok || !data.ok) { status.textContent = "Falscher Code oder Postfach nicht erreichbar."; status.className = "form-status show error"; - inboxSection.style.display = "none"; - return; + return false; } - status.className = "form-status"; + session.isOwner = data.isOwner; + session.roleLabel = data.roleLabel; + + gateContainer.style.display = "none"; + sessionBar.style.display = "block"; inboxSection.style.display = "block"; - renderInbox(json.bewerbungen); + roleBadge.textContent = `Eingeloggt als: ${data.roleLabel}`; + renderInbox(data.bewerbungen); + + if (data.isOwner) { + adminSection.style.display = "block"; + loadRoles(); + } else { + adminSection.style.display = "none"; + } + return true; } catch { status.textContent = "Postfach gerade nicht erreichbar. Bitte später erneut versuchen."; status.className = "form-status show error"; + return false; } } - form.addEventListener("submit", (e) => { + async function loadRoles() { + try { + const res = await fetch(`${API_BASE_URL}/admin/roles/list`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ownerCode: session.code }), + }); + const data = await res.json(); + if (!res.ok || !data.ok) { + rolesList.innerHTML = `

Konnte Codes nicht laden.

`; + return; + } + if (!data.roles.length) { + rolesList.innerHTML = `

Noch keine Helfer-Codes vergeben.

`; + return; + } + rolesList.innerHTML = data.roles + .map( + (r) => ` +
+ ${r.role} +

${r.label}

+

Erstellt: ${new Date(r.createdAt).toLocaleDateString("de-DE")}

+ +
` + ) + .join(""); + + rolesList.querySelectorAll(".revoke-btn").forEach((btn) => { + btn.addEventListener("click", async () => { + if (!confirm("Diesen Code wirklich entziehen? Die Person kann sich danach nicht mehr einloggen.")) return; + await fetch(`${API_BASE_URL}/admin/roles/revoke`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ownerCode: session.code, keyId: btn.dataset.key }), + }); + loadRoles(); + }); + }); + } catch { + rolesList.innerHTML = `

Konnte Codes nicht laden.

`; + } + } + + form.addEventListener("submit", async (e) => { e.preventDefault(); if (!API_BASE_URL) { status.textContent = "Postfach ist noch nicht eingerichtet (siehe Hinweis unten)."; status.className = "form-status show error"; return; } - lastCode = document.getElementById("code-input").value; - loadInbox(lastCode); + session.code = document.getElementById("code-input").value; + const ok = await loadInbox(); + if (ok) { + status.className = "form-status"; + form.reset(); + } + }); + + logoutBtn.addEventListener("click", () => { + session = { code: "", isOwner: false, roleLabel: "" }; + gateContainer.style.display = "block"; + sessionBar.style.display = "none"; + inboxSection.style.display = "none"; + adminSection.style.display = "none"; + status.textContent = ""; + status.className = "form-status"; }); refreshBtn.addEventListener("click", () => { - if (lastCode) loadInbox(lastCode); + if (session.code) loadInbox(); + }); + + roleCreateForm.addEventListener("submit", async (e) => { + e.preventDefault(); + const role = document.getElementById("role-select").value; + const label = document.getElementById("role-label").value.trim(); + if (!label) return; + + try { + const res = await fetch(`${API_BASE_URL}/admin/roles/create`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ownerCode: session.code, role, label }), + }); + const data = await res.json(); + if (!res.ok || !data.ok) { + roleCreateStatus.textContent = "Konnte keinen Code erzeugen."; + roleCreateStatus.className = "form-status show error"; + return; + } + newCodeReveal.style.display = "block"; + newCodeReveal.innerHTML = ` + Neuer Code für ${data.label} (${data.role}):
+ ${data.code}
+ Jetzt sichern/weitergeben — dieser Code wird nirgends mehr angezeigt!`; + roleCreateStatus.textContent = ""; + roleCreateStatus.className = "form-status"; + roleCreateForm.reset(); + loadRoles(); + } catch { + roleCreateStatus.textContent = "Postfach gerade nicht erreichbar."; + roleCreateStatus.className = "form-status show error"; + } }); }); diff --git a/sitemap.xml b/sitemap.xml index 8671383..688e56a 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -7,7 +7,7 @@ https://REPLACE-WITH-YOUR-DOMAIN.tld/streamplan.html2026-07-30 https://REPLACE-WITH-YOUR-DOMAIN.tld/community.html2026-07-30 https://REPLACE-WITH-YOUR-DOMAIN.tld/hasidog.html2026-07-30 - https://REPLACE-WITH-YOUR-DOMAIN.tld/kasper.html2026-07-30 + https://REPLACE-WITH-YOUR-DOMAIN.tld/casper.html2026-07-30 https://REPLACE-WITH-YOUR-DOMAIN.tld/aktionen.html2026-07-30 https://REPLACE-WITH-YOUR-DOMAIN.tld/zeitreise.html2026-07-30 https://REPLACE-WITH-YOUR-DOMAIN.tld/medien.html2026-07-30 diff --git a/streamplan.html b/streamplan.html index 6655a1b..e94f0c9 100644 --- a/streamplan.html +++ b/streamplan.html @@ -32,22 +32,27 @@

Streamplan & Livebereich

Wann ist Dogfather live?

-

Wochenplan

-
-

Montag

TODO: Uhrzeit/Format

-

Mittwoch

TODO: Uhrzeit/Format

-

Freitag

TODO: Uhrzeit/Format

-

Sonntag

TODO: Uhrzeit/Format

+
+
+ Fixer Termin +

🌙 Jeden Tag ab 20 Uhr

+

Dogfather geht jeden Tag live — verlässlich ab 20:00 Uhr. Das ist der feste Abend-Stream, auf den man sich einstellen kann.

+
+
+ Bonus +

☀️ Extra-Streams tagsüber

+

Oft geht's auch schon tagsüber live — die Uhrzeit dafür variiert von Tag zu Tag. Am besten TikTok-Benachrichtigungen für @dogfather0804 aktivieren, um keinen Stream zu verpassen.

+
- TODO: Echte Streamtage/-zeiten und Formate mit Dogi klären (Masterplan Block D) und hier eintragen. + TODO: Streamformate, wiederkehrende Segmente und Running Gags (Masterplan Block D) hier noch ergänzen, sobald geklärt.