Die rechte Hand fuehrt ihre Aufgaben, jede Karte hat denselben Fuss, und Dubletten lassen sich aufraeumen

=== 1. BEARBEITEN UND LOESCHEN, WAS SIE ANGELEGT HAT ===

Filipe: "kuemmer dich bitte auch drum dass die rechte hand, wenn sie
aufgaben an die modis oder linke hand erstellt, will ich dass sie die
moeglichkeit hat die auch zu bearbeiten und zu loeschen bitte.
perfektionier das fuer sie und fuer dogfather."

WARUM ES VORHER NICHT GING, und es sah nicht danach aus: `creator_id`
heisst nicht "wer hat sie angelegt", sondern "zu wem gehoert sie" (so
steht es am Tabellenkopf). Verteilt die rechte Hand eine Aufgabe an
einen Modi, steht dort der MODI. Sie erfuellte damit an ihrer eigenen
Aufgabe keine der drei Bedingungen von `darfAendern` und bekam 403 --
auf einen Knopf, den die Oberflaeche ihr trotzdem anbot, weil sie ihn
an `darf_verteilen` haengte: eine Auskunft ueber die PERSON, wo die
Frage der AUFGABE gilt.

Die Spalte `erstellt_von` gibt es seit jeher und wird beim Anlegen
gefuellt -- die Sichtbarkeitsregeln fragen sie an sechs Stellen ab. Sie
stand nur nie in dieser einen Zeile. Und sie fehlte in SPALTEN, kam
also in keiner Aufgabe mit: Die neue Regel waere ein Vergleich gegen
`undefined` geblieben.

DIE REGEL IST ALLGEMEIN, NICHT AUF EINE ROLLE GEMUENZT: wer etwas
angelegt hat, darf es auch aendern. Ein Rollenname waere die naechste
zweite Wahrheit -- in dieser Woche ist genau das dreimal veraltet.

LOESCHEN BEKOMMT EINE EIGENE FRAGE, weil es das Einzige ist, was sich
nicht zuruecknehmen laesst: `darfAufgabenVerteilen(person) &&
darfAendern(person, aufgabe)`. Damit darf sie ihre eigenen -- und der
Modi, bei dem die Aufgabe LIEGT, darf sie weiterhin bearbeiten, aber
nicht verschwinden lassen. Ablehnen und Abbrechen sind die Wege dafuer.

Die Loesch-Route holt die Aufgabe jetzt mit der Sichtbarkeitsregel und
antwortet mit 404 statt 403, wenn es sie fuer diese Person nicht gibt
-- sonst liesse sich durch Ausprobieren herausfinden, welche Nummern
vergeben sind. Beim Aendern stand das schon so, eine Route weiter oben.

pruef-verteilen: 19 -> 30 Punkte. Mit drei Gegenproben, ohne die "sie
darf" auch dann gruen waere, wenn jeder alles duerfte: der Modi wird
abgewiesen (403), die Aufgabe steht danach noch da, und eine FREMDE
Aufgabe loescht sie nicht.

=== 2. JEDE KARTE HAT DENSELBEN FUSS ===

Filipe: "wer hat sie soll bitte bei all diesen aufgaben stehen. bei all
diesen kategorien da. ... es soll auch immer gleich aussehen und nicht
manchmal verschoben und so."

ZWEI URSACHEN, und keine davon war Zufall:

  a) "Wer hat sie?" entstand nur, solange oben "Alle" gewaehlt war
     (`if (anAlle)`). Wer auf einen Namen tippte, verlor den Knopf an
     ALLEN zwoelf Karten, ohne dass irgendwo stand, warum. Die Auskunft
     "wer aus dem Team hat diese Vorlage" haengt aber an der VORLAGE,
     nicht an der Auswahl -- sie daran zu binden war der Fehler.

  b) Der Fuss war EINE Reihe mit `flex-wrap`, und wie viele Angaben
     darin stehen, haengt von der Karte ab: "Frist" immer, "fuer:
     Rolle" manchmal, "liegt bei 4 von 5" nur, wenn schon jemand sie
     hat. Karten ohne den dritten Text hatten noch Platz fuer einen
     Knopf, Karten mit ihm nicht -- also stand "An alle" mal neben der
     Frist und mal darunter. Zwoelf Karten, drei verschiedene Fuesse.

Jetzt zwei Reihen mit fester Aufgabe: oben, was man LIEST; unten, was
man DRUECKT. Die Knopfreihe ist immer die letzte Zeile und sitzt am
unteren Rand, also stehen die Knoepfe bei allen Karten einer Reihe auf
derselben Hoehe -- auch wenn der Text darueber verschieden lang ist.

