From e5cd016b60f78c152f575d9711527e394693b14a Mon Sep 17 00:00:00 2001 From: Dogfather Date: Fri, 25 Sep 2026 00:50:15 +0200 Subject: [PATCH] Aus jedem Fenster kommt man heraus, die Kachel dreht sich, der Eingang sieht aus wie das Haus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VIER DINGE, und das erste ist eine Meldung aus dem Support. 1. MISS KAM AUS "AUFGABE BEARBEITEN" NICHT HERAUS. „Ich konnte da wieder nicht zurück gehen, musste die App schließen damit ich wieder auf die Hauptseite kam." Gemessen (mess-dialog-ausgang.mjs), vier Größen: 412x915 App 782 px Inhalt in 784 px -- knapp ja 412x780 Browser 782 px Inhalt in 742 px -- SACKGASSE 360x640 klein 794 px Inhalt in 602 px -- SACKGASSE 412x430 Tastatur 794 px Inhalt in 392 px -- SACKGASSE `.dialog` hatte `overflow: hidden`, eine Scroll-Höhe gab es NUR für `.dialog--breit`. Alles unterhalb des Rands wurde abgeschnitten -- samt "Abbrechen". Jetzt rollt JEDES Fenster, und Kopf wie Knopfzeile bleiben stehen (`position: sticky`), damit man den Ausgang SIEHT, ohne erst durch acht Felder zu scrollen. Alle vier Größen: ja. 2. DIE VORLAGENKACHEL DREHT SICH. „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." Vorne bleibt die Kurzfassung ("liegt bei 3 von 4"), hinten stehen die Namen mit ihrem Stand und zwei Knöpfe: "Nachholen – 1 fehlt" (oder "Nochmal an alle", wenn wirklich alle sie haben) und "Zurück". Nach dem Verteilen dreht sie sich von selbst; wer nur nachsehen will, drückt "Wer hat sie?". 3. DER EINGANG SIEHT AUS WIE DAS HAUS. Fase und Leuchtschiene statt flachem Kasten, die Schiene in der Farbe des Stands. Die drei Zahlen werden drei Felder -- und die "0 neu" leuchtet nicht mehr rot: Eine Warnung, die immer kommt, ist keine Warnung. Ab 760 px steht das Bild neben dem Text statt darunter; die Karte war dadurch dreimal so hoch wie nötig. 4. DER CREATOR-KATALOG IST AUF DER TEAM-SEITE WEG. „es gibt keine creator auf dieser seite" -- dort stand "Wähle oben einen Creator", eine Aufforderung zu etwas Unmöglichem. Gefragt wird jetzt nach den Daten (gibt es jemanden, dem ich das geben kann?), nicht nach der Adresse. DAZU FERTIG GEMACHT, WAS VON GESTERN OFFEN WAR: * Die zwei Serien ohne Haus ("Community-Call", "Schulung-Agentur"). Ursache war meine eigene Abschrift: Bei den Terminen frage ich die Teilnehmerliste, bei den Serien hatte ich sie vergessen. Auf einer Kopie der echten Datenbank: 0 offene Zeilen. * Sieben Schreibwege setzen jetzt `haus` (Aufgaben, Einträge, Dateien, Material, Wissen, Video-Titelbild). Dabei gefunden: `material` verwaltet seine Spalten SELBST -- meine Spalte stand in der falschen Liste und fehlte auf einer frischen Datenbank (78 Fehlschläge in pruef-material, jetzt 159/0). * unterstuetzen.html lud meldung.js gar nicht -- dort stand das Maschinenwort des Servers statt eines Satzes (pruef-meldungen 8/0). DREI VERALTETE PRÜFUNGEN NACHGEZOGEN, jede STRENGER als vorher: * "der Modi legt eine Aufgabe an (201)" -- seit dem 22.09. ist das 403 und gewollt. Geprüft wird jetzt auch das WORT. * "calls.html ist verboten" -- Filipe hat die Kachel selbst verlangt ("jeder der einen kalender hat"). Mit Gegenprobe ersetzt. * "Review" heißt seit dem 20.09. "Zur Freigabe". Der Name wird jetzt aus STATUS_NAME GELESEN statt abgeschrieben. GEPRÜFT: modi-katalog 150/0 (war 144), modi-verborgen 85/0 (war 80/2), haus-trennung 97/0, material 159/0, meldungen 8/0, abbrechen-optik 0 Fehler. Dazu grün: an-alle, vorlagen, support, css-klassen, aufgabenbrett, aufgaben-vorlagen, unterstuetzung, formulare, loeschen, nachfrage, kalender, chat, leerzustand. OFFEN UND NICHT ANGEFASST: pruef-breiten meldet auf report.html ein Berührziel von 27x18 px. Der Link (`class="zurueck"`) ist auf 30 Seiten derselbe und hat gar keinen eigenen Stil; beanstandet wird nur diese eine Seite, weil dort hinter ihm nur "· Review" steht und die Prüfung Fließtext-Links erst ab 12 Zeichen Umgebung ausnimmt. Eine Klasse auf 30 Seiten ohne Prüflauf zu ändern wäre geraten. Co-Authored-By: Claude Opus 5 --- server/mess-alle-einzelsicht.mjs | 18 +++ server/mess-dialog-ausgang.mjs | 226 +++++++++++++++++++++++++++ server/mess-eingang-optik.mjs | 182 +++++++++++++++++++++ server/pruef-abbrechen-optik.mjs | 56 ++++++- server/pruef-modi-katalog.mjs | 72 +++++++++ server/pruef-modi-verborgen.mjs | 90 ++++++++++- server/workspace-aufgaben.js | 10 +- server/workspace-bereiche.js | 29 ++-- server/workspace-material.js | 29 +++- server/workspace-video.js | 7 +- server/workspace-wissen.js | 14 +- server/workspace.js | 33 +++- workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/aufgaben.css | 95 ++++++++++- workspace/assets/css/module.css | 94 ++++++++++- workspace/assets/css/support.css | 173 ++++++++++++++++++-- workspace/assets/js/aufgaben.js | 43 ++++- workspace/assets/js/support.js | 8 +- workspace/assets/js/unterstuetzen.js | 23 ++- workspace/assets/js/vorlagenbrett.js | 119 +++++++++++++- workspace/aufgaben.html | 38 ++--- workspace/automation.html | 32 ++-- workspace/befinden.html | 34 ++-- workspace/bereich.html | 44 +++--- workspace/bewerben.html | 38 ++--- workspace/bewerbungen.html | 36 ++--- workspace/calls.html | 34 ++-- workspace/chat.html | 38 ++--- workspace/content.html | 34 ++-- workspace/crew-index.html | 12 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 ++-- workspace/entwicklung.html | 36 ++--- workspace/hilfe.html | 30 ++-- workspace/index.html | 10 +- workspace/kalender.html | 34 ++-- workspace/leistung.html | 32 ++-- workspace/material.html | 32 ++-- workspace/personen.html | 32 ++-- workspace/profil.html | 36 ++--- workspace/rechte.html | 34 ++-- workspace/report.html | 36 ++--- workspace/scouting.html | 32 ++-- workspace/start.html | 32 ++-- workspace/startcheck.html | 34 ++-- workspace/steckbrief.html | 36 ++--- workspace/support.html | 28 ++-- workspace/talente.html | 36 ++--- workspace/team.html | 32 ++-- workspace/teamlage.html | 34 ++-- workspace/teilen.html | 30 ++-- workspace/treff-moderation.html | 32 ++-- workspace/treff-regeln.html | 32 ++-- workspace/uebersicht.html | 32 ++-- workspace/unsere-seiten.html | 30 ++-- workspace/unterstuetzen.html | 34 ++-- workspace/werdegang.html | 32 ++-- workspace/willkommen.html | 30 ++-- workspace/wissen.html | 34 ++-- 60 files changed, 1890 insertions(+), 693 deletions(-) create mode 100644 server/mess-dialog-ausgang.mjs create mode 100644 server/mess-eingang-optik.mjs 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`. --> - - + + - +