Files
dogfather-universe/workspace/assets/js/gate.js
T
DogFatherGitandClaude Opus 5 8bcbd83061 Die Modi-Seite gehoert Dogi und den Modis -- Wand, Zeichen, Reiter
Filipe zum Bildschirmfoto der Zugangswand auf crew.: "alles soll auf
dogfather und die modis perfektionniert werden auf der neuen modi seite."

Dort stand bis eben die gewohnte Wand: Spicy-Media-Logo, "Creator
Workspace", der Satz ueber Manager, Scouts und Creator, fuenf
Rollenkacheln. Fuer einen Modi war davon nichts richtig.

DIE KACHELREIHE FAELLT WEG, und das ist eine Verbesserung: Eine Reihe mit
genau einem Eintrag ist keine Auswahl, sondern eine Huerde -- man muesste
erst daraufdruecken, bevor das Codefeld etwas annimmt. gate.js prueft
jetzt, OB es Kacheln gibt, statt sie vorauszusetzen.

DIE BUEHNE IST AUS DEM VORHANDENEN MOTIV GEBAUT, nicht neu erfunden, und
das ist kein Sparen: Die Anmeldeseite ist eine MECHANIK. Rechts steht im
Bild eine leere Tafel, und gate.css setzt die Karte auf Hundertstel genau
dort hinein. Ein frei erfundenes Bild haette diese vier Zahlen
mitgenommen. Also dieselbe Szene, ueber den Mischmodus "color" ins Blau
umgefaerbt (hue-rotate haette Rot nach Cyan UND Blau nach Gelb gedreht),
Dogi anstelle des Spicy-Medaillons, "TEAM DOGI" darunter.
Gemessen: rote Bildpunkte 18,1 % -> 0,0 %, Karte auf 0 px genau.

NACH DER ANMELDUNG geht es weiter: kopf.js zieht Reitertitel und Zeichen
aus `ich.marke` nach -- "Aufgaben · Team Dogi" statt "· Spicy & Dogi", mit
dem eigenen Symbol. An einer Stelle statt in 30 HTML-Dateien.

DREI FUNDE, DIE NICHT AUS DEM KOPF KAMEN

1. DER KNOPF WAERE SCHLECHTER LESBAR GEWORDEN. Mein erstes Blau endete
   bei #4aa4cf -- weisse Schrift darauf: 2,8 zu 1. Der rot-blaue Verlauf,
   den er ersetzt, haelt ueber seine GANZE Laenge 4,65; er war offenbar
   genau darauf gebaut. Jetzt 5,10 zu 1, am fertigen Bildschirmfoto
   gemessen statt aus einer einzelnen Farbe hergeleitet.

2. DIE NEUE WAND WAR AUCH AUF workspace. ABRUFBAR. Sie liegt als Datei im
   selben Ordner. Aufgefallen ist das, weil der Wortleck-Test sie ueberhaupt
   las -- die Frage WARUM war die Antwort. Ausserhalb von crew. antwortet
   sie jetzt mit 404; auf den Pruefadressen bleibt sie erreichbar, sonst
   koennte die Pruefung sie nicht mehr oeffnen und waere gruen ohne etwas
   zu messen.

3. ZWEI GLEICHE KACHELN AUF DER STARTSEITE, seit dem letzten Deploy live.
   Die Team-Lage-Kachel von heute Mittag hatte Name, Zeichen, Ton UND
   Gruppe einer schon vorhandenen -- fuehrte aber woandershin. Gefunden
   von "jede Kachel hat ihre eigene Farbe (23 Farben auf 24 Kacheln)".
   Jetzt "Eingang", Gruppe Taeglich, neuer Ton 24 (#8a20cf, Abstand 38,5
   im CIELAB-Raum; reines Blau haette 52 gehabt und waere auf dunklem
   Grund am schlechtesten zu fokussieren). Eine Fehlermeldung zeigte
   dadurch auf die falsche Seite -- ebenfalls behoben.
   Und die Pruefung, die es fand, zaehlte nur bereiche.js: Vom Server
   angehaengte Kacheln kannte sie nicht. Sie fragt jetzt beide Quellen.

Der Wortleck-Test meldete ausserdem sieben Fundstellen -- allesamt
Kommentare, die ich beim Bauen selbst geschrieben hatte.

