"Wer sieht was" auf beiden Adressen — und die Tafel lässt sich umstellen

Filipe: "ich will die kategorie auch auf der team dogi seite für die
dogfather und rechte hand rollen ... und wenn man drauf drückt,
perfektionieren so dass ich die da auch manuell wechseln und speichern
kann."

DIE KACHEL STAND NUR AUF DER AGENTURSEITE. Auf der Team-Dogi-Seite
arbeitet ihr aber täglich; dort nicht nachsehen zu können, wer was darf,
hieße, die Frage an dem Ort zu stellen, an dem man gerade nicht ist. Sie
steht jetzt auf beiden — einmal beschrieben, zweimal benutzt.

ANSEHEN BEIDE, UMSTELLEN NUR DOGFATHER. Die rechte Hand bekommt
dieselbe Tafel und keine Knöpfe; nicht weil das Skript sie versteckt,
sondern weil der Server darf_aendern: false schickt. Wer Rechte vergeben
kann, kann sich Rechte vergeben — diese eine Tür bleibt bei ihm.

WAS GESPEICHERT WIRD, IST NUR DER UNTERSCHIED. In der Datenbank steht
eine Zeile nur, wenn sie vom Grundstand in rechte.js abweicht. Damit
bedeutet der Code weiterhin etwas: Man sieht, was gedacht war, und
daneben, was jemand daraus gemacht hat. "Zurück auf Grundstand" ist ein
DELETE, und eine neue Seite erbt automatisch den Grundstand — eine
vollständige Kopie in der Datenbank hätte sie nicht gekannt und sie wäre
für alle zu gewesen, ohne dass es jemand entschieden hätte.

DREI FELDER SIND FEST: DogFather kann sich die Rechte-, die Personen-
und die Startseite nicht selbst wegnehmen. Eine Einstellung, aus der man
sich aussperren kann, ist keine Einstellung, sondern eine Falle — und
sie wäre genau einen Fehlklick entfernt gewesen.

GESPEICHERT WIRD SOFORT, mit jedem Klick. Kein "Speichern" am Ende: Bei
zweihundert Feldern ist das die Stelle, an der eine halbe Änderung
verlorengeht, und eine halbe Änderung an Rechten ist die gefährlichste
Lage von allen. Rückfrage gibt es nur beim ÖFFNEN — etwas wegzunehmen
sieht sofort jemand, etwas aufzumachen unter Umständen lange niemand.

DER BEFUND, DEN DIE PRÜFUNG GEFUNDEN HAT: Nimmt man einem Modi eine
Seite weg, greift die Schranke sofort — und die KACHEL blieb stehen. Ein
Knopf, der auf die Startseite zurückwirft. Der Satz "Kachel und Tür
gehören zusammen" steht seit dem 06.09. im Code; bis heute war er eine
Bitte an den, der beides pflegt. Jetzt ist er eine Rechnung: Beide
Kachellisten — die des Servers und die im Browser — werden gegen
dieselbe Tafel gefiltert, aus der die Schranke ihre Entscheidung holt.

Geprüft wird nicht, ob die Antwort 200 lautet, sondern ob sich das
VERHALTEN ändert: Der Modi kommt danach wirklich nicht mehr hinein —
mit Gegenprobe davor. Und eine Umstellung überlebt einen Neustart; ohne
tafelLaden() beim Start hätte wieder der Grundstand gegolten, und es
hätte ausgesehen wie vorher.

