diff --git a/server/mess-alle-einzelsicht.mjs b/server/mess-alle-einzelsicht.mjs index 51c9cb48..e4ee57ab 100644 --- a/server/mess-alle-einzelsicht.mjs +++ b/server/mess-alle-einzelsicht.mjs @@ -235,6 +235,24 @@ try { if (await karte.count()) { await karte.screenshot({ path: `server/mess-alle-${wie}.png` }); console.log(` Bild: server/mess-alle-${wie}.png`); + /* UND DIE RUECKSEITE. Ohne sie waere nur bewiesen, dass die + Namen von der Vorderseite verschwunden sind -- nicht, dass + sie hinten ankommen. */ + await karte.locator('.vorlagenbrett__drehknopf').click().catch(() => {}); + await seite.waitForTimeout(600); + const hinten = await seite.evaluate(() => { + const k = document.querySelector('.vorlagenbrett__karte[data-gedreht="ja"]'); + const r = k?.querySelector('.vorlagenbrett__seite--hinten'); + return { + gedreht: !!k, + schild: r?.querySelector('.vorlagenbrett__wer-schild')?.textContent, + knoepfe: [...(r?.querySelectorAll('button') || [])].map((b) => b.textContent), + namen: [...(r?.querySelectorAll('.vorlagenbrett__wer-name') || [])] + .map((x) => x.textContent), + }; + }); + console.log(' Rueckseite:', JSON.stringify(hinten)); + await karte.screenshot({ path: `server/mess-alle-${wie}-hinten.png` }).catch(() => {}); } await kontext.close(); } diff --git a/server/mess-dialog-ausgang.mjs b/server/mess-dialog-ausgang.mjs new file mode 100644 index 00000000..9308dcda --- /dev/null +++ b/server/mess-dialog-ausgang.mjs @@ -0,0 +1,226 @@ +/* ===================================================================== + KOMMT MAN AUS EINEM FENSTER WIEDER HERAUS? (25.09.2026) + + --------------------------------------------------------------------- + DER ANLASS + + Miss, über den Support-Weg: „Ich konnte da wieder nicht zurück gehen, + musste die App schließen damit ich wieder auf die Hauptseite kam." + Dazu ein Bildschirmfoto von „Aufgabe bearbeiten" auf dem Handy. + + „WIEDER" IST DAS WICHTIGE WORT: Es ist nicht das erste Mal. Ein + Fenster, aus dem man nur herauskommt, indem man die App beendet, ist + die schlimmste Sorte Sackgasse -- man verliert dabei auch alles, was + man gerade eingetippt hat. + + --------------------------------------------------------------------- + WAS GEMESSEN WIRD + + Für JEDES Fenster auf JEDER Seite, auf der es eines gibt: + + 1. Ist ein Ausgang (Abbrechen/Schließen) überhaupt im Baum? + 2. Liegt er INNERHALB des sichtbaren Bereichs -- oder unterhalb? + 3. Und wenn er unterhalb liegt: Lässt sich dorthin SCROLLEN? + + Punkt 3 ist der eigentliche: Ein Knopf weiter unten ist harmlos, + solange man hinkommt. `.dialog` hat aber `overflow: hidden`, und nur + `.dialog--breit .neu` bekommt eine Scroll-Höhe. Bei allen anderen + wird der Rest schlicht abgeschnitten. + + GEMESSEN WIRD AUF 412 x 915 -- der häufigsten Android-Größe. Auf dem + Rechner fällt das nie auf: Dort ist der Bildschirm hoch genug. + + Aufrufen mit: node server/mess-dialog-ausgang.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 = 5399, HP = 5400; +const CREW = "crew.dogfather-universe.com"; +const ordner = mkdtempSync(join(tmpdir(), "ws-dialog-")); +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-dialog-ausgang"); +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; +}; +const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); +const idModi = anlegen("Miss", "modi", "CODE-MODI-0001"); +/* Eine Aufgabe, die dem Modi gehoert -- sonst gibt es nichts zu + bearbeiten, und die Messung waere gruen ueber einer Null. */ +d.prepare(`INSERT INTO aufgaben (titel, beschreibung, status, prioritaet, + verantwortlich_id, frist, erstellt, erstellt_von, haus) + VALUES (?,?, 'offen', 'hoch', ?, ?, ?, ?, 'crew')`) + .run("Die Regeln des Rudels einmal ganz lesen", + "Nicht überfliegen. Wer sie kennt, kann sie erklären – und Erklären ist in " + + "neun von zehn Fällen wirksamer als Löschen.", + idModi, "2026-09-26", jetzt, idDogi); +d.close(); + +const anfrage = (pfad, art, koerper) => 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" }, + }, (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: "modi", code: "CODE-MODI-0001" }); +if (an.code !== 200) { console.log(`ABBRUCH: Anmeldung ${an.code}`); process.exit(2); } +const keks = (an.kopf["set-cookie"] || []).map((x) => x.split(";")[0]).join("; "); + +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"], +}); +/* Die Hoehen, die es wirklich gibt -- nicht die aus dem Datenblatt. + `app` ist die installierte App (nur die Systemleiste), `browser` + zusaetzlich mit Adresszeile, `klein` ein aelteres Geraet, `tastatur` + dasselbe Geraet mit offener Bildschirmtastatur. Die letzte ist die + wichtigste: Man hat ja gerade etwas eingetippt. */ +const GROESSEN = [ + ["app", 412, 915], + ["browser", 412, 780], + ["klein", 360, 640], + ["tastatur", 412, 430], +]; +let schlimm = 0; +try { + /* MEHRERE GROESSEN UND NICHT EINE (25.09.2026). Der erste Anlauf + mass nur 412 x 915 -- die Angabe aus dem Geraetedatenblatt. Dort + passte es KNAPP: 782 px Inhalt in 784 px Fenster, Ausgang gerade + noch sichtbar, und die Messung meldete "JA". + + Das ist der BESTFALL und nicht der Alltag: Im Browser steht die + Adresszeile darueber, in der App die Systemleiste, und beim Tippen + kommt die Tastatur. Jede dieser Hoehen ist kleiner. Eine Messung, + die nur den guenstigsten Fall kennt, sagt genau dann "in Ordnung", + wenn der Nutzer schon feststeckt. */ + for (const [wie, breite, hoehe] of GROESSEN) { + 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(); + await seite.goto(`https://${CREW}:${HP}/workspace/aufgaben.html`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(1200); + + /* Das Bearbeiten-Fenster oeffnen -- ueber den Stift an der Aufgabe, + genau so wie Miss es getan hat. */ + const stift = seite.locator('.aufgabe [title*="earbeit"], .aufgabe button').first(); + if (await stift.count()) { await stift.click().catch(() => {}); } + await seite.waitForTimeout(400); + const offen = await seite.evaluate(() => { + const dlg = document.getElementById("bearbeiten-dialog"); + if (dlg && !dlg.open) { try { dlg.showModal(); } catch { /* egal */ } } + return !!(dlg && dlg.open); + }); + await seite.waitForTimeout(500); + + const mass = await seite.evaluate(() => { + const dlg = document.getElementById("bearbeiten-dialog"); + if (!dlg || !dlg.open) return null; + const raus = dlg.querySelector("#b-abbrechen"); + const rd = raus ? raus.getBoundingClientRect() : null; + const dd = dlg.getBoundingClientRect(); + /* WELCHES ELEMENT KANN ROLLEN? Gefragt wird der gemessene Zustand + (scrollHeight > clientHeight UND overflow erlaubt es), nicht die + Absicht im Stilblatt. */ + const rollt = (el) => { + if (!el) return false; + const s = getComputedStyle(el); + const geht = /auto|scroll/.test(s.overflowY); + return geht && el.scrollHeight - el.clientHeight > 4; + }; + const form = dlg.querySelector("form"); + return { + fensterHoehe: window.innerHeight, + dialogOben: Math.round(dd.top), dialogUnten: Math.round(dd.bottom), + dialogHoehe: Math.round(dd.height), + inhaltHoehe: form ? form.scrollHeight : null, + dialogOverflow: getComputedStyle(dlg).overflowY, + formOverflow: form ? getComputedStyle(form).overflowY : null, + ausgangDa: !!raus, + ausgangUnten: rd ? Math.round(rd.bottom) : null, + ausgangSichtbar: !!(rd && rd.bottom <= window.innerHeight + 1 && rd.top >= -1 + && rd.width > 0 && rd.height > 0), + dialogRollt: rollt(dlg), + formRollt: rollt(form), + }; + }); + + console.log(`\n=== Aufgabe bearbeiten, ${breite} x ${hoehe} (${wie}) ===\n`); + if (!offen || !mass) { + console.log(" ABBRUCH: Das Fenster liess sich nicht oeffnen -- gemessen wurde nichts."); + schlimm = 1; + } else { + for (const [k, v] of Object.entries(mass)) console.log(` ${k.padEnd(16)} ${v}`); + const rauskommen = mass.ausgangSichtbar || mass.dialogRollt || mass.formRollt; + console.log(`\n ==> Kommt man heraus? ${rauskommen ? "JA" : "NEIN -- SACKGASSE"}`); + if (!rauskommen) schlimm = 1; + await seite.locator("#bearbeiten-dialog") + .screenshot({ path: `server/mess-dialog-${wie}.png` }).catch(() => {}); + console.log(` Bild: server/mess-dialog-${wie}.png`); + } + await kontext.close(); + } +} finally { + await browser.close(); + vorbau.close(); + try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } + process.exit(schlimm); +} diff --git a/server/mess-eingang-optik.mjs b/server/mess-eingang-optik.mjs new file mode 100644 index 00000000..296dc60d --- /dev/null +++ b/server/mess-eingang-optik.mjs @@ -0,0 +1,182 @@ +/* ===================================================================== + WIE DER EINGANG AUSSIEHT (25.09.2026) + + Filipe zum Bildschirmfoto der Seite: „lass das bitte auch geiler + aussehen bitte." + + SIE PRUEFT NICHTS und meldet keinen Fehlschlag -- sie macht Bilder, + auf denen man nachsehen kann. Deshalb `mess-` und nicht `pruef-`. + + ZWEI BREITEN, und die kleinere ist die wichtigere: Ab 760 px steht + das Bild neben dem Text, darunter untereinander. Wer nur die grosse + ansieht, sieht die Haelfte der Regel nicht. + + EIGENE WEGWERF-DATENBANK: Es entsteht eine echte Meldung mit Bild. + In einem laufenden System waeren das Testdaten, und die sind + verboten. Hier liegt alles in einem `mktemp`-Ordner. + + Aufrufen mit: node server/mess-eingang-optik.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 = 5401, HP = 5402; +const CREW = "crew.dogfather-universe.com"; +const ordner = mkdtempSync(join(tmpdir(), "ws-eingang-")); +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-eingang-optik"); +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; +}; +anlegen("Filipe", "admin", "CODE-DOGI-0001"); +const idModi = anlegen("Miss", "modi", "CODE-MODI-0001"); +const idM2 = anlegen("Marina", "modi", "CODE-MODI-0002"); + +/* Ein winziges PNG als Anhang -- es geht um die FORM der Karte, nicht + um das Motiv. 1x1 Bildpunkt reicht dafuer nicht: Der Kasten waere + dann zwei Pixel gross und man saehe die Spaltenaufteilung nicht. + Deshalb ein hochkantes Rechteck, so wie ein Handy-Bildschirmfoto. */ +const png = Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAGQAAADwCAYAAAB7ovsMAAAAWklEQVR42u3QMQEAAAjAoNu/" + + "tBmeIAfpVoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAA" + + "AQIECBAgQIAAAQIECBAgQIAAAe4W1H8B4aXvOgAAAABJRU5ErkJggg==", "base64"); +d.prepare(`INSERT INTO support_meldungen + (person_id, text, seite, bild_datei, bild_typ, bild_breite, bild_hoehe, + stand, erstellt, geaendert) + VALUES (?,?,?,?,?,?,?,?,?,?)`) + .run(idModi, + "Ich konnte da wieder nicht zurück gehen, musste die App schließen " + + "damit ich wieder auf die Hauptseite kam", + "/workspace/support.html", "probe.png", "image/png", 100, 240, + "in_arbeit", jetzt, jetzt); +d.prepare(`INSERT INTO support_meldungen + (person_id, text, seite, stand, erstellt, geaendert) + VALUES (?,?,?,?,?,?)`) + .run(idM2, "Der Kalender springt beim Wischen manchmal zwei Monate weiter.", + "/workspace/kalender.html", "neu", jetzt, jetzt); +/* Die Bilddatei dort ablegen, wo die Route sie sucht. */ +const { DATEN_ORDNER } = await import("./workspace.js"); +try { + const { mkdirSync, writeFileSync } = await import("node:fs"); + const ziel = join(DATEN_ORDNER, "support"); + mkdirSync(ziel, { recursive: true }); + writeFileSync(join(ziel, "probe.png"), png); +} catch (f) { console.log("Bild nicht abgelegt:", f?.message); } +d.close(); + +const anfrage = (pfad, art, koerper) => 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" }, + }, (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: Anmeldung ${an.code}`); process.exit(2); } +const keks = (an.kopf["set-cookie"] || []).map((x) => x.split(";")[0]).join("; "); + +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 { + for (const [wie, breite, hoehe] of [["computer", 1280, 1100], ["handy", 412, 915]]) { + 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/support.html`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(1400); + const blick = await seite.evaluate(() => { + const block = document.getElementById("eingang-block"); + const karten = [...document.querySelectorAll(".s-karte")]; + const mitBild = karten.find((k) => k.querySelector(".s-karte__bild")); + const spalten = mitBild ? getComputedStyle(mitBild).gridTemplateColumns : "-"; + return { + eingangSichtbar: !!(block && !block.hidden), + karten: karten.length, + mitBild: !!mitBild, + spalten, + felder: document.querySelectorAll(".s-bilanz__feld").length, + leerMarkiert: document.querySelectorAll('.s-bilanz__feld[data-leer="ja"]').length, + }; + }); + console.log(`\n=== Eingang, ${breite} x ${hoehe} (${wie}) ===`); + for (const [k, v] of Object.entries(blick)) console.log(` ${k.padEnd(16)} ${v}`); + if (konsole.length) console.log(" FEHLER IN DER KONSOLE:", konsole); + const ziel = seite.locator("#eingang-block"); + if (await ziel.count()) { + await ziel.screenshot({ path: `server/mess-eingang-${wie}.png` }).catch(() => {}); + console.log(` Bild: server/mess-eingang-${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-abbrechen-optik.mjs b/server/pruef-abbrechen-optik.mjs index a2f1ef11..f86b30ae 100644 --- a/server/pruef-abbrechen-optik.mjs +++ b/server/pruef-abbrechen-optik.mjs @@ -49,6 +49,48 @@ notbremse(480_000, "pruef-abbrechen-optik"); await new Promise((r) => setTimeout(r, 900)); const BASIS = `http://127.0.0.1:${PORT}`; +/* ==== DER ANZEIGENAME FUER „review" -- ABGELESEN, NICHT GETIPPT ===== + + Er steht in `STATUS_NAME` in workspace/assets/js/aufgaben.js und + hiess bis zum 20.09.2026 „Review", seither „Zur Freigabe". Diese + Pruefung hatte ihn abgeschrieben und war seitdem rot. + + GELESEN WIRD DIE DATEI, nicht importiert: aufgaben.js ist ein + Browser-Skript (eine IIFE ohne Export) -- ein `import` bekaeme dort + nichts. Ein Auszug aus dem Quelltext ist der einzige Weg, der ohne + eine zweite Wahrheit auskommt. + + FINDET ER NICHTS, BRICHT DIE PRUEFUNG AB. Ein stiller Rueckfall auf + „Review" waere genau der Zustand, der hier gerade repariert wird: + ein Wort, das niemand mehr pflegt. */ +const WORT_REVIEW = await (async () => { + const { readFileSync } = await import("node:fs"); + const { fileURLToPath } = await import("node:url"); + const { dirname, join: verbinde } = await import("node:path"); + const hier = dirname(fileURLToPath(import.meta.url)); + const quelle = readFileSync( + verbinde(hier, "..", "workspace", "assets", "js", "aufgaben.js"), "utf8"); + /* GESUCHT WIRD IN STATUS_WORT, nicht irgendwo. Der erste Anlauf + nahm das erste `review:` der Datei -- und das gehoert einer + Uebergangstabelle (`review: 'erledigt'`, was der naechste + Schritt ist, nicht der Name). Die Pruefung las dadurch + "erledigt" und blieb rot. Ein Ausdruck, der die falsche + Stelle trifft, ist schlimmer als keiner: Er sieht aus wie + eine Ableitung. */ + /* SIE HEISST STATUS_NAME, nicht STATUS_WORT -- die zweite + heisst so in entwicklung.js. Zwei Dateien, zwei Namen fuer + dieselbe Tafel; nachgesehen statt erinnert. */ + const tafel = /STATUS_NAME\s*=\s*{([^}]*)}/.exec(quelle); + const treffer = tafel && /review:\s*'([^']+)'/.exec(tafel[1]); + if (!treffer) { + console.error("ABBRUCH: In aufgaben.js steht kein Anzeigename fuer `review`. " + + "Wurde STATUS_NAME umbenannt? Ohne ihn prueft diese Datei nichts."); + process.exit(3); + } + return treffer[1]; +})(); +console.log(` -- Anzeigename fuer "review": "${WORT_REVIEW}" (aus aufgaben.js gelesen)`); + const PROTOKOLL = join(process.cwd(), "pruef-abbrechen-optik-lauf.txt"); try { rmSync(PROTOKOLL, { force: true }); } catch { /* gab es noch nicht */ } const melde = (z) => { @@ -172,7 +214,17 @@ melde("\n=== Der Dialog verlangt einen Grund ==="); /* Sagt er, was passiert -- und was NICHT passiert? Die Sorge bei so einem Knopf ist immer, ob etwas verloren geht. */ const was = await seite.$eval("#abbruch-was", (e) => e.textContent); - ok(/nicht gelöscht/i.test(was) && /Review/.test(was), + /* „REVIEW" HEISST SEIT DEM 20.09.2026 „ZUR FREIGABE" (nachgezogen + 25.09.2026). Die Oberflaeche wurde umbenannt, diese Pruefung + nicht -- sie war seither rot, ohne dass etwas kaputt war, und + eine Warnung, die immer kommt, wird ueberlesen. + + GEPRUEFT WIRD GEGEN DIE EINE QUELLE, nicht gegen ein zweites + abgeschriebenes Wort: `STATUS_WORT` in aufgaben.js ist die Stelle, + an der der Anzeigename steht. Wird er morgen wieder geaendert, + zieht diese Zeile von selbst mit -- und wer ihn aendert, ohne + dass der Dialog mitgeht, wird hier rot. */ + ok(/nicht gelöscht/i.test(was) && new RegExp(WORT_REVIEW).test(was), `er erklärt es: "${was.trim().slice(0, 95)}…"`); /* Ohne Grund abschicken -- der Dialog muss offen bleiben. */ @@ -236,7 +288,7 @@ melde("\n=== Der Bereich 'Abgebrochen' ==="); ok(/Kampagne verschoben/.test(grund), `der Grund steht dran: "${grund}"`); const vorher = await seite.$eval(".abbruch-karte__vorher", (e) => e.textContent.trim()); - ok(vorher === "Review", `und in welchem Status sie war: "${vorher}"`); + ok(vorher === WORT_REVIEW, `und in welchem Status sie war: "${vorher}"`); const wer = await seite.$eval(".abbruch-karte__wer", (e) => e.textContent.trim()); ok(/Filipe/.test(wer), `und wer es war: "${wer}"`); diff --git a/server/pruef-modi-katalog.mjs b/server/pruef-modi-katalog.mjs index e1d2a0e1..2a1439c5 100644 --- a/server/pruef-modi-katalog.mjs +++ b/server/pruef-modi-katalog.mjs @@ -822,6 +822,78 @@ console.log(""); ok(/Marina/.test(nachWahl.liste), 'die Aufgabenliste darunter gehoert ihr ("' + nachWahl.liste + '")'); + /* ==== DIE KACHEL DREHT SICH UM (25.09.2026) ====================== + + Filipe: „wenn ich drauf drücke dreht sich die kachel und dan seh + ich wer es gemacht hat, und immer noch die option es nochmal zu + verteilen falls neue leute ins team zustoßen." + + DREI DINGE, UND JEDES EINZELN: + 1. Bei „Alle" gibt es ueberhaupt einen Weg auf die Rueckseite. + 2. Dort stehen die NAMEN -- so viele, wie es Zielbare gibt. + 3. Und ein Knopf, der wieder verteilt. + + DIE ZAHL GEHOERT IN DIE BEDINGUNG: Eine leere Rueckseite wuerde + sonst als "gedreht, alles gut" durchgehen -- der Fehler, der in + diesem Haus am 01.09. dreimal gruen dastand. */ + await seite.evaluate(() => { + document.querySelector('.stufenleiter--wer [data-alle="ja"]')?.click(); + }); + await seite.waitForTimeout(900); + const dreh = await seite.evaluate(() => { + const k = document.querySelector('.vorlagenbrett__karte[data-dreh="ja"]'); + if (!k) return { drehbar: false }; + const knopf = k.querySelector(".vorlagenbrett__drehknopf"); + knopf?.click(); + const hinten = k.querySelector(".vorlagenbrett__seite--hinten"); + return { + drehbar: true, + gedreht: k.dataset.gedreht === "ja", + namen: [...(hinten?.querySelectorAll(".vorlagenbrett__wer-name") || [])] + .map((x) => x.textContent), + knoepfe: [...(hinten?.querySelectorAll("button") || [])] + .map((b) => b.textContent.trim()), + /* Und vorne darf danach NICHTS mehr von der Rueckseite stehen + -- sonst waere es kein Umdrehen, sondern ein Aufklappen. */ + vorneNamen: k.querySelectorAll( + ".vorlagenbrett__seite--vorn .vorlagenbrett__wer-name").length, + }; + }); + ok(dreh.drehbar, "bei \u201eAlle\u201c ist die Kachel drehbar"); + ok(dreh.gedreht === true, `und ein Druck dreht sie um (${dreh.gedreht})`); + ok((dreh.namen || []).length >= 2, + `hinten stehen die Namen (${(dreh.namen || []).join(", ") || "keine"})`); + ok((dreh.knoepfe || []).some((t) => /Nachholen|Nochmal/.test(t)) + && (dreh.knoepfe || []).some((t) => /Zur\u00fcck/.test(t)), + `mit Verteil- und Zurueck-Knopf (${(dreh.knoepfe || []).join(" | ") || "keine"})`); + ok(dreh.vorneNamen === 0, + `und vorne steht kein Name mehr (${dreh.vorneNamen})`); + + /* ==== UND DIE AUSWAHL WIEDER ZURUECKSTELLEN ===================== + + Der Klick auf "Alle" hat die gewaehlte Person veraendert -- und + die Pruefungen weiter unten arbeiten mit Marina. Beim ersten + Anlauf fielen dadurch zwei Zeilen um ("die Aufgabe liegt in + ihrer Liste (2 -> 2)"), und zwar an Stellen, an denen nichts + kaputt war. Eine Pruefung, die den Zustand fuer die naechste + verstellt, misst deren Ergebnis mit. + + ES WIRD NACHGEWIESEN, NICHT GEHOFFT: Ohne die Zeile darunter + waere ein misslungenes Zuruecksetzen wieder ein Fehler an der + falschen Stelle -- und man suchte ihn dort. */ + await seite.evaluate(() => { + const b = [...document.querySelectorAll(".stufenleiter--wer .stufenleiter__knopf")] + .find((x) => /Marina/.test(x.textContent)); + b?.click(); + }); + await seite.waitForTimeout(900); + const zurueckGestellt = await seite.evaluate(() => + [...document.querySelectorAll(".stufenleiter--wer .stufenleiter__knopf")] + .filter((x) => x.getAttribute("aria-pressed") === "true") + .map((x) => x.textContent.trim().replace(/\s+/g, " "))); + ok(zurueckGestellt.some((t) => /Marina/.test(t)), + `die Auswahl steht wieder auf Marina (${zurueckGestellt.join(", ") || "keine"})`); + /* GEZAEHLT WIRD IN IHRER LISTE, NICHT AUF DEM BRETT. Auf "Aufgaben" stand unter dem Katalog das Brett, und die diff --git a/server/pruef-modi-verborgen.mjs b/server/pruef-modi-verborgen.mjs index a8ca27ca..6835fedb 100644 --- a/server/pruef-modi-verborgen.mjs +++ b/server/pruef-modi-verborgen.mjs @@ -325,11 +325,33 @@ console.log("\n=== 6. Was ein Modi auf seiner Startseite sieht ===\n"); /* Nichts aus der Agentur. Nicht "moeglichst wenig", sondern keine einzige -- diese Seiten drehen sich um betreute Creator, um Rechte oder um Automationen. */ + /* `calls.html` STAND HIER UND IST AM 25.09.2026 HERAUSGENOMMEN. + + Nicht, weil die Pruefung zu streng war, sondern weil sich die + Sache geaendert hat: Filipe hat die Calls-Kachel ausdruecklich + fuer alle verlangt, die einen Kalender haben („jeder der einen + kalender hat soll auch sowas haben danke"), und ein Modi hat + einen. Seither war diese Zeile rot, ohne dass etwas kaputt war + -- und eine Warnung, die immer kommt, wird ueberlesen. + + DIE LISTE BLEIBT EINE LISTE VON HAND, und das ist ihre Schwaeche: + Sie altert genau so wie diese Zeile gealtert ist. Deshalb steht + darunter eine Gegenprobe -- ohne sie waere „nichts Verbotenes + dabei" auch dann gruen, wenn die Pruefung gar nichts mehr + prueft. */ const verboten = ["personen.html", "scouting.html", "automation.html", - "report.html", "calls.html", "uebersicht.html", "content.html"]; - const drin = (b || []).map((x) => x.ziel).filter((z) => verboten.includes(z)); - ok(Array.isArray(b) && drin.length === 0, - drin.length ? `ABER AUCH: ${drin.join(", ")} -- FEHLER` : "und nichts aus der Agentur"); + "report.html", "uebersicht.html", "content.html"]; + const ziele = (b || []).map((x) => x.ziel); + const drin = ziele.filter((z) => verboten.includes(z)); + ok(Array.isArray(b) && verboten.length >= 6 && drin.length === 0, + drin.length ? `ABER AUCH: ${drin.join(", ")} -- FEHLER` + : `und nichts aus der Agentur (${verboten.length} Seiten geprueft)`); + /* GEGENPROBE: Haette er eine davon, MUSS die Zeile darueber + anschlagen. Gepruft wird an einer erfundenen Liste, nicht an + echten Daten -- es wird nichts angelegt und nichts veraendert. */ + const alsHaette = [...ziele, "scouting.html"].filter((z) => verboten.includes(z)); + ok(alsHaette.length === 1, + `und sie wuerde es merken, wenn doch (Probe: ${alsHaette.join(", ") || "merkt nichts"})`); /* Die Beschriftung gehoert zum Empfaenger: "Alle Creator auf einen Blick" waere fuer jemanden ohne Creator schlicht falsch. */ ok((b || []).every((x) => x.name && x.unter && x.zeichen && x.ziel), @@ -409,6 +431,44 @@ console.log("\n=== 7. Dieselbe Seite mit einem echten Browser ===\n"); await seite.screenshot({ path: "pruef-modi-start.png", fullPage: true }); console.log(" -- Bildschirmfoto: server/pruef-modi-start.png"); + + /* ==== DER CREATOR-KATALOG GEHOERT IHM NICHT (25.09.2026) ======== + + Filipe, zu einem Bildschirmfoto der Aufgabenseite: „es gibt + keine creator auf dieser seite. die app ist nur fuer modis und + community also steht das falsch da." + + Dort stand ein Katalog mit vier Bereichen und vier Stufen -- der + fuer CREATOR -- und darunter „Waehle oben einen Creator, dann + lassen sich diese Aufgaben uebernehmen". Auf dieser Adresse gibt + es keine; der Satz forderte zu etwas auf, das nie geht. + + GEPRUEFT WIRD DER KASTEN UND DER SATZ, nicht nur einer von + beiden: Verschwindet der Satz und der leere Kasten bleibt, ist + es dieselbe Sackgasse in leise. */ + await seite.goto(BASIS + "/workspace/aufgaben.html", { waitUntil: "networkidle" }); + await seite.waitForTimeout(1200); + const katalog = await seite.evaluate(() => { + const kasten = document.getElementById("vorlagenbrett"); + return { + daUeberhaupt: !!kasten, + sichtbar: !!(kasten && !kasten.hidden && kasten.offsetHeight > 0), + text: (document.body.textContent || "").includes("Wähle oben einen Creator"), + /* GEGENPROBE IM SELBEN ZUG: Sein EIGENES Aufgabenbrett muss da + sein. Waere die ganze Seite leer, meldeten die zwei Zeilen + darueber brav "nichts Fremdes zu sehen". */ + eigenes: !!document.querySelector("#brett, .spalten, .aufgabe, #aufgaben"), + }; + }); + ok(katalog.daUeberhaupt, + "der Kasten fuer den Katalog steht im Bauplan der Seite (Gegenprobe)"); + ok(!katalog.sichtbar, + `der Creator-Katalog wird ihm NICHT gezeigt (sichtbar: ${katalog.sichtbar})`); + ok(!katalog.text, + "und der Satz „Waehle oben einen Creator“ steht nirgends"); + ok(katalog.eigenes, + "sein eigenes Aufgabenbrett ist trotzdem da (Gegenprobe)"); + await kontext.close(); } finally { await browser.close(); @@ -434,7 +494,27 @@ console.log("\n=== 8. Aufgaben, Einträge und Dateien eines Modis ===\n"); method: "POST", headers: { "Content-Type": "application/json", cookie: an.keks }, body: JSON.stringify({ titel: MERKMAL + " Aufgabe" }), }); - ok(t1.status === 201, `der Modi legt eine Aufgabe an (${t1.status})`); + /* ==== UMGEDREHT -- UND STRENGER (25.09.2026) ==================== + + Hier stand `status === 201`: „der Modi legt eine Aufgabe an". Das + war bis zum 22.09.2026 richtig. An dem Tag hat Filipe es + abgeschafft: „sie sich nicht selber aufgaben geben" und „die modis + sollen nur diese aufgaben wo sie von den drauf markiert werden + annehmen koennen". Seither antwortet der Server mit 403 -- und + diese Zeile war seitdem rot, ohne dass etwas kaputt war. + + SIE PRUEFT JETZT MEHR ALS VORHER, nicht weniger: nicht nur die + Nummer, sondern auch das WORT. Eine 403 aus einem anderen Grund + (etwa weil die Anmeldung kaputt ist) saehe sonst genauso aus wie + die gewollte Absage -- und die Pruefung waere gruen ueber einem + ganz anderen Fehler. + + WARUM DAS WORT ZAEHLT: `nur_leitung_legt_an` hat in meldung.js + einen ganzen Satz („Aufgaben werden dir zugeteilt -- anlegen + kannst du hier keine"). Eine nackte 403 haette ihn nicht. */ + const t1Fehler = (await t1.json().catch(() => ({})))?.fehler; + ok(t1.status === 403 && t1Fehler === "nur_leitung_legt_an", + `der Modi legt KEINE Aufgabe an -- mit Begruendung (${t1.status} ${t1Fehler || "ohne Wort"})`); /* (b) Ein Bereichs-Eintrag -- ebenfalls ueber den normalen Weg. */ const t2 = await fetch(BASIS + "/workspace/api/bereich/live", { diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index cc03d844..8cdb0183 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -902,13 +902,17 @@ aufgabenRouter.post("/workspace/api/aufgaben", gleicheHerkunft, (req, res) => { INSERT INTO aufgaben (titel, beschreibung, status, prioritaet, creator_id, verantwortlich_id, creator_extern, verantwortlich_extern, frist, kategorie, kanal, - erstellt, erstellt_von) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run( + erstellt, erstellt_von, haus) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run( + /* DAS HAUS WIRD BEIM ANLEGEN GESCHRIEBEN (25.09.2026) -- siehe + das letzte Argument. Ohne es bekaeme eine Aufgabe, an der nur + DogFather haengt (Pool-Aufgabe ohne Verantwortlichen), kein + Haus -- und waere in beiden sichtbar. */ aus.titel, aus.beschreibung ?? null, aus.status ?? "offen", aus.prioritaet ?? "mittel", aus.creator_id ?? null, aus.verantwortlich_id ?? null, aus.creator_extern ?? null, aus.verantwortlich_extern ?? null, aus.frist ?? null, aus.kategorie ?? null, aus.kanal ?? null, - jetzt(), req.person.id); + jetzt(), req.person.id, req.person.haus || null); /* AN MENSCHEN VERTEILEN (21.09.2026, Abschnitt 3). Nur wer verteilen darf -- sonst koennte sich jemand selbst eine diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 00fe270a..8907e857 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -2125,10 +2125,11 @@ bereicheRouter.post("/workspace/api/bereich/:bereich/start", gleicheHerkunft, (r die Zeile laesst sich hinterher nicht mehr aendern. */ if (!Object.hasOwn(einstellung.arten, v.art)) continue; const { lastInsertRowid } = db().prepare(` - INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von) - VALUES (?,?,?,?,?, 'offen', ?, ?)`) + INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, + erstellt_von, haus) + VALUES (?,?,?,?,?, 'offen', ?, ?, ?)`) .run(bereich, v.art, v.titel, v.text ?? null, - jetzt().slice(0, 10), jetzt(), req.person.id); + jetzt().slice(0, 10), jetzt(), req.person.id, req.person.haus || null); /* UND SOFORT FREIGEBEN (17.09.2026) -- der Fehler, den Filipe auf seinem Bildschirm gesehen hat. @@ -2237,11 +2238,12 @@ bereicheRouter.post("/workspace/api/bereich/:bereich/:id/daraus", const { lastInsertRowid } = db().prepare(` INSERT INTO eintraege (bereich, art, titel, text, datum, status, - erstellt, erstellt_von, aus_eintrag_id) - VALUES (?,?,?,?,?, 'offen', ?,?,?)`) + erstellt, erstellt_von, aus_eintrag_id, haus) + VALUES (?,?,?,?,?, 'offen', ?,?,?,?)`) .run(nach, art, quelle.titel, String(req.body?.text ?? "").trim().slice(0, TEXT_MAX) || null, - jetzt().slice(0, 10), jetzt(), req.person.id, quelle.id); + jetzt().slice(0, 10), jetzt(), req.person.id, quelle.id, + req.person.haus || null); protokolliere("bereich_daraus", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), @@ -2500,8 +2502,8 @@ bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, re INSERT INTO eintraege (bereich, art, titel, text, datum, uhrzeit, bewertung, dringlichkeit, status, creator_id, creator_extern, erstellt, erstellt_von, hook, format, saeule_id, geplant, - event_ende, event_aufgaben, event_regeln, einsatz, nur_leitung) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run( + event_ende, event_aufgaben, event_regeln, einsatz, nur_leitung, haus) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run( bereich, aus.art, aus.titel, aus.text ?? null, aus.datum, aus.uhrzeit ?? null, aus.bewertung ?? null, aus.dringlichkeit ?? "mittel", aus.status ?? "offen", aus.creator_id ?? null, aus.creator_extern ?? null, jetzt(), req.person.id, @@ -2512,7 +2514,9 @@ bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, re unbekannt ist bei einer Vertraulichkeit die falsche Antwort. Wer nichts waehlt, schreibt fuers Team -- das ist der offene Fall, und der soll der Normalfall sein. */ - aus.nur_leitung ?? 0); + aus.nur_leitung ?? 0, + /* Das Haus aus der Adresse -- letzte Spalte, letzter Wert. */ + req.person.haus || null); protokolliere("eintrag_angelegt", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), @@ -2686,10 +2690,11 @@ bereicheRouter.post("/workspace/api/bereich/:bereich/:id(\\d+)/bild", const info = db().prepare( "INSERT INTO dateien (name_original, name_datei, groesse, typ, status," - + " notiz, hochgeladen_von, erstellt, eintrag_id)" - + " VALUES (?,?,?,?,'entwurf',NULL,?,?,?)") + + " notiz, hochgeladen_von, erstellt, eintrag_id, haus)" + + " VALUES (?,?,?,?,'entwurf',NULL,?,?,?,?)") .run(name, dateiname, req.body.length, erkannt.typ, - req.person.id, new Date().toISOString(), eintrag.id); + req.person.id, new Date().toISOString(), eintrag.id, + req.person.haus || null); protokolliere("eintrag_bild", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), diff --git a/server/workspace-material.js b/server/workspace-material.js index db5380cf..6416e8a5 100644 --- a/server/workspace-material.js +++ b/server/workspace-material.js @@ -127,7 +127,25 @@ function tabellen() { Fehler, ein neues CREATE wuerde die vorhandenen Zeilen nicht erreichen. Also nachsehen, was da ist, und nur ergaenzen, was fehlt -- dieselbe Bauart wie bei den Personen-Spalten. */ - const NOETIG = [["gilt_ab", "TEXT"], ["gilt_bis", "TEXT"]]; + /* ==== DAS HAUS GEHOERT IN DIESE LISTE, NICHT IN DIE ANDERE ====== + + (25.09.2026.) Beim Bau der Haustrennung habe ich `["material", + "haus", "TEXT"]` in die Spaltenliste von `workspace.js` gesetzt -- + dort stehen die anderen sieben Wurzeltabellen. `material` ist die + einzige, die NICHT dort angelegt wird, sondern hier. + + Auf der echten Datenbank fiel das nicht auf: Die Tabelle gab es + laengst, also hat `ALTER TABLE` gegriffen. Auf einer FRISCHEN + Datenbank lief die Liste in workspace.js dagegen, bevor es die + Tabelle gab -- sie wird dort uebersprungen --, und danach legte + diese Datei sie ohne `haus` an. Ergebnis: 78 Fehlschlaege mit + "table material has no column named haus", und zwar erst beim + SCHREIBEN, nicht beim Starten. + + Gefunden hat das keine Ueberlegung, sondern `pruef-material`, weil + sie mit einer frischen Datenbank arbeitet. Dieselbe Falle wie + ueberall in diesem Haus: eine Liste an zwei Orten. */ + const NOETIG = [["gilt_ab", "TEXT"], ["gilt_bis", "TEXT"], ["haus", "TEXT"]]; const da = new Set(d.prepare("PRAGMA table_info(material)").all().map((c) => c.name)); for (const [spalte, form] of NOETIG) { if (da.has(spalte)) continue; @@ -369,10 +387,13 @@ materialRouter.post( const { lastInsertRowid } = db().prepare(` INSERT INTO material (datei, dateiname, typ, art, groesse, text, von_id, erstellt, - gilt_ab, gilt_bis) - VALUES (?,?,?,?,?,?,?,?,?,?) + gilt_ab, gilt_bis, haus) + VALUES (?,?,?,?,?,?,?,?,?,?,?) `).run(datei, name, typ, art, req.body.length, text, req.person.id, - new Date().toISOString(), ab, bis); + new Date().toISOString(), ab, bis, + /* AM MATERIAL HAENGT NUR `von_id` -- ist das DogFather, gibt es + sonst keinen Beleg. Genau deshalb steht das Haus hier. */ + req.person.haus || null); protokolliere(req.person.id, "material_eingestellt", `${art} "${name}" (${req.body.length} Bytes)`, echteIp(req)); diff --git a/server/workspace-video.js b/server/workspace-video.js index 20cc3835..505055bd 100644 --- a/server/workspace-video.js +++ b/server/workspace-video.js @@ -290,10 +290,11 @@ videoRouter.post("/workspace/api/bereich/:bereich/video", async (req, res) => { if (abgelegt) { db().prepare(` INSERT INTO dateien (name_original, name_datei, groesse, typ, status, notiz, - hochgeladen_von, erstellt, eintrag_id) - VALUES (?,?,?,?, 'entwurf', NULL, ?,?,?)`) + hochgeladen_von, erstellt, eintrag_id, haus) + VALUES (?,?,?,?, 'entwurf', NULL, ?,?,?,?)`) .run(`cover-${auskunft.kanal}${abgelegt.name.slice(abgelegt.name.lastIndexOf("."))}`, - abgelegt.name, abgelegt.groesse, abgelegt.art, req.person.id, jetzt, eintragId); + abgelegt.name, abgelegt.groesse, abgelegt.art, req.person.id, jetzt, eintragId, + req.person.haus || null); abgelegt = null; /* ab hier gehoert die Datei dem Eintrag */ } diff --git a/server/workspace-wissen.js b/server/workspace-wissen.js index d4378d1f..684bf017 100644 --- a/server/workspace-wissen.js +++ b/server/workspace-wissen.js @@ -432,12 +432,20 @@ wissenRouter.post( const { lastInsertRowid } = db().prepare(` INSERT INTO wissen (titel, beschreibung, kategorie, stufe, geraet, tags, - name_original, name_datei, groesse, wichtig, veroeffentlicht, hochgeladen, erstellt_von) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run( + name_original, name_datei, groesse, wichtig, veroeffentlicht, hochgeladen, + erstellt_von, haus) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run( aus.titel, aus.beschreibung ?? null, aus.kategorie, aus.stufe ?? "einsteiger", aus.geraet ?? "egal", aus.tags ?? null, name, dateiname, req.body.length, aus.wichtig ?? 0, - aus.veroeffentlicht ?? heute(), jetzt(), req.person.id); + aus.veroeffentlicht ?? heute(), jetzt(), req.person.id, + /* Die Wissensablage gibt es auf der Team-Adresse gar nicht + (siehe `angemeldet` weiter oben) -- hier kann also nur + "agentur" herauskommen. Trotzdem aus `person.haus` und nicht + fest: Eine feste Zeichenkette waere eine zweite Wahrheit + neben der Schranke, und die beiden liefen beim naechsten + Umbau auseinander. */ + req.person.haus || null); protokolliere("wissen_hochgeladen", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), diff --git a/server/workspace.js b/server/workspace.js index 58a7bdad..f57e9534 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -3132,8 +3132,31 @@ export function haeuserNachtragen(d) { ["wissen", (r) => [r.erstellt_von].filter((x) => x != null), "titel", () => "agentur"], ["material", (r) => [r.von_id].filter((x) => x != null), "dateiname"], - ["termin_serien", (r) => [r.creator_id, r.teilnehmer_id, r.erstellt_von] - .filter((x) => x != null), "titel"], + /* DIE TEILNEHMER DER SERIE ZAEHLEN MIT (nachgetragen 25.09.2026). + + Hier standen nur `creator_id`, `teilnehmer_id` und + `erstellt_von` -- bei den TERMINEN eine Zeile hoeher wird + dagegen auch `termin_teilnehmer` gefragt. Dieselbe Frage, zwei + verschiedene Antworten: genau die Bauart, vor der in dieser + Datei an fuenfzehn Stellen gewarnt wird, und ich habe sie beim + Nachruesten der Serien selbst gebaut. + + GEMESSEN AM ECHTEN STAND nach dem Ausliefern: Zwei Serien + blieben ohne Haus -- „Community-Call" und „Schulung-Agentur". + An beiden haengt nur DogFather; ihre Teilnehmer sind fuenf + Creator und Scouts, also eindeutig die Agentur. Der Beleg war + da, er wurde nur nicht gelesen. + + WAS DAS GEKOSTET HAETTE: Die 53 bereits erzeugten Termine sind + richtig zugeordnet (sie haben ja Teilnehmer). Aber jeder KUENFTIG + erzeugte haette `haus = null` geerbt -- und ein `null` ist in + BEIDEN Haeusern sichtbar. Die Trennung waere Woche fuer Woche + ein Stueck weiter aufgegangen, ohne dass etwas rot wird. */ + ["termin_serien", (r) => [ + ...d.prepare("SELECT person_id AS p FROM serie_teilnehmer WHERE serie_id = ?") + .all(r.id).map((z) => z.p), + r.creator_id, r.teilnehmer_id, r.erstellt_von, + ].filter((x) => x != null), "titel"], ]; /* DIE EINTRAEGE ZUERST -- die Dateien erben von ihnen. Stuenden @@ -4112,7 +4135,11 @@ function umstellungen(d) { ["eintraege", "haus", "TEXT"], ["dateien", "haus", "TEXT"], ["wissen", "haus", "TEXT"], - ["material", "haus", "TEXT"], + /* `material` steht hier ABSICHTLICH NICHT. Seine Tabelle wird in + workspace-material.js angelegt, und diese Schleife laeuft auf + einer frischen Datenbank vorher -- sie haette die Spalte still + uebersprungen, und das Schreiben waere erst im Betrieb + gescheitert. Sie steht dort, wo die Tabelle wohnt. */ /* Die WIEDERHOLUNGSREGEL braucht es auch: Der Nachfueller erzeugt daraus Termine, und die muessen wissen, wohin sie gehoeren. Ohne sie erbten sie `null` -- und ein `null` ist in beiden Haeusern diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index f6c99064..f76d2f83 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`. --> - - + + - +