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. */
+130 -6
View File
@@ -101,19 +101,45 @@ export const BEREICHE = {
sieht man den Unterschied in einer Liste nicht.
Bewertung: nein. Hier wird nichts benotet; es ist ein
Zusammenarbeits-Bereich, kein Analysebereich. */
/* FUER ALLE, NICHT FUER EINEN (07.09.2026).
Die anderen fuenf Bereiche sind Betreuungsakten: Dort steht etwas
UEBER einen Creator, und nur er und seine Betreuung sehen es. Die
Agentur ist das Gegenteil -- ein Event, eine Schulung, eine
Zustaendigkeit gilt fuer alle gleichzeitig.
Bis heute lief sie trotzdem unter der Akten-Regel, und das hatte
eine Folge, die niemandem auffiel: Ein Event ohne Creator-Zuordnung
traf weder `creator_id = ich` noch `erstellt_von = ich` -- fuer
jeden Creator war die Agentur-Seite schlicht LEER. Nicht "kaputt",
nicht "Fehler": leer, so wie eine Seite aussieht, auf der noch
nichts steht. Gemessen am 07.09.2026: Von drei Eintraegen sah die
Creatorin genau den einen, der ihr zugeordnet war.
Zwei Schalter, weil es zwei verschiedene Fragen sind:
fuerAlle -- jeder sieht jeden Eintrag dieses Bereichs
ohneCreatorBezug -- ein Eintrag gehoert hier NIEMANDEM einzeln */
agentur: {
name: "Agentur",
arten: {
kampagne: "Event & Kampagne",
kampagne: "Agentur-Events",
schulung: "Schulung",
anliegen: "Anliegen an die Agentur",
zustaendig: "Zuständigkeit",
},
bewertung: false,
dringlichkeit: true,
fuerAlle: true,
ohneCreatorBezug: true,
},
};
/* Aus den Bereichseinstellungen abgeleitet, nicht danebengeschrieben.
Eine zweite Liste waere beim naechsten Bereich auseinandergelaufen --
und zwar still. */
const BEREICHE_FUER_ALLE = Object.entries(BEREICHE)
.filter(([, e]) => e.fuerAlle).map(([k]) => k);
const DRINGLICHKEITEN = ["hoch", "mittel", "niedrig"];
const STATUS = ["offen", "erledigt"];
const TITEL_MAX = 160;
@@ -256,10 +282,41 @@ export function sichtbar(person) {
: { wo: "e.erstellt_von = ?", werte: [person.id] };
}
/* Dieselbe Regel, aber mit der Ausnahme fuer Bereiche, die allen
gehoeren.
BEWUSST EINE EIGENE FUNKTION, NICHT EINE AENDERUNG AN sichtbar():
sichtbar() beantwortet auch Fragen zu Aufgaben, Dateien, Terminen und
Calls (siehe die Aufrufe in workspace-aufgaben.js,
workspace-dateien.js, workspace-kalender.js, workspace-calls.js). Wer
dort "1=1" einschleust, weil er an Agentur-Eintraege dachte, oeffnet
nebenbei fremde Dateien und fremde Termine -- ein Loch, das man dem
Code nicht ansieht, weil an der geaenderten Stelle nichts davon
steht.
Nur Eintraege gehen hier durch, und nur die aus einem Bereich mit
`fuerAlle`. Der Rest faellt woertlich auf die alte Regel zurueck.
`praefix` ist da, weil die Suche dieselbe Tabelle unter demselben
Kuerzel "e" fuehrt -- geht das eines Tages auseinander, faellt es
beim Aufruf auf, nicht erst im Betrieb. */
export function sichtbarEintrag(person, praefix = "e") {
const regel = sichtbar(person);
if (!regel) return regel;
if (regel.wo === "1=1") return regel; // DogFather sieht ohnehin alles
if (!BEREICHE_FUER_ALLE.length) return regel;
const liste = BEREICHE_FUER_ALLE.map(() => "?").join(", ");
return {
wo: `(${praefix}.bereich IN (${liste}) OR ${regel.wo})`,
werte: [...BEREICHE_FUER_ALLE, ...regel.werte],
};
}
const SPALTEN = `
e.id, e.bereich, e.art, e.titel, e.text, e.datum, e.bewertung,
e.dringlichkeit, e.status, e.creator_id, e.erstellt, e.erstellt_von, e.geaendert,
e.hook, e.format, e.saeule_id, e.geplant,
e.event_ende, e.event_aufgaben, e.event_regeln,
e.creator_extern,
${externSql("pc.name", "e.creator_extern")} AS creator_name,
pe.name AS erstellt_name,
@@ -289,7 +346,7 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => {
const einstellung = BEREICHE[bereich];
if (!einstellung) return res.status(404).json({ fehler: "nicht_gefunden" });
const regel = sichtbar(req.sicht || req.person);
const regel = sichtbarEintrag(req.sicht || req.person);
if (!regel) return res.status(404).json({ fehler: "nicht_gefunden" });
const eintraege = db().prepare(`
@@ -389,6 +446,35 @@ function pruefe(bereich, körper, { neu }) {
} else aus.geplant = g;
}
}
/* ---- Agentur-Events: Zeitraum, Aufgaben, Regeln -------------------
Nur im Agentur-Bereich, nach demselben Muster wie die vier
Content-Felder darueber: anderswo werden sie stillschweigend
verworfen. Sonst haenge eines Tages ein Preisausschreiben an einem
Schutzvorfall. */
if (bereich === "agentur") {
if (körper.event_ende !== undefined) {
const e = String(körper.event_ende ?? "").trim();
if (!e) aus.event_ende = null;
else if (!/^\d{4}-\d{2}-\d{2}$/.test(e) || Number.isNaN(Date.parse(e))) {
fehler.push("Das Ende des Zeitraums ist ungültig.");
} else aus.event_ende = e;
}
for (const [feld, name] of [["event_aufgaben", "Die Aufgaben"], ["event_regeln", "Die Regeln"]]) {
if (körper[feld] === undefined) continue;
const t = String(körper[feld] ?? "").trim();
if (t.length > TEXT_MAX) fehler.push(`${name} sind zu lang.`);
else aus[feld] = t || null;
}
/* "Bis" vor "von" ist kein Zeitraum, sondern ein Tippfehler -- und
einer, den man einer Liste nicht ansieht: Der Eintrag stuende da,
liefe aber schon abgelaufen ein. Geprueft wird gegen das Datum,
das GERADE gesetzt wird; beim Aendern nur einer der beiden Seiten
ergaenzt der Aufrufer die andere mit (siehe PATCH). */
if (aus.event_ende && aus.datum && aus.event_ende < aus.datum) {
fehler.push("Das Ende liegt vor dem Beginn.");
}
}
if (körper.creator_id !== undefined) {
const w = körper.creator_id;
if (w === null || w === "") aus.creator_id = null;
@@ -405,6 +491,27 @@ function pruefe(bereich, körper, { neu }) {
Konto; sichtbar bleibt er ueber erstellt_von (siehe sichtbar()). */
externPruefen(körper, aus, "creator", fehler);
/* Ein Agentur-Eintrag gehoert NIEMANDEM einzeln (07.09.2026).
Er gilt fuer alle, und genau deshalb sieht ihn auch jeder (siehe
`fuerAlle` oben). Eine Zuordnung waere hier nicht bloss unnoetig,
sie waere irrefuehrend: Ein Event mit dem Namen einer Creatorin
daneben liest sich, als sei es ihres. Die Oberflaeche bietet
deshalb nur noch "Agentur" an -- und diese Zeilen sorgen dafuer,
dass ein Aufruf an der Oberflaeche vorbei zum selben Ergebnis
kommt. Eine Regel, die nur im Formular steht, ist keine Regel.
GANZ ZUM SCHLUSS, und das ist der Punkt: Der freie Name wird eine
Zeile darueber gesetzt. Stuende dieser Riegel vorher -- der
naheliegende Ort, gleich bei der Creator-Zuordnung --, haette
externPruefen ihn danach wieder aufgemacht, und ein getippter Name
stuende trotz allem am Event. Zwei richtige Regeln, falsche
Reihenfolge, kein Fehler zu sehen. */
if (einstellung.ohneCreatorBezug) {
aus.creator_id = null;
aus.creator_extern = null;
}
return { aus, fehler };
}
@@ -457,12 +564,14 @@ bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, re
const { lastInsertRowid } = db().prepare(`
INSERT INTO eintraege
(bereich, art, titel, text, datum, bewertung, dringlichkeit, status,
creator_id, creator_extern, erstellt, erstellt_von, hook, format, saeule_id, geplant)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
creator_id, creator_extern, erstellt, erstellt_von, hook, format, saeule_id, geplant,
event_ende, event_aufgaben, event_regeln)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
bereich, aus.art, aus.titel, aus.text ?? null, aus.datum,
aus.bewertung ?? null, aus.dringlichkeit ?? "mittel", aus.status ?? "offen",
aus.creator_id ?? null, aus.creator_extern ?? null, jetzt(), req.person.id,
aus.hook ?? null, aus.format ?? null, aus.saeule_id ?? null, aus.geplant ?? null);
aus.hook ?? null, aus.format ?? null, aus.saeule_id ?? null, aus.geplant ?? null,
aus.event_ende ?? null, aus.event_aufgaben ?? null, aus.event_regeln ?? null);
protokolliere("eintrag_angelegt", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
@@ -478,7 +587,7 @@ bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, re
/* ---------- Ändern und Löschen ------------------------------------------- */
function holen(req, id) {
const regel = sichtbar(req.person);
const regel = sichtbarEintrag(req.person);
if (!regel) return null;
return db().prepare(`SELECT e.* ${VERBUND} WHERE ${regel.wo} AND e.id = ?`)
.get(...regel.werte, id);
@@ -497,6 +606,21 @@ bereicheRouter.patch("/workspace/api/bereich/:bereich/:id", gleicheHerkunft, (re
if (fehler.length) return res.status(400).json({ fehler: fehler.join(" ") });
if (!istLeitung(req.person)) delete aus.creator_id;
/* Zeitraum beim AENDERN -- genauso wichtig wie beim Anlegen und
leichter zu uebersehen: pruefe() sieht nur die Felder, die im
Aufruf stehen. Wer nur das Ende verschiebt, schickt kein `datum`
mit; die Pruefung dort haette nichts zu vergleichen und liesse
jedes Ende durch. Deshalb hier gegen den GESPEICHERTEN Wert, und
zwar in beide Richtungen -- man kann den Zeitraum auch dadurch
verdrehen, dass man den Beginn nach hinten schiebt. */
if (bereich === "agentur" && (aus.event_ende !== undefined || aus.datum !== undefined)) {
const von = aus.datum !== undefined ? aus.datum : eintrag.datum;
const bis = aus.event_ende !== undefined ? aus.event_ende : eintrag.event_ende;
if (von && bis && bis < von) {
return res.status(400).json({ fehler: "Das Ende liegt vor dem Beginn." });
}
}
/* Beim Aendern gilt dieselbe Regel -- sonst waere die Pruefung beim
Anlegen wertlos: Man legt ohne Saeule an und haengt sie danach an. */
if (aus.saeule_id !== undefined) {
+14 -1
View File
@@ -136,7 +136,20 @@ hinweisRouter.get("/workspace/api/hinweise", (req, res) => {
WHERE ${d.wo} AND d.status = 'review'`, d.werte));
}
/* ---------- Betreuungsbereiche ---------- */
/* ---------- Betreuungsbereiche ----------
HIER BLEIBT ES ABSICHTLICH BEI sichtbar() (07.09.2026).
Seit heute gibt es daneben sichtbarEintrag(), das Agentur-
Eintraege fuer alle sichtbar macht. Hier waere das falsch, und
zwar aus zwei Gruenden: Der Hinweis fuehrt fest nach
`bereich.html?b=live` -- ein Creator bekaeme also eine Zahl aus
der Agentur und landete beim Klick in einem Bereich, in dem
nichts davon steht. Und ein dringender Agentur-Punkt ist die
Aufgabe der Agentur, nicht seine; ein Hinweis, den man nicht
abarbeiten kann, ist Laerm.
Wer das eines Tages umstellen will, muss zuerst das Sprungziel
mitwandern lassen. */
const e = sichtbarEintraege(person);
if (e) {
dazu("bereiche_dringend", "warnung",
+5 -1
View File
@@ -30,7 +30,11 @@ import {
import { sichtbar as sichtbarAufgaben } from "./workspace-aufgaben.js";
import { sichtbar as sichtbarTermine } from "./workspace-kalender.js";
import { sichtbar as sichtbarDateien } from "./workspace-dateien.js";
import { sichtbar as sichtbarEintraege, BEREICHE } from "./workspace-bereiche.js";
/* sichtbarEintrag, NICHT sichtbar: Sonst faende ein Creator ueber die
Suche kein einziges Agentur-Event -- obwohl es auf der Agentur-Seite
vor ihm steht. Eine Suche, die weniger findet als die Seite zeigt,
liest sich wie "gibt es nicht". */
import { sichtbarEintrag as sichtbarEintraege, BEREICHE } from "./workspace-bereiche.js";
export const sucheRouter = express.Router();
+41 -3
View File
@@ -246,6 +246,28 @@ function umstellungen(d) {
["aufgaben", "abgebrochen_am", "TEXT"],
["aufgaben", "abbruch_von", "INTEGER REFERENCES personen(id) ON DELETE SET NULL"],
["aufgaben", "status_vorher", "TEXT"],
/* AGENTUR-EVENTS (07.09.2026).
Ein Event ist kein Notizzettel mit Ueberschrift und Text. Wer
mitmachen soll, muss vier Dinge finden, ohne zu fragen: bis wann
es laeuft, was zu tun ist, was es zu gewinnen gibt und was
verboten ist. Genau daran scheitern Aktionen -- nicht am Aufruf,
sondern an den Rueckfragen danach.
Der BEGINN ist `datum` (gibt es schon, ist NOT NULL). Neu ist
das Ende und die beiden Textfelder. `event_`-Vorsatz, weil es im
Workspace bereits eine Tabelle `aufgaben` gibt -- eine Spalte
`aufgaben` auf `eintraege` waere beim Lesen von Code jedes Mal
eine Verwechslung.
ADD COLUMN und kein Tabellenneubau: Es aendert sich kein CHECK,
also darf die Tabelle stehen bleiben. Der Neubau vom 06.09. war
noetig, weil dort der erlaubte Wertebereich von `bereich` wuchs
-- und er ist der Weg, bei dem man Inhalte verlieren kann. */
["eintraege", "event_ende", "TEXT"],
["eintraege", "event_aufgaben", "TEXT"],
["eintraege", "event_regeln", "TEXT"],
]) {
try {
const vorhanden = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((s) => s.name);
@@ -501,15 +523,31 @@ function umstellungen(d) {
format TEXT,
saeule_id INTEGER,
geplant TEXT,
creator_extern TEXT
creator_extern TEXT,
/* Die drei Event-Spalten MUESSEN hier mit stehen (07.09.2026).
Der Spalten-Nachtrag weiter oben laeuft frueher als dieser
Neubau. Auf einer Datenbank, die noch den alten CHECK hat
-- eine Sicherung von vor dem 06.09., in einen heutigen
Stand eingespielt --, waeren die drei Spalten also erst
angelegt und hier sofort wieder weggeworfen worden: mit
allem, was drinsteht, ohne Fehlermeldung, und die
Zeilenzahl haette weiterhin gestimmt. Genau die
Verlustart, vor der der Kommentar zu dieser Umstellung
warnt. */
event_ende TEXT,
event_aufgaben TEXT,
event_regeln TEXT
);
INSERT INTO eintraege_neu
(id, bereich, art, titel, text, datum, bewertung, dringlichkeit, status,
creator_id, erstellt, erstellt_von, geaendert,
hook, format, saeule_id, geplant, creator_extern)
hook, format, saeule_id, geplant, creator_extern,
event_ende, event_aufgaben, event_regeln)
SELECT id, bereich, art, titel, text, datum, bewertung, dringlichkeit, status,
creator_id, erstellt, erstellt_von, geaendert,
hook, format, saeule_id, geplant, creator_extern
hook, format, saeule_id, geplant, creator_extern,
event_ende, event_aufgaben, event_regeln
FROM eintraege;
DROP TABLE eintraege;
ALTER TABLE eintraege_neu RENAME TO eintraege;