Die Rueckseite verteilt jetzt IMMER an alle. Vorher nahm sie
`katalogZiel()`; solange sie nur bei "Alle" existierte, war das
dasselbe. Seit sie immer da ist, waere es eine Falle: Der Knopf sagt
"Nachholen - 3 fehlen" und gaebe sie einer einzigen Person.

=== 3. DUBLETTEN AUFRAEUMEN ===

Filipe zu "Diene x6 - Ghost x6 - Marina x6 - Miss x6" bei "0 von 24":
"mach aus den 6 1 mal bitte, ich hab mich da geirrt."

`tools/aufgaben-doppelte.mjs` raeumt das auf. Es TUT VON SICH AUS
NICHTS: ohne `--wirklich` zeigt es nur, was passieren wuerde. Mit
`--wirklich` legt es ZUERST eine Kopie der Datenbank an (`VACUUM INTO`,
nicht `cp` -- eine blosse Dateikopie kann das WAL verlieren) und nennt
den Befehl, mit dem man zurueckkommt.

WELCHE BLEIBT, ist nicht beliebig: eine erledigte, wenn es sie gibt
(getane Arbeit wirft man nicht weg), sonst eine begonnene, sonst die
aelteste. An einer Wegwerf-Datenbank durchgespielt: 12 Aufgaben, zwei
Menschen, einer mit einer erledigten darunter -- es blieben genau die
richtigen zwei stehen, die Einzelaufgabe blieb unberuehrt, und das
Nachzaehlen am Ende meldete null Dubletten.

