diff --git a/server/mess-alle-einzelsicht.mjs b/server/mess-alle-einzelsicht.mjs new file mode 100644 index 00000000..51c9cb48 --- /dev/null +++ b/server/mess-alle-einzelsicht.mjs @@ -0,0 +1,246 @@ +/* ===================================================================== + WER HAT SIE, WER NICHT — die Einzelansicht bei „An alle" (24.09.2026) + + --------------------------------------------------------------------- + WARUM ES DIESE DATEI GIBT + + Filipe: „wenn ich eine aufgabe an alle verteile will ich dass + dogfather und die rechte hand individuel von jedem sehen wer es + gemacht hat oder nicht. aufgabe an alle verteilen aber individuel + sehen wir das." + + Gebaut ist das in `vorlagenbrett.js`. Ob es AUSSIEHT wie gedacht, + beantwortet kein Quelltext: Die Marken stehen in einer Reihe, die + umbricht, in einer Karte, die schmaler wird — und auf einem Handy + ist die Karte 380 px breit, nicht 400. + + SIE PRUEFT NICHTS und meldet keinen Fehlschlag. Sie macht ein Bild, + auf dem man nachsehen kann. Deshalb `mess-` und nicht `pruef-`: Die + Portvergabe leitet sich aus den `pruef-`-Namen ab, und eine + Messdatei darf die Nummern der Pruefungen nicht verschieben. + + --------------------------------------------------------------------- + EIGENE WEGWERF-DATENBANK, NIE DIE ECHTE + + Es entstehen echte Aufgaben — vier Stueck, damit alle vier Zustaende + im Bild sind. In einem laufenden System waeren das Testdaten, und die + sind verboten. Hier liegt die Datenbank in einem `mktemp`-Ordner und + wird am Ende geloescht. + + Aufrufen mit: node server/mess-alle-einzelsicht.mjs + ===================================================================== */ +import { mkdtempSync, rmSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { execFileSync } from "node:child_process"; +import { request as httpAnfrage } from "node:http"; +import { createServer as httpsServer } from "node:https"; + +const PORT = 5397, HP = 5398; +const CREW = "crew.dogfather-universe.com"; +const ordner = mkdtempSync(join(tmpdir(), "ws-alle-sicht-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { + return ec.call(this, n, w, { ...(o || {}), secure: false }); +}; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(240_000, "mess-alle-einzelsicht"); +await new Promise((r) => setTimeout(r, 900)); + +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); +process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const sch = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", sch, jetzt); + +const anlegen = (name, rolle, code) => { + const s = randomBytes(16).toString("hex"); + const h = scryptSync(code, s, 64, + { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + return d.prepare("INSERT INTO personen" + + " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + + " VALUES (?,?,?,?,?,?,1,?) RETURNING id") + .get(name, rolle, h, s, 32768, + createHmac("sha256", sch).update(code).digest("hex"), jetzt).id; +}; + +/* Vier Empfaenger und eine rechte Hand — genau die Besetzung, um die es + geht: Die Haende sind getrennt (eine verteilt, eine bekommt). */ +const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); +const idHand = anlegen("VanVan", "hand", "CODE-HAND-0001"); +const idLinke = anlegen("Kessi", "linke", "CODE-LINKE-0001"); +const idM1 = anlegen("Diene", "modi", "CODE-MODI-0001"); +const idM2 = anlegen("Marina", "modi", "CODE-MODI-0002"); +const idM3 = anlegen("Ghost", "modi", "CODE-MODI-0003"); +void idDogi; void idHand; +d.close(); + +const anfrage = (pfad, art, koerper, keks) => new Promise((fertig, schief) => { + const a = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: pfad, method: art, + headers: { + Host: CREW, "Content-Type": "application/json", + ...(keks ? { Cookie: keks } : {}), + }, + }, (r) => { + let t = ""; r.on("data", (x) => { t += x; }); + r.on("end", () => fertig({ code: r.statusCode, kopf: r.headers, text: t })); + }); + a.on("error", schief); + if (koerper) a.write(JSON.stringify(koerper)); + a.end(); +}); + +const an = await anfrage("/workspace/api/anmelden", "POST", + { rolle: "admin", code: "CODE-DOGI-0001" }); +if (an.code !== 200) { + console.log(`ABBRUCH: DogFather ließ sich nicht anmelden (HTTP ${an.code}).`); + process.exit(2); +} +const keks = (an.kopf["set-cookie"] || []).map((x) => x.split(";")[0]).join("; "); + +/* ---- Ein Stand, auf dem man ALLE VIER Zustaende sieht ---------------- + Sonst zeigt das Bild nur „hat sie nicht", und man weiss nachher + nicht, ob die Farben stimmen. */ +/* Der Katalog steckt UNTER `katalog` in der Antwort -- die Route + liefert daneben noch Bewerbungen und Rechte. */ +const kat = JSON.parse((await anfrage("/workspace/api/vorlagen", "GET", null, keks)).text); +const liste = kat?.katalog?.aufgaben; +if (!Array.isArray(liste) || !liste.length) { + console.log("ABBRUCH: kein Katalog in der Antwort --", + JSON.stringify(kat).slice(0, 300)); + process.exit(3); +} +const v0 = liste.map((v, i) => ({ ...v, nr: i }))[0]; +const verteilt = await anfrage("/workspace/api/vorlagen/uebernehmen", "POST", + { art: "katalog", nr: v0.nr, gruppe: v0.gruppe, person_id: "alle" }, keks); +console.log(`Verteilt an alle: HTTP ${verteilt.code} ${verteilt.text.slice(0, 80)}`); + +const d2 = new DatabaseSync(process.env.WORKSPACE_DB); +/* Einer hat sie erledigt, einer ist ueberfaellig, einer hat sie gar + nicht — die Linke Hand behaelt ihr schlichtes „Offen". */ +d2.prepare("UPDATE aufgaben SET status = 'erledigt' WHERE vorlage = ? AND verantwortlich_id = ?") + .run(v0.schluessel, idM1); +d2.prepare("UPDATE aufgaben SET frist = '2026-09-01' WHERE vorlage = ? AND verantwortlich_id = ?") + .run(v0.schluessel, idM2); +d2.prepare("DELETE FROM aufgaben WHERE vorlage = ? AND verantwortlich_id = ?") + .run(v0.schluessel, idM3); +const stand = d2.prepare( + "SELECT verantwortlich_id AS w, status, frist FROM aufgaben WHERE vorlage = ?") + .all(v0.schluessel); +d2.close(); +console.log("Stand:", JSON.stringify(stand), + `\n (${idLinke}=Kessi/linke, ${idM1}=Diene, ${idM2}=Marina, ${idM3}=Ghost)`); + +const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt"); +execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", schl, + "-out", zert, "-days", "2", "-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`], +{ stdio: "ignore" }); +const vorbau = httpsServer({ key: readFileSync(schl), cert: readFileSync(zert) }, + (von, nach) => { + const w = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: von.url, method: von.method, + headers: { ...von.headers, host: `${CREW}:${HP}` }, + }, (a) => { nach.writeHead(a.statusCode || 502, a.headers); a.pipe(nach); }); + von.pipe(w); + }); +await new Promise((r) => vorbau.listen(HP, "127.0.0.1", r)); + +const pw = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await pw.chromium.launch({ + args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors"], +}); +try { + /* ZWEI BREITEN. 412 px ist die haeufigste Android-Breite; an ihr ist + am 06.09. die Kopfleiste gescheitert. 1280 px ist der kleinste + Rechner, an dem noch jemand arbeitet. */ + for (const [wie, breite, hoehe] of [["handy", 412, 1400], ["computer", 1280, 1600]]) { + const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe } }); + await kontext.addCookies(keks.split("; ").map((k) => { + const [n, ...r] = k.split("="); + return { name: n, value: r.join("="), domain: CREW, path: "/" }; + })); + const seite = await kontext.newPage(); + const konsole = []; + seite.on("console", (e) => { + if (e.type() === "error" && !/favicon|ERR_CERT|net::ERR/i.test(e.text())) { + konsole.push(e.text().slice(0, 140)); + } + }); + await seite.goto(`https://${CREW}:${HP}/workspace/entwicklung.html`, + { waitUntil: "networkidle" }); + await seite.waitForTimeout(1200); + + /* Das Brett startet zugeklappt — aufklappen, sonst ist das Bild leer. */ + await seite.evaluate(() => { + const kopf = document.querySelector('.vorlagenbrett__kopf[aria-expanded="false"]'); + if (kopf) kopf.click(); + }); + await seite.waitForTimeout(700); + + /* Den Alle-Knopf waehlen -- das ist der Fall, um den es geht. */ + const traf = await seite.evaluate(() => { + const k = document.querySelector('.stufenleiter--wer [data-alle="ja"]'); + if (!k) return false; + k.click(); return true; + }); + await seite.waitForTimeout(1400); + + const gesehen = await seite.evaluate(() => { + const karte = document.querySelector('.vorlagenbrett__karte .vorlagenbrett__wer') + ?.closest('.vorlagenbrett__karte'); + const reihe = [...(karte?.querySelectorAll('.vorlagenbrett__wer-p') || [])] + .map((m) => ({ + wer: m.querySelector('.vorlagenbrett__wer-name')?.textContent, + stand: m.querySelector('.vorlagenbrett__wer-stand')?.textContent, + art: m.dataset.stand, + })); + return { + wen: [...document.querySelectorAll('.stufenleiter--wer .stufenleiter__knopf')] + .map((k) => k.textContent.trim().replace(/\s+/g, ' ')), + titel: karte?.querySelector('.vorlagenbrett__titel')?.textContent, + fuss: karte?.querySelector('.vorlagenbrett__schon')?.textContent, + knopf: karte?.querySelector('.knopf')?.textContent, + hinweis: karte?.querySelector('.knopf')?.title, + reihe, + mitReihe: document.querySelectorAll('.vorlagenbrett__wer').length, + }; + }); + + console.log(`\n=== ${wie} (${breite} px) ===`); + console.log(" Alle-Knopf getroffen:", traf); + console.log(" An wen:", JSON.stringify(gesehen.wen)); + console.log(" Karte :", gesehen.titel); + console.log(" Fuss :", gesehen.fuss, "| Knopf:", gesehen.knopf); + console.log(" Hinweis:", gesehen.hinweis); + console.log(" Karten mit Namensreihe:", gesehen.mitReihe); + for (const r of gesehen.reihe) console.log(` ${r.wer} -> ${r.stand} [${r.art}]`); + if (konsole.length) console.log(" FEHLER IN DER KONSOLE:", konsole); + + const karte = seite.locator('.vorlagenbrett__karte', { has: seite.locator('.vorlagenbrett__wer') }).first(); + if (await karte.count()) { + await karte.screenshot({ path: `server/mess-alle-${wie}.png` }); + console.log(` Bild: server/mess-alle-${wie}.png`); + } + await kontext.close(); + } +} finally { + await browser.close(); + vorbau.close(); + try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } + process.exit(0); +} diff --git a/server/pruef-modi-katalog.mjs b/server/pruef-modi-katalog.mjs index 0cf47a6b..e1d2a0e1 100644 --- a/server/pruef-modi-katalog.mjs +++ b/server/pruef-modi-katalog.mjs @@ -444,7 +444,15 @@ console.log("\n=== 4. DogFather trägt für einen Modi ein ===\n"); method: "POST", headers: jsonKopf, body: JSON.stringify({ art: "katalog", nr, gruppe: "monat", person_id: idCreat }), }); - ok(b.status === 404, `an einen Creator dagegen nicht (${b.status})`); + /* STRENGER ALS VORHER (24.09.2026). Hier stand nur `status === 404`. + Das war zugleich die Antwort auf „die Person gibt es nicht" -- und + auf dem Bildschirm stand deshalb „Das gibt es nicht mehr, lade die + Seite neu" zu jemandem, den es sehr wohl gibt. Ein Neuladen half + nie. Seit heute trennt der Server die beiden Faelle, und geprueft + wird das WORT, nicht nur die Nummer: Ein Zahlendreher woanders + koennte sonst denselben Haken setzen. */ + ok(b.status === 400 && b.daten?.fehler === "rolle_ohne_katalog", + `an einen Creator dagegen nicht (${b.status} ${b.daten?.fehler || ""})`); /* Und ohne Person gar nicht -- eine Aufgabe ohne Adressaten ist ein Zettel. */ @@ -1205,6 +1213,165 @@ console.log("\n=== 7. Gegenprobe: kann das hier ueberhaupt scheitern? ===\n"); ok(e.status === 400, `und eine erfundene Vorlage in der Bewerbung (${e.status})`); } +/* ---------- 8. Wer aus dem Katalog etwas bekommen kann --------------- * + * + * ER STEHT GANZ HINTEN, OBWOHL ER INHALTLICH ZU 4 GEHOERT. + * + * Grund: Er legt Aufgaben an, und Abschnitt 6 sieht im Browser nach, + * wie viele Karten noch einen "Bewerben"-Knopf tragen -- eine Karte, + * die man schon hat, traegt keinen. Stand er vorn, fiel diese Zahl von + * zehn auf neun, und eine Pruefung wurde rot, an der nichts kaputt war. + * + * Die Schwelle dort (knoepfe.length >= 10) ist eine feste Zahl und + * damit eine Rechnung von gestern -- wer dem Modi kuenftig Aufgaben + * gibt, bringt sie wieder zum Kippen. Das hier ist die kleine Loesung + * (nicht stoeren); die grosse waere, die Zahl dort abzuleiten statt sie + * zu setzen. Wer das anfasst, darf diesen Abschnitt zurueckschieben. + */ +/* ===================================================================== + Filipe, 24.09.2026: „wenn ich eine aufgabe an alle verteile will ich + dass dogfather und die rechte hand individuel von jedem sehen wer es + gemacht hat oder nicht." + + WARUM DIESER ABSCHNITT NEU IST. „An alle" gab es seit dem Vormittag, + und geprueft war davon genau ein Fall: dass eine erfundene Gruppe + abgelehnt wird. Der Weg, der tatsaechlich benutzt wird -- an alle + verteilen und nachsehen, wer sie hat --, lief ungeprueft. + + UND DABEI LAG ER FALSCH: Die „An wen"-Reihe im Browser zeigte sechs + Personen (rechte Hand, linke Hand, vier Modis), der Server nahm aber + nur Modis an. „An alle 6" legte vier Aufgaben an, und die beiden + Haende gingen leer aus -- ohne ein Wort. Das ist kein Randfall: Es + ist der Normalfall dieses Knopfes. + + GEPRUEFT WIRD IN BEIDE RICHTUNGEN. Wer etwas bekommen soll, bekommt + es (200); wer nicht, bekommt eine Absage MIT GRUND (400). Eine + Pruefung, die nur das Gelingen kennt, kann nicht scheitern -- und + bestaetigt damit nichts. + ===================================================================== */ +console.log("\n=== 8. Wer aus dem Katalog etwas bekommen kann ===\n"); +{ + /* VORLAGEN, DIE IN DIESEM LAUF NOCH NIEMAND HAT -- gefragt, nicht + getippt. Im ersten Anlauf stand hier `gruppe === "monat"`; diese + Gruppe gibt es gar nicht (sie stammte aus einer Absage-Pruefung, + in der der Name nie geprueft wurde), und `frei[0]` war undefined. + Eine abgeschriebene Kategorie ist dieselbe Falle wie eine feste + Zahl. So gefragt bleibt die Pruefung richtig, auch wenn der + Katalog morgen andere Gruppen hat. */ + /* EIN EIGENER, LESENDER GRIFF. Der von ganz oben ist seit Zeile 87 + zu -- weiter oben machen es die Bewerbungen mit `d2` schon so. Ein + offen gelassener Griff waere schlimmer: Die Pruefung wuerde am + Ende nicht mehr enden. */ + const { DatabaseSync: DB4 } = await import("node:sqlite"); + const d4 = new DB4(process.env.WORKSPACE_DB, { readOnly: true }); + const belegt = new Set(d4.prepare( + "SELECT DISTINCT vorlage AS v FROM aufgaben WHERE vorlage IS NOT NULL") + .all().map((x) => x.v)); + d4.close(); + const frei = katalog.aufgaben + .map((v, i) => ({ ...v, nr: i })) + .filter((v) => !belegt.has(v.schluessel)); + ok(frei.length >= 2, + `es gibt genug unbenutzte Vorlagen zum Pruefen (${frei.length})`); + + /* --- Die linke Hand GEHOERT DAZU (neu seit 24.09.2026) ------------ + Filipes Regel vom 22.09.: „die modis und linke hand sollen ... sich + fuer aufgaben bewerben koennen aber die rechte hand oder dogfather + muessen annehmen oder ablehnen". Also ist sie Empfaengerin. */ + const l = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog", nr: frei[0].nr, gruppe: frei[0].gruppe, person_id: idLinke }), + }); + ok(l.status === 200 && l.daten?.angelegt === 1, + `an die linke Hand geht es (${l.status})`); + + /* --- Die rechte Hand NICHT. Sie verteilt; stuende sie in ihrer + eigenen Verteilliste, waere „an alle" auch „an mich selbst". */ + const h = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog", nr: frei[0].nr, gruppe: frei[0].gruppe, person_id: idHand }), + }); + ok(h.status === 400 && h.daten?.fehler === "rolle_ohne_katalog", + `an die rechte Hand dagegen nicht (${h.status} ${h.daten?.fehler || ""})`); + + /* --- Und jetzt „an alle" ------------------------------------------ + Im Haus dieses Laufs gibt es genau EINEN Modi und EINE linke Hand. + „Alle" muss also ZWEI Aufgaben anlegen -- nicht eine (dann waere + die linke Hand wieder vergessen) und nicht drei (dann haette die + rechte Hand eine bekommen). Die Zahl allein reicht aber nicht: + Drei falsche Leute waeren auch drei. Deshalb wird danach + nachgesehen, WER sie wirklich hat. */ + const alle = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog", nr: frei[1].nr, gruppe: frei[1].gruppe, person_id: "alle" }), + }); + ok(alle.status === 200 && alle.daten?.angelegt === 2, + `„an alle" legt bei beiden Empfaengern an (${alle.status}, ${alle.daten?.angelegt})`); + + const d5 = new DB4(process.env.WORKSPACE_DB, { readOnly: true }); + const hats = new Set(d5.prepare( + "SELECT verantwortlich_id AS w FROM aufgaben WHERE vorlage = ?") + .all(frei[1].schluessel).map((x) => Number(x.w))); + ok(hats.has(idModi) && hats.has(idLinke), + `und zwar beim Modi UND bei der linken Hand (${[...hats].join(", ")})`); + ok(!hats.has(idHand) && !hats.has(idCreat), + "und bei niemandem sonst"); + d5.close(); + + /* --- "AN ALLE" FUELLT LUECKEN, ES DOPPELT NICHT ------------------- + Filipe, 24.09.2026: „Nur die 2 Fehlenden bekommen sie." + Derselbe Druck ein zweites Mal: Beide haben sie schon, also darf + KEINE dazukommen -- und der Server muss es SAGEN, sonst sieht ein + Knopf, der nichts tut, wie ein Fehler aus. Am 22.09. sind aus zwei + Klicks 112 Aufgaben geworden, weil niemand nachgezaehlt hat. */ + const wieder = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog", nr: frei[1].nr, gruppe: frei[1].gruppe, person_id: "alle" }), + }); + const zaehle = (schluessel) => { + const dz = new DB4(process.env.WORKSPACE_DB, { readOnly: true }); + const n = dz.prepare( + "SELECT COUNT(*) AS n FROM aufgaben WHERE vorlage = ?").get(schluessel)?.n; + dz.close(); + return Number(n); + }; + ok(zaehle(frei[1].schluessel) === 2, + `ein zweiter Druck legt nichts dazu (${zaehle(frei[1].schluessel)} statt 2)`); + ok(wieder.daten?.uebersprungen === 2, + `und er sagt, dass er zwei uebersprungen hat (${wieder.daten?.uebersprungen})`); + + /* --- ABER DIE GEGENPROBE: gemeint ist gemeint --------------------- + Eine Sperre, die sich nicht bewusst umgehen laesst, verbietet die + Wochenaufgabe. Steht am Knopf "Nochmal" (weil wirklich alle sie + haben), schickt der Browser `nochmal: true` -- und DANN muss die + Zahl steigen. Ohne diese Probe koennte die Sperre auch schlicht + alles blockieren und saehe von aussen genauso richtig aus. */ + const absicht = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ + art: "katalog", nr: frei[1].nr, gruppe: frei[1].gruppe, + person_id: "alle", nochmal: true, + }), + }); + ok(absicht.status === 200 && zaehle(frei[1].schluessel) === 4, + `ein gewolltes "Nochmal" legt sehr wohl an (${zaehle(frei[1].schluessel)} statt 4)`); + + /* --- Und eine LUECKE wird wirklich gefuellt ------------------------ + Der eigentliche Fall: Einer hat sie, der andere nicht. Dann darf + genau EINE entstehen -- nicht null (dann waere die Sperre zu + scharf) und nicht zwei (dann doppelt sie). `frei[0]` liegt seit + oben bei der linken Hand, beim Modi nicht. */ + const luecke = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog", nr: frei[0].nr, gruppe: frei[0].gruppe, person_id: "alle" }), + }); + ok(luecke.daten?.angelegt === 1 && luecke.daten?.uebersprungen === 1, + `eine Luecke wird gefuellt, der Rest nicht gedoppelt ` + + `(${luecke.daten?.angelegt} neu, ${luecke.daten?.uebersprungen} uebersprungen)`); + ok(zaehle(frei[0].schluessel) === 2, + `und danach hat sie jeder genau einmal (${zaehle(frei[0].schluessel)})`); +} + console.log(`\n${fehler ? "FEHLGESCHLAGEN" : "BESTANDEN"} -- ` + `${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } diff --git a/server/workspace-entwicklung.js b/server/workspace-entwicklung.js index ba0e9969..2ed87fa5 100644 --- a/server/workspace-entwicklung.js +++ b/server/workspace-entwicklung.js @@ -61,7 +61,7 @@ import express from "express"; import { db, sitzungLesen, protokolliere, echteIp, fuehrtTeamDogi, ROLLEN_NAME, - ROLLEN_SORTIERUNG, MODI_KATEGORIEN, kategorienFuer, + ROLLEN_SORTIERUNG, MODI_KATEGORIEN, kategorienFuer, darfKatalogBekommen, } from "./workspace.js"; /* Die Aufgaben, die ein Neuer bekommen kann -- dieselbe Liste, aus der auch die Aufgabenseite ihre Vorschlaege baut. Eine zweite waere die, @@ -532,6 +532,14 @@ entwicklungRouter.get("/workspace/api/entwicklung/lage", (req, res) => { personen: personen.map((p) => ({ id: p.id, name: p.name, rolle_name: ROLLEN_NAME[p.rolle] ?? p.rolle, + /* OB SIE EINE KATALOG-VORLAGE BEKOMMEN KANN -- vom Server + beantwortet, nicht im Browser nachgerechnet. Der Browser + kennt die Rolle nur als ANZEIGENAME ("Rechte Hand"); wer + danach entscheidet, entscheidet nach einem Text, den man + aendern darf. Und eine zweite Rollenliste im Browser waere + genau die Doppelung, die diesen Fehler erzeugt hat -- die + "An wen"-Reihe zeigte sechs Leute, beliefert wurden vier. */ + kann_katalog: darfKatalogBekommen(p.rolle), dabei_seit_tagen: tageSeit(p.erstellt), gesetzt: meineZahl.get(p.id) || 0, veraltet: alteZahl.get(p.id) || 0, diff --git a/server/workspace-vorlagen.js b/server/workspace-vorlagen.js index d6509583..42655be7 100644 --- a/server/workspace-vorlagen.js +++ b/server/workspace-vorlagen.js @@ -48,6 +48,8 @@ import { MODI_STUFEN, entscheidetUeberAufgaben, darfAufgabenVerteilen, + KATALOG_EMPFAENGER, + KATALOG_EMPFAENGER_SQL, } from "./workspace.js"; /* Dieselbe Meldung wie beim anderen Bewerbungsweg -- eine Stelle, @@ -1928,15 +1930,27 @@ vorlagenRouter.post("/workspace/api/vorlagen/uebernehmen", gleicheHerkunft, /* Nur an jemanden, fuer den die Kategorien ueberhaupt gelten -- sonst landete eine Modi-Aufgabe samt Kategorie an einem Creator, und die Trennung waere von innen aufgeweicht. */ + const platzE = KATALOG_EMPFAENGER_SQL.map(() => "?").join(", "); const zielListe = anAlle ? db().prepare( - "SELECT id, rolle FROM personen WHERE aktiv = 1 AND rolle = 'modi' ORDER BY id").all() + `SELECT id, rolle FROM personen + WHERE aktiv = 1 AND rolle IN (${platzE}) ORDER BY id`) + .all(...KATALOG_EMPFAENGER_SQL) : [db().prepare( "SELECT id, rolle FROM personen WHERE id = ? AND aktiv = 1").get(zielId)] .filter(Boolean); - if (!zielListe.length || zielListe.some((z) => z.rolle !== "modi")) { + if (!zielListe.length) { return res.status(404).json({ fehler: "nicht_gefunden" }); } + /* EINE EIGENE ANTWORT FUER „DIE ROLLE BEKOMMT HIER NICHTS". + Vorher lief das ueber dieselbe 404 wie „die Person gibt es + nicht" -- und auf dem Bildschirm stand „lade die Seite neu", + was an einer Person, die sehr wohl existiert, nur ratlos + macht. Der Unterschied ist keine Feinheit: Das eine repariert + ein Neuladen, das andere nie. */ + if (zielListe.some((z) => !KATALOG_EMPFAENGER.has(z.rolle))) { + return res.status(400).json({ fehler: "rolle_ohne_katalog" }); + } /* Nur noch als Beleg, dass die Liste nicht leer ist -- gelesen wird sie in der Schleife weiter unten. Ein ungenutzter Wert waere Rauschen; dieser sagt etwas. */ @@ -1998,9 +2012,32 @@ vorlagenRouter.post("/workspace/api/vorlagen/uebernehmen", gleicheHerkunft, Bei einer einzelnen Person aendert sich nichts: Die Schleife laeuft dann genau einmal. */ + /* ==== "AN ALLE" FUELLT LUECKEN, ES DOPPELT NICHT (24.09.2026) + + Filipe, zur Frage "3 von 5 haben sie schon": „Nur die 2 + Fehlenden bekommen sie." + + GEMESSEN, NICHT GEGLAUBT. Der Kommentar zwei Absaetze + hoeher behauptete, die Sperre werde "bei alle pro Person neu + gestellt". Er beschreibt nur, WO sie steht -- aktiv war sie + allein bei `katalog_alle`. Zweimal "An alle" auf dieselbe + Karte ergab deshalb VIER Aufgaben statt zwei. Gefunden hat + das keine Ueberlegung, sondern eine Pruefung, die nachzaehlt. + + UND DIE BEWUSSTE WIEDERHOLUNG BLEIBT MOEGLICH. Manches steht + jede Woche an; wer es erneut verteilen will, drueckt einen + Knopf, der dann auch "Nochmal" heisst -- und der Browser sagt + das hier ausdruecklich (`nochmal`). Nicht die Sperre + verschwindet, sondern es wird gesagt, dass man sie meint. + Ein stillschweigendes Doppeln waere das, was am 22.09. aus + zwei Klicks 112 Aufgaben gemacht hat. + + EINE EINZELNE PERSON BLEIBT UNBERUEHRT: Dort ist ein zweiter + Druck ohnehin eine Entscheidung, und der Knopf sagt es. */ + const luecken = anAlle && req.body.nochmal !== true; const lagSchonDa = []; for (const ziel of zielListe) { - const liegtSchon = art === "katalog_alle" + const liegtSchon = (art === "katalog_alle" || luecken) ? new Set(db().prepare( `SELECT vorlage FROM aufgaben WHERE verantwortlich_id = ? diff --git a/server/workspace.js b/server/workspace.js index c9967c67..b878140e 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -139,6 +139,48 @@ export const WIE_RECHTE_HAND = new Set(["hand", "linke"]); /** Hat diese Person die Team-Rechte einer Hand (rechts oder links)? */ export const istHand = (person) => !!person && WIE_RECHTE_HAND.has(person.rolle); +/* ===================================================================== + WER EINE KATALOG-VORLAGE BEKOMMEN KANN (24.09.2026) + ===================================================================== + + Filipe: „wenn ich eine aufgabe an alle verteile will ich dass + dogfather und die rechte hand individuel von jedem sehen wer es + gemacht hat oder nicht." + + WARUM DIESE MENGE HIER STEHT UND NICHT DREIMAL VERSTREUT. Bis heute + gab es zwei Listen, die dasselbe meinten und es nicht taten: + + * Die „An wen"-Reihe im Browser bekam ihre Leute aus + `KARTEN_ROLLEN` (hand, linke, modi) -- gemessen am echten Stand + SECHS Personen. + * Der Katalog-Weg im Server nahm `rolle = 'modi'` -- VIER. + + Folge: „Alle" versprach sechs und belieferte vier; die beiden + Haende gingen leer aus, ohne ein Wort. Und wer einzeln auf „An + Rechte Hand" drueckte, bekam vom Server 404 und auf dem Bildschirm + „Das gibt es nicht mehr -- wahrscheinlich hat es jemand geloescht". + Eine Meldung, die in die Irre fuehrt: Die Person gibt es sehr wohl. + + ES IST EINE MENGE UND KEINE ABSCHRIFT. Wer sie erweitert, erweitert + damit die SQL-Abfrage, die Annahme-Pruefung und die Liste im + Browser zugleich -- die drei koennen gar nicht mehr auseinander- + laufen. Genau das war der Fehler, nicht die Zahl. + + WARUM MODI UND LINKE HAND. Filipes Regel vom 22.09.2026: „die modis + und linke hand sollen ... sich fuer aufgaben bewerben koennen aber + die rechte hand oder dogfather muessen annehmen oder ablehnen". + Also sind das die beiden Rollen, die Aufgaben BEKOMMEN -- und + DogFather und die rechte Hand die, die verteilen und zusehen. */ +export const KATALOG_EMPFAENGER = new Set(["modi", "linke"]); + +/** Kann diese Rolle eine Vorlage aus dem Katalog bekommen? */ +export const darfKatalogBekommen = (rolle) => KATALOG_EMPFAENGER.has(rolle); + +/** Die Rollen als SQL-Platzhalterliste -- abgeleitet, nicht getippt. + * Eine Abfrage, die die Namen selbst noch einmal enthielte, waere die + * vierte Liste und damit genau das Problem von vorn. */ +export const KATALOG_EMPFAENGER_SQL = [...KATALOG_EMPFAENGER]; + /* DIE EINE REIHENFOLGE, IN DER ROLLEN UEBERALL ERSCHEINEN. Sie steht hier einmal, damit keine Liste eine eigene erfindet. diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index ab44479d..65e74f4d 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`. --> - - + + - +