Filipe: "Es darf vor allem keine Stellen geben, an denen ein Benutzer
etwas falsch machen kann, nur weil die Seite es nicht verstaendlich
genug erklaert."
Gemessen: 30 Stellen in 16 Dateien benutzten confirm() oder prompt().
Das Haus hatte die richtige Bauweise laengst -- einen <dialog>, in
aufgaben.html sogar ausfuehrlich begruendet -- aber sie stand IN EINER
SEITE. Wer anderswo etwas loeschen liess, hatte sie nicht.
confirm('Wirklich loeschen?') stellt die falsche Frage: Es fragt, ob
man sicher ist, und nennt nicht, WAS passiert, was BLEIBT und ob es
ZURUECK geht. Jetzt beantwortet jeder der 41 Dialoge alle drei.
Neu: workspace/assets/js/nachfrage.js -- window.frageNach() mit
Pflichtgrund, Zahlenfeld, einzeiliger Eingabe und Abtippsicherung.
Drei Ausgaenge: <dialog> / confirm()-Notnagel fuer Safari vor 15.4 /
Abbruch (Esc, Klick daneben, "Doch nicht" -- immer false).
DREIMAL DERSELBE FALLSTRICK, dreimal nachgemessen statt vermutet:
.dialog stand in aufgaben.css und leistung.css -> auf dateien.html
waere der Dialog ein weisser Systemkasten gewesen. 14 Regeln
klammergenau nach module.css verschoben (Klammern gezaehlt, nicht
per Muster geschnitten -- heute frueh hat ein nicht-gieriges
Muster schon einmal CSS zerrissen).
Das Formular trug .neu neu--blank -- und .neu gibt seine Abstaende
nur in aufgaben.css. Gemessen: padding 0px, und die Felder
verloren ihre height:44px. Jetzt steht alles unter
.nachfrage__form in module.css; der Dialog borgt nichts mehr.
Die erste Fassung der Pruefung zaehlte nachfrage.js SELBST als
Nutzer -- damit war jede Seite trivialerweise "Nutzer" und die
Pruefung gruen ohne Inhalt. Jetzt ausdruecklich ausgenommen.
ZWEI FUNDE NEBENBEI:
hilfeAufraeumen() wird im Betrieb NIE aufgerufen. Der Kommentar
behauptete "wird beim Start aufgerufen (siehe index.js)" -- das
war nie wahr; einziger Aufrufer ist die eigene Pruefung. Folge:
geschlossene vertrauliche Faelle bleiben unbegrenzt stehen. NICHT
eingeschaltet (das loescht echte Daten und ist Filipes
Entscheidung), sondern der Kommentar richtiggestellt.
Einen Hilfe-Fall zu schliessen ist endgueltig -- es gibt keine
Route, die ihn wieder oeffnet. Vorher stand darueber nur die
Frage nach einem Schlusswort. Jetzt sagt der Dialog es.
pruef-struktur hat meine eigene Pruefung von heute Nachmittag
erwischt: Sie bildete ihr Datum aus UTC. Beim Beheben erst
heuteLokal(datum) genommen -- die Funktion nimmt gar kein Argument
und haette still "heute" statt "+3 Tage" geliefert. Jetzt tagLokal(3),
nachgerechnet: Abstand 3 Tage.
Am Bildschirm angesehen (Rechner 1280, Handy 390): passt rein, Esc
ergibt false, Fokus liegt auf dem harmlosen Knopf, Knoepfe 44px auf
Touch. Der Platzhalter im Abtippfeld zeigte den erwarteten Namen --
das sah aus wie ein schon ausgefuelltes Feld, entfernt.
Neu: server/pruef-nachfrage.mjs -- 17/0, mit sechs Gegenproben und
beiden Richtungen (wer fragt, laedt die Datei; wer nie fragt, laedt
sie nicht -- sonst truege die Anmeldewand 4,8 KB fuer nichts).
pruef-meldungen 8/0, pruef-css-klassen gruen, pruef-struktur gruen,
pruef-leistung gruen.
246 lines
9.3 KiB
JavaScript
246 lines
9.3 KiB
JavaScript
/* =====================================================================
|
||
„Wer sieht was" — ansehen und umstellen (11.09.2026)
|
||
|
||
Filipe: *„so dass ich die da auch manuell wechseln und speichern
|
||
kann."*
|
||
|
||
---------------------------------------------------------------------
|
||
GESPEICHERT WIRD SOFORT, MIT JEDEM KLICK
|
||
|
||
Kein „Speichern"-Knopf am Ende. Bei einer Tafel mit zweihundert
|
||
Feldern ist der die Stelle, an der man eine halbe Änderung verliert —
|
||
und eine halbe Änderung an Rechten ist die gefährlichste Lage von
|
||
allen. Zurückgenommen wird mit demselben Klick.
|
||
|
||
---------------------------------------------------------------------
|
||
WAS HIER NICHT ENTSCHIEDEN WIRD
|
||
|
||
Ob jemand etwas darf. Der Browser fragt und zeichnet; jede Absage
|
||
kommt vom Server. Die rechte Hand sieht dieselbe Tafel, bekommt aber
|
||
keine Knöpfe — nicht weil dieses Skript sie versteckt, sondern weil
|
||
der Server `darf_aendern: false` schickt. Ein Knopf weniger ist
|
||
Bequemlichkeit, kein Riegel.
|
||
|
||
---------------------------------------------------------------------
|
||
UND EIN UNTERSCHIED, DEN MAN SEHEN MUSS
|
||
|
||
Der Code in rechte.js ist der GRUNDSTAND. Was davon abweicht, ist von
|
||
Hand gesetzt — und das steht an der Zelle. Ohne diese Markierung wüsste
|
||
nach vier Wochen niemand mehr, was gedacht war und was jemand
|
||
daraus gemacht hat.
|
||
===================================================================== */
|
||
(() => {
|
||
const $ = (id) => document.getElementById(id);
|
||
const el = (t, k, text) => {
|
||
const n = document.createElement(t);
|
||
if (k) n.className = k;
|
||
if (text !== undefined) n.textContent = text;
|
||
return n;
|
||
};
|
||
const melde = (t, art) => {
|
||
const f = $('fehler');
|
||
if (!f) return;
|
||
f.textContent = t;
|
||
f.hidden = !t;
|
||
f.dataset.art = art || 'fehler';
|
||
};
|
||
|
||
let daten = null;
|
||
|
||
async function hole(pfad, optionen = {}) {
|
||
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
|
||
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
|
||
if (a.status === 404) { location.assign('/workspace/start.html'); throw new Error('unbekannt'); }
|
||
return a;
|
||
}
|
||
|
||
/* ---- Ein Feld ------------------------------------------------------- */
|
||
function feld(seite, rolle) {
|
||
const darf = seite.rollen.includes(rolle.schluessel);
|
||
const anders = darf !== seite.grund.includes(rolle.schluessel);
|
||
const fest = seite.fest.includes(rolle.schluessel);
|
||
|
||
const td = document.createElement('td');
|
||
if (anders) td.dataset.anders = 'ja';
|
||
|
||
/* OHNE RECHT ZUM UMSTELLEN BLEIBT ES EIN ZEICHEN, KEIN KNOPF.
|
||
Ein Knopf, der nichts tut, ist schlimmer als keiner — man drückt
|
||
ihn immer wieder. */
|
||
if (!daten.darf_aendern) {
|
||
td.append(zeichen(darf));
|
||
if (anders) td.title = 'Von Hand gesetzt – weicht vom Grundstand ab.';
|
||
return td;
|
||
}
|
||
|
||
const b = el('button', 'rechte-feld');
|
||
b.type = 'button';
|
||
b.setAttribute('aria-pressed', String(darf));
|
||
b.setAttribute('aria-label',
|
||
`${rolle.name} · ${seite.datei}: ${darf ? 'darf' : 'darf nicht'}`);
|
||
if (fest) b.dataset.fest = 'ja';
|
||
b.title = fest
|
||
? 'Diese Seite kannst du dir nicht selbst wegnehmen – sonst kämst du '
|
||
+ 'nicht mehr an die Rechte, die Zugänge oder die Startseite.'
|
||
: (anders ? 'Von Hand gesetzt. Klicken stellt es um.' : 'Klicken stellt es um.');
|
||
b.append(zeichen(darf));
|
||
b.addEventListener('click', () => umstellen(seite, rolle, !darf, b));
|
||
td.append(b);
|
||
return td;
|
||
}
|
||
|
||
function zeichen(darf) {
|
||
if (darf) {
|
||
const punkt = el('span', 'rechte-ja');
|
||
punkt.setAttribute('role', 'img');
|
||
punkt.setAttribute('aria-label', 'darf');
|
||
return punkt;
|
||
}
|
||
/* Ein Zeichen und nicht ein leeres Feld: Ein Bildschirmleser liest
|
||
eine leere Zelle als nichts vor — man hört dann nicht, dass die
|
||
Antwort „nein" ist, sondern gar nichts. */
|
||
const nein = el('span', 'rechte-nein', '–');
|
||
nein.setAttribute('aria-label', 'darf nicht');
|
||
return nein;
|
||
}
|
||
|
||
async function umstellen(seite, rolle, erlaubt, knopf) {
|
||
/* EINE RÜCKFRAGE NUR BEIM ÖFFNEN, nicht beim Schließen.
|
||
Etwas wegzunehmen kann man in einer Sekunde zurückdrehen und es
|
||
sieht sofort jemand. Etwas aufzumachen merkt unter Umständen
|
||
lange niemand — und genau das ist der Klick, bei dem eine
|
||
Sekunde Nachdenken etwas wert ist. */
|
||
if (erlaubt && !await frageNach({
|
||
titel: `„${rolle.name}" darf danach ${seite.datei} öffnen`,
|
||
was: 'Das gilt sofort, für alle mit dieser Rolle.',
|
||
bleibt: 'Zurücknehmen geht jederzeit mit demselben Schalter – aber bis '
|
||
+ 'dahin hat jeder mit dieser Rolle Zugang.',
|
||
ja: 'Freigeben',
|
||
})) return;
|
||
|
||
knopf.disabled = true;
|
||
try {
|
||
const a = await hole('/workspace/api/rechte/feld', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ pfad: seite.pfad, rolle: rolle.schluessel, erlaubt }),
|
||
});
|
||
if (!a.ok) {
|
||
melde(sagWas((await a.json().catch(() => ({}))).fehler, 'Ging nicht.'));
|
||
return;
|
||
}
|
||
melde(`Gespeichert: „${rolle.name}" darf ${seite.datei} `
|
||
+ `${erlaubt ? 'jetzt' : 'nicht mehr'} öffnen.`, 'gut');
|
||
await laden();
|
||
} finally {
|
||
knopf.disabled = false;
|
||
}
|
||
}
|
||
|
||
/* ---- Zeichnen --------------------------------------------------------- */
|
||
function zeichne() {
|
||
const gesamt = daten.seiten.length;
|
||
|
||
/* Die Zahlen je Rolle. Sortiert nach Anzahl — man sieht auf einen
|
||
Blick, wer viel sieht und wer wenig, und ob irgendwo eine Rolle
|
||
steht, die mehr sieht als sie sollte. */
|
||
const zahlen = $('zahlen');
|
||
zahlen.textContent = '';
|
||
for (const rolle of [...daten.rollen].sort((a, b) => b.seiten - a.seiten)) {
|
||
const li = document.createElement('li');
|
||
li.append(rolle.name + ' ');
|
||
const b = document.createElement('b');
|
||
b.textContent = `${rolle.seiten} von ${gesamt}`;
|
||
li.append(b);
|
||
if (rolle.abweichend) {
|
||
const m = el('span', 'rechte-zahlen__anders', ` · ${rolle.abweichend} von Hand`);
|
||
m.title = 'So viele Felder weichen bei dieser Rolle vom Grundstand ab.';
|
||
li.append(m);
|
||
}
|
||
zahlen.append(li);
|
||
}
|
||
|
||
/* Die Leiste: was ist von Hand gesetzt, und wie kommt man zurück. */
|
||
const leiste = $('leiste');
|
||
leiste.textContent = '';
|
||
if (daten.abweichungen) {
|
||
leiste.hidden = false;
|
||
leiste.append(el('span', 'rechte-leiste__satz',
|
||
`${daten.abweichungen} ${daten.abweichungen === 1 ? 'Feld ist' : 'Felder sind'} `
|
||
+ 'von Hand gesetzt und weicht vom Grundstand ab.'));
|
||
if (daten.darf_aendern) {
|
||
const b = el('button', 'schritt', 'Alles zurück auf Grundstand');
|
||
b.type = 'button';
|
||
b.addEventListener('click', async () => {
|
||
if (!await frageNach({
|
||
titel: `${daten.abweichungen} Änderungen zurücknehmen?`,
|
||
was: 'Danach gilt wieder genau das, was im Code steht.',
|
||
bleibt: 'Die Rechte selbst gehen nicht verloren – nur die von Hand '
|
||
+ 'gesetzten Abweichungen. Du kannst sie jederzeit neu setzen.',
|
||
ja: 'Auf Grundstand zurück',
|
||
})) return;
|
||
b.disabled = true;
|
||
const a = await hole('/workspace/api/rechte/abweichungen', { method: 'DELETE' });
|
||
b.disabled = false;
|
||
if (!a.ok) { melde('Ging nicht.'); return; }
|
||
melde('Alles zurück auf Grundstand.', 'gut');
|
||
await laden();
|
||
});
|
||
leiste.append(b);
|
||
}
|
||
} else {
|
||
leiste.hidden = false;
|
||
leiste.append(el('span', 'rechte-leiste__satz',
|
||
daten.darf_aendern
|
||
? 'Alles steht so, wie es im Code steht. Klick auf ein Feld, um es umzustellen – '
|
||
+ 'das wird sofort gespeichert.'
|
||
: 'Alles steht so, wie es im Code steht. Umstellen kann das nur DogFather.'));
|
||
}
|
||
|
||
/* Die Tafel. */
|
||
const ziel = $('tafel');
|
||
ziel.textContent = '';
|
||
const tab = document.createElement('table');
|
||
|
||
const kopf = document.createElement('thead');
|
||
const kz = document.createElement('tr');
|
||
kz.append(el('th', null, 'Seite'));
|
||
for (const rolle of daten.rollen) {
|
||
const th = el('th', null, rolle.name);
|
||
/* Der Schlüssel als Titel: Im Alltag liest man „Community", im
|
||
Code steht „gast". Wer die beiden nicht zusammenbringen kann,
|
||
sucht im Quelltext an der falschen Stelle. */
|
||
th.title = rolle.schluessel;
|
||
kz.append(th);
|
||
}
|
||
kopf.append(kz);
|
||
tab.append(kopf);
|
||
|
||
const koerper = document.createElement('tbody');
|
||
for (const seite of daten.seiten) {
|
||
const tr = document.createElement('tr');
|
||
tr.append(el('td', null, seite.datei));
|
||
for (const rolle of daten.rollen) tr.append(feld(seite, rolle));
|
||
koerper.append(tr);
|
||
}
|
||
tab.append(koerper);
|
||
ziel.append(tab);
|
||
ziel.setAttribute('aria-busy', 'false');
|
||
|
||
const offen = $('offen');
|
||
offen.textContent = '';
|
||
for (const pfad of daten.ohne_anmeldung) offen.append(el('li', null, pfad));
|
||
}
|
||
|
||
async function laden() {
|
||
let a;
|
||
try {
|
||
a = await hole('/workspace/api/rechte/tafel');
|
||
} catch { return; }
|
||
if (!a.ok) { melde('Die Tafel konnte nicht geladen werden.'); return; }
|
||
daten = await a.json();
|
||
zeichne();
|
||
}
|
||
|
||
(async () => { try { await laden(); } catch { /* umgeleitet */ } })();
|
||
})();
|