Den Monatswechsel durchgespielt -- und zwei stille Fehler gefunden
Der zentrale Weg dieser Kachel war nie gelaufen: „Am 1. jedes Monats
um 00:00 Uhr starten alle Zaehler automatisch bei 0." Der erste
Oktober lag vor der Auslieferung, der erste November liegt dahinter.
Er laeuft ohne Zutun und betrifft alle gleichzeitig -- waere er
falsch, waere er fuer alle auf einmal falsch, und niemand wuesste
warum.
Dieselbe Lage gab es am 01.09.2026 schon einmal in RunOne (der Umbau
zur Hashkette war seit Tagen live und nie gelaufen). Die Lehre stand
danach in der Projektnotiz, und sie gilt hier woertlich: Was sich
nicht zuruecknehmen laesst, wird vorher auf einer Kopie durchgespielt.
`pruef-monatswechsel.mjs` verstellt dafuer die Uhr -- eine Huelle um
`Date`, ganz oben, vor jedem Import. Damit laeuft der ECHTE Code
durch zwei echte Monatswechsel (20. Oktober -> 1. November, 00:05 ->
3. Dezember), ohne dass eine Zeile dafuer umgebaut werden muesste.
41 Pruefungen, 0 Fehler: Zaehler bei 0, keine Warnung am ersten Tag,
die neuen Zielzahlen greifen, die alten Eintraege stehen unveraendert
da, der Oktober ist zu, der Verlauf misst ihn am OKTOBER-Ziel, und
„Neuer Monat" geht an alle drei -- auch an den, der den Oktober voll
hatte.
WAS DABEI AUFGEFALLEN IST -- zwei Fehler, beide stumm
1. DIE SUCHE NACH DEM ROLLENWECHSEL MASS DIE FALSCHE PERSON.
`WHERE person_id = ?` im Protokoll findet den, der die Aenderung
GEMACHT hat -- also DogFather --, nicht den, dessen Rolle sich
geaendert hat (workspace-personen.js schreibt die betroffene
Nummer ins `detail`). Fuer den Betroffenen fand die Abfrage
deshalb nie etwas; bei DogFather schob jede fremde
Rollenaenderung SEINEN Pflichtbeginn. In der echten Datenbank
stand bei ihm „grund: rollenwechsel" -- ein plausibler Wert aus
der falschen Zeile.
NACHGESEHEN, OB ES SCHADET: Alle sechs Zeilen stehen auf 2026-10,
und das ist ohnehin der frueheste moegliche Monat. Der Fehler
hatte noch keine Wirkung -- er haette sie beim naechsten
Rollenwechsel bekommen.
2. `node:sqlite` BINDET EINE ZAHL ALS REAL.
Der erste Versuch der Reparatur lautete
`detail LIKE ('#' || ? || ' %')` und traf NIE -- ohne Fehler, ohne
Warnung, immer leer. Nachgemessen:
SELECT ('#' || ? || ' %') mit der Zahl 42 -> '#42.0 %'
Gesucht wurde „#42.0 ", gespeichert ist „#42 ". Mit derselben Zahl
als Zeichenkette stimmt es sofort. Das Muster wird jetzt in
JavaScript gebaut; im uebrigen Haus kommt dieselbe Verkettung mit
einer Zahl nicht vor (nachgesehen).
Gefunden hat das nicht das Lesen, sondern eine Pruefung, die den
ECHTEN Weg benutzt (die Route der Personenverwaltung) statt den
Protokolltext selbst zu schreiben. Haette sie ihn selbst
geschrieben, haette sie ihre eigene Annahme geprueft und waere
gruen gewesen.
AUSSERDEM BERICHTIGT
Der Pflichtbeginn wurde EINMAL gesetzt und nie wieder angesehen
(`INSERT OR IGNORE`). Wer die Rolle verliert und spaeter zurueck-
bekommt, haette damit keinen Schonmonat mehr bekommen, obwohl die
Vorlage ihn zusichert. Jetzt wird neu gerechnet, wenn seit der
Festlegung ein Rollenwechsel dazugekommen ist -- und nur dann;
geprueft wird ausdruecklich, dass ein zweiter Abruf nichts bewegt.
GEPRUEFT: 216 + 41 = 257 Pruefungen, 0 Fehler
Mit drei Gegenproben an der neuen Stelle: DogFathers Pflichtbeginn
bewegt sich NICHT, wenn er die Rolle eines anderen aendert; ein
unbeteiligter Scout behaelt seinen Wert; und ein zweiter Abruf
rechnet nichts neu. Ohne die erste waere der Fehler von oben
unentdeckt geblieben, ohne die zweite hiesse „einer hat sich
geaendert" vielleicht „alle".
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -268,23 +268,99 @@ function zielFuer(monat, rolle, aufgabe) {
|
||||
bei jeder Abfrage daraus gerechnet, verschöbe ein Aufräumlauf
|
||||
rückwirkend, seit wann jemand verantwortlich ist.
|
||||
===================================================================== */
|
||||
/** Wann hat sich DIE ROLLE DIESER PERSON zuletzt geändert?
|
||||
*
|
||||
* BERICHTIGT AM 02.10.2026 -- und das war ein echter Fehler, kein
|
||||
* Schönheitsfleck.
|
||||
*
|
||||
* Hier stand `WHERE person_id = ?`. Im Protokoll steht unter
|
||||
* `person_id` aber der, der die Änderung GEMACHT hat (also
|
||||
* DogFather), nicht der, dessen Rolle sich geändert hat -- siehe
|
||||
* workspace-personen.js: `protokolliere("rolle_geaendert",
|
||||
* { personId: req.person.id, … detail: "#<id> <name>: alt -> neu" })`.
|
||||
*
|
||||
* Die Abfrage hat damit zwei Dinge falsch gemacht, und beide sahen
|
||||
* richtig aus:
|
||||
*
|
||||
* 1. Für den BETROFFENEN fand sie nie etwas -- sein Pflichtbeginn
|
||||
* wurde aus dem Anlegedatum gerechnet, als hätte er die Rolle
|
||||
* seit jeher.
|
||||
* 2. Für DogFather fand sie JEDE Rollenänderung, die er an anderen
|
||||
* vorgenommen hat, und schob SEINEN Pflichtbeginn davon. In der
|
||||
* echten Datenbank stand deshalb bei ihm „grund: rollenwechsel"
|
||||
* -- ein plausibler Wert, der aus der falschen Zeile kam.
|
||||
*
|
||||
* Gefunden hat es nicht die Prüfung, sondern ein Blick in die Quelle
|
||||
* beim Nachlesen, wie das Protokoll geschrieben wird. Das ist genau
|
||||
* die Sorte Fehler, vor der die Hausnotiz warnt: Eine Prüfung, die
|
||||
* grün ist, weil sie das Falsche misst.
|
||||
*
|
||||
* JETZT ÜBER DAS `detail`, denn dort steht die betroffene Nummer.
|
||||
* Das ist eine Textsuche in einem Protokolltext und damit die
|
||||
* schwächste Stelle hier -- deshalb wird das Ergebnis nicht jedes
|
||||
* Mal neu gerechnet, sondern in `mz_start` festgehalten. */
|
||||
function rollenWechselAm(personId) {
|
||||
/* DAS MUSTER WIRD HIER GEBAUT UND NICHT IN SQL ZUSAMMENGESETZT.
|
||||
|
||||
Hier stand `detail LIKE ('#' || ? || ' %')`, und das hat NIE
|
||||
getroffen -- ohne Fehler, ohne Warnung, einfach immer leer.
|
||||
Nachgemessen in einer leeren Datenbank:
|
||||
|
||||
SELECT ('#' || ? || ' %') mit der Zahl 42 -> '#42.0 %'
|
||||
|
||||
`node:sqlite` bindet eine JavaScript-Zahl als REAL, und SQLite
|
||||
macht aus 42.0 beim Verketten den Text „42.0". Gesucht wurde
|
||||
also nach „#42.0 ", gespeichert ist „#42 ". Mit derselben Zahl
|
||||
als Zeichenkette (`"42"`) stimmt es sofort.
|
||||
|
||||
Das ist genau die Sorte Fehler, vor der die Hausnotiz warnt: Die
|
||||
Abfrage lief, sie war fehlerfrei, und sie war wertlos. Gefunden
|
||||
hat sie nicht das Lesen, sondern eine Pruefung, die den ECHTEN
|
||||
Weg benutzt hat statt den Protokolltext selbst zu schreiben.
|
||||
|
||||
`Number.isInteger` davor ist kein Schmuck: Eine Zeichenkette mit
|
||||
`%` darin waere sonst ein Platzhalter im Muster. */
|
||||
if (!Number.isInteger(Number(personId))) return null;
|
||||
try {
|
||||
return db().prepare(
|
||||
`SELECT zeitpunkt FROM protokoll
|
||||
WHERE aktion = 'rolle_geaendert' AND detail LIKE ?
|
||||
ORDER BY zeitpunkt DESC LIMIT 1`)
|
||||
.get(`#${Number(personId)} %`)?.zeitpunkt || null;
|
||||
} catch (fehler) {
|
||||
console.error("[manager-ziele] Rollenwechsel suchen:", fehler?.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function startMonatFuer(personId) {
|
||||
const d = db();
|
||||
const da = d.prepare("SELECT erster_monat FROM mz_start WHERE person_id = ?").get(personId);
|
||||
if (da) return da.erster_monat;
|
||||
const da = d.prepare(
|
||||
"SELECT erster_monat, gesetzt_am FROM mz_start WHERE person_id = ?").get(personId);
|
||||
const wechsel = rollenWechselAm(personId);
|
||||
|
||||
/* NEU GERECHNET WIRD NUR, WENN SICH SEITHER ETWAS GEÄNDERT HAT.
|
||||
Vorher stand hier `if (da) return` -- einmal gesetzt, für immer.
|
||||
Damit bekam jemand, der die Rolle verliert und später
|
||||
zurückbekommt, KEINEN Schonmonat mehr, obwohl die Vorlage ihm
|
||||
genau den zusichert („Wer erst im Laufe des Monats eine der
|
||||
Rollen bekommt …"). Es wäre erst in Monaten aufgefallen, und
|
||||
dann als „der wird zu Unrecht gemahnt". */
|
||||
if (da && !(wechsel && wechsel > da.gesetzt_am)) return da.erster_monat;
|
||||
|
||||
const p = d.prepare("SELECT erstellt FROM personen WHERE id = ?").get(personId);
|
||||
if (!p) return ERSTER_MONAT;
|
||||
if (!p) return da?.erster_monat || ERSTER_MONAT;
|
||||
|
||||
/* Der jüngste Rollenwechsel schlägt das Anlegedatum: Wer seit einem
|
||||
Jahr Creator war und gestern Scout wurde, ist nicht seit einem
|
||||
Jahr pflichtig. */
|
||||
const wechsel = d.prepare(
|
||||
`SELECT zeitpunkt FROM protokoll
|
||||
WHERE person_id = ? AND aktion = 'rolle_geaendert'
|
||||
ORDER BY zeitpunkt DESC LIMIT 1`).get(personId);
|
||||
Jahr pflichtig.
|
||||
|
||||
const quelle = wechsel?.zeitpunkt || p.erstellt;
|
||||
JEDER Rollenwechsel zählt, auch der von Scout zu Manager. Das ist
|
||||
eine Entscheidung und keine Nachlässigkeit: Seit die Zielzahlen
|
||||
je Rolle gelten, bekommt man mit einer neuen Rolle wirklich neue
|
||||
Pflichten -- und Rollen ändert ohnehin nur DogFather, hier ist
|
||||
also nichts zu holen. */
|
||||
const quelle = wechsel || p.erstellt;
|
||||
let grund = wechsel ? "rollenwechsel" : "angelegt";
|
||||
let monat = ERSTER_MONAT;
|
||||
try {
|
||||
@@ -295,8 +371,13 @@ function startMonatFuer(personId) {
|
||||
}
|
||||
if (monat < ERSTER_MONAT) { monat = ERSTER_MONAT; grund += "+erster_monat"; }
|
||||
|
||||
d.prepare(`INSERT OR IGNORE INTO mz_start (person_id, erster_monat, gesetzt_am, grund)
|
||||
VALUES (?,?,?,?)`).run(personId, monat, jetzt(), grund);
|
||||
d.prepare(`INSERT INTO mz_start (person_id, erster_monat, gesetzt_am, grund)
|
||||
VALUES (?,?,?,?)
|
||||
ON CONFLICT(person_id) DO UPDATE SET
|
||||
erster_monat = excluded.erster_monat,
|
||||
gesetzt_am = excluded.gesetzt_am,
|
||||
grund = excluded.grund`)
|
||||
.run(personId, monat, jetzt(), grund);
|
||||
return monat;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user