Aus jedem Fenster kommt man heraus, die Kachel dreht sich, der Eingang sieht aus wie das Haus

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 <[email protected]>
This commit is contained in:
2026-09-25 00:50:15 +02:00
co-authored by Claude Opus 5
parent 14d6f5000c
commit e5cd016b60
60 changed files with 1890 additions and 693 deletions
+18
View File
@@ -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();
}
+226
View File
@@ -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);
}
+182
View File
@@ -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);
}
+54 -2
View File
@@ -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}"`);
+72
View File
@@ -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
+85 -5
View File
@@ -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", {
+7 -3
View File
@@ -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
+17 -12
View File
@@ -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),
+25 -4
View File
@@ -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));
+4 -3
View File
@@ -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 */
}
+11 -3
View File
@@ -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),
+30 -3
View File
@@ -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