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]>
193 lines
7.6 KiB
JavaScript
193 lines
7.6 KiB
JavaScript
/* ===================================================================
|
||
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; }
|
||
}
|
||
}
|
||
})();
|