Files
dogfather-universe/gate.html
T
DogFatherGitandClaude Opus 5 51c3d4402b Audit des Creator Workspace, App-Symbole fuer alle Apps der Domain
AUDIT (Auftrag: vollstaendiger Durchgang, Fehler direkt beheben)

Ausgangslage waren 40 Pruefungen mit 1616 Einzelpunkten, alle gruen.
Acht neue Pruefungen kamen dazu; sie haben gefunden, was die alten nicht
sehen konnten.

Der schwerste Fund: Die Zugangsschranke verglich req.path EXAKT gegen
eine Liste. Express raeumt Punkt-Segmente selbst weg, mehrfache
Schraegstriche aber nicht. Damit kam //workspace/start.html OHNE
Anmeldung mit HTTP 200, und ein Creator bekam ueber
/workspace//personen.html die Verwaltungsseite. Die DATEN waren nie
betroffen (nachgemessen: 404 bzw. 401). Behoben durch Normalisierung
UND eine Umkehr der Logik -- jetzt ist jede .html geschuetzt ausser der
Anmeldeseite, statt nur die in der Liste. Eine vergessene neue Seite
steht damit nicht mehr versehentlich offen.

Weiter behoben:
  * Kaputter/abgebrochener Rumpf ergab 500 in HTML statt 400 in JSON --
    die Oberflaeche ruft ueberall a.json() und lief in einen zweiten
    Fehler; der Knopf hing ohne Meldung.
  * POST /zustand/sichern war der einzige von 60 schreibenden Wegen
    ohne Herkunftspruefung.
  * workspace-sicherung.js gab interne Pfade in Fehlermeldungen nach
    aussen; alle 23 anderen Module antworten neutral.
  * admin_notiz war als einziges von 14 Feldern ohne <label>.
  * Der aktive Filter hatte keinen sichtbaren Fokus (CSS-Spezifitaet
    0,3,0 schlug 0,2,0) -- genau der Knopf, auf dem man steht.
  * h1 -> h3 ohne Zwischenstufe auf zwei Seiten.
  * HSTS ging auch ueber http mit (RFC 6797, 7.2 verbietet das).
  * upgrade-insecure-requests galt auch auf 127.0.0.1 -- dadurch war
    WebKit/Safari ueberhaupt nicht pruefbar, also der Browser, den
    jedes iPhone benutzt.
  * pruef-grosscheck las readdirSync(".") und pruefte aus server/
    gestartet NULL oeffentliche Seiten -- meldete aber "ok".

Neue Pruefungen: struktur, schranke, haerte, alle-wege,
barrierefrei-workspace, breiten, tempo-workspace, browser.
Jede mit Gegenprobe und mit der geprueften Anzahl in der Bedingung.
Vier davon sind beim Bauen durch die eigene Gegenprobe aufgeflogen und
haetten sonst dauerhaft gruen gemeldet, ohne etwas zu messen.

APP-SYMBOLE (Wunsch: alle Apps der Domain, jede anders, ausser
safeaddress)

Zehn Apps, zehn Stile, zehn in OKLCH gerechnete Farben. Zusammen haelt
sie dasselbe Logo, dieselbe Eckenrundung und eine gemeinsame gedeckte
Farbreihe. Beim Bauen wird gemessen, ob sich das Logo vom Grund abhebt
(19 bis 58 Helligkeitsstufen).

Dabei aufgefallen: Das Kundenportal hatte kein eigenes Manifest und
trug Namen und Symbol der Webdesign-Seite. Der Workspace hatte gar
keins und war als App nicht installierbar. Beide haben jetzt eins.

Geaendert wurde AUSSCHLIESSLICH das Symbol. Ein Zwischenstand hatte
auch die Themenfarben gesetzt; das war mehr als bestellt und wurde
zurueckgenommen.

Werkzeuge: tools/logo-freistellen.mjs, tools/app-symbole.mjs,
tools/app-symbole-einbinden.mjs -- alles im Browser gerechnet, kein
Bildprogramm, keine neue Abhaengigkeit.

Gitea und Nextcloud sind bereits live und nachgeprueft.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-05 11:42:17 +02:00

561 lines
31 KiB
HTML

