Agentur: Eintraege gehoeren allen -- und Events bekommen ein eigenes Formular

Gemessen am 07.09.2026, bevor irgendetwas geaendert wurde: Von drei
Agentur-Eintraegen sah eine Creatorin genau EINEN -- den, der ihr
zugeordnet war. Ein Event fuer alle traf weder `creator_id = ich` noch
`erstellt_von = ich`; die Agentur-Seite war fuer jeden Creator leer.
Nicht kaputt, nicht fehlerhaft: leer, so wie eine Seite aussieht, auf
der noch nichts steht.

Die Pruefung dazu war gruen. Sie legte ihre Testeintraege mit
`creator_id: idLuna` an und pruefte damit einen Fall, den es im Alltag
nicht gibt.

- Bereichseinstellung `fuerAlle` + `ohneCreatorBezug`, daraus abgeleitet
  sichtbarEintrag(). BEWUSST neben sichtbar() statt darin: an derselben
  Funktion haengen Aufgaben, Dateien, Termine und Calls -- wer dort
  "1=1" einschleust, gibt nebenbei fremde Akten frei. Eine Gegenprobe
  mit einer zweiten Creatorin haelt das fest.
- Die Zuordnung bietet nur noch "Agentur" an. Der Server verwirft eine
  Zuordnung ausserdem selbst -- inklusive des frei getippten Namens, und
  zwar NACH externPruefen: davor haette der Name den Riegel wieder
  aufgemacht.
- "Event & Kampagne" heisst jetzt "Agentur-Events" und hat ein eigenes
  Formular: Von/Bis, Titel, Beschreibung, Aufgaben (Punkte und Preise),
  Regeln. Die Karte zeigt den Zustand als WORT (laeuft bis / startet /
  vorbei seit), nicht nur als Farbe.
- Drei neue Spalten -- und sie stehen auch im Tabellenneubau vom 06.09.
  Der laeuft NACH dem Spaltennachtrag und haette sie samt Inhalt
  weggeworfen, ohne Fehler und mit stimmender Zeilenzahl.
- Creator sehen weiterhin alles und tragen weiterhin nichts ein (403).
  Die Unterzeile sagt jetzt "alles, was hier steht" statt "alles, was zu
  dir gehoert" -- eine vollstaendige Liste soll sich nicht wie ein
  Ausschnitt lesen.