Geprüft: rollen 315 · start-ansicht 147 · crew-adresse 132 · sicht 84 ·
modi-verborgen 80 · neue-seiten 70 · treff-werkzeuge 70 · nachwuchs 69 ·
treff 58 · rechte-umstellen 46 · entwicklung 40 · css-klassen 30 ·
zwischenspeicher 21 · alle-wege 19 · rechtetafel 19 — alles grün.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-11 22:42:21 +02:00
co-authored by Claude Opus 5
parent dfd951861a
commit 44e5ace109
38 changed files with 1485 additions and 520 deletions
+207 -92
View File
@@ -1,14 +1,33 @@
/* =====================================================================
„Wer sieht was" — die Rechtetafel, gezeichnet (11.09.2026)
„Wer sieht was" — ansehen und umstellen (11.09.2026)
Dieses Skript ENTSCHEIDET nichts. Es holt die Tafel vom Server und
zeichnet sie. Jede Zahl und jeder Punkt darin kommt aus rechte.js —
derselben Datei, aus der auch die Schranke ihre Antwort holt.
Filipe: *„so dass ich die da auch manuell wechseln und speichern
kann."*
WARUM DAS WICHTIG IST: Eine Übersicht, die im Browser gebaut wird
(„Rolle X darf wohl Y, weil …"), wäre eine zweite Meinung über die
Rechte — und ausgerechnet die, der man glaubt, weil sie wie eine
Auskunft aussieht. Sie würde altern, ohne dass man es ihr ansieht.
---------------------------------------------------------------------
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);
@@ -18,104 +37,200 @@
if (text !== undefined) n.textContent = text;
return n;
};
const melde = (t) => {
const melde = (t, art) => {
const f = $('fehler');
if (f) { f.textContent = t; f.hidden = !t; }
if (!f) return;
f.textContent = t;
f.hidden = !t;
f.dataset.art = art || 'fehler';
};
(async () => {
let r;
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 && !confirm(
`„${rolle.name}" darf danach ${seite.datei} öffnen.\n\n`
+ 'Das gilt sofort, für alle mit dieser Rolle. Sicher?')) return;
knopf.disabled = true;
try {
const a = await fetch('/workspace/api/rechte/tafel', { credentials: 'same-origin' });
if (a.status === 401) { location.assign('/workspace/'); return; }
/* 404 heißt hier: diese Seite gibt es für dich nicht. Die
Schranke hätte schon vorher umgeleitet; kommt es trotzdem
hierher, ist die Startseite die richtige Antwort — und nicht
eine Fehlermeldung, die verrät, dass es die Auskunft gibt. */
if (a.status === 404) { location.assign('/workspace/start.html'); return; }
if (!a.ok) { melde('Die Tafel konnte nicht geladen werden.'); return; }
r = await a.json();
} catch {
melde('Die Tafel konnte nicht geladen werden.');
return;
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((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;
}
}
const gesamt = r.seiten.length;
/* ---- Zeichnen --------------------------------------------------------- */
function zeichne() {
const gesamt = daten.seiten.length;
/* ---- Die Zahlen je Rolle ---------------------------------------
Sortiert nach Anzahl, absteigend. Eine alphabetische Reihenfolge
hätte dieselben Zahlen gezeigt und die Frage nicht beantwortet:
Man sieht hier auf einen Blick, wer viel sieht und wer wenig —
und ob irgendwo eine Rolle steht, die mehr sieht, als sie sollte. */
/* 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');
if (zahlen) {
zahlen.textContent = '';
for (const rolle of [...r.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);
zahlen.append(li);
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 Tafel --------------------------------------------------- */
/* 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 (!confirm(`${daten.abweichungen} Änderungen zurücknehmen?\n\n`
+ 'Danach gilt wieder genau das, was im Code steht.')) 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');
if (ziel) {
ziel.textContent = '';
const tab = document.createElement('table');
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 r.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 r.seiten) {
const tr = document.createElement('tr');
tr.append(el('td', null, seite.datei));
for (const rolle of r.rollen) {
const td = document.createElement('td');
if (seite.rollen.includes(rolle.schluessel)) {
const punkt = el('span', 'rechte-ja');
punkt.setAttribute('role', 'img');
punkt.setAttribute('aria-label', 'darf');
td.append(punkt);
} else {
/* 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');
td.append(nein);
}
tr.append(td);
}
koerper.append(tr);
}
tab.append(koerper);
ziel.append(tab);
ziel.setAttribute('aria-busy', 'false');
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');
/* ---- Ohne Anmeldung --------------------------------------------- */
const offen = $('offen');
if (offen) {
offen.textContent = '';
for (const pfad of r.ohne_anmeldung) {
offen.append(el('li', null, pfad));
}
}
})();
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 */ } })();
})();
+26 -2
View File
@@ -155,7 +155,17 @@
"keine Kacheln". Die beiden auseinanderzuhalten ist kein
Feinschliff: Verwechselte man sie, haetten die fuenf bekannten
Rollen ab sofort eine leere Startseite. */
function eigeneGruppen(wer) {
/* Ist die Seite hinter diesem Ziel fuer mich offen?
Ohne Auskunft vom Server: ja -- siehe oben. */
function seiteOffen(ziel) {
const offen = ich?.seiten;
if (!Array.isArray(offen) || !offen.length) return true;
const z2 = String(ziel || "");
if (!z2) return true;
return offen.includes('/workspace/' + z2.split('?')[0]);
}
function eigeneGruppen(wer) {
const vomServer = ich && ich.bereiche;
if (Array.isArray(vomServer)) {
/* Nach Gruppen buendeln, in der Reihenfolge, in der sie kommen
@@ -186,7 +196,21 @@
kaputtgeht. */
const zusatz = Array.isArray(ich?.bereiche_zusatz) ? ich.bereiche_zusatz : [];
const gruppen = GRUPPEN
.map((g) => ({ ...g, bereiche: g.bereiche.filter((b) => b.rollen.includes(wer.rolle)) }))
/* UND GEGEN DIE RECHTETAFEL (11.09.2026).
`rollen` an der Kachel ist die Liste in dieser Datei -- sie
wird von Hand gepflegt und kennt nichts davon, dass DogFather
eine Seite umgestellt hat. `ich.seiten` kommt vom Server und
ist dieselbe Auskunft, aus der auch die Schranke ihre
Entscheidung holt.
BEIDES UND NICHT NUR EINES: Die Liste hier entscheidet, was
ueberhaupt angeboten wird; die Tafel, ob es noch offen ist.
Faellt `ich.seiten` aus (alte Fassung im Zwischenspeicher),
bleibt es beim alten Verhalten -- eine Kachel zu viel ist
aergerlich, eine fehlende sieht aus wie ein Fehler. */
.map((g) => ({ ...g, bereiche: g.bereiche.filter((b) => b.rollen.includes(wer.rolle)
&& seiteOffen(b.ziel)) }))
.filter((g) => g.bereiche.length);
for (const b of zusatz) {
const g = gruppen.find((x) => x.name === b.gruppe);