/* ===================================================================== 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);