pruef-agentur: 61 Pruefungen (vorher 40), alle gruen. Zwei eigene
Fehler nebenbei gefunden und behoben: eine Beschriftung mit 11,2 px
(Grenze 11,5) und ein Testdatum aus UTC statt Ortszeit.
Zusaetzlich gruen: css-klassen, struktur, formulare,
barrierefrei-workspace, handy.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-07 02:31:47 +02:00
co-authored by Claude Opus 5
parent 3914adf46c
commit 947c9d8a26
26 changed files with 898 additions and 204 deletions
+244 -5
View File
@@ -122,11 +122,16 @@ function anlegen(d, name, rolle, code) {
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
let idDogi, idLuna;
/* Mia kam am 07.09.2026 dazu, und sie ist kein Beiwerk: Sie legt nichts
an, ihr gehoert nichts, sie betreut niemanden. Genau deshalb beweist
erst SIE, dass ein Agentur-Eintrag wirklich fuer alle sichtbar ist --
bei Luna allein bliebe offen, ob es an ihr oder an der Regel lag. */
let idDogi, idLuna, idMia;
{
const d = new DatabaseSync(DB);
idDogi = anlegen(d, "Filipe", "admin", "CODE-DOGI-0001");
idLuna = anlegen(d, "Luna", "creator", "CODE-CREA-0001");
idMia = anlegen(d, "Mia", "creator", "CODE-CREA-0002");
d.close();
}
@@ -262,6 +267,24 @@ ok(/Bereich 'agentur' freigeschaltet/.test(ausgabe),
ok(mitExtern?.creator_extern === "Gastauftritt Mia",
`die Spalte "creator_extern" ebenfalls ("${mitExtern?.creator_extern}")`);
/* DIE DREI EVENT-SPALTEN MUESSEN DEN TAUSCH UEBERLEBEN (07.09.2026).
Sie entstehen frueher als diese Umstellung (Spalten-Nachtrag in
workspace.js), und der Neubau hier schreibt eine feste Spaltenliste
ab. Wer sie dort vergisst, verliert sie samt Inhalt -- ohne
Fehlermeldung, und die Zeilenzahl stimmte weiterhin. Genau dieser
Weg wird hier wirklich gegangen: Die Tabelle stand vorher im ALTEN
Bauplan, ohne diese Spalten.
`PRAGMA table_info` und nicht "SELECT laeuft durch": Eine fehlende
Spalte faellt beim SELECT zwar auch auf -- aber erst irgendwann,
beim naechsten Event, in einem 503. */
const spalten = d.prepare("PRAGMA table_info(eintraege)").all().map((s) => s.name);
const fehlend = ["event_ende", "event_aufgaben", "event_regeln"].filter((s) => !spalten.includes(s));
ok(fehlend.length === 0,
fehlend.length ? `nach der Umstellung fehlen: ${fehlend.join(", ")}`
: "die drei Event-Spalten haben die Umstellung ueberlebt");
/* Der Index muss nach dem Tausch wieder da sein -- er wird beim DROP
mit entfernt. Ohne ihn laeuft alles weiter, nur langsam, und
niemand merkt es je. */
@@ -312,6 +335,7 @@ async function anmelden(rolle, code) {
}
const keksDogi = await anmelden("admin", "CODE-DOGI-0001");
const keksLuna = await anmelden("creator", "CODE-CREA-0001");
const keksMia = await anmelden("creator", "CODE-CREA-0002");
async function ruf(art, weg, keks, koerper) {
const a = await fetch(BASIS + weg, {
@@ -340,11 +364,29 @@ const ARTEN = ["kampagne", "schulung", "anliegen", "zustaendig"];
"Bewertung ist aus — hier wird zusammengearbeitet, nicht benotet");
}
/* ANGELEGT WIE IM ECHTEN BETRIEB: OHNE ZUORDNUNG (07.09.2026).
Hier stand `creator_id: idLuna` -- und damit prueften die Zeilen
weiter unten einen Fall, den es im Alltag nicht gibt. Ein Agentur-
Event gehoert niemandem einzeln; Filipe legt es fuer alle an, das
Feld bleibt leer. Weil die Pruefung es aber jedes Mal Luna zuwies,
sah Luna es auch -- und die Zeile "sie sieht ihre 4 Eintraege" war
grün.
In Wirklichkeit war die Agentur-Seite fuer JEDEN Creator leer: Ein
Eintrag ohne creator_id traf weder `creator_id = ich` noch
`erstellt_von = ich`. Gemessen am 07.09.2026, bevor es repariert
wurde: von drei Eintraegen sah die Creatorin genau den einen, den
die Pruefung ihr zugeschoben hatte.
Das ist die dritte Sorte Fehler aus CLAUDE.md -- nicht uebersprungen,
nicht kaputt: gelaufen, gruen, und am Thema vorbei. Deshalb wird ab
jetzt der Weg gegangen, den es wirklich gibt. */
let angelegt = 0;
for (const art of ARTEN) {
const { status } = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, {
art, titel: `Test ${art}`, text: "Inhalt", datum: heute,
dringlichkeit: "mittel", creator_id: idLuna,
dringlichkeit: "mittel",
});
if (status === 200 || status === 201) angelegt++;
else ok(false, `Art "${art}" liess sich nicht anlegen (HTTP ${status})`);
@@ -360,13 +402,141 @@ ok(angelegt === ARTEN.length, `alle ${angelegt} Arten lassen sich anlegen`);
ok(status >= 400, `eine erfundene Art wird abgelehnt (HTTP ${status})`);
}
/* Der Creator SIEHT seinen Agentur-Bereich -- er ist der Grund, warum
es ihn gibt. */
/* =======================================================================
4b. FUER ALLE, NICHT FUER EINEN
Die Agentur ist der einzige Bereich, in dem ein Eintrag ALLEN gilt.
Geprueft wird deshalb beides: dass jeder Creator alles sieht -- und
dass diese Ausnahme NICHT auf die Betreuungsbereiche abfaerbt. Der
zweite Teil ist der wichtigere: Eine Sichtbarkeitsregel, die man an
der falschen Stelle aufmacht, gibt fremde Akten frei, und man sieht
es der Zeile nicht an.
======================================================================= */
melde("\n=== Fuer alle, nicht fuer einen ===");
{
const { status, daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksLuna);
ok(status === 200, `Luna darf den Bereich oeffnen (HTTP ${status})`);
ok((daten?.eintraege || []).length === ARTEN.length,
`sie sieht ihre ${(daten?.eintraege || []).length} Eintraege`);
`sie sieht alle ${(daten?.eintraege || []).length} Eintraege — obwohl keiner ihr zugeordnet ist`);
ok((daten?.eintraege || []).every((e) => e.creator_id === null),
"und keiner davon traegt eine Creator-Zuordnung");
}
{
/* Mia ist eine ZWEITE Creatorin, die nichts angelegt hat und der
nichts gehoert. Wenn die Regel nur ueber `erstellt_von` gestolpert
waere, saehe Luna vielleicht etwas und Mia nichts. */
const { daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksMia);
ok((daten?.eintraege || []).length === ARTEN.length,
`auch Mia sieht alle ${(daten?.eintraege || []).length} — sie hat mit keinem davon etwas zu tun`);
}
{
/* Eine ausdrueckliche Zuordnung muss der Server verwerfen. Sonst
genuegt ein Aufruf an der Oberflaeche vorbei -- oder ein spaeteres
Formular, das das Feld wieder mitschickt --, und das Event ist
fuer alle anderen wieder unsichtbar. */
await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, {
art: "kampagne", titel: "Versuch mit Zuordnung", datum: heute, creator_id: idLuna,
});
const { daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksMia);
const e = (daten?.eintraege || []).find((x) => x.titel === "Versuch mit Zuordnung");
ok(!!e, "ein Eintrag, der ausdruecklich Luna zugewiesen wurde, ist trotzdem fuer Mia da");
ok(e?.creator_id === null, `und seine Zuordnung wurde verworfen (creator_id = ${e?.creator_id})`);
/* Und derselbe Riegel fuer den freien Namen -- er wird an einer
ANDEREN Stelle gesetzt (externPruefen) und waere sonst das Loch
neben der geschlossenen Tuer. */
await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, {
art: "kampagne", titel: "Versuch mit Namen", datum: heute, creator_extern: "Marke XY",
});
const { daten: d2 } = await ruf("GET", "/workspace/api/bereich/agentur", keksMia);
const n = (d2?.eintraege || []).find((x) => x.titel === "Versuch mit Namen");
ok(n?.creator_extern === null || n?.creator_extern === undefined,
`auch ein frei getippter Name wird verworfen (creator_extern = ${JSON.stringify(n?.creator_extern)})`);
}
{
/* DIE ENTSCHEIDENDE GEGENPROBE.
Die Ausnahme haette man auch in sichtbar() einbauen koennen -- eine
Zeile weniger, und dieselbe Wirkung fuer die Agentur. Nur haengen
an derselben Funktion auch Aufgaben, Dateien, Termine und Calls.
Diese Pruefung ist der Grund, warum das auffiele. */
await ruf("POST", "/workspace/api/bereich/technik", keksDogi, {
art: "problem", titel: "Nur fuer Mia", datum: heute, creator_id: idMia,
});
const { daten } = await ruf("GET", "/workspace/api/bereich/technik", keksLuna);
const fremd = (daten?.eintraege || []).filter((e) => e.titel === "Nur fuer Mia");
ok(fremd.length === 0,
`Luna sieht Mias Technik-Eintrag NICHT — die Agentur-Ausnahme faerbt nicht ab (${fremd.length} gefunden)`);
}
{
/* Sehen ja, schreiben nein -- ausdruecklicher Wunsch: "die sollen
alles sehen was wir eintragen aber nicht eintragen koennen". */
const { status } = await ruf("POST", "/workspace/api/bereich/agentur", keksLuna, {
art: "kampagne", titel: "Von Luna", datum: heute,
});
ok(status === 403, `Luna darf hier NICHTS eintragen (HTTP ${status})`);
}
/* =======================================================================
4c. AGENTUR-EVENTS: Zeitraum, Aufgaben, Regeln
======================================================================= */
melde("\n=== Agentur-Events ===");
{
/* ORTSZEIT, nicht UTC. `new Date(...).toISOString().slice(0,10)` waere
zwischen Mitternacht und zwei Uhr einen Tag daneben -- und weil der
Server den Zeitraum gegen `heute` (Ortszeit) prueft, waere diese
Pruefung nachts rot, ohne dass sich etwas geaendert haette.
Gefunden von pruef-struktur, das genau danach sucht. Mittags
gerechnet, damit auch eine Zeitumstellung im Zeitraum nichts
verschiebt. */
const ende = (() => {
const d = new Date(`${heute}T12:00:00`);
d.setDate(d.getDate() + 14);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
})();
const { status } = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, {
art: "kampagne", titel: "Sommer-Aktion", text: "Beschreibung", datum: heute,
event_ende: ende, event_aufgaben: "1 Punkt je Live-Stunde", event_regeln: "Nur eigenes Konto",
});
ok(status === 201, `ein Event mit Zeitraum, Aufgaben und Regeln laesst sich anlegen (HTTP ${status})`);
const { daten } = await ruf("GET", "/workspace/api/bereich/agentur", keksLuna);
const e = (daten?.eintraege || []).find((x) => x.titel === "Sommer-Aktion");
ok(e?.event_ende === ende, `das Ende kommt beim Creator an (${e?.event_ende})`);
ok(e?.event_aufgaben === "1 Punkt je Live-Stunde", "die Aufgaben kommen an");
ok(e?.event_regeln === "Nur eigenes Konto", "die Regeln kommen an");
/* GEGENPROBEN. Ohne sie hiesse "es laesst sich anlegen" nur, dass der
Server irgendetwas annimmt. */
const verdreht = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, {
art: "kampagne", titel: "Verdreht", datum: ende, event_ende: heute });
ok(verdreht.status === 400, `ein Ende VOR dem Beginn wird abgelehnt (HTTP ${verdreht.status})`);
const unsinn = await ruf("POST", "/workspace/api/bereich/agentur", keksDogi, {
art: "kampagne", titel: "Unsinn", datum: heute, event_ende: "31.12.2026" });
ok(unsinn.status === 400, `ein Datum in falscher Schreibweise wird abgelehnt (HTTP ${unsinn.status})`);
/* Beim AENDERN gilt dasselbe -- und zwar auch dann, wenn nur EINE
der beiden Seiten mitgeschickt wird. Genau hier waere die Pruefung
sonst blind: pruefe() sieht nur, was im Aufruf steht. */
const { daten: d3 } = await ruf("GET", "/workspace/api/bereich/agentur", keksDogi);
const id = (d3?.eintraege || []).find((x) => x.titel === "Sommer-Aktion")?.id;
const nurEnde = await ruf("PATCH", `/workspace/api/bereich/agentur/${id}`, keksDogi,
{ event_ende: "2020-01-01" });
ok(nurEnde.status === 400,
`auch beim Aendern nur des Endes schlaegt der verdrehte Zeitraum an (HTTP ${nurEnde.status})`);
const nurBeginn = await ruf("PATCH", `/workspace/api/bereich/agentur/${id}`, keksDogi,
{ datum: "2099-01-01" });
ok(nurBeginn.status === 400,
`und ebenso, wenn nur der Beginn nach hinten geschoben wird (HTTP ${nurBeginn.status})`);
/* Die Event-Felder gehoeren der Agentur. In einem anderen Bereich
muessen sie stillschweigend verfallen -- sonst haengt eines Tages
ein Preisausschreiben an einem Schutzvorfall. */
await ruf("POST", "/workspace/api/bereich/schutz", keksDogi, {
art: "vorfall", titel: "Kein Event", datum: heute, event_regeln: "darf nicht ankommen" });
const { daten: d4 } = await ruf("GET", "/workspace/api/bereich/schutz", keksDogi);
const v = (d4?.eintraege || []).find((x) => x.titel === "Kein Event");
ok(!v?.event_regeln, `im Schutz-Bereich verfallen die Event-Felder (${JSON.stringify(v?.event_regeln)})`);
}
/* =======================================================================
@@ -451,6 +621,75 @@ melde("\n=== Die Kachel ===");
fehlend.length ? `diese Eintraege fehlen auf der Seite: ${fehlend.join(", ")}`
: `alle vier Titel stehen da: ${kartenTitel.join(" · ")}`);
/* ---- Das Event auf dem Bildschirm --------------------------------
Bis hierher ist bewiesen, dass die Daten stimmen. Das sagt noch
nichts darueber, ob jemand sie SIEHT: Zeitraum, Aufgaben und
Regeln koennen vollstaendig in der Antwort stehen und trotzdem
nirgends gezeichnet werden -- ein vergessener Aufruf in karte()
reicht, und niemand merkt es, weil die Karte ja da ist. */
const event = await seite.$$eval(".eintrag-karte", (karten) => {
const k = karten.find((x) => x.querySelector(".eintrag-titel")?.textContent.trim() === "Sommer-Aktion");
if (!k) return null;
return {
stand: k.dataset.event,
marke: k.querySelector(".event-zeitraum__marke")?.textContent.trim(),
spanne: k.querySelector(".event-zeitraum__spanne")?.textContent.trim(),
schilder: [...k.querySelectorAll(".event-block__schild")].map((s) => s.textContent.trim()),
texte: [...k.querySelectorAll(".event-block__text")].map((s) => s.textContent.trim()),
};
});
ok(!!event, "die Event-Karte Sommer-Aktion steht auf der Seite");
ok(event?.stand === "laeuft", `sie ist als laufend gekennzeichnet ("${event?.stand}")`);
ok(/läuft bis/.test(event?.marke || ""),
`der Zustand steht als WORT da, nicht nur als Farbe ("${event?.marke}")`);
ok(/–/.test(event?.spanne || ""), `und der Zeitraum als Spanne ("${event?.spanne}")`);
ok(event?.schilder.join(",") === "Aufgaben,Regeln",
`beide Bloecke sind beschriftet (${event?.schilder.join(", ")})`);
ok(event?.texte.includes("1 Punkt je Live-Stunde") && event?.texte.includes("Nur eigenes Konto"),
"und tragen wirklich ihren Inhalt");
/* GEGENPROBE dazu: Eine Karte, die KEIN Event ist, darf nichts davon
haben. Sonst bewiese der Block oben nur, dass die Elemente
existieren -- nicht, dass sie zur richtigen Art gehoeren. */
const schulung = await seite.$$eval(".eintrag-karte", (karten) => {
const k = karten.find((x) => x.querySelector(".eintrag-titel")?.textContent.trim() === "Test schulung");
return k ? { event: k.dataset.event ?? null, bloecke: k.querySelectorAll(".event-block").length } : null;
});
ok(schulung && !schulung.event && schulung.bloecke === 0,
`eine Schulung bekommt weder Zustand noch Event-Bloecke (${JSON.stringify(schulung)})`);
/* ---- Die Zuordnung bietet nur "Agentur" an ------------------------
Der Ausgangspunkt des ganzen Umbaus: Hier stand die Creator-Liste,
und ein versehentlicher Klick darauf machte das Event fuer alle
anderen unsichtbar. */
await seite.click("#neu-oeffnen");
await seite.waitForTimeout(400);
const wahl = await seite.$$eval("#f-creator option", (o) => o.map((x) => x.textContent.trim()));
ok(wahl.length === 1 && wahl[0] === "Agentur",
`die Zuordnung bietet genau eine Wahl an: ${JSON.stringify(wahl)}`);
const hinweis = await seite.$eval("#hinweis-creator", (e) => e.textContent.trim()).catch(() => "");
ok(/allen/.test(hinweis), `und sagt, was das bedeutet ("${hinweis}")`);
/* ---- Das Formular wechselt mit der Art ---------------------------
Beim Oeffnen steht die erste Art (Agentur-Events) drin, also
muessen die Event-Felder sichtbar sein. Wechselt man auf Schulung,
muessen sie verschwinden -- und geleert werden, sonst faehrt ein
unsichtbares Enddatum an einer Schulung mit. */
const sichtbar = (id) => seite.$eval(id, (e) => !e.hidden);
ok(await sichtbar("#feld-ende") && await sichtbar("#feld-aufgaben") && await sichtbar("#feld-regeln"),
"bei Agentur-Events stehen Zeitraum, Aufgaben und Regeln im Formular");
const schild = await seite.$eval("#schild-titel", (e) => e.textContent.trim());
ok(schild === "Titel des Events", `und die Beschriftung passt zur Art ("${schild}")`);
await seite.fill("#f-ende", "2030-01-01");
await seite.selectOption("#f-art", "schulung");
await seite.waitForTimeout(300);
ok(!(await sichtbar("#feld-ende")) && !(await sichtbar("#feld-aufgaben")),
"bei Schulung verschwinden sie wieder");
const restEnde = await seite.$eval("#f-ende", (e) => e.value);
ok(restEnde === "",
`und das Enddatum faehrt nicht unsichtbar mit (Wert: "${restEnde}")`);
/* GEGENPROBE zum Selektor: Ein erfundener Bereich darf NICHT wie ein
echter aussehen -- weder in der Ueberschrift noch mit Eintraegen.
Die Anwendung schickt einen dorthin zurueck, wo er hingehoert. */