GEMESSEN
pruef-crew-adresse     103   pruef-crew-wand-bild    43 (neu)
pruef-modi-checkliste   59   pruef-modi-verborgen    75
pruef-rollen           245   pruef-zwischenspeicher  21
pruef-modi-wortleck      4   pruef-start-ansicht     gruen

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 13:59:54 +02:00

193 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ===================================================================
Creator Workspace - Anmeldeseite
Kein Framework, keine externen Abhängigkeiten.
=================================================================== */
(() => {
'use strict';
const $ = (id) => document.getElementById(id);
const rollen = Array.from(document.querySelectorAll('.rolle'));
const formular = $('formular');
const code = $('code');
const auge = $('auge');
const fehler = $('fehler');
const hinweis = $('hinweis');
const knopf = $('knopf');
const fuer = $('fuer');
/* Die fuenfte Rolle stand hier nicht -- deshalb las man unter dem
Codefeld "PERSOENLICHER ZUGANGSCODE · undefined" (07.09.2026).
Eine zweite Namensliste im Browser neben der im Server
(ROLLEN_NAME in workspace.js) ist genau die Stelle, die beim
naechsten Mal wieder vergessen wird. Sie bleibt hier trotzdem: Die
Anmeldeseite laeuft, BEVOR es eine Sitzung gibt, kann also nichts
abfragen. Damit es auffaellt, faellt ein unbekannter Schluessel
jetzt auf den Schluessel selbst zurueck statt auf `undefined` --
"spicy" waere haesslich gewesen, aber sichtbar. */
const NAMEN = { spicy: 'Spicy Media', admin: 'DogFather', manager: 'Manager',
scout: 'Scout', creator: 'Creator' };
const rollenName = (r) => NAMEN[r] || r || '';
const SPEICHER = 'dfw.rolle';
let gewaehlt = null;
/* AUF DER ZWEITEN ADRESSE GIBT ES KEINE KACHELN (10.09.2026).
Dort fuehrt genau eine Sorte Zugang hinein, also ist nichts zu
waehlen -- jene Wand liefert die Reihe erst gar nicht mit.
Geprueft wird deshalb, OB es Kacheln gibt, statt sie
vorauszusetzen. Ohne diese Zeile bliebe `gewaehlt` dort ewig null,
und der Anmeldeknopf antwortete auf jeden Druck mit "Bitte zuerst
auswaehlen, womit du dich anmeldest" -- vor einer Seite, auf der es
nichts auszuwaehlen gibt. Die Anmeldung waere unbenutzbar gewesen,
und zwar ohne Fehlermeldung im Protokoll. */
const ohneKacheln = rollen.length === 0;
/* ---------- Rolle wählen ------------------------------------------ */
function waehle(rolle, fokus = false) {
gewaehlt = rolle;
rollen.forEach((b) => b.setAttribute('aria-checked', String(b.dataset.rolle === rolle)));
fuer.textContent = `· ${rollenName(rolle)}`;
fuer.hidden = false;
melde('');
try { localStorage.setItem(SPEICHER, rolle); } catch { /* Privater Modus */ }
if (fokus) code.focus();
}
rollen.forEach((b) => {
b.addEventListener('click', () => waehle(b.dataset.rolle, true));
// Pfeiltasten wie bei echten Radio-Feldern
b.addEventListener('keydown', (e) => {
const i = rollen.indexOf(b);
let ziel = null;
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') ziel = rollen[(i + 1) % rollen.length];
if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') ziel = rollen[(i - 1 + rollen.length) % rollen.length];
if (!ziel) return;
e.preventDefault();
ziel.focus();
waehle(ziel.dataset.rolle);
});
});
// Zuletzt genutzte Rolle vorauswählen - spart bei täglicher Nutzung einen Klick.
try {
const zuletzt = localStorage.getItem(SPEICHER);
if (zuletzt && NAMEN[zuletzt]) waehle(zuletzt);
} catch { /* egal */ }
/* ---------- Code anzeigen / verbergen ------------------------------ */
auge.addEventListener('click', () => {
const sichtbar = code.type === 'text';
code.type = sichtbar ? 'password' : 'text';
auge.setAttribute('aria-pressed', String(!sichtbar));
auge.setAttribute('aria-label', sichtbar ? 'Code anzeigen' : 'Code verbergen');
code.focus();
});
/* ---------- Feststelltaste ----------------------------------------- */
const pruefeFeststell = (e) => {
if (typeof e.getModifierState !== 'function') return;
hinweis.hidden = !e.getModifierState('CapsLock');
};
code.addEventListener('keyup', pruefeFeststell);
code.addEventListener('keydown', pruefeFeststell);
code.addEventListener('blur', () => { hinweis.hidden = true; });
/* ---------- Rückmeldung -------------------------------------------- */
function melde(text) { fehler.textContent = text; }
function laden(an) {
knopf.disabled = an;
knopf.querySelector('.knopf__text').textContent = an ? 'Prüfe …' : 'Anmelden';
}
/* ---------- Absenden ------------------------------------------------ */
formular.addEventListener('submit', async (e) => {
e.preventDefault();
const wert = code.value.trim();
if (!ohneKacheln && !gewaehlt) { melde('Bitte zuerst auswählen, womit du dich anmeldest.'); rollen[0].focus(); return; }
if (!wert) { melde('Bitte den Zugangscode eingeben.'); code.focus(); return; }
melde('');
laden(true);
try {
const antwort = await fetch('/workspace/api/anmelden', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
/* Ohne Kacheln geht ein leerer Rollenname mit. Der Server
braucht ihn dort nicht: Er prueft den verborgenen Zugang
VOR dem gewohnten Weg, und ein leerer Name faellt danach in
dieselbe Antwort wie ein erfundener. */
body: JSON.stringify({ rolle: gewaehlt || '', code: wert }),
credentials: 'same-origin',
});
if (antwort.ok) {
const ziel = (await antwort.json().catch(() => ({}))).weiter || '/workspace';
document.body.classList.add('oeffnet');
setTimeout(() => { location.assign(ziel); }, 260);
return;
}
// 404 heißt: Es gibt noch gar keine Gegenstelle. Das ist derzeit der
// Normalfall - die Seite steht schon, der Login-Server wird noch gebaut.
// Eine ehrliche Meldung ist hier besser als "Code stimmt nicht".
if (antwort.status === 404) {
melde('Der Zugang wird gerade eingerichtet – Anmelden ist noch nicht möglich.');
return;
}
// Bewusst keine Auskunft darüber, ob Rolle oder Code falsch war.
melde(antwort.status === 429
? 'Zu viele Versuche. Bitte kurz warten.'
: 'Zugangscode stimmt nicht.');
code.select();
} catch {
melde('Keine Verbindung zum Server. Bitte erneut versuchen.');
} finally {
laden(false);
}
});
/* ---------- Sehr dezente Parallaxe ---------------------------------- */
const ruhig = matchMedia('(prefers-reduced-motion: reduce)');
const bild = document.querySelector('.buehne__bild');
const grob = matchMedia('(pointer: coarse)');
if (bild && !ruhig.matches && !grob.matches) {
let ziel = { x: 0, y: 0 }, ist = { x: 0, y: 0 }, laeuft = false;
addEventListener('pointermove', (e) => {
ziel.x = (e.clientX / innerWidth - 0.5) * -10; // max. 10 px
ziel.y = (e.clientY / innerHeight - 0.5) * -6;
if (!laeuft) { laeuft = true; requestAnimationFrame(schritt); }
}, { passive: true });
function schritt() {
ist.x += (ziel.x - ist.x) * 0.06;
ist.y += (ziel.y - ist.y) * 0.06;
/* NICHT MEHR DIREKT AUF DAS BILD (06.09.2026), sondern als zwei
Groessen am <body>. Der Grund: Seit die Anmeldekarte in der
GEMALTEN Tafel des Motivs sitzt, muss sie jede Bewegung des
Bildes mitmachen -- sonst rutscht der Neonrahmen unter ihr
weg. Gemessen waren es bis zu 10 px, und das sieht man.
Ueber die Variablen bewegen sich Bild UND Anker mit
derselben Zahl; die Karte bleibt geklebt. */
document.body.style.setProperty('--parallax-x', ist.x.toFixed(2) + 'px');
document.body.style.setProperty('--parallax-y', ist.y.toFixed(2) + 'px');
if (Math.abs(ziel.x - ist.x) > 0.05 || Math.abs(ziel.y - ist.y) > 0.05) {
requestAnimationFrame(schritt);
} else { laeuft = false; }
}
}
})();