From 1a14eb7449199a15e5b7fdfc460a9ce394769c15 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Fri, 25 Sep 2026 03:20:04 +0200 Subject: [PATCH] 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 --- server/pruef-verteilen.mjs | 96 ++++++++++++++ server/workspace-aufgaben.js | 92 ++++++++++++- tools/aufgaben-doppelte.mjs | 185 +++++++++++++++++++++++++++ workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/aufgaben.css | 37 +++++- workspace/assets/js/aufgaben.js | 16 ++- workspace/assets/js/vorlagenbrett.js | 88 ++++++++++--- workspace/aufgaben.html | 38 +++--- workspace/automation.html | 32 ++--- workspace/befinden.html | 34 ++--- workspace/bereich.html | 44 +++---- workspace/bewerben.html | 38 +++--- workspace/bewerbungen.html | 36 +++--- workspace/calls.html | 34 ++--- workspace/chat.html | 38 +++--- workspace/content.html | 34 ++--- workspace/crew-index.html | 12 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 ++--- workspace/entwicklung.html | 36 +++--- workspace/hilfe.html | 30 ++--- workspace/index.html | 10 +- workspace/kalender.html | 34 ++--- workspace/leistung.html | 32 ++--- workspace/material.html | 32 ++--- workspace/personen.html | 32 ++--- workspace/profil.html | 36 +++--- workspace/rechte.html | 34 ++--- workspace/report.html | 36 +++--- workspace/scouting.html | 32 ++--- workspace/start.html | 32 ++--- workspace/startcheck.html | 34 ++--- workspace/steckbrief.html | 36 +++--- workspace/support.html | 28 ++-- workspace/talente.html | 36 +++--- workspace/team.html | 32 ++--- workspace/teamlage.html | 34 ++--- workspace/teilen.html | 30 ++--- workspace/treff-moderation.html | 32 ++--- workspace/treff-regeln.html | 32 ++--- workspace/uebersicht.html | 32 ++--- workspace/unsere-seiten.html | 30 ++--- workspace/unterstuetzen.html | 32 ++--- workspace/werdegang.html | 32 ++--- workspace/willkommen.html | 30 ++--- workspace/wissen.html | 34 ++--- 47 files changed, 1121 insertions(+), 653 deletions(-) create mode 100644 tools/aufgaben-doppelte.mjs diff --git a/server/pruef-verteilen.mjs b/server/pruef-verteilen.mjs index 628022a5..8b267823 100644 --- a/server/pruef-verteilen.mjs +++ b/server/pruef-verteilen.mjs @@ -263,6 +263,102 @@ ok(vonModi.code === 403, "der leere Wert heisst „noch niemand“ und nicht nur „—“"); } +/* ===================================================================== + BEARBEITEN UND LOESCHEN, WAS SIE ANGELEGT HAT (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 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". Verteilt die rechte Hand 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 zeigte. Die Spalte `erstellt_von` gibt + es seit jeher; sie stand nur nie in dieser Regel. + + GEPRUEFT WIRD BEIDES: dass sie darf -- und dass ein Modi es NICHT + darf. Ohne die zweite Haelfte waere „sie darf" auch dann gruen, wenn + jeder alles duerfte. + ===================================================================== */ +melde("\n=== 5. Die rechte Hand an ihren eigenen Aufgaben ==="); +{ + const neu = await ruf("/workspace/api/aufgaben", kHand, "POST", + { titel: "Von der rechten Hand, zum Aendern", verantwortlich_id: idModi, + prioritaet: "mittel" }); + const id = json(neu).id; + ok(neu.code === 201 && Number.isInteger(id), + `die rechte Hand legt eine Aufgabe fuer Diene an (${neu.code}, #${id})`); + + /* DIE AUSKUNFT AN DIE OBERFLAECHE ZUERST. Sie entscheidet, ob der + Knopf ueberhaupt erscheint -- und genau sie hat gefehlt: Bisher + fragte die Seite `darf_verteilen` (eine Auskunft ueber die + PERSON), waehrend Loeschen an der AUFGABE haengt. */ + const ihre = (json(await ruf("/workspace/api/aufgaben", kHand)).aufgaben || []) + .find((a) => a.id === id); + ok(!!ihre && ihre.darf_aendern === true, + `sie bekommt gesagt, dass sie sie aendern darf (${ihre?.darf_aendern})`); + ok(!!ihre && ihre.darf_loeschen === true, + `und dass sie sie loeschen darf (${ihre?.darf_loeschen})`); + + const geaendert = await ruf(`/workspace/api/aufgaben/${id}`, kHand, "PATCH", + { titel: "Von der rechten Hand, geaendert" }); + ok(geaendert.code === 200, `und sie aendert sie wirklich (${geaendert.code})`); + + /* GEGENPROBE 1: Der Modi, bei dem sie LIEGT, darf sie aendern (das + ist sein Arbeitsstand) -- aber nicht loeschen. Verschwinden lassen + ist keine Antwort auf eine Aufgabe; ablehnen und abbrechen sind + die Wege dafuer. */ + const beiModi = (json(await ruf("/workspace/api/aufgaben", kModi)).aufgaben || []) + .find((a) => a.id === id); + ok(!!beiModi && beiModi.darf_loeschen === false, + `Gegenprobe: dem Modi wird Loeschen NICHT angeboten (${beiModi?.darf_loeschen})`); + const modiWeg = await ruf(`/workspace/api/aufgaben/${id}`, kModi, "DELETE"); + ok(modiWeg.code === 403, + ` und der Server lehnt es auch ab (${modiWeg.code})`); + + /* GEGENPROBE 2: Die Aufgabe steht danach noch da. Ein abgelehnter + Versuch, der trotzdem etwas aendert, waere der schlimmste Ausgang. */ + const d2 = new DatabaseSync(process.env.WORKSPACE_DB); + const nochDa = d2.prepare("SELECT titel FROM aufgaben WHERE id = ?").get(id); + d2.close(); + ok(!!nochDa, ` und die Aufgabe steht noch ("${nochDa?.titel}")`); + + /* UND JETZT SIE SELBST. */ + const handWeg = await ruf(`/workspace/api/aufgaben/${id}`, kHand, "DELETE"); + ok(handWeg.code === 200, `die rechte Hand loescht ihre eigene (${handWeg.code})`); + const d3 = new DatabaseSync(process.env.WORKSPACE_DB); + const weg = d3.prepare("SELECT 1 AS x FROM aufgaben WHERE id = ?").get(id); + d3.close(); + ok(!weg, " und sie ist wirklich weg"); + + /* UND DOGFATHER UNVERAENDERT -- „perfektionier das fuer sie UND fuer + dogfather". Seine Rechte kamen aus `istLeitung` und bleiben, wo + sie waren; das hier ist der Nachweis, dass der Umbau sie nicht + nebenbei mitgenommen hat. */ + const seine = await ruf("/workspace/api/aufgaben", kDogi, "POST", + { titel: "Von DogFather, zum Loeschen", verantwortlich_id: idModi2, + prioritaet: "mittel" }); + const id2 = json(seine).id; + const dogiWeg = await ruf(`/workspace/api/aufgaben/${id2}`, kDogi, "DELETE"); + ok(seine.code === 201 && dogiWeg.code === 200, + `DogFather legt an und loescht wie bisher (${seine.code} / ${dogiWeg.code})`); + + /* UND EINE FREMDE: Die rechte Hand an einer Aufgabe, die DogFather + fuer sich selbst angelegt hat. Sie hat sie nicht angelegt, sie + liegt nicht bei ihr -- also nein. Ohne diese Zeile hiesse „sie + darf ihre eigenen" in Wahrheit vielleicht „sie darf alle". */ + const fremd = await ruf("/workspace/api/aufgaben", kDogi, "POST", + { titel: "Nur fuer DogFather", verantwortlich_id: idDogi, prioritaet: "mittel" }); + const id3 = json(fremd).id; + const handFremd = await ruf(`/workspace/api/aufgaben/${id3}`, kHand, "DELETE"); + ok(fremd.code === 201 && handFremd.code === 403, + `Gegenprobe: eine fremde Aufgabe loescht sie nicht (${handFremd.code})`); +} + console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } process.exit(fehler ? 1 : 0); diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index 8cdb0183..26dcea6a 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -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", { diff --git a/tools/aufgaben-doppelte.mjs b/tools/aufgaben-doppelte.mjs new file mode 100644 index 00000000..32ede87e --- /dev/null +++ b/tools/aufgaben-doppelte.mjs @@ -0,0 +1,185 @@ +/* ===================================================================== + DOPPELT VERTEILTE AUFGABEN AUFRAEUMEN + ===================================================================== + + Filipe, 25.09.2026, zu einer Karte mit „Diene ×6 · Ghost ×6 · + Marina ×6 · Miss ×6" und „0 von 24": + „mach aus den 6 1 mal bitte, ich hab mich da geirrt." + + WAS PASSIERT IST: Dieselbe Vorlage wurde mehrfach an dieselben + Menschen verteilt. Das Brett zeigt das seit dem 22.09. selbst an + (das Band „N Aufgaben stehen doppelt im Brett" und das „×N" an den + Namen) -- es konnte es bisher nur nicht aufraeumen. + + --------------------------------------------------------------------- + WAS DIESES WERKZEUG TUT + + Es sucht Gruppen aus derselben VORLAGE bei derselben PERSON und + laesst von jeder Gruppe genau EINE Aufgabe stehen. + + WELCHE BLEIBT, und das ist nicht beliebig: + 1. eine erledigte, wenn es sie gibt -- getane Arbeit wirft man + nicht weg, auch nicht versehentlich; + 2. sonst eine begonnene; + 3. sonst die aelteste (kleinste Nummer) -- die, an der die + Frist und die Vorgeschichte haengen. + + --------------------------------------------------------------------- + ES TUT VON SICH AUS NICHTS + + Ohne `--wirklich` wird nur gezeigt, was passieren WUERDE. Das ist + Absicht: Dies ist ein laufendes System, und Loeschen ist das + Einzige, was sich nicht zuruecknehmen laesst. + + MIT `--wirklich` wird ZUERST eine Kopie der Datenbank angelegt und + ihr Pfad ausgegeben, und erst dann geloescht. Geht etwas schief, + steht in einer Zeile, wie man zurueckkommt. + + `PRAGMA foreign_keys = ON` ist keine Foermlichkeit: Ohne sie bleiben + die Zuteilungs-Zeilen (`aufgaben_zuteilung`) als Reste zurueck, die + auf eine Aufgabe zeigen, die es nicht mehr gibt. + + --------------------------------------------------------------------- + AUFRUFEN + + node tools/aufgaben-doppelte.mjs (nur ansehen) + node tools/aufgaben-doppelte.mjs --wirklich (aufraeumen) + + Mit WORKSPACE_DB laesst sich eine andere Datenbank angeben -- so + probiert man es zuerst auf einer Kopie aus. + ===================================================================== */ +import { DatabaseSync } from "node:sqlite"; +import { existsSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HIER = dirname(fileURLToPath(import.meta.url)); +const PFAD = process.env.WORKSPACE_DB + || join(HIER, "..", "..", "workspace-daten", "workspace.db"); + +if (!existsSync(PFAD)) { + console.error(`ABBRUCH: Datenbank nicht gefunden: ${PFAD}`); + console.error("Mit WORKSPACE_DB= den richtigen Ort angeben."); + process.exit(2); +} + +const wirklich = process.argv.includes("--wirklich"); +console.log(`Datenbank: ${PFAD}`); +console.log(wirklich ? "Modus: AUFRAEUMEN" : "Modus: nur ansehen (--wirklich fehlt)"); +console.log(""); + +const d = new DatabaseSync(PFAD); +d.exec("PRAGMA foreign_keys = ON"); + +/* Nur Aufgaben AUS EINER VORLAGE und nur mit einer verantwortlichen + Person. Zwei frei getippte Aufgaben mit demselben Titel sind keine + Dublette -- vielleicht macht jemand dasselbe zweimal mit Absicht. + Die Vorlage ist der Beleg, dass es derselbe Handgriff war. */ +const reihen = d.prepare(` + SELECT a.id, a.vorlage, a.verantwortlich_id, a.status, a.titel, a.erstellt, + p.name AS wer + FROM aufgaben a + LEFT JOIN personen p ON p.id = a.verantwortlich_id + WHERE a.vorlage IS NOT NULL AND a.verantwortlich_id IS NOT NULL + ORDER BY a.vorlage, a.verantwortlich_id, a.id`).all(); + +const gruppen = new Map(); +for (const r of reihen) { + const schluessel = `${r.vorlage} ${r.verantwortlich_id}`; + if (!gruppen.has(schluessel)) gruppen.set(schluessel, []); + gruppen.get(schluessel).push(r); +} + +/* Je weiter eine Aufgabe ist, desto eher bleibt sie stehen. */ +const RANG = { erledigt: 0, in_arbeit: 1, angenommen: 2, offen: 3 }; +const rangVon = (r) => (RANG[r.status] ?? 4); + +const weg = []; +let betroffen = 0; +for (const [, liste] of gruppen) { + if (liste.length < 2) continue; + betroffen++; + const sortiert = [...liste].sort((a, b) => rangVon(a) - rangVon(b) || a.id - b.id); + const bleibt = sortiert[0]; + for (const r of sortiert.slice(1)) weg.push({ ...r, statt: bleibt.id, stattStatus: bleibt.status }); +} + +if (!weg.length) { + console.log("Nichts doppelt. Es gibt nichts aufzuraeumen."); + d.close(); + process.exit(0); +} + +/* Nach Mensch und Vorlage zusammengefasst -- eine Liste mit 92 Zeilen + liest niemand, „Diene: 5 von 6 weg" schon. */ +/* DER SCHLUESSEL IST EIN PAAR, KEIN ZUSAMMENGEKLEBTER TEXT. Beim + ersten Anlauf stand hier Name + Leerzeichen + Titel und unten ein + split(" ") -- damit endete jeder Titel beim ersten Leerzeichen. + Ein Trennzeichen, das im Inhalt vorkommt, ist keins. */ +const proMensch = new Map(); +for (const r of weg) { + const k = JSON.stringify([r.wer || "?", r.titel]); + proMensch.set(k, (proMensch.get(k) || 0) + 1); +} +console.log(`${betroffen} Vorlage/Person-Paare haben Dubletten.`); +console.log(`${weg.length} Aufgaben wuerden geloescht, je eine bleibt stehen:\n`); +for (const [k, n] of [...proMensch].sort((a, b) => b[1] - a[1])) { + const [wer, titel] = JSON.parse(k); + console.log(` ${String(n).padStart(3)}x weg ${wer.padEnd(12)} ${titel.slice(0, 60)}`); +} +console.log(""); + +if (!wirklich) { + console.log("Nichts geaendert. Zum Aufraeumen denselben Befehl mit --wirklich"); + d.close(); + process.exit(0); +} + +/* ---- Erst sichern, dann loeschen ---------------------------------- */ +const stempel = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); +const sicherung = `${PFAD}.vor-doppelte-${stempel}`; +/* `VACUUM INTO` statt `copyFileSync`: Es schreibt eine in sich + geschlossene Kopie, auch wenn gerade ein WAL danebenliegt. Eine + blosse Dateikopie kann die letzten Schreibvorgaenge verlieren -- + genau die, die man im Ernstfall braucht. */ +try { + d.exec(`VACUUM INTO '${sicherung.replace(/'/g, "''")}'`); +} catch (f) { + console.error("ABBRUCH: Sicherung fehlgeschlagen --", f?.message); + console.error("Es wurde NICHTS geloescht."); + d.close(); + process.exit(3); +} +console.log(`Sicherung: ${sicherung}`); + +const loeschen = d.prepare("DELETE FROM aufgaben WHERE id = ?"); +let n = 0; +d.exec("BEGIN"); +try { + for (const r of weg) { loeschen.run(r.id); n++; } + d.exec("COMMIT"); +} catch (f) { + d.exec("ROLLBACK"); + console.error("ABBRUCH beim Loeschen --", f?.message); + console.error("Es wurde nichts geaendert (Rollback)."); + d.close(); + process.exit(4); +} + +/* NACHZAEHLEN, NICHT GLAUBEN. Die Zahl aus der Schleife sagt, wie oft + der Befehl lief -- nicht, wie es danach aussieht. */ +const nachher = d.prepare(` + SELECT COUNT(*) AS n FROM ( + SELECT vorlage, verantwortlich_id + FROM aufgaben + WHERE vorlage IS NOT NULL AND verantwortlich_id IS NOT NULL + GROUP BY vorlage, verantwortlich_id + HAVING COUNT(*) > 1)`).get().n; + +console.log(`\n${n} Aufgaben geloescht.`); +console.log(nachher === 0 + ? "Nachgezaehlt: keine Dublette mehr uebrig." + : `ACHTUNG: es stehen noch ${nachher} Paare doppelt -- bitte melden.`); +console.log(`\nZurueck geht es mit: cp "${sicherung}" "${PFAD}"`); +d.close(); +process.exit(nachher === 0 ? 0 : 5); diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index e154979a..60292bd2 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +