Files
dogfather-universe/tools/aufgaben-doppelte.mjs
T
DogFatherGitandClaude Opus 5 1a14eb7449 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]>
2026-09-25 03:20:04 +02:00

186 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
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=<pfad> 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);