<!DOCTYPE html>
<html lang="de" data-theme="dogfather">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Zugang — DOGFATHER UNIVERSE</title>
<meta name="robots" content="noindex, nofollow" />
<link rel="icon" type="image/png" href="/assets/img/app-symbole/universe-32.png" />
<!-- BUGFIX (19.08.2026, Nutzer-Report vom Handy: "hab es installiert und klappt nicht"):
Diese Seite ist der ERSTE Bildschirm, den man ueberhaupt zu sehen bekommt (ohne gueltige
Sitzung leitet gate.js JEDEN Aufruf hierher um) -- also auch der Bildschirm, von dem aus
man die App installiert. Sie hatte aber weder Manifest-Verweis noch Service-Worker-
Registrierung, und beides verlangt Chrome, damit ueberhaupt eine echte, installierbare
App entsteht. Ergebnis: Wer vor dem ersten Login installiert hat, bekam bestenfalls eine
nackte Verknuepfung ohne Icon/Standalone-Modus. Exakt derselbe Fehler wie am 07.08.2026
in VanVans Shop (dort gefixt) -- hier war er nur nie aufgefallen, weil die Seite hinter
der Schranke (index.html) das alles laengst hatte. -->
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#0b0d10" />
<!-- iOS ignoriert das Manifest fuer das Startbildschirm-Symbol komplett und nimmt NUR
apple-touch-icon. Fehlt es, zeigt iPhone/iPad einen unscharfen Screenshot der Seite
statt des Logos. Bewusst eine eigene Datei OHNE Transparenz: iOS ersetzt Alpha durch
Schwarz, und icon-512.png hat transparente Ecken (nachgemessen) -> haette schwarze
Ecken gegeben. -->
<link rel="apple-touch-icon" href="/assets/img/app-symbole/universe-180.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="DogFather" />
<link rel="stylesheet" href="assets/css/main.css?v=20260828e" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=20260828e" />
<style>
/* =====================================================================
Zugangsseite — bewusst kein braves Standard-Login-Formular, sondern
die volle Bühne für das Logo (Nutzer-Wunsch 03.08.2026: "übelst geil",
"überrasch mich", Logo verwenden). Reines CSS, keine zusätzlichen
Bild-Assets nötig — nur das vorhandene Logo + prozedural erzeugte
Sterne/Funken (siehe Skript unten, gleiche Machart wie der
Nebel-Effekt in main.js).
Lektion aus diesem Projekt beachtet: rotierende Ringe verzerren sich
auf nicht-runden Formen (siehe TikTok-Button-Lektion) — hier deshalb
bewusst KEIN rotierender Ring um das (unregelmäßig geformte) Logo,
stattdessen nur eine ruhig pulsierende Glut dahinter. overflow:hidden
am Body sorgt dafür, dass nichts über den Rand hinausblutet.
===================================================================== */
html, body { height: 100%; }
body {
/* align-items:center zuerst als Fallback fuer sehr alte Browser, die "safe" nicht
kennen (eine ungueltige Deklaration wird ignoriert, die vorige bleibt bestehen) --
align-items:safe center sorgt dafuer, dass zentrierter Inhalt beim Scrollen nie am
Anfang "abgeschnitten" wirkt. */
display: flex; align-items: center; align-items: safe center; justify-content: center;
min-height: 100vh; padding: 2rem 1.2rem; position: relative;
/* BUGFIX (07.08.2026, Nutzer-Report vom Handy: "Code eintippen geht, aber dann ist
der 'Weiter'-Knopf nirgendwo"): overflow:hidden auf dem BODY verhinderte jegliches
Scrollen -- sobald die virtuelle Tastatur einen grossen Teil des Bildschirms
einnimmt, ragte der Knopf unterhalb der sichtbaren Flaeche heraus und war komplett
unerreichbar, ohne jede Moeglichkeit hinzuscrollen (mit echter Tastatur-Simulation
nachgestellt und bestaetigt). overflow-y:auto erlaubt Scrollen NUR wenn wirklich
noetig -- im Normalfall ohne Tastatur aendert sich optisch nichts. overflow-x
bleibt hidden, das war der eigentliche urspruengliche Zweck (verhindert einen
ungewollten horizontalen Scrollbalken durch die Aurora-Nebel-Kreise, die
absichtlich ueber den Bildschirmrand hinausragen). */
overflow-x: hidden; overflow-y: auto; background: radial-gradient(ellipse at 50% 30%, #12161d 0%, #06070a 62%, #030304 100%);
}
/* ---------- Aurora-Nebel, langsam driftend (3 Farbtöne der Marke) ---------- */
.gate-aurora { position: fixed; inset: -10%; z-index: 0; pointer-events: none; filter: blur(2px); }
.gate-aurora span { position: absolute; border-radius: 50%; opacity: .5; filter: blur(70px); mix-blend-mode: screen; }
.gate-aurora .a1 { width: 55vw; height: 55vw; left: -10%; top: -8%; background: radial-gradient(circle, rgba(143,217,234,.55), transparent 70%); animation: gate-drift-1 26s ease-in-out infinite; }
.gate-aurora .a2 { width: 50vw; height: 50vw; right: -12%; top: 8%; background: radial-gradient(circle, rgba(139,92,246,.5), transparent 70%); animation: gate-drift-2 32s ease-in-out infinite; }
.gate-aurora .a3 { width: 46vw; height: 46vw; left: 20%; bottom: -18%; background: radial-gradient(circle, rgba(212,175,55,.4), transparent 70%); animation: gate-drift-3 38s ease-in-out infinite; }
@keyframes gate-drift-1 { 0%,100% { transform: translate(0,0) scale(1); } 50% { transform: translate(6vw,4vh) scale(1.12); } }
@keyframes gate-drift-2 { 0%,100% { transform: translate(0,0) scale(1); } 50% { transform: translate(-5vw,5vh) scale(1.08); } }
@keyframes gate-drift-3 { 0%,100% { transform: translate(0,0) scale(1); } 50% { transform: translate(4vw,-4vh) scale(1.1); } }
@media (prefers-reduced-motion: reduce) { .gate-aurora span { animation: none; } }
/* ---------- Sternenstaub/Funken (per JS positioniert, siehe unten) ---------- */
.gate-funken { position: fixed; inset: 0; z-index: 1; pointer-events: none; }
.gate-funken span {
position: absolute; width: 3px; height: 3px; border-radius: 50%;
background: #ffe9a8; box-shadow: 0 0 6px 1px rgba(255,233,168,.9);
animation: gate-funke-twinkle 3.5s ease-in-out infinite;
}
@keyframes gate-funke-twinkle { 0%,100% { opacity: .15; transform: scale(.7); } 50% { opacity: 1; transform: scale(1.3); } }
@media (prefers-reduced-motion: reduce) { .gate-funken span { animation: none; opacity: .5; } }
/* ---------- Bühne: Logo + Karte übereinander ----------
Nutzer-Feedback 03.08.2026: das Logo sollte nicht wie ein Sticker in
einer weißen Kreisscheibe auf dem Hintergrund "kleben", sondern direkt
IM Hintergrund stecken. Grund für den weißen Kreis vorher: die
Original-PNG hatte selbst einen weißen Hintergrund eingebrannt (kein
Alphakanal). Freigestellt (Rand-Flood-Fill in Python/Pillow, nur die
mit dem Bildrand verbundene Fläche wurde transparent gemacht — innen-
liegende helle Stellen wie Augen-Glanzlicht blieben unangetastet) nach
logo-dogfather-transparent.png. Dadurch kein Kreis-Container/Ring mehr
nötig — nur noch eine weiche Glut dahinter, die durch die jetzt echten
Konturen des Hunde-Kopfs hindurch scheint. */
.gate-stage { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; width: 100%; max-width: 440px; }
.gate-logo-wrap {
position: relative; width: 260px; margin-bottom: -34px; z-index: 3;
animation: gate-logo-float 6s ease-in-out infinite;
}
@keyframes gate-logo-float { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
@media (prefers-reduced-motion: reduce) { .gate-logo-wrap { animation: none; } }
/* Pulsierende Glut hinter der freigestellten Form (rund, aber sitzt HINTER
dem Logo statt es einzurahmen — scheint an den Konturen vorbei durch). */
.gate-logo-glow {
position: absolute; inset: -22%; border-radius: 50%; z-index: -1;
background: radial-gradient(circle, rgba(255,233,168,.65) 0%, rgba(143,217,234,.35) 40%, rgba(139,92,246,.2) 62%, transparent 76%);
filter: blur(26px); animation: gate-glow-puls 3.2s ease-in-out infinite;
}
@keyframes gate-glow-puls { 0%,100% { opacity: .6; transform: scale(.92); } 50% { opacity: 1; transform: scale(1.05); } }
.gate-logo-img {
position: relative; display: block; width: 100%; height: auto;
filter: drop-shadow(0 0 18px rgba(255,233,168,.5)) drop-shadow(0 22px 34px rgba(0,0,0,.65));
}
/* ---------- Die eigentliche Karte ---------- */
.gate-box {
position: relative; width: 100%; text-align: center;
padding: 4.2rem 2rem 2.4rem;
background: linear-gradient(180deg, rgba(20,23,29,.92), rgba(10,12,15,.96));
backdrop-filter: blur(14px);
border: 1.5px solid rgba(212,175,55,.4);
box-shadow: 0 0 0 1px rgba(212,175,55,.06) inset, 0 40px 80px -30px rgba(0,0,0,.75);
overflow: hidden;
}
.gate-box::before {
content: ""; position: absolute; top: 0; left: 8%; right: 8%; height: 1px;
background: linear-gradient(90deg, transparent, rgba(255,214,102,.95), transparent);
animation: gate-goldglanz 4.5s ease-in-out infinite;
}
@keyframes gate-goldglanz { 0%,100% { opacity: .3; } 50% { opacity: 1; } }
.gate-box .eyebrow { color: #ffe9a8; border-color: rgba(255,214,102,.5); }
.gate-box h1 {
font-size: clamp(1.6rem, 4vw, 2.1rem); margin: .5rem 0 .7rem; letter-spacing: -.01em;
background: linear-gradient(135deg, #fff, #ffe9a8 55%, #d4af37);
-webkit-background-clip: text; background-clip: text; color: transparent;
}
.gate-box p { font-size: 1rem; }
.gate-box input {
width: 100%; box-sizing: border-box; padding: 1rem 1.1rem; border-radius: 12px; text-align: center;
letter-spacing: .1em; font-size: 1.1rem; font-weight: 600; margin: 1.4rem 0 1rem;
background: rgba(255,255,255,.04); border: 1.5px solid var(--border); color: var(--text);
transition: border-color .15s ease, box-shadow .15s ease, background .15s ease;
}
.gate-box input::placeholder { letter-spacing: normal; font-weight: 400; color: rgba(154,164,178,.55); }
.gate-box input:focus {
outline: none; border-color: rgba(212,175,55,.7); background: rgba(255,255,255,.06);
box-shadow: 0 0 0 4px rgba(212,175,55,.14);
}
/* "Weiter"-Knopf: der schon fertig gebaute, aber bislang ungenutzte
.btn-holo-Stil (main.css) — Trading-Card-Holo-Folie in genau den drei
Farben, die auch schon den Hintergrund dieser Seite tragen (Babyblau/
Lila/Gold, siehe .gate-aurora), daher hier bewusst statt des
generischen .btn-primary. Volle Breite + zentrierter Text sind eine
seiteneigene Ergänzung (der Stil selbst ist an anderer Stelle als
Inline-Button mit automatischer Breite gedacht). */
.gate-box .btn-holo { width: 100%; justify-content: center; margin-top: .2rem; }
.gate-fehler { color: #ff9a9a; font-size: .9rem; margin-top: .9rem; display: none; }
.gate-fehler.zeige { display: block; }
/* ---------- Zwei getrennte Zugangskacheln (Dogi/VanVan) statt einer
gemeinsamen Box — Nutzer-Wunsch 07.08.2026 ("Eingang von VanVan wie bei
ihrer normalen Seite mit den 2 Kacheln"). Gleiches Muster wie schon in
verwaltung.html (.vw-gate-card) etabliert, hier nur farblich an die
bereits vorhandene Gold/Babyblau-Optik dieser Seite angepasst. Nur die
Kacheln sind neu — Hintergrund/Logo/Bühne bleiben unverändert. ---------- */
.gate-cards {
position: relative; z-index: 2; display: flex; flex-wrap: wrap; gap: 1.1rem;
justify-content: center; width: 100%; margin-top: 1.3rem;
}
.gate-card {
position: relative; isolation: isolate; overflow: hidden;
flex: 1 1 220px; max-width: 280px; text-align: center;
padding: 1.6rem 1.3rem 1.4rem; border-radius: 18px; border: 1.5px solid transparent;
background:
linear-gradient(180deg, rgba(20,23,29,.92), rgba(10,12,15,.96)) padding-box,
linear-gradient(110deg, #8fd9ea, #38bdf8, #d4af37, #8fd9ea) border-box;
background-size: 100% 100%, 320% 100%;
box-shadow: 0 30px 70px -25px rgba(0,0,0,.75), 0 0 50px -20px rgba(143,217,234,.3);
}
/* VanVans Kachel: eigene, femininere Farbwelt (Rosé/Lavendel/Gold statt
Babyblau) — gleiches Prinzip wie in verwaltung.html, keine bloße
Umfärbung von Dogis Kachel. */
.gate-card--vanvan {
background:
linear-gradient(180deg, rgba(24,15,20,.92), rgba(15,9,13,.96)) padding-box,
linear-gradient(110deg, #ff8fc6, #c084fc, #d4af37, #ff8fc6) border-box;
box-shadow: 0 30px 70px -25px rgba(0,0,0,.75), 0 0 50px -20px rgba(255,143,198,.35);
}
.gate-card-icon {
display: block; width: 60px; height: 60px; margin: 0 auto .6rem; border-radius: 50%;
background-size: cover; background-position: center;
border: 2px solid rgba(143,217,234,.6);
box-shadow: 0 0 18px rgba(143,217,234,.4), inset 0 0 10px rgba(255,255,255,.08);
}
.gate-card-icon--dogi { background-image: url('assets/img/vw-gate-dogi.jpg'); }
.gate-card-icon--vanvan { background-image: url('assets/img/vw-gate-vanvan.jpg'); }
.gate-card--vanvan .gate-card-icon { border-color: rgba(255,143,198,.65); }
.gate-card-name { font-size: 1.05rem; font-weight: 800; margin: 0 0 .8rem; color: var(--text); }
.gate-card--vanvan .gate-card-name { color: #ffd1e8; }
.gate-card input {
width: 100%; box-sizing: border-box; padding: .8rem .9rem; border-radius: 12px; text-align: center;
letter-spacing: .1em; font-size: 1rem; font-weight: 600; margin-bottom: .9rem;
background: rgba(255,255,255,.04); border: 1.5px solid var(--border); color: var(--text);
transition: border-color .15s ease, box-shadow .15s ease, background .15s ease;
}
.gate-card input::placeholder { letter-spacing: normal; font-weight: 400; color: rgba(154,164,178,.55); }
.gate-card input:focus {
outline: none; border-color: rgba(212,175,55,.7); background: rgba(255,255,255,.06);
box-shadow: 0 0 0 4px rgba(212,175,55,.14);
}
.gate-card--vanvan input:focus { border-color: rgba(255,143,198,.7); box-shadow: 0 0 0 4px rgba(255,143,198,.16); }
.gate-card .btn-holo.gate-card-btn { width: 100%; justify-content: center; padding: .65rem 1rem; font-size: .88rem; gap: .3rem; }
.gate-card-btn--vanvan {
background: linear-gradient(120deg, #ff8fc6 0%, #c084fc 30%, #ffd76a 55%, #ff8fc6 80%, #c084fc 100%);
background-size: 340% 340%;
}
/* Dienes Kachel (21.08.2026, Nutzer-Wunsch: "mach eine 3 Zugangskiste ...
ich will nämlich dass Diene schon jetzt auch die Seite sieht, damit wir
mit mehreren Augen die letzten kleinen Details verbessern"). Wieder eine
EIGENE Farbwelt (Smaragd/Mint/Gold) statt einer Umfärbung der beiden
anderen -- so ist auf einen Blick klar, welche Kachel wem gehört. */
.gate-card--diene {
background:
linear-gradient(180deg, rgba(13,24,20,.92), rgba(8,15,13,.96)) padding-box,
linear-gradient(110deg, #6ee7b7, #34d399, #d4af37, #6ee7b7) border-box;
box-shadow: 0 30px 70px -25px rgba(0,0,0,.75), 0 0 50px -20px rgba(52,211,153,.35);
}
.gate-card-icon--diene { background-image: url('assets/img/avatar-diene.jpg'); }
.gate-card--diene .gate-card-icon { border-color: rgba(110,231,183,.65); }
.gate-card--diene .gate-card-name { color: #c7f5e3; }
.gate-card--diene input:focus { border-color: rgba(52,211,153,.7); box-shadow: 0 0 0 4px rgba(52,211,153,.16); }
.gate-card-btn--diene {
background: linear-gradient(120deg, #6ee7b7 0%, #34d399 30%, #ffd76a 55%, #6ee7b7 80%, #34d399 100%);
background-size: 340% 340%;
}
.gate-card .gate-fehler { margin-top: .7rem; }
@media (max-width: 480px) {
.gate-cards { flex-direction: column; align-items: center; }
/* Bugfix 21.08.2026 (Nutzer-Report per Bild: "sieht man die buttons
nicht ganz wenn man auf dem handy schaut"). Ursache: .gate-card trägt
"flex: 1 1 220px" für die Desktop-Reihe (220px = gewünschte BREITE).
Sobald .gate-cards hier auf flex-direction:column umschaltet, gilt
diese Zahl aber für die HÖHE, nicht mehr die Breite -- die Karte
wurde dadurch zwangsweise auf genau 220px Höhe gequetscht. Normal
verhindert das ein unsichtbares Mindestmaß (min-height:auto), aber
.gate-card hat "overflow:hidden" gesetzt, was dieses Mindestmaß laut
Spezifikation auf 0 zurücksetzt -- der "Öffnen"-Knopf (der bei
normaler, am Inhalt orientierter Höhe locker hineingepasst hätte)
wurde dadurch unten abgeschnitten. flex:none macht die Karte wieder
so hoch wie ihr Inhalt braucht, width:100% (samt max-width darüber)
bestimmt weiterhin allein die Breite. */
.gate-card { max-width: 300px; width: 100%; flex: none; }
}
/* ---------- Countdown zum öffentlichen Start (21.08.2026) ----------
Nutzer-Wunsch: "ich will dass du auch ein countdown zu der seite
hinzufügst wo wir den zugangscode eingeben müssen. den um 21h heute
abend geht die seite live." Eigene, ruhige Optik statt einer reinen
Zahlenanzeige — passt zur restlichen Gold/Aurora-Bühne der Seite.
[hidden] per Default: erscheint erst, wenn server/gate.js per
/gate-launch-info wirklich einen Start-Zeitpunkt meldet (siehe Skript
unten) — ohne das bleibt die Seite unverändert wie vorher. */
.gate-countdown {
position: relative; z-index: 2; width: 100%; text-align: center;
margin-top: 1.3rem; padding: 1.1rem 1.4rem;
border-radius: 16px; border: 1.5px solid rgba(212,175,55,.35);
background: linear-gradient(180deg, rgba(20,23,29,.85), rgba(10,12,15,.9));
box-shadow: 0 20px 50px -25px rgba(0,0,0,.7);
}
.gate-countdown[hidden] { display: none; }
.gate-countdown-label {
margin: 0 0 .5rem; font-size: .78rem; letter-spacing: .1em; text-transform: uppercase;
color: var(--text-muted);
}
.gate-countdown-zeit {
margin: 0; font-family: var(--font-head); font-weight: 800; letter-spacing: .04em;
font-size: clamp(1.7rem, 6vw, 2.4rem); font-variant-numeric: tabular-nums;
background: linear-gradient(135deg, #fff, #ffe9a8 55%, #d4af37);
-webkit-background-clip: text; background-clip: text; color: transparent;
animation: gate-countdown-puls 2.4s ease-in-out infinite;
}
@keyframes gate-countdown-puls { 0%,100% { opacity: .92; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .gate-countdown-zeit { animation: none; } }
.gate-countdown-hinweis { margin: .55rem 0 0; font-size: .82rem; color: var(--text-muted); }
</style>
</head>
<body>
<div class="gate-aurora" aria-hidden="true"><span class="a1"></span><span class="a2"></span><span class="a3"></span></div>
<div class="gate-funken" id="gate-funken" aria-hidden="true"></div>
<div class="gate-stage">
<div class="gate-logo-wrap" aria-hidden="true">
<div class="gate-logo-glow"></div>
<img class="gate-logo-img" src="assets/img/logo-dogfather-transparent.png" alt="" width="472" height="476" />
</div>
<div class="card gate-box">
<!-- Nutzer-Wunsch 04.08.2026: Besucher ohne Zugangscode (z.B. über einen
gefundenen/geteilten Link) sollen HIER nicht mehr "Noch nicht
öffentlich / im Aufbau" lesen — stattdessen ein normal wirkender
Link auf die eigene Domain (die spätere, öffentliche Hauptseite). -->
<h1>DOGFATHER UNIVERSE</h1>
<p class="small" style="opacity:.85;">
<a href="https://dogfather-universe.com/" style="color:#8fd9ea; font-weight:700; text-decoration: underline; text-underline-offset: .2em;">dogfather-universe.com</a>
</p>
</div>
<!-- Countdown zum öffentlichen Start (21.08.2026) — bleibt leer/unsichtbar,
solange server/gate.js keinen SITE_PUBLIC_LAUNCH_AT meldet (siehe
/gate-launch-info im Skript unten). -->
<div class="gate-countdown" id="gate-countdown" hidden>
<p class="gate-countdown-label">🎉 Startet automatisch in</p>
<p class="gate-countdown-zeit" id="gate-countdown-zeit">--:--:--</p>
<p class="gate-countdown-hinweis">Kein Neuladen nötig — die Seite öffnet sich von selbst.</p>
</div>
<!-- Nutzer-Wunsch 07.08.2026: statt einem gemeinsamen Eingabefeld für
Dogi/VanVan jetzt 2 getrennte Kacheln, gleiches Muster wie schon in
verwaltung.html — beide Formulare rufen weiterhin denselben
/gate-auth-Endpunkt auf, der Zugangscode selbst entscheidet
serverseitig, wer sich anmeldet (siehe server/gate.js). -->
<div class="gate-cards">
<form id="gate-form-dogi" class="gate-card gate-card--dogi" novalidate>
<span class="gate-card-icon gate-card-icon--dogi" aria-hidden="true"></span>
<h2 class="gate-card-name">Dogi</h2>
<!-- autofocus hier bewusst ENTFERNT (21.08.2026, Rückmeldung von Diene:
wenn ich bei mir Zugangscode eingebe, springt er danach zu dir hoch,
mein Name ist dann nicht mehr sichtbar). Auf dem Handy tippt man
unten in die eigene Kachel, während die Seite noch lädt — sobald sie
fertig ist, zieht autofocus den Cursor zurück in DIESES Feld ganz
oben, das Handy scrollt mit und die eigene Kachel ist weg. Bei drei
gleichberechtigten Kacheln ist ein vorausgewähltes Feld ohnehin
falsch: für zwei von drei Personen öffnet sich die Tastatur an der
falschen Stelle. -->
<input type="text" id="gate-code-dogi" placeholder="Zugangscode" autocomplete="off" autocapitalize="off" spellcheck="false" />
<button type="submit" class="btn-holo gate-card-btn">
<span class="btn-holo-text">💙 Öffnen</span>
</button>
<p class="gate-fehler" id="gate-fehler-dogi"></p>
</form>
<form id="gate-form-vanvan" class="gate-card gate-card--vanvan" novalidate>
<span class="gate-card-icon gate-card-icon--vanvan" aria-hidden="true"></span>
<h2 class="gate-card-name">VanVan</h2>
<input type="text" id="gate-code-vanvan" placeholder="Zugangscode" autocomplete="off" autocapitalize="off" spellcheck="false" />
<button type="submit" class="btn-holo gate-card-btn gate-card-btn--vanvan">
<span class="btn-holo-text">💗 Öffnen</span>
</button>
<p class="gate-fehler" id="gate-fehler-vanvan"></p>
</form>
<form id="gate-form-diene" class="gate-card gate-card--diene" novalidate>
<span class="gate-card-icon gate-card-icon--diene" aria-hidden="true"></span>
<h2 class="gate-card-name">Diene</h2>
<input type="text" id="gate-code-diene" placeholder="Zugangscode" autocomplete="off" autocapitalize="off" spellcheck="false" />
<button type="submit" class="btn-holo gate-card-btn gate-card-btn--diene">
<span class="btn-holo-text">💚 Öffnen</span>
</button>
<p class="gate-fehler" id="gate-fehler-diene"></p>
</form>
</div>
</div>
<script>
(function () {
// Sternenstaub — gleiche Machart wie der Nebel-Effekt in main.js
// (renderFog): zufällig positionierte, unterschiedlich große/verzögerte
// Punkte statt einer festen Deko.
const feld = document.getElementById("gate-funken");
let html = "";
for (let i = 0; i < 44; i++) {
const size = 2 + Math.round(Math.random() * 3);
const top = Math.round(Math.random() * 100);
const left = Math.round(Math.random() * 100);
const delay = (Math.random() * 3.5).toFixed(1);
const dauer = (2.5 + Math.random() * 2.5).toFixed(1);
html += `<span style="width:${size}px;height:${size}px;top:${top}%;left:${left}%;animation-delay:${delay}s;animation-duration:${dauer}s;"></span>`;
}
feld.innerHTML = html;
const params = new URLSearchParams(location.search);
const next = params.get("next") || "/index.html";
// Beide Kacheln (Dogi/VanVan) nutzen dieselbe Logik und denselben
// /gate-auth-Endpunkt — nur das jeweils eigene Formular/Eingabefeld/
// Fehlerfeld unterscheidet sich.
/* BUGFIX (19.08.2026, Nutzer-Report vom Handy): Der Knopf gab beim Antippen KEINERLEI
Rueckmeldung und die Anfrage hatte kein Zeitlimit. Bleibt im Mobilfunknetz eine Antwort
aus, haengt fetch() unbegrenzt -- fuer die Person sieht es exakt so aus, als sei der Tap
gar nicht angekommen ("er nimmt meinen Code nicht an"), obwohl in Wahrheit nur die
Antwort fehlt. Zusaetzlich liess sich mehrfach hintereinander absenden, was bei der
5-Versuche-Sperre in server/gate.js besonders unguenstig ist: Doppel-Taps zaehlen als
mehrere Fehlversuche und koennen die eigene IP fuer 15 Minuten aussperren.
Derselbe Fix wie am 06.08.2026 in VanVans gate.html/verwaltung-code.html. */
const ZEITLIMIT_MS = 10000;
function bindeGateKachel(formId, inputId, fehlerId) {
const form = document.getElementById(formId);
const fehlerEl = document.getElementById(fehlerId);
const btn = form.querySelector("button[type=submit]");
const btnText = btn ? btn.querySelector(".btn-holo-text") : null;
const originalText = btnText ? btnText.textContent : "";
let laeuft = false;
function sperren(an) {
laeuft = an;
if (btn) btn.disabled = an;
if (btnText) btnText.textContent = an ? "Wird geprüft…" : originalText;
form.setAttribute("aria-busy", an ? "true" : "false");
}
form.addEventListener("submit", async function (e) {
e.preventDefault();
if (laeuft) return; // Doppel-Tap ignorieren
fehlerEl.classList.remove("zeige");
const code = document.getElementById(inputId).value.trim();
if (!code) {
fehlerEl.textContent = "Bitte zuerst den Zugangscode eingeben.";
fehlerEl.classList.add("zeige");
return;
}
sperren(true);
/* AbortController statt endlosem Warten. AbortSignal.timeout() waere kuerzer, kennen
aeltere Handy-Browser aber nicht -- deshalb bewusst der breiter unterstuetzte Weg. */
const abbruch = new AbortController();
const timer = setTimeout(() => abbruch.abort(), ZEITLIMIT_MS);
try {
const res = await fetch("/gate-auth", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code, next }),
signal: abbruch.signal,
});
const data = await res.json().catch(() => null);
if (!res.ok || !data || !data.ok) {
fehlerEl.textContent = (data && data.error) || "Falscher Zugangscode.";
fehlerEl.classList.add("zeige");
sperren(false);
return;
}
/* Erfolg: Knopf bewusst gesperrt lassen — die Seite wird gleich gewechselt, ein
erneuter Tap in der Zwischenzeit waere nur verwirrend. */
if (btnText) btnText.textContent = "Willkommen 💙";
location.href = data.next || "/index.html";
} catch (err) {
fehlerEl.textContent =
err && err.name === "AbortError"
? "Zeitüberschreitung — Verbindung zu langsam. Bitte nochmal versuchen."
: "Verbindung fehlgeschlagen. Bitte nochmal versuchen.";
fehlerEl.classList.add("zeige");
sperren(false);
} finally {
clearTimeout(timer);
}
});
}
bindeGateKachel("gate-form-dogi", "gate-code-dogi", "gate-fehler-dogi");
bindeGateKachel("gate-form-vanvan", "gate-code-vanvan", "gate-fehler-vanvan");
bindeGateKachel("gate-form-diene", "gate-code-diene", "gate-fehler-diene");
/* ---------- Countdown zum öffentlichen Start (21.08.2026) ----------
Nutzer-Wunsch: "um 21h heute abend geht die seite live. kannst du das
sogar so anpassen dass es automatisch läuft?" -- server/gate.js meldet
über /gate-launch-info den echten Start-Zeitpunkt (SITE_PUBLIC_LAUNCH_AT
in der .env) und lässt ab genau diesem Moment jeden ohne Code durch.
Diese Uhr hier ist rein die SICHTBARE Anzeige dafür + der automatische
Sprung zur Zielseite, sobald es soweit ist -- die eigentliche
Freischaltung passiert unabhängig davon serverseitig.
Läuft bewusst auf der SERVERZEIT, nicht der eigenen Uhr: viele Handys/
PCs gehen ein paar Minuten falsch, das würde sonst entweder zu früh
"00:00:00" zeigen (und dann an der noch verschlossenen Schranke
scheitern) oder unnötig lange nachzählen. offsetMs wird einmal beim
Laden bestimmt (Server-Zeit minus eigene Zeit) und auf die eigene,
lokal tickende Uhr addiert -- kein Dauer-Nachfragen nötig, nur beim
Erreichen von Null ein letzter Abgleich als Sicherheitsnetz. */
const countdownBox = document.getElementById("gate-countdown");
const countdownZeit = document.getElementById("gate-countdown-zeit");
function formatiereCountdown(ms) {
const gesamtSekunden = Math.max(0, Math.round(ms / 1000));
const std = Math.floor(gesamtSekunden / 3600);
const min = Math.floor((gesamtSekunden % 3600) / 60);
const sek = gesamtSekunden % 60;
const zweistellig = (n) => String(n).padStart(2, "0");
return `${zweistellig(std)}:${zweistellig(min)}:${zweistellig(sek)}`;
}
async function holeLaunchInfo() {
try {
const res = await fetch("/gate-launch-info", { cache: "no-store" });
const data = await res.json();
return data && data.ok ? data : null;
} catch {
return null;
}
}
if (countdownBox && countdownZeit) {
async function richteCountdownEin(info) {
const launchAtMs = new Date(info.launchAt).getTime();
const offsetMs = new Date(info.serverTime).getTime() - Date.now();
countdownBox.hidden = false;
const timerId = setInterval(async () => {
const serverJetzt = Date.now() + offsetMs;
const rest = launchAtMs - serverJetzt;
if (rest > 0) {
countdownZeit.textContent = formatiereCountdown(rest);
return;
}
countdownZeit.textContent = "00:00:00";
clearInterval(timerId);
// Letzter Abgleich mit dem Server statt blind der eigenen Uhr zu
// vertrauen -- erst wenn ER bestätigt "live", wird weitergeleitet.
// Sagt er "noch nicht" (z.B. Uhr wich ein paar Sekunden ab, oder
// der Zeitpunkt wurde inzwischen verschoben), richtet sich der
// Countdown mit den frischen Werten einfach neu ein, statt
// stumm bei "00:00:00" stehen zu bleiben.
const bestaetigung = await holeLaunchInfo();
if (bestaetigung && bestaetigung.isLive) location.href = next;
else if (bestaetigung && bestaetigung.launchAt) richteCountdownEin(bestaetigung);
}, 1000);
}
(async function starteCountdown() {
const info = await holeLaunchInfo();
if (!info || !info.launchAt) return; // kein Starttermin gesetzt -> Box bleibt versteckt
if (info.isLive) {
location.href = next; // schon live (z.B. Seite kurz nach dem Start neu geöffnet)
return;
}
richteCountdownEin(info);
})();
}
/* Service-Worker-Registrierung — auf allen anderen Seiten macht das main.js, diese Seite
ist aber bewusst eigenstaendig und laedt main.js nicht. Ohne registrierten Service
Worker bietet Chrome hier gar kein "App installieren" an (siehe Kommentar im Kopfbereich).
Bewusst identisch defensiv wie in main.js: nur ueber http(s), Fehler still schlucken. */
if ("serviceWorker" in navigator && /^https?:$/.test(location.protocol)) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("/sw.js").catch(() => {});
});
}
})();
</script>
</body>
</html>