Geprueft: pruef-verteilen (30), pruef-vorlagen (24),
pruef-aufgaben-vorlagen, pruef-aufgabenbrett, pruef-modi-katalog (150),
pruef-bewerbung-aufgaben (101).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-25 03:20:04 +02:00
co-authored by Claude Opus 5
parent c64732b16d
commit 1a14eb7449
47 changed files with 1121 additions and 653 deletions
+88 -4
View File
@@ -327,7 +327,53 @@ function sichtbarRoh(person) {
function darfAendern(person, aufgabe) {
if (istLeitung(person)) return true;
return aufgabe.creator_id === person.id || aufgabe.verantwortlich_id === person.id;
/* WER SIE ANGELEGT HAT, DARF SIE AUCH AENDERN (25.09.2026).
Filipe: „kuemmer dich bitte auch drum dass die rechte hand, wenn
sie aufgaben an die modis oder linke hand erstellt, will ich dass
sie die moeglichkeit hat die auch zu bearbeiten und zu loeschen."
WARUM DAS VORHER NICHT GING, obwohl es so aussah: `creator_id`
sagt nicht, WER die Aufgabe angelegt hat -- es sagt, ZU WEM sie
gehoert (so steht es am Tabellenkopf). Verteilt die rechte Hand
eine Aufgabe an einen Modi, steht dort der Modi. Sie hatte damit
an ihrer eigenen Aufgabe keine einzige der drei Bedingungen
erfuellt und bekam vom Server ein 403 -- auf einen Knopf, den die
Oberflaeche ihr trotzdem zeigte.
`erstellt_von` GIBT ES SEIT JEHER und wird beim Anlegen gefuellt;
die Sichtbarkeitsregeln fragen sie an sechs Stellen ab. Sie stand
nur nie in dieser Zeile.
DIE REGEL IST ALLGEMEIN, nicht auf eine Rolle gemuenzt: Wer etwas
angelegt hat, darf es auch wieder anfassen. Ein Rollenname hier
waere die naechste zweite Wahrheit -- und in diesem Haus ist genau
das in derselben Woche schon dreimal veraltet. */
return aufgabe.creator_id === person.id
|| aufgabe.verantwortlich_id === person.id
|| aufgabe.erstellt_von === person.id;
}
/* ===== UND LOESCHEN? =================================================
Loeschen ist das Einzige, was sich nicht zuruecknehmen laesst --
deshalb eine eigene Frage und nicht einfach `darfAendern`.
ZWEI BEDINGUNGEN, UND BEIDE MUESSEN STIMMEN:
1. Die Person darf Aufgaben ueberhaupt VERTEILEN. Damit ist
ausgeschlossen, dass jemand, dem eine Aufgabe nur zugeteilt
wurde, sie loescht statt sie abzulehnen -- ein Modi soll sie
ablehnen oder abbrechen, nicht verschwinden lassen.
2. Die Person darf sie aendern (siehe oben). Damit ist es genau
das, was Filipe gesagt hat: ihre eigenen.
DOGFATHER UND DIE MANAGER bleiben unveraendert: `istLeitung` macht
`darfAendern` fuer sie immer wahr, und verteilen duerfen sie
ohnehin. Die Zeile ist damit eine Erweiterung, keine Einschraenkung
-- nachgewiesen in pruef-aufgaben-loeschen mit einer Gegenprobe. */
function darfLoeschen(person, aufgabe) {
return darfAufgabenVerteilen(person) && darfAendern(person, aufgabe);
}
const SPALTEN = `
@@ -346,6 +392,16 @@ const SPALTEN = `
daran haengt, ob eine uebernommene Aufgabe fuer die anderen noch
offen ist. */
a.verteilart,
/* WER SIE ANGELEGT HAT (25.09.2026). Sie stand in den
Sichtbarkeitsregeln laengst, fehlte aber in dieser Spaltenliste --
und damit in jeder Aufgabe, die darfAendern zu sehen bekam. Ohne
sie waere die neue Regel eine Zeile, die nie zutrifft: ein
Vergleich gegen undefined.
KEINE RUECKSTRICHE IN DIESEM KOMMENTAR: Die Liste ist ein
Vorlagentext (Backticks), und ein Rueckstrich darin beendet ihn
mitten im Satz. Beim ersten Anlauf hat node genau das gemeldet. */
a.erstellt_von,
/* Die Vorlage, aus der sie entstanden ist (09.09.2026) -- damit das
Vorlagenbrett zeigen kann, was schon geholt wurde. */
a.vorlage,
@@ -423,7 +479,16 @@ aufgabenRouter.get("/workspace/api/aufgaben", (req, res) => {
Schreiben. Zwei Stellen, die dieselbe Frage beantworten, geben
irgendwann zwei Antworten -- und die falsche ist dann die
sichtbare. */
for (const r of reihen) r.darf_aendern = darfAendern(req.person, r);
for (const r of reihen) {
r.darf_aendern = darfAendern(req.person, r);
/* DIE OBERFLAECHE SOLL DEN KNOPF NUR ZEIGEN, WENN ER GEHT.
Bisher entschied sie es an `darf_verteilen` -- einer Auskunft
ueber die PERSON. Loeschen haengt aber an der AUFGABE: Die
rechte Hand darf verteilen, sah also den Knopf an jeder
Aufgabe, und der Server sagte bei fremden 403. Ein Knopf, der
eine Absage holt, ist schlimmer als keiner. */
r.darf_loeschen = darfLoeschen(req.person, r);
}
/* WER HAT DIESE AUFGABE -- UND WIE STEHT SIE BEI IHM (21.09.2026).
In EINER Abfrage fuer alle Zeilen; eine je Aufgabe waeren bei
@@ -1124,12 +1189,31 @@ aufgabenRouter.post("/workspace/api/aufgaben/:id/wiederaufnehmen", gleicheHerkun
aufgabenRouter.delete("/workspace/api/aufgaben/:id", gleicheHerkunft, (req, res) => {
try {
if (!istLeitung(req.person)) return res.status(403).json({ fehler: "nicht_erlaubt" });
const id = Number(req.params.id);
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
const aufgabe = db().prepare("SELECT id, titel FROM aufgaben WHERE id = ?").get(id);
/* ERST MIT DER SICHTBARKEITSREGEL HOLEN, dann fragen, ob sie
geloescht werden darf (25.09.2026).
Vorher stand hier `if (!istLeitung) 403` und danach ein
ungefiltertes `SELECT ... WHERE id = ?`. Zwei Dinge sind daran
jetzt anders:
* Die rechte Hand darf ihre eigenen loeschen -- das ist der
Auftrag. Die Rollenfrage allein konnte das nicht
beantworten, weil sie die AUFGABE nicht ansieht.
* Was jemand nicht sehen darf, gibt es fuer ihn nicht: 404
statt 403. Sonst liesse sich durch Ausprobieren
herausfinden, welche Nummern vergeben sind -- dieselbe
Ueberlegung wie beim Aendern eine Route weiter oben, wo sie
schon stand. */
const { wo, werte } = sichtbar(req.person);
const aufgabe = db().prepare(
`SELECT a.* ${VERBUND} WHERE ${wo} AND a.id = ?`).get(...werte, id);
if (!aufgabe) return res.status(404).json({ fehler: "nicht_gefunden" });
if (!darfLoeschen(req.person, aufgabe)) {
return res.status(403).json({ fehler: "nicht_erlaubt" });
}
db().prepare("DELETE FROM aufgaben WHERE id = ?").run(id);
protokolliere("aufgabe_geloescht", {