Files
DogFatherGitandClaude Opus 5 603319a145 Arbeitsschloss: zwei Sitzungen sehen sich jetzt
HEUTE ZWEIMAL NUR GUTGEGANGEN. Zwei Claude-Sitzungen arbeiteten
gleichzeitig in diesem Verzeichnis, ohne voneinander zu wissen.
Keine hat etwas falsch gemacht — sie konnten es nicht wissen.

  * Beide haben `git add -A` benutzt. Haette die eine
    unfestgeschriebene Arbeit der anderen im Baum gehabt, waere sie
    mitcommittet worden — unter fremdem Namen, in einer fremden
    Begruendung, und niemandem waere es aufgefallen. Nachgesehen:
    diesmal war nichts dabei.
  * Beide haben den Stempellauf gestartet. Der schreibt 45 Dateien
    um. Wer dort eine offen hatte, bekam sie unter den Haenden weg
    geaendert.

Dasselbe hat in RunOne am 03.09.2026 sieben Minuten Ausfall
gekostet. Dort gibt es seitdem `arbeitsschloss.sh`; diese Fassung
uebernimmt seine Lehren.

WAS ES IST UND WAS NICHT. Es ist kein Riegel — wer wirklich muss,
kommt vorbei. Es beantwortet die eine Frage, die heute niemand
beantworten konnte: „arbeitet hier gerade sonst jemand?"

Die teuren Fehler liegen bei einem Schloss alle in derselben
Richtung: Es blockiert zu viel und wird deshalb abgeschafft. Also:

  * Es blockiert NICHT, wenn das Schloss DEINES ist. RunOnes erste
    Fassung fragte „ist abgeschlossen" statt „haelt es jemand
    anders" — damit haette, wer ordentlich abschliesst, nie mehr
    ausliefern koennen. Dafuer gibt es `fremd`.
  * Es VERFAELLT nach zwei Stunden, und dass da jemand war, steht
    beim Uebernehmen dabei.
  * Es blockiert NICHT, wenn es selbst unlesbar ist — dritter
    Ausgang, kein Stillstand.
  * Notausgang: SCHLOSS_ZWANG=ja git commit …

DAS PROBLEM, AN DEM RUNONE HAENGT, IST HIER GELOEST. Dort faellt die
Kennung im Zweifel auf den Systembenutzer zurueck, und zwei
Claudian-Sitzungen laufen BEIDE als `claudian` — die Sicherung griff
ausgerechnet zwischen den zwei Faellen nicht, fuer die sie gebaut
wurde. Deshalb ist dort `export ARBEITER=…` Pflicht, und Pflicht
heisst: man vergisst es.

Hier steht `CLAUDE_CODE_SESSION_ID` in jeder Sitzung und ist je
Sitzung verschieden (nachgesehen, 36 Zeichen UUID). Zwei Sitzungen
auf demselben Windows-Benutzer unterscheiden sich damit von selbst,
ohne dass jemand etwas tun muss. Reihenfolge: ARBEITER, dann
Sitzungskennung, dann Benutzername MIT Warnung.

NIEMAND MUSS DARAN DENKEN:
  * die beiden Stempelwerkzeuge nehmen es selbst und geben es selbst
    frei — auch nach einem Absturz und bei Strg+C (wie `bauen.sh` im
    Shop; eines, an das man denken muss, wird vergessen und ab da
    umgangen)
  * `tools/git-haken/pre-commit` bricht jeden Commit ab, solange
    jemand ANDERS das Schloss haelt. Das ist die Stelle, die heute
    gefehlt hat: `git add -A` ist der Griff, den man ohne Nachdenken
    macht, und gegen einen Reflex hilft keine Regel auf Papier.
  * `core.hooksPath` statt `.git/hooks` — letzteres ist nicht
    versioniert und waere nach einem Klon genau dann leer, wenn es
    gebraucht wird.

GEPRUEFT, server/pruef-arbeitsschloss.mjs: 33 Pruefungen, 0 Fehler —
in einem WEGWERF-Verzeichnis mit eigenem git, damit kein echtes
Schloss angefasst wird. Darunter am echten git:

    ohne Schloss committen        -> geht      (0)
    mit dem EIGENEN Schloss       -> geht      (0)
    mit einem FREMDEN Schloss     -> bricht ab (1), nennt wer und warum
    und es stehen genau ZWEI Commits da, nicht drei
    SCHLOSS_ZWANG=ja              -> kommt vorbei
    ohne das Schloss-Werkzeug     -> laesst durch

Dazu: verfallenes Schloss laesst durch, mit laengerer Frist blockt
dasselbe Schloss wieder (Gegenprobe), unlesbarer Inhalt und
unlesbarer Zeitstempel blockieren nicht.

Portnummern nach der neuen Pruefdatei nachgemessen: Pruefbereich bis
5415, 462 Nummern, 0 Kollisionen. pruef-struktur 75/0,
pruef-fingermass 5/0, pruef-ports 10/0.

In DEPLOY.md steht es jetzt an erster Stelle — eine Sicherung, von
der nur der weiss, der sie gebaut hat, ist die erste, die umgangen
wird.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-01 20:04:28 +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?v=202610012001" />
<!-- 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="#065f76" />
<!-- 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?v=202610012001" />
<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=202610012001" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css?v=202610012001" />
<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?v=202610012001'); }
.gate-card-icon--vanvan { background-image: url('assets/img/vw-gate-vanvan.jpg?v=202610012001'); }
.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?v=202610012001'); }
.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?v=202610012001" 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>