=== 1. UNTERSTUETZEN (neue Kachel im Community-Bereich) ===
Filipe: "ich brauch auch noch eine kachel im community bereich. wo mein
paypal und meine amazon liste sein wird. wo die leute alle supporten
koennen auf andere art anstatt nur tiktok. jeder soll diese kachel sehen
aber nur dogfather soll sie veraendern koennen oder vieles mehr sehen."
Neu: unterstuetzen.html + css + js, server/workspace-unterstuetzung.js,
server/unterstuetzung-tabellen.js. Ton 45 (#7567fe) ist gerechnet, das
Kachelzeichen ist ein Herz ueber zwei offenen Haenden.
DIE WEGE STEHEN IN DER DATENBANK, nicht im Quelltext -- ein Recht, das
man nur ueber einen Entwickler ausueben kann, ist keines. DogFather
schreibt Titel, Text, Knopf und Adresse selbst, blendet Wege aus und
nimmt neue dazu.
DER PAYPAL-LINK STEHT LEER UND UNSICHTBAR DA. Filipe schrieb "mein
paypal kennst du ja schon" -- gesucht im ganzen Haus und im Vault,
nirgends gefunden. Eine Zahlungsadresse zu RATEN waere der
gefaehrlichste Fehler dieser Seite: Geld an einen Fremden, und niemand
merkt es. Also steht dort nichts, der Weg ist ausgeblendet, und die
Seite sagt genau EINER Person, dass er fehlt.
DREI ARTEN, UND DIE DRITTE IST DIE WICHTIGSTE: geld, geschenk, frei.
"Kostet nichts" bekommt dieselbe Kartenform und dieselbe Groesse.
Waeren die freien Wege kleiner oder weiter unten, waere die Aussage
"das ist zweite Wahl" -- und die Mehrheit derer, die hier lesen, waere
damit zweite Wahl.
DIE ZAEHLUNG IST ANONYM, UND ZWAR VON DER TABELLE HER. DogFather sieht,
wie oft ein Weg geoeffnet wurde (7/30 Tage/gesamt). Was er NICHT sieht,
ist WER -- weil es in unterstuetzung_striche keine Spalte dafuer gibt.
Geprueft wird das ueber PRAGMA table_info, nicht ueber eine Abfrage:
eine Abfrage liesse sich morgen erweitern, eine fehlende Spalte nicht.
Schreiben haengt an istDogFather, nicht an istLeitung -- die rechte Hand
fuehrt dieses Team mit und kommt trotzdem nicht an diesen Link. Jede
Adresse wird beim SCHREIBEN geprueft (nur https:// oder eine Seite
dieses Hauses); javascript:, data:, http:// und // werden abgewiesen.
Neu: server/pruef-unterstuetzung.mjs -- 70 Pruefungen, alle gruen.
Sie misst ueber die CREW-ADRESSE: Beim ersten Lauf waren vier Rollen
gruen und der Zuschauer rot, und es sah nach einem Rechtefehler aus.
Es war ein Messfehler -- ueber 127.0.0.1 landet man still im
Agenturhaus, und dort gibt es die Rolle "gast" gar nicht.
=== 2. DIE GESPRAECHSLISTE (screen1 + screen2) ===
Filipe: "ich will dass die komplette kachel viel krasser und geiler
aussieht. der hintergrund der kachel soll gleich bleiben."
Der Hintergrund ist unangetastet. Zwei echte Fehler auf seinem Bild:
- Bei "Das Rudel" stand das "@" rechts oben und die orange "6" eine
ZEILE TIEFER. Der Knopf ist ein Raster mit DREI Spalten und bekam
VIER Kinder -- das vierte fiel um. Ausgerechnet die wichtigste
Auskunft der Liste landete an der unauffaelligsten Stelle.
- Jedes Gespraech mit Profilbild haengte das Bild ZWEIMAL ein: ein
Block stand Zeichen fuer Zeichen doppelt da. Zu sehen war nichts,
gekostet hat es die doppelte Ladelast bei jedem Neuzeichnen.
Und eine tote Regel: `.chat-raum__knopf[data-an="ja"]` beschrieb die
Schiene am offenen Gespraech -- gesetzt wird aber `data-offen` am <li>.
Die Regel griff nie, und daneben stand eine zweite, blassere Fassung
derselben Sache. Jetzt steht alles einmal, und die Schiene ist da.
Dazu: Zeilen als Karten, 44px-Gesichter, ein Zaehler neben "Gespraeche",
Suchfeld als Pille mit Lupe, runder Farbfleck statt Quadrat, und der
leere Raum rechts bekommt eine Mitte statt eines Satzes in der Ecke.
=== 3. VIER FUNDE NEBENBEI ===
- supportAufraeumen() war exportiert und wurde NIRGENDS gerufen. Die
Meldungen samt Bildschirmfotos waeren fuer immer liegen geblieben,
obwohl "90 Tage" dokumentiert ist. Jetzt im Loeschkonzept.
- aufgaben_zuteilung fehlte ebenfalls im Loeschkonzept (seit 21.09.).
pruef-aufbewahrung ist damit wieder gruen.
- pruef-start-ansicht war rot, seit "Aufgaben" am 23.09. die silberne
Kachel bekam -- die ueberschreibt ihr --ton absichtlich. Die
Pruefung nimmt sie jetzt aus UND prueft die Ausnahme selbst.
- pruef-buehne meldete auf entwicklung.html 1,79:1 Kontrast bei "Wen
gehst du durch?" -- die Ueberschrift lag direkt auf dem Foto. Sie
steht jetzt auf einer deckenden Flaeche.
OFFEN: pruef-buehne meldet treff-regeln.html mal "0 von 40", mal "3 von
40" -- dieselbe Seite, verschiedene Antworten. Eine Pruefung, die
schwankt, ist schlimmer als eine rote. Nicht in diesem Zug behoben.
Gemessen: pruef-unterstuetzung 70, pruef-start-ansicht, pruef-buehne
(bis auf den Wackler), pruef-workspace-seiten, pruef-aufbewahrung 45,
pruef-rechtetafel 19, pruef-css-klassen, pruef-chatkachel 36,
pruef-chat-ausbau, pruef-entwicklung-kacheln 31 -- gruen.
Neu: server/mess-chat-liste.mjs (zeigt die Liste mit sieben Gespraechen
verschiedener Art, prueft nichts).
Co-Authored-By: Claude Opus 5 <[email protected]>
504 lines
24 KiB
JavaScript
504 lines
24 KiB
JavaScript
/* =====================================================================
|
|
UNTERSTÜTZEN — sieht jeder, ändert nur einer (24.09.2026)
|
|
|
|
Filipe: „jeder soll diese kachel sehen aber nur dogfather soll sie
|
|
verändern können oder vieles mehr sehen. alle anderen, rechte und
|
|
linke hand, modis und community sollen supporten können."
|
|
|
|
---------------------------------------------------------------------
|
|
AUF DIESER SEITE STEHT EIN ZAHLUNGSLINK. Das macht sie zur einzigen
|
|
im Haus, bei der ein Rechtefehler unmittelbar Geld kostet: Wer die
|
|
Adresse ändern darf, kann sie auf ein fremdes Konto zeigen lassen,
|
|
und es fällt niemandem auf. Deshalb wird hier nicht geprüft, ob die
|
|
Knöpfe fehlen — sondern ob der Server ABLEHNT.
|
|
|
|
---------------------------------------------------------------------
|
|
GEMESSEN WIRD ÜBER DIE CREW-ADRESSE, NICHT ÜBER 127.0.0.1
|
|
|
|
Das ist keine Formsache. Welches „Haus" eine Anmeldung betrifft,
|
|
leitet der Server aus dem `Host`-Kopf ab (`istCrewAdresse`). Über
|
|
127.0.0.1 landet man still im Agenturhaus — und dort gibt es die
|
|
Rolle `gast` gar nicht. Beim ersten Lauf dieser Datei waren deshalb
|
|
vier Rollen grün und der Zuschauer rot, und es sah nach einem
|
|
Rechtefehler aus. Es war ein MESSFEHLER: Ich hatte die falsche
|
|
Adresse gemessen.
|
|
|
|
`fetch` kann das nicht: `Host` ist ein verbotener Kopf und wird
|
|
verworfen. Deshalb `node:http` für die Schnittstelle und ein
|
|
HTTPS-Vorbau plus `--host-resolver-rules` für den Browser —
|
|
dasselbe Vorgehen wie in pruef-alter und pruef-chat-neu.
|
|
|
|
---------------------------------------------------------------------
|
|
SECHS ABSCHNITTE
|
|
|
|
1. Jede Rolle kommt auf die Seite und sieht die sichtbaren Wege.
|
|
2. NUR DogFather darf ändern. Die rechte Hand ausdrücklich NICHT —
|
|
sie führt das Team mit, und diese eine Tür bleibt trotzdem zu.
|
|
3. Eine Adresse wird geprüft, bevor sie in der Datenbank steht:
|
|
`javascript:`, `data:` und `http://` kommen nicht durch.
|
|
4. Was unsichtbar ist, wird auch nicht ausgeliefert — nicht
|
|
versteckt im Browser, sondern gar nicht erst geschickt.
|
|
5. Die Zählung ist anonym, und zwar von der TABELLE her: Es gibt
|
|
keine Spalte für eine Person. Geprüft über PRAGMA table_info,
|
|
nicht über eine Abfrage — eine Abfrage ließe sich ändern.
|
|
6. Die Seite steht im Browser und die Kachel im Community-Bereich.
|
|
|
|
MIT GEGENPROBEN: Jede Ablehnung wird gegen einen Fall gehalten, der
|
|
durchgehen MUSS. Sonst hieße „abgelehnt" vielleicht nur, dass gar
|
|
nichts geht.
|
|
|
|
Aufrufen mit: node server/pruef-unterstuetzung.mjs
|
|
Bild: SCHIRM=1 node server/pruef-unterstuetzung.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";
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
|
|
const PORT = await eigenerPort(import.meta, "pruef-unterstuetzung");
|
|
const HP = PORT + 1;
|
|
const CREW = "crew.dogfather-universe.com";
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-unter-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = String(PORT);
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
|
|
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, "pruef-unterstuetzung");
|
|
await new Promise((r) => setTimeout(r, 800));
|
|
|
|
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
|
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const melde = (t) => console.log(t);
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
/* ---- Sprechen über die Crew-Adresse ---------------------------------- */
|
|
function anfrage(pfad, art, koerper, keks) {
|
|
return new Promise((fertig, schief) => {
|
|
const kopf = { Host: CREW };
|
|
if (keks) kopf.Cookie = keks;
|
|
if (koerper) kopf["Content-Type"] = "application/json";
|
|
const a = httpAnfrage(
|
|
{ host: "127.0.0.1", port: PORT, path: pfad, method: art, headers: kopf },
|
|
(antwort) => {
|
|
let text = "";
|
|
antwort.on("data", (s) => { text += s; });
|
|
antwort.on("end", () => fertig({
|
|
code: antwort.statusCode, text, kopf: antwort.headers,
|
|
}));
|
|
});
|
|
a.on("error", schief);
|
|
if (koerper) a.write(JSON.stringify(koerper));
|
|
a.end();
|
|
});
|
|
}
|
|
const json = (a) => { try { return JSON.parse(a.text); } catch { return null; } };
|
|
|
|
/* ---- Der Bestand ----------------------------------------------------- */
|
|
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 schluessel = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", schluessel, jetzt);
|
|
|
|
function anlegen(name, rolle, code) {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salt, 64,
|
|
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
|
d.prepare("INSERT INTO personen"
|
|
+ " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
|
+ " VALUES (?,?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salt, 32768,
|
|
createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
|
|
/* Die fünf Rollen, die Filipe genannt hat — DogFather, rechte und
|
|
linke Hand, ein Modi und die Community. */
|
|
const ROLLEN = [
|
|
["Filipe", "admin", "CODE-DOGI-0001"],
|
|
["VanVan", "hand", "CODE-HAND-0001"],
|
|
["Kessi", "linke", "CODE-LINKE-0001"],
|
|
["Diene", "modi", "CODE-MODI-0001"],
|
|
["Zuschauer", "gast", "CODE-GAST-0001"],
|
|
];
|
|
for (const [name, rolle, code] of ROLLEN) anlegen(name, rolle, code);
|
|
|
|
/* Die Spalten — Abschnitt 5 prüft dagegen. Gelesen aus dem SCHEMA,
|
|
nicht aus einer Abfrage: Eine Abfrage ließe sich morgen erweitern,
|
|
eine fehlende Spalte nicht. */
|
|
const STRICH_SPALTEN = d.prepare("PRAGMA table_info(unterstuetzung_striche)")
|
|
.all().map((z) => z.name);
|
|
const WEG_SPALTEN = d.prepare("PRAGMA table_info(unterstuetzung_wege)")
|
|
.all().map((z) => z.name);
|
|
d.close();
|
|
|
|
async function anmelden(rolle, code) {
|
|
/* `alter_ok` NUR BEI DER COMMUNITY: Sie bestätigt beim ersten
|
|
Hereinkommen das Mindestalter (workspace.js). Wer zum Team gehört,
|
|
hat seinen Zugang persönlich bekommen. */
|
|
const a = await anfrage("/workspace/api/anmelden", "POST",
|
|
rolle === "gast" ? { rolle, code, alter_ok: true } : { rolle, code });
|
|
if (a.code !== 200) {
|
|
/* DER DRITTE AUSGANG: Ohne Anmeldung kann diese Prüfung gar nichts
|
|
beantworten — und das ist etwas anderes als „nicht in Ordnung". */
|
|
melde(` ABBRUCH: ${rolle} ließ sich nicht anmelden (HTTP ${a.code}, `
|
|
+ `${a.text.slice(0, 90)}). Ohne Anmeldung misst diese Prüfung nichts.`);
|
|
process.exit(2);
|
|
}
|
|
return (a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
|
|
}
|
|
const kekse = {};
|
|
for (const [, rolle, code] of ROLLEN) kekse[rolle] = await anmelden(rolle, code);
|
|
|
|
const lies = async (keks) => {
|
|
const a = await anfrage("/workspace/api/unterstuetzung", "GET", null, keks);
|
|
return { status: a.code, daten: a.code === 200 ? json(a) : null };
|
|
};
|
|
const schreib = (keks, id, koerper, art = "PUT") =>
|
|
anfrage(`/workspace/api/unterstuetzung${id ? "/" + id : ""}`, art, koerper, keks);
|
|
|
|
/* =====================================================================
|
|
1. JEDE ROLLE SIEHT DIE SEITE
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 1. Sieht jeder die Wege? ===");
|
|
const gesehen = {};
|
|
for (const [, rolle] of ROLLEN) {
|
|
const r = await lies(kekse[rolle]);
|
|
gesehen[rolle] = r.daten;
|
|
ok(r.status === 200 && Array.isArray(r.daten?.wege),
|
|
`${rolle}: bekommt die Wege (HTTP ${r.status}, ${r.daten?.wege?.length ?? "?"} Stück)`);
|
|
}
|
|
ok(gesehen.gast?.wege.length > 0, `und es sind wirklich welche da (${gesehen.gast?.wege.length})`);
|
|
/* Die Wunschliste ist der Weg, den Filipe mitgeliefert hat — sie muss
|
|
bei JEDEM ankommen, nicht nur bei der Leitung. */
|
|
const wunsch = gesehen.gast?.wege.find((w) => w.schluessel === "amazon");
|
|
ok(!!wunsch && wunsch.ziel.startsWith("https://www.amazon.de/"),
|
|
`die Wunschliste steht auch beim Zuschauer (${wunsch?.ziel.slice(0, 42)}…)`);
|
|
|
|
/* GEGENPROBE ZUM GANZEN ABSCHNITT: Ohne Anmeldung gibt es nichts. */
|
|
{
|
|
const a = await anfrage("/workspace/api/unterstuetzung", "GET");
|
|
ok(a.code === 401, `ohne Anmeldung gar nichts (HTTP ${a.code})`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
2. ÄNDERN DARF NUR EINER
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 2. Wer darf ändern? ===");
|
|
ok(gesehen.admin?.darf_aendern === true, "DogFather: darf_aendern = true");
|
|
for (const rolle of ["hand", "linke", "modi", "gast"]) {
|
|
ok(gesehen[rolle]?.darf_aendern === false, `${rolle}: darf_aendern = false`);
|
|
}
|
|
|
|
const amazonId = gesehen.admin.wege.find((w) => w.schluessel === "amazon").id;
|
|
/* UND ZWAR NICHT NUR IM BROWSER. Jede der vier Rollen versucht es
|
|
wirklich — eine Oberfläche, die den Knopf versteckt, ist keine
|
|
Sperre, sondern eine Bitte. */
|
|
for (const rolle of ["hand", "linke", "modi", "gast"]) {
|
|
const a = await schreib(kekse[rolle], amazonId, { titel: "Übernommen" });
|
|
ok(a.code === 404, `${rolle}: PUT wird abgewiesen (HTTP ${a.code})`);
|
|
}
|
|
{
|
|
const a = await schreib(kekse.hand, amazonId, null, "DELETE");
|
|
ok(a.code === 404, `die rechte Hand kann auch nicht löschen (HTTP ${a.code})`);
|
|
const b = await schreib(kekse.hand, null, { titel: "Eigener Weg" }, "POST");
|
|
ok(b.code === 404, `und keinen neuen anlegen (HTTP ${b.code})`);
|
|
}
|
|
/* GEGENPROBE: DogFather kommt durch. Ohne sie hieße „404 für alle"
|
|
vielleicht nur, dass es die Route gar nicht gibt. */
|
|
{
|
|
const a = await schreib(kekse.admin, amazonId, { titel: "Wunschliste" });
|
|
ok(a.code === 200, `DogFather kommt durch (HTTP ${a.code})`);
|
|
const nach = await lies(kekse.gast);
|
|
ok(nach.daten.wege.find((w) => w.id === amazonId)?.titel === "Wunschliste",
|
|
"und die Änderung kommt bei allen an");
|
|
}
|
|
|
|
/* =====================================================================
|
|
3. DIE ADRESSE WIRD GEPRÜFT
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 3. Was darf in eine Adresse? ===");
|
|
const BOESE = [
|
|
["javascript:alert(1)", "ein Skript"],
|
|
["data:text/html,<h1>x", "eine ganze Seite im Link"],
|
|
["http://example.com/pay", "unverschlüsselt"],
|
|
["//example.com/pay", "ohne Schema"],
|
|
];
|
|
for (const [ziel, warum] of BOESE) {
|
|
const a = await schreib(kekse.admin, amazonId, { ziel, knopf: "Los" });
|
|
ok(a.code === 400, `abgelehnt: ${warum} (${ziel.slice(0, 24)} → HTTP ${a.code})`);
|
|
}
|
|
/* GEGENPROBE: Die beiden erlaubten Formen gehen durch. */
|
|
const AMAZON = "https://www.amazon.de/hz/wishlist/ls/3BGGNNHYAO5QL";
|
|
{
|
|
const a = await schreib(kekse.admin, amazonId,
|
|
{ ziel: AMAZON, knopf: "Wunschliste ansehen" });
|
|
ok(a.code === 200, `https:// geht durch (HTTP ${a.code})`);
|
|
const b = await schreib(kekse.admin,
|
|
gesehen.admin.wege.find((w) => w.schluessel === "draussen").id,
|
|
{ ziel: "unsere-seiten.html", knopf: "Zu Draußen" });
|
|
ok(b.code === 200, `eine Seite des Hauses auch (HTTP ${b.code})`);
|
|
}
|
|
/* EIN KNOPF OHNE ZIEL IST EIN VERSPRECHEN INS LEERE. */
|
|
{
|
|
const a = await schreib(kekse.admin, amazonId, { ziel: "", knopf: "Drück mich" });
|
|
ok(a.code === 400, `Knopf ohne Adresse wird abgelehnt (HTTP ${a.code})`);
|
|
await schreib(kekse.admin, amazonId, { ziel: AMAZON, knopf: "Wunschliste ansehen" });
|
|
}
|
|
|
|
/* =====================================================================
|
|
4. UNSICHTBAR HEISST NICHT AUSGELIEFERT
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 4. Was unsichtbar ist ===");
|
|
{
|
|
const beiAdmin = (await lies(kekse.admin)).daten.wege;
|
|
const beiGast = (await lies(kekse.gast)).daten.wege;
|
|
const paypal = beiAdmin.find((w) => w.schluessel === "paypal");
|
|
ok(!!paypal, "DogFather sieht den PayPal-Weg");
|
|
ok(paypal.sichtbar === false && paypal.ziel === "",
|
|
"er steht ohne Adresse da und ist ausgeblendet — eine Zahlungsadresse wird nicht geraten");
|
|
ok(!beiGast.some((w) => w.schluessel === "paypal"),
|
|
"und beim Zuschauer kommt er gar nicht erst an");
|
|
ok(beiAdmin.length > beiGast.length,
|
|
`DogFather sieht mehr (${beiAdmin.length} gegen ${beiGast.length})`);
|
|
ok(beiGast.every((w) => w.sichtbar), "beim Zuschauer ist alles Gelieferte auch sichtbar");
|
|
ok((await lies(kekse.admin)).daten.zahlen !== null, "DogFather bekommt die Zahlen");
|
|
for (const rolle of ["hand", "linke", "modi", "gast"]) {
|
|
ok((await lies(kekse[rolle])).daten.zahlen === null, `${rolle}: keine Zahlen`);
|
|
}
|
|
}
|
|
|
|
/* =====================================================================
|
|
5. DIE ZÄHLUNG IST ANONYM — VON DER TABELLE HER
|
|
===================================================================== */
|
|
melde("");
|
|
melde("=== 5. Zählt jemand mit? ===");
|
|
ok(STRICH_SPALTEN.length > 0,
|
|
`die Zähltabelle hat ${STRICH_SPALTEN.length} Spalten (${STRICH_SPALTEN.join(", ")})`);
|
|
const VERDAECHTIG = ["person_id", "person", "von_id", "ip", "sitzung", "name", "wer"];
|
|
ok(!STRICH_SPALTEN.some((s) => VERDAECHTIG.includes(s)),
|
|
"keine Spalte für eine Person, eine IP oder eine Sitzung");
|
|
/* GEGENPROBE: Derselbe Sucher findet so etwas auch wirklich — die
|
|
Wege-Tabelle hat `geaendert_von`, und das MUSS er sehen. Ohne diese
|
|
Zeile hieße „nichts gefunden" vielleicht nur, dass er nicht sucht. */
|
|
ok(WEG_SPALTEN.includes("geaendert_von"),
|
|
"Gegenprobe: in der Wege-Tabelle findet dieselbe Suche sehr wohl eine Person (geaendert_von)");
|
|
|
|
{
|
|
const zuvor = (await lies(kekse.admin)).daten.zahlen?.[amazonId]?.gesamt || 0;
|
|
const a = await anfrage(`/workspace/api/unterstuetzung/${amazonId}/strich`,
|
|
"POST", null, kekse.gast);
|
|
ok(a.code === 200, `ein Zuschauer darf zählen (HTTP ${a.code})`);
|
|
const danach = (await lies(kekse.admin)).daten.zahlen?.[amazonId]?.gesamt || 0;
|
|
ok(danach === zuvor + 1, `und der Strich kommt an (${zuvor} → ${danach})`);
|
|
/* EINMAL JE TAG. Ohne diese Sperre wäre die Zahl das Ergebnis von
|
|
Dauerdrücken und keine Auskunft. */
|
|
await anfrage(`/workspace/api/unterstuetzung/${amazonId}/strich`, "POST", null, kekse.gast);
|
|
const nochmal = (await lies(kekse.admin)).daten.zahlen?.[amazonId]?.gesamt || 0;
|
|
ok(nochmal === danach, `zweimal derselbe zählt nicht doppelt (${danach} → ${nochmal})`);
|
|
/* Aber ein ANDERER schon — sonst wäre die Sperre zu breit. */
|
|
await anfrage(`/workspace/api/unterstuetzung/${amazonId}/strich`, "POST", null, kekse.modi);
|
|
const dritter = (await lies(kekse.admin)).daten.zahlen?.[amazonId]?.gesamt || 0;
|
|
ok(dritter === nochmal + 1, `ein anderer Mensch zählt sehr wohl (${nochmal} → ${dritter})`);
|
|
}
|
|
/* EIN UNSICHTBARER WEG WIRD NICHT GEZÄHLT — sonst stünde eine Zahl an
|
|
einem Weg, den außer DogFather niemand sehen kann. */
|
|
{
|
|
const paypalId = (await lies(kekse.admin)).daten.wege
|
|
.find((w) => w.schluessel === "paypal").id;
|
|
const a = await anfrage(`/workspace/api/unterstuetzung/${paypalId}/strich`,
|
|
"POST", null, kekse.gast);
|
|
ok(a.code === 404, `ein ausgeblendeter Weg lässt sich nicht zählen (HTTP ${a.code})`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
6. DIE SEITE UND DIE KACHEL
|
|
=====================================================================
|
|
|
|
Der Browser spricht über die Crew-Adresse, sonst misst er das
|
|
Agenturhaus (siehe den Kopf). Dafür ein HTTPS-Vorbau mit einem
|
|
selbst ausgestellten Zeugnis und `--host-resolver-rules` — dasselbe
|
|
Vorgehen wie in pruef-chat-neu. */
|
|
melde("");
|
|
melde("=== 6. Im Browser ===");
|
|
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 SEITE = `https://${CREW}:${HP}`;
|
|
|
|
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 rolle of ["admin", "gast"]) {
|
|
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
|
|
await kontext.addCookies(kekse[rolle].split("; ").map((k) => {
|
|
const [name, ...rest] = k.split("=");
|
|
return { name, value: rest.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, 120));
|
|
}
|
|
});
|
|
seite.on("pageerror", (e) => konsole.push("Absturz: " + String(e).slice(0, 120)));
|
|
|
|
/* DIE KACHEL — ohne sie findet niemand die Seite. */
|
|
await seite.goto(`${SEITE}/workspace/start.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(700);
|
|
const kachel = await seite.evaluate(() => {
|
|
const a = [...document.querySelectorAll('a[href^="unterstuetzen.html"]')][0];
|
|
if (!a) return null;
|
|
return {
|
|
name: a.querySelector(".kachel__name")?.textContent
|
|
|| a.textContent.trim().slice(0, 40),
|
|
zeichen: !!a.querySelector("svg"),
|
|
};
|
|
});
|
|
ok(!!kachel, `${rolle}: die Kachel steht auf der Startseite (${kachel?.name || "—"})`);
|
|
ok(!!kachel?.zeichen, `${rolle}: und sie trägt ihr Zeichen`);
|
|
|
|
await seite.goto(`${SEITE}/workspace/unterstuetzen.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForSelector(".u-weg", { timeout: 15_000 });
|
|
await seite.waitForTimeout(500);
|
|
|
|
const b = await seite.evaluate(() => ({
|
|
karten: document.querySelectorAll(".u-weg").length,
|
|
gruppen: [...document.querySelectorAll(".u-gruppe__titel")].map((x) => x.textContent),
|
|
leitung: !document.getElementById("leitung")?.hidden,
|
|
knoepfe: [...document.querySelectorAll(".u-weg__knopf")].map((a) => ({
|
|
ziel: a.getAttribute("href"), rel: a.getAttribute("rel") || "",
|
|
neu: a.getAttribute("target") || "",
|
|
hoch: Math.round(a.getBoundingClientRect().height),
|
|
})),
|
|
}));
|
|
ok(b.karten >= 4, `${rolle}: ${b.karten} Wege stehen da`);
|
|
ok(b.gruppen.length === 2, `${rolle}: zwei Gruppen (${b.gruppen.join(" / ")})`);
|
|
ok(b.leitung === (rolle === "admin"),
|
|
`${rolle}: der Leitungskasten steht ${rolle === "admin" ? "da" : "NICHT da"}`);
|
|
|
|
/* JEDER WEG NACH DRAUSSEN BRAUCHT `noopener` — ohne ihn kann die
|
|
geöffnete Seite über `window.opener` auf diese zugreifen. */
|
|
const raus = b.knoepfe.filter((k) => /^https:/.test(k.ziel));
|
|
ok(raus.length > 0, `${rolle}: ${raus.length} Weg(e) führen nach draußen`);
|
|
ok(raus.every((k) => k.rel.includes("noopener") && k.rel.includes("noreferrer")),
|
|
`${rolle}: und alle tragen rel="noopener noreferrer"`);
|
|
ok(raus.every((k) => k.neu === "_blank"), `${rolle}: und öffnen ein neues Fenster`);
|
|
/* AM FINGER TREFFBAR. Dieser Knopf führt zu einer Zahlung; ihn zu
|
|
verfehlen ist ärgerlicher als anderswo. */
|
|
ok(b.knoepfe.length > 0 && b.knoepfe.every((k) => k.hoch >= 44),
|
|
`${rolle}: jeder Knopf ist mindestens 44 px hoch `
|
|
+ `(${b.knoepfe.map((k) => k.hoch).join(", ")})`);
|
|
|
|
/* ==== DER WEG, DEN FILIPE WIRKLICH GEHEN WIRD ================
|
|
|
|
Sein PayPal-Link steht nirgends im Haus ("mein paypal kennst du
|
|
ja schon" -- gesucht, nicht gefunden), und geraten wird er
|
|
nicht. Er traegt ihn also selbst ein, hier, auf dieser Seite.
|
|
|
|
GENAU DIESER ABLAUF WIRD GEMESSEN und nicht nur die Route
|
|
darunter: Knopf druecken, Felder ausfuellen, speichern, und
|
|
danach muss der Weg bei einem ZUSCHAUER ankommen. Eine Route,
|
|
die 200 sagt, waehrend das Formular das falsche Feld schickt,
|
|
waere gruen und trotzdem kaputt. */
|
|
if (rolle === "admin") {
|
|
await seite.click("#aendern-an");
|
|
await seite.waitForTimeout(400);
|
|
const felder = await seite.evaluate(() =>
|
|
document.querySelectorAll('.u-weg .u-form [data-feld="ziel"]').length);
|
|
ok(felder >= 4, `Änderungsmodus: ${felder} Karten zeigen ihre Felder`);
|
|
|
|
const getippt = await seite.evaluate(() => {
|
|
const karte = [...document.querySelectorAll(".u-weg")]
|
|
.find((k) => k.dataset.schluessel === "paypal");
|
|
if (!karte) return false;
|
|
const setz = (feld, wert) => {
|
|
const e = karte.querySelector(`[data-feld="${feld}"]`);
|
|
if (!e) return;
|
|
if (e.type === "checkbox") e.checked = wert; else e.value = wert;
|
|
};
|
|
setz("ziel", "https://www.paypal.com/paypalme/beispiel");
|
|
setz("knopf", "Bei PayPal öffnen");
|
|
setz("sichtbar", true);
|
|
[...karte.querySelectorAll("button")]
|
|
.find((b) => b.textContent === "Speichern")?.click();
|
|
return true;
|
|
});
|
|
ok(getippt, "die PayPal-Karte lässt sich ausfüllen");
|
|
await seite.waitForTimeout(900);
|
|
|
|
const jetztDa = await seite.evaluate(() => {
|
|
const k = [...document.querySelectorAll(".u-weg")]
|
|
.find((x) => x.dataset.schluessel === "paypal");
|
|
return { aus: k?.dataset.aus || "", offen: !!k };
|
|
});
|
|
ok(jetztDa.offen && jetztDa.aus === "", "und steht danach nicht mehr ausgeblendet da");
|
|
|
|
/* UND JETZT DER PUNKT: Kommt sie beim Zuschauer an? */
|
|
const beimGast = (await lies(kekse.gast)).daten.wege
|
|
.find((w) => w.schluessel === "paypal");
|
|
ok(!!beimGast && beimGast.ziel === "https://www.paypal.com/paypalme/beispiel"
|
|
&& beimGast.knopf === "Bei PayPal öffnen",
|
|
`der Zuschauer sieht den Weg jetzt (${beimGast?.knopf || "—"})`);
|
|
|
|
/* ZURUECK IN DEN ANZEIGEMODUS -- das Bildschirmfoto soll zeigen,
|
|
was Filipe im Alltag sieht, nicht das Formular. */
|
|
await seite.click("#aendern-an");
|
|
await seite.waitForTimeout(500);
|
|
}
|
|
|
|
if (rolle === "admin" && process.env.SCHIRM) {
|
|
await seite.screenshot({ path: "server/pruef-unterstuetzung.png" });
|
|
melde(" -- Bildschirmfoto: server/pruef-unterstuetzung.png");
|
|
}
|
|
ok(konsole.length === 0, konsole.length
|
|
? `${rolle}: Fehler in der Konsole: ${konsole[0]}`
|
|
: `${rolle}: keine Fehler in der Konsole`);
|
|
await kontext.close();
|
|
}
|
|
} finally {
|
|
await browser.close();
|
|
try { vorbau.close(); } catch { /* egal */ }
|
|
}
|
|
|
|
melde("");
|
|
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(fehler ? 1 : 0);
|