Rollenbasiertes Postfach, große Startseite, Casper-Fix, Streamplan/TikTok-Korrekturen

This commit is contained in:
2026-07-30 20:25:56 +02:00
parent f8e4cf82db
commit b1712ec33b
10 changed files with 496 additions and 82 deletions
+1 -1
View File
@@ -24,7 +24,7 @@
label: "HasiDog", label: "HasiDog",
children: [ children: [
{ label: "HasiDog-Welt", href: "hasidog.html" }, { label: "HasiDog-Welt", href: "hasidog.html" },
{ label: "Kasper", href: "kasper.html" }, { label: "Casper", href: "casper.html" },
{ label: "Community", href: "community.html" }, { label: "Community", href: "community.html" },
{ label: "Stimmen", href: "stimmen.html" }, { label: "Stimmen", href: "stimmen.html" },
], ],
+9 -9
View File
@@ -3,18 +3,18 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Kasper — DOGFATHER UNIVERSE</title> <title>Casper — DOGFATHER UNIVERSE</title>
<meta name="description" content="Kasper (Husky Casper): das tierische Gesicht der HasiDog-Welt." /> <meta name="description" content="Der Husky an Dogfathers Seite — das tierische Gesicht der HasiDog-Welt." />
<link rel="icon" type="image/svg+xml" href="assets/img/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="assets/img/favicon.svg" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:site_name" content="DOGFATHER UNIVERSE" /> <meta property="og:site_name" content="DOGFATHER UNIVERSE" />
<meta property="og:title" content="Kasper — DOGFATHER UNIVERSE" /> <meta property="og:title" content="Casper — DOGFATHER UNIVERSE" />
<meta property="og:description" content="Kasper (Husky Casper): das tierische Gesicht der HasiDog-Welt." /> <meta property="og:description" content="Der Husky an Dogfathers Seite — das tierische Gesicht der HasiDog-Welt." />
<meta property="og:image" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/assets/img/og-cover.jpg" /> <meta property="og:image" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/assets/img/og-cover.jpg" />
<meta property="og:url" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/kasper.html" /> <meta property="og:url" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/casper.html" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Kasper — DOGFATHER UNIVERSE" /> <meta name="twitter:title" content="Casper — DOGFATHER UNIVERSE" />
<meta name="twitter:description" content="Kasper (Husky Casper): das tierische Gesicht der HasiDog-Welt." /> <meta name="twitter:description" content="Der Husky an Dogfathers Seite — das tierische Gesicht der HasiDog-Welt." />
<meta name="twitter:image" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/assets/img/og-cover.jpg" /> <meta name="twitter:image" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css" />
@@ -29,7 +29,7 @@
<section class="hero"> <section class="hero">
<div class="container"> <div class="container">
<span class="eyebrow">Welt 2 — HasiDog</span> <span class="eyebrow">Welt 2 — HasiDog</span>
<h1>Kasper 🐾</h1> <h1>Casper 🐾</h1>
<p class="lead worlds">Der Husky an Dogfathers Seite — Namensgeber vieler Herzen in der Community.</p> <p class="lead worlds">Der Husky an Dogfathers Seite — Namensgeber vieler Herzen in der Community.</p>
</div> </div>
</section> </section>
@@ -40,7 +40,7 @@
Foto: Casper<br><span class="small">Foto folgt (assets/img/casper-1.jpg — ggf. aus HEIC konvertieren)</span> Foto: Casper<br><span class="small">Foto folgt (assets/img/casper-1.jpg — ggf. aus HEIC konvertieren)</span>
</div> </div>
<div> <div>
<h2>Wer ist Kasper?</h2> <h2>Wer ist Casper?</h2>
<p>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.</p> <p>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.</p>
<p>Die gemeinsame Geschichte von Dogfather und Casper gibt es sogar als Buch: <strong>„Hasidog &amp; Casper"</strong>.</p> <p>Die gemeinsame Geschichte von Dogfather und Casper gibt es sogar als Buch: <strong>„Hasidog &amp; Casper"</strong>.</p>
</div> </div>
+20 -4
View File
@@ -58,10 +58,26 @@ und `postfach.html` kann sie mit dem in Schritt 3 gesetzten Code anzeigen.
## Wer den Code kennen darf ## Wer den Code kennen darf
Der Code aus Schritt 3 ist der einzige Schlüssel zum Postfach. Nur an Personen weitergeben, die Der Code aus Schritt 3 ist dein **Owner-Code** — nur du (Dogi) solltest ihn kennen. Damit kannst
wirklich Zugriff auf Bewerbungen haben sollen. Code ändern: einfach `wrangler secret put du nicht nur Bewerbungen sehen, sondern auch Helfer-Codes für andere Personen vergeben (siehe
POSTFACH_CODE` erneut ausführen und neuen Wert eingeben — alte Bewerbungen bleiben erhalten, unten). Code ändern: einfach `wrangler secret put POSTFACH_CODE` erneut ausführen und neuen Wert
nur der Zugangscode ändert sich. 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 ## Wartung
+145 -12
View File
@@ -1,20 +1,38 @@
/* ===================================================================== /* =====================================================================
DOGFATHER UNIVERSE — Bewerbungs-Postfach Worker DOGFATHER UNIVERSE — Bewerbungs-Postfach Worker
Nimmt Bewerbungen von bewerben.html entgegen, speichert sie in KV, Nimmt Bewerbungen von bewerben.html entgegen, speichert sie in KV,
und gibt sie nur nach korrektem Code (serverseitig geprüft, per und gibt sie nur nach korrektem Code (serverseitig geprüft) an
Secret gespeichert) an postfach.html zurück. 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: Endpoints:
POST /submit { type: "creator"|"scout"|"kooperation", data: {...} } POST /submit { type, data } -> Bewerbung speichern
POST /postfach { code: "..." } -> Liste aller Bewerbungen 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 GET /health -> einfacher Check ob der Worker läuft
Secrets/Bindings (siehe wrangler.toml + README.md): 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) Secret: POSTFACH_CODE (mit `wrangler secret put POSTFACH_CODE` setzen)
===================================================================== */ ===================================================================== */
const ALLOWED_TYPES = ["creator", "scout", "kooperation"]; const ALLOWED_TYPES = ["creator", "scout", "kooperation"];
const ROLE_KEY_PREFIX = "role:";
function corsHeaders() { function corsHeaders() {
// TODO: sobald die Domain final steht, statt "*" die echte Domain eintragen // 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) { async function handleSubmit(request, env) {
let body; let body;
try { try {
@@ -47,7 +89,6 @@ async function handleSubmit(request, env) {
return json({ ok: false, error: "Ungültige Bewerbungsdaten." }, 400); 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()); const values = Object.values(data).map((v) => String(v || "").trim());
if (values.every((v) => v === "")) { if (values.every((v) => v === "")) {
return json({ ok: false, error: "Leere Bewerbung." }, 400); return json({ ok: false, error: "Leere Bewerbung." }, 400);
@@ -57,10 +98,7 @@ async function handleSubmit(request, env) {
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const key = `sub:${timestamp}:${id}`; const key = `sub:${timestamp}:${id}`;
await env.BEWERBUNGEN.put( await env.BEWERBUNGEN.put(key, JSON.stringify({ id, type, data, timestamp }));
key,
JSON.stringify({ id, type, data, timestamp })
);
return json({ ok: true }); return json({ ok: true });
} }
@@ -74,7 +112,14 @@ async function handlePostfach(request, env) {
} }
const code = (body && body.code) || ""; 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 // bewusst generische Fehlermeldung, kein Hinweis ob Code existiert
return json({ ok: false, error: "Falscher Code." }, 401); return json({ ok: false, error: "Falscher Code." }, 401);
} }
@@ -91,7 +136,83 @@ async function handlePostfach(request, env) {
.filter(Boolean) .filter(Boolean)
.sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)); // neueste zuerst .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 { export default {
@@ -114,6 +235,18 @@ export default {
return handlePostfach(request, env); 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); return json({ ok: false, error: "Not found." }, 404);
}, },
}; };
+1 -1
View File
@@ -42,7 +42,7 @@
<div> <div>
<h2>Wer ist Dogfather?</h2> <h2>Wer ist Dogfather?</h2>
<p class="lead">Content Creator, Streamer auf TikTok (<strong>@dogfather0804</strong>) und das Herz einer wachsenden Community.</p> <p class="lead">Content Creator, Streamer auf TikTok (<strong>@dogfather0804</strong>) und das Herz einer wachsenden Community.</p>
<p>An seiner Seite: Husky <a href="kasper.html">Casper</a> 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.</p> <p>An seiner Seite: Husky <a href="casper.html">Casper</a> 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.</p>
<div class="todo-note"> <div class="todo-note">
<strong>Hinweis zur Privatsphäre:</strong> 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). <strong>Hinweis zur Privatsphäre:</strong> 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).
</div> </div>
+2 -2
View File
@@ -43,7 +43,7 @@
<h2>Was ist HasiDog?</h2> <h2>Was ist HasiDog?</h2>
<p>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.</p> <p>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.</p>
<div class="btn-row"> <div class="btn-row">
<a class="btn btn-primary" href="kasper.html">Zur Kasper-Seite 🐾</a> <a class="btn btn-primary" href="casper.html">Zur Casper-Seite 🐾</a>
<a class="btn btn-outline" href="community.html">Zur Community</a> <a class="btn btn-outline" href="community.html">Zur Community</a>
</div> </div>
</div> </div>
@@ -55,7 +55,7 @@
<h2 class="text-center">Die HasiDog-Familie</h2> <h2 class="text-center">Die HasiDog-Familie</h2>
<div class="grid grid-3"> <div class="grid grid-3">
<div class="card text-center"><h3>🎵 Musik</h3><p>Songs unter dem Namen „Hasidog" auf Spotify.</p></div> <div class="card text-center"><h3>🎵 Musik</h3><p>Songs unter dem Namen „Hasidog" auf Spotify.</p></div>
<div class="card text-center"><h3>🐕 Casper</h3><p>Der Husky, der zum Gesicht der HasiDog-Welt wurde — eigene Seite: <a href="kasper.html">Kasper</a>.</p></div> <div class="card text-center"><h3>🐕 Casper</h3><p>Der Husky, der zum Gesicht der HasiDog-Welt wurde — eigene Seite: <a href="casper.html">Casper</a>.</p></div>
<div class="card text-center"><h3>📖 Buch</h3><p>„Hasidog &amp; Casper" — die Geschichte in Buchform.</p></div> <div class="card text-center"><h3>📖 Buch</h3><p>„Hasidog &amp; Casper" — die Geschichte in Buchform.</p></div>
</div> </div>
<div class="todo-note"> <div class="todo-note">
+133 -18
View File
@@ -26,11 +26,18 @@
<div id="site-header"></div> <div id="site-header"></div>
<main> <main>
<!-- ================= HERO ================= -->
<section class="hero"> <section class="hero">
<div class="container"> <div class="container">
<span class="eyebrow">🩵 DOGFATHER UNIVERSE</span> <span class="eyebrow">🩵 DOGFATHER UNIVERSE</span>
<h1>Streamer. Community. HasiDog. Manager.<br>Gemeinsam stark.</h1> <h1>Streamer. Community. HasiDog. Manager.<br>Gemeinsam stark.</h1>
<p class="lead worlds">Drei Welten, eine Familie: Team Dogi.</p> <p class="lead worlds">Drei Welten, eine Familie: Team Dogi.</p>
<div class="card" style="max-width:420px;margin:1.4rem auto 0;padding:1rem 1.4rem;">
<span class="badge rank-1">🔴 Live-Status</span>
<p class="small" style="margin:.5rem 0 0;">Dogfather geht <strong>jeden Tag ab 20 Uhr</strong> live — oft schon tagsüber, zu wechselnden Zeiten. <a href="streamplan.html">Streamplan ansehen →</a></p>
</div>
<div class="btn-row" style="justify-content:center;"> <div class="btn-row" style="justify-content:center;">
<a class="btn btn-primary" href="links.html">Alle Links 🩵</a> <a class="btn btn-primary" href="links.html">Alle Links 🩵</a>
<a class="btn btn-outline" href="bewerben.html">Teil vom Team werden</a> <a class="btn btn-outline" href="bewerben.html">Teil vom Team werden</a>
@@ -56,39 +63,147 @@
</div> </div>
</section> </section>
<!-- ================= STATS ================= -->
<section class="section-tight"> <section class="section-tight">
<div class="container"> <div class="container">
<div class="grid grid-3"> <div class="grid grid-4" id="stats-bar">
<div class="card text-center"> <div class="card text-center"><h3 id="stat-team"></h3><p class="small muted">Menschen im Team Dogi</p></div>
<h3>🐾 Team Dogi ist Familie</h3> <div class="card text-center"><h3>Täglich</h3><p class="small muted">live ab 20 Uhr</p></div>
<p>Unsere Mission: Jeder soll lächeln und sich nicht allein fühlen.</p> <div class="card text-center"><h3>3</h3><p class="small muted">Welten, eine Familie</p></div>
</div> <div class="card text-center"><h3>🩵</h3><p class="small muted">Team Dogi ist Familie</p></div>
<div class="card text-center">
<h3>🎥 TikTok @dogfather0804</h3>
<p>Halligalli, Eindeutig Zweideutig & Hasencontent — Content mit Herz und Humor.</p>
</div>
<div class="card text-center">
<h3>🎵 Musik: Hasidog &amp; Van-Van</h3>
<p>Eigene Musikprojekte auf Spotify — von Dogfather und VanVan.</p>
</div>
</div> </div>
</div> </div>
</section> </section>
<!-- ================= WAS DICH ERWARTET ================= -->
<section>
<div class="container">
<span class="eyebrow text-center" style="display:block;text-align:center;">Ein bisschen von allem</span>
<h2 class="text-center">Was dich im Universum erwartet</h2>
<div class="grid grid-4">
<a class="card" href="streamer.html"><h3>🎥 Streamer</h3><p class="small">Halligalli, Humor &amp; Hasencontent.</p></a>
<a class="card" href="zeitreise.html"><h3>🕰️ Zeitreise</h3><p class="small">Die Geschichte von Dogfather &amp; Team Dogi.</p></a>
<a class="card" href="community.html"><h3>🩵 Community</h3><p class="small">Team Dogi ist Familie.</p></a>
<a class="card" href="casper.html"><h3>🐾 Casper</h3><p class="small">Der Husky an Dogfathers Seite.</p></a>
<a class="card" href="team-modis.html"><h3>🧑‍🤝‍🧑 Modi-Team</h3><p class="small">VanVan, Diene &amp; Co.</p></a>
<a class="card" href="aktionen.html"><h3>🎁 Aktionen</h3><p class="small">Projekte, Kooperationen, Tierwohl.</p></a>
<a class="card" href="medien.html"><h3>🖼️ Galerie</h3><p class="small">Fotos aus allen drei Welten.</p></a>
<a class="card" href="videos.html"><h3>▶️ Videos</h3><p class="small">Die besten Highlights.</p></a>
</div>
</div>
</section>
<!-- ================= TEAM PREVIEW ================= -->
<section class="section-tight">
<div class="container">
<h2 class="text-center">Team Dogi 🩵</h2>
<p class="lead text-center" style="max-width:60ch;margin:0 auto;">Von VanVan bis zu den Scouts — die Menschen, die DOGFATHER UNIVERSE tragen.</p>
<div id="team-preview" class="grid grid-4"></div>
<div class="btn-row" style="justify-content:center;">
<a class="btn btn-outline" href="team-modis.html">Modi-Team ansehen</a>
<a class="btn btn-outline" href="team-scouts.html">Scout-Team ansehen</a>
</div>
</div>
</section>
<!-- ================= AKTIONEN TEASER ================= -->
<section>
<div class="container">
<h2 class="text-center">Was gerade läuft</h2>
<div id="aktionen-teaser" class="grid grid-3"></div>
<div class="btn-row" style="justify-content:center;">
<a class="btn btn-outline" href="aktionen.html">Alle Aktionen &amp; Projekte</a>
</div>
</div>
</section>
<!-- ================= GALERIE TEASER ================= -->
<section class="section-tight">
<div class="container">
<h2 class="text-center">Impressionen</h2>
<div class="grid grid-4">
<div class="img-placeholder">Foto folgt</div>
<div class="img-placeholder">Foto folgt</div>
<div class="img-placeholder">Foto folgt</div>
<div class="img-placeholder">Foto folgt</div>
</div>
<div class="btn-row" style="justify-content:center;">
<a class="btn btn-outline" href="medien.html">Zur ganzen Galerie</a>
</div>
</div>
</section>
<!-- ================= TESTIMONIAL ================= -->
<section> <section>
<div class="container text-center"> <div class="container text-center">
<span class="eyebrow">Neu in der Community?</span> <span class="eyebrow">Was andere sagen</span>
<h2>Schau dir an, wer Team Dogi ausmacht</h2> <div class="card" style="max-width:640px;margin:1rem auto 0;">
<p class="lead" style="max-width:60ch;margin:0 auto;">Von den Scouts über die Modis bis zu VanVan — lerne die Menschen hinter Dogfather kennen.</p> <p class="quote" style="font-size:1.15rem;">„Platzhalter-Zitat — echtes Testimonial folgt."</p>
<p class="small muted">— Community-Mitglied</p>
</div>
<div class="btn-row" style="justify-content:center;"> <div class="btn-row" style="justify-content:center;">
<a class="btn btn-secondary" href="team-modis.html">Modi-Team ansehen</a> <a class="btn btn-outline" href="stimmen.html">Mehr Stimmen</a>
<a class="btn btn-outline" href="team-scouts.html">Scout-Team ansehen</a> </div>
</div>
</section>
<!-- ================= FINALER CTA ================= -->
<section class="hero">
<div class="container text-center">
<span class="eyebrow">🩵 Bereit?</span>
<h2>Werde Teil von DOGFATHER UNIVERSE</h2>
<p class="lead" style="max-width:56ch;margin:0 auto;">Als Zuschauer:in, Creator, Scout oder Kooperationspartner — hier ist für jeden Platz.</p>
<div class="btn-row" style="justify-content:center;">
<a class="btn btn-primary" href="links.html">Alle Links 🩵</a>
<a class="btn btn-secondary" href="bewerben.html">Jetzt bewerben</a>
</div> </div>
</div> </div>
</section> </section>
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
<script src="assets/js/data-modis.js"></script>
<script src="assets/js/data-scouts.js"></script>
<script src="assets/js/data-aktionen.js"></script>
<script src="assets/js/main.js"></script> <script src="assets/js/main.js"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
// Stats: Team-Größe aus den Datenobjekten berechnen
const teamCount = (window.MODIS || []).length + (window.SCOUTS || []).length;
const statTeam = document.getElementById("stat-team");
if (statTeam) statTeam.textContent = teamCount > 0 ? `${teamCount}+` : "";
// Team-Vorschau: VanVan zuerst, dann die nächsten nach Rang, plus 1-2 Scouts
const teamPreview = document.getElementById("team-preview");
if (teamPreview && window.MODIS) {
const people = [...window.MODIS].sort((a, b) => a.rank - b.rank).slice(0, 4);
teamPreview.innerHTML = people
.map(
(p) => `
<div class="profile-card">
<div class="avatar">${p.photo ? `<img src="${p.photo}" alt="${p.name}">` : p.name.slice(0, 2).toUpperCase()}</div>
<h3>${p.name}</h3>
<div class="role">${p.role}</div>
</div>`
)
.join("");
}
// Aktionen-Teaser: die ersten 3 Einträge aus data-aktionen.js
const aktionenTeaser = document.getElementById("aktionen-teaser");
if (aktionenTeaser && window.AKTIONEN) {
aktionenTeaser.innerHTML = window.AKTIONEN.slice(0, 3)
.map(
(a) => `
<a class="card" href="aktion-detail.html?id=${a.id}">
<span class="tag">${a.tag} · ${a.status}</span>
<h3>${a.title}</h3>
<p>${a.summary}</p>
</a>`
)
.join("");
}
});
</script>
</body> </body>
</html> </html>
+170 -25
View File
@@ -7,17 +7,6 @@
<title>Postfach — DOGFATHER UNIVERSE</title> <title>Postfach — DOGFATHER UNIVERSE</title>
<meta name="description" content="Internes Bewerbungs-Postfach — nur mit Code zugänglich." /> <meta name="description" content="Internes Bewerbungs-Postfach — nur mit Code zugänglich." />
<link rel="icon" type="image/svg+xml" href="assets/img/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="assets/img/favicon.svg" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="DOGFATHER UNIVERSE" />
<meta property="og:title" content="Postfach — DOGFATHER UNIVERSE" />
<meta property="og:description" content="Internes Bewerbungs-Postfach — nur mit Code zugänglich." />
<meta property="og:image" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/assets/img/og-cover.jpg" />
<meta property="og:url" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/postfach.html" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Postfach — DOGFATHER UNIVERSE" />
<meta name="twitter:description" content="Internes Bewerbungs-Postfach — nur mit Code zugänglich." />
<meta name="twitter:image" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/assets/img/og-cover.jpg" />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" /> <link rel="stylesheet" href="assets/css/theme-dogfather.css" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" /> <link rel="stylesheet" href="assets/css/theme-hasidog.css" />
@@ -36,7 +25,7 @@
</section> </section>
<section class="section-tight"> <section class="section-tight">
<div class="container" style="max-width:520px;"> <div class="container" style="max-width:520px;" id="gate-container">
<form id="code-gate" class="app-form" novalidate> <form id="code-gate" class="app-form" novalidate>
<div class="field"> <div class="field">
<label for="code-input">Zugangscode</label> <label for="code-input">Zugangscode</label>
@@ -56,6 +45,15 @@
</div> </div>
</section> </section>
<!-- ===== Eingeloggt: Kopfzeile mit Rolle + Logout ===== -->
<section class="section-tight" id="session-bar" style="display:none;padding-top:0;">
<div class="container" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;">
<span class="badge rank-1" id="role-badge">Eingeloggt</span>
<button class="btn btn-outline" id="logout-btn" type="button">Abmelden</button>
</div>
</section>
<!-- ===== Mailbox (für alle mit gültigem Code) ===== -->
<section class="section-tight" id="inbox-section" style="display:none;"> <section class="section-tight" id="inbox-section" style="display:none;">
<div class="container"> <div class="container">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;margin-bottom:1.4rem;"> <div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;margin-bottom:1.4rem;">
@@ -65,6 +63,40 @@
<div id="inbox-list" class="grid grid-2"></div> <div id="inbox-list" class="grid grid-2"></div>
</div> </div>
</section> </section>
<!-- ===== Rollen-Verwaltung (NUR für Dogi/Owner sichtbar) ===== -->
<section id="admin-section" style="display:none;">
<div class="container">
<h2 class="text-center">Rollen verwalten</h2>
<p class="lead text-center" style="max-width:60ch;margin:0 auto;">
Vergib Codes an Rechte Hand, Linke Hand oder wen du sonst reinlassen willst. Diese Codes
zeigen <strong>ausschließlich die Bewerbungen</strong> — nichts an der Website selbst kann
damit verändert oder gelöscht werden, dafür gibt es hier schlicht keine Funktion.
</p>
<form id="role-create-form" class="app-form" style="max-width:520px;margin:1.6rem auto;" novalidate>
<div class="field">
<label for="role-select">Rolle</label>
<select id="role-select" name="role">
<option value="Rechte Hand">Rechte Hand</option>
<option value="Linke Hand">Linke Hand</option>
<option value="Helfer">Sonstige / frei</option>
</select>
</div>
<div class="field">
<label for="role-label">Für wen? (Name/Bezeichnung)</label>
<input id="role-label" name="label" type="text" placeholder="z.B. VanVan" required />
</div>
<button class="btn btn-primary" type="submit">Neuen Code erzeugen</button>
<div class="form-status" id="role-create-status"></div>
</form>
<div class="todo-note" id="new-code-reveal" style="display:none;max-width:520px;margin:0 auto 2rem;"></div>
<h3 class="text-center">Bestehende Codes</h3>
<div id="roles-list" class="grid grid-2"></div>
</div>
</section>
</main> </main>
<div id="site-footer"></div> <div id="site-footer"></div>
@@ -75,12 +107,21 @@
const API_BASE_URL = ""; const API_BASE_URL = "";
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const gateContainer = document.getElementById("gate-container");
const form = document.getElementById("code-gate"); const form = document.getElementById("code-gate");
const status = document.getElementById("gate-status"); 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 inboxSection = document.getElementById("inbox-section");
const inboxList = document.getElementById("inbox-list"); const inboxList = document.getElementById("inbox-list");
const refreshBtn = document.getElementById("refresh-btn"); const refreshBtn = document.getElementById("refresh-btn");
const notConfigured = document.getElementById("not-configured-note"); 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) { if (!API_BASE_URL) {
notConfigured.style.display = "block"; notConfigured.style.display = "block";
@@ -88,7 +129,7 @@ document.addEventListener("DOMContentLoaded", () => {
notConfigured.style.display = "none"; notConfigured.style.display = "none";
} }
let lastCode = ""; let session = { code: "", isOwner: false, roleLabel: "" };
function renderInbox(bewerbungen) { function renderInbox(bewerbungen) {
if (!bewerbungen.length) { if (!bewerbungen.length) {
@@ -111,44 +152,148 @@ document.addEventListener("DOMContentLoaded", () => {
.join(""); .join("");
} }
async function loadInbox(code) { async function loadInbox() {
status.className = "form-status show"; status.className = "form-status show";
status.textContent = "Lade Postfach …"; status.textContent = "Lade Postfach …";
try { try {
const res = await fetch(`${API_BASE_URL}/postfach`, { const res = await fetch(`${API_BASE_URL}/postfach`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code }), body: JSON.stringify({ code: session.code }),
}); });
const json = await res.json(); const data = await res.json();
if (!res.ok || !json.ok) { if (!res.ok || !data.ok) {
status.textContent = "Falscher Code oder Postfach nicht erreichbar."; status.textContent = "Falscher Code oder Postfach nicht erreichbar.";
status.className = "form-status show error"; status.className = "form-status show error";
inboxSection.style.display = "none"; return false;
return;
} }
status.className = "form-status"; session.isOwner = data.isOwner;
session.roleLabel = data.roleLabel;
gateContainer.style.display = "none";
sessionBar.style.display = "block";
inboxSection.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 { } catch {
status.textContent = "Postfach gerade nicht erreichbar. Bitte später erneut versuchen."; status.textContent = "Postfach gerade nicht erreichbar. Bitte später erneut versuchen.";
status.className = "form-status show error"; 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 = `<p class="muted">Konnte Codes nicht laden.</p>`;
return;
}
if (!data.roles.length) {
rolesList.innerHTML = `<p class="muted">Noch keine Helfer-Codes vergeben.</p>`;
return;
}
rolesList.innerHTML = data.roles
.map(
(r) => `
<div class="card">
<span class="tag">${r.role}</span>
<h3>${r.label}</h3>
<p class="small muted">Erstellt: ${new Date(r.createdAt).toLocaleDateString("de-DE")}</p>
<button class="btn btn-outline revoke-btn" data-key="${r.keyId}" type="button">Code entziehen</button>
</div>`
)
.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 = `<p class="muted">Konnte Codes nicht laden.</p>`;
}
}
form.addEventListener("submit", async (e) => {
e.preventDefault(); e.preventDefault();
if (!API_BASE_URL) { if (!API_BASE_URL) {
status.textContent = "Postfach ist noch nicht eingerichtet (siehe Hinweis unten)."; status.textContent = "Postfach ist noch nicht eingerichtet (siehe Hinweis unten).";
status.className = "form-status show error"; status.className = "form-status show error";
return; return;
} }
lastCode = document.getElementById("code-input").value; session.code = document.getElementById("code-input").value;
loadInbox(lastCode); 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", () => { 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 = `
<strong>Neuer Code für ${data.label} (${data.role}):</strong><br>
<span style="font-size:1.3rem;font-family:monospace;letter-spacing:.05em;">${data.code}</span><br>
<span class="small">Jetzt sichern/weitergeben — dieser Code wird nirgends mehr angezeigt!</span>`;
roleCreateStatus.textContent = "";
roleCreateStatus.className = "form-status";
roleCreateForm.reset();
loadRoles();
} catch {
roleCreateStatus.textContent = "Postfach gerade nicht erreichbar.";
roleCreateStatus.className = "form-status show error";
}
}); });
}); });
</script> </script>
+1 -1
View File
@@ -7,7 +7,7 @@
<url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/streamplan.html</loc><lastmod>2026-07-30</lastmod></url> <url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/streamplan.html</loc><lastmod>2026-07-30</lastmod></url>
<url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/community.html</loc><lastmod>2026-07-30</lastmod></url> <url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/community.html</loc><lastmod>2026-07-30</lastmod></url>
<url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/hasidog.html</loc><lastmod>2026-07-30</lastmod></url> <url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/hasidog.html</loc><lastmod>2026-07-30</lastmod></url>
<url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/kasper.html</loc><lastmod>2026-07-30</lastmod></url> <url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/casper.html</loc><lastmod>2026-07-30</lastmod></url>
<url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/aktionen.html</loc><lastmod>2026-07-30</lastmod></url> <url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/aktionen.html</loc><lastmod>2026-07-30</lastmod></url>
<url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/zeitreise.html</loc><lastmod>2026-07-30</lastmod></url> <url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/zeitreise.html</loc><lastmod>2026-07-30</lastmod></url>
<url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/medien.html</loc><lastmod>2026-07-30</lastmod></url> <url><loc>https://REPLACE-WITH-YOUR-DOMAIN.tld/medien.html</loc><lastmod>2026-07-30</lastmod></url>
+13 -8
View File
@@ -32,22 +32,27 @@
<h1>Streamplan &amp; Livebereich</h1> <h1>Streamplan &amp; Livebereich</h1>
<p class="lead worlds">Wann ist Dogfather live?</p> <p class="lead worlds">Wann ist Dogfather live?</p>
<div class="btn-row" style="justify-content:center;"> <div class="btn-row" style="justify-content:center;">
<a class="btn btn-primary" href="https://www.tiktok.com/@dogfather0804" target="_blank" rel="noopener">Zum TikTok-Livestream</a> <a class="btn btn-primary" href="https://www.tiktok.com/@dogfather0804" target="_blank" rel="noopener">Zur Dogi TikTok-Seite</a>
</div> </div>
</div> </div>
</section> </section>
<section class="section-tight"> <section class="section-tight">
<div class="container"> <div class="container">
<h2 class="text-center">Wochenplan</h2> <div class="grid grid-2" style="align-items:center;">
<div class="grid grid-4"> <div class="card">
<div class="card text-center"><h3>Montag</h3><p class="small muted">TODO: Uhrzeit/Format</p></div> <span class="tag">Fixer Termin</span>
<div class="card text-center"><h3>Mittwoch</h3><p class="small muted">TODO: Uhrzeit/Format</p></div> <h3>🌙 Jeden Tag ab 20 Uhr</h3>
<div class="card text-center"><h3>Freitag</h3><p class="small muted">TODO: Uhrzeit/Format</p></div> <p>Dogfather geht <strong>jeden Tag</strong> live — verlässlich ab <strong>20:00 Uhr</strong>. Das ist der feste Abend-Stream, auf den man sich einstellen kann.</p>
<div class="card text-center"><h3>Sonntag</h3><p class="small muted">TODO: Uhrzeit/Format</p></div> </div>
<div class="card">
<span class="tag">Bonus</span>
<h3>☀️ Extra-Streams tagsüber</h3>
<p>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.</p>
</div>
</div> </div>
<div class="todo-note"> <div class="todo-note">
<strong>TODO:</strong> Echte Streamtage/-zeiten und Formate mit Dogi klären (Masterplan Block D) und hier eintragen. <strong>TODO:</strong> Streamformate, wiederkehrende Segmente und Running Gags (Masterplan Block D) hier noch ergänzen, sobald geklärt.
</div> </div>
</div> </div>
</section> </section>