diff --git a/pruef-protokoll-auf.png b/pruef-protokoll-auf.png new file mode 100644 index 00000000..75777512 Binary files /dev/null and b/pruef-protokoll-auf.png differ diff --git a/pruef-protokoll-zu.png b/pruef-protokoll-zu.png new file mode 100644 index 00000000..1cde832a Binary files /dev/null and b/pruef-protokoll-zu.png differ diff --git a/server/pruef-protokoll-klappe.mjs b/server/pruef-protokoll-klappe.mjs new file mode 100644 index 00000000..28b83a6d --- /dev/null +++ b/server/pruef-protokoll-klappe.mjs @@ -0,0 +1,165 @@ +/* =================================================================== + Pruefung: Lange Protokoll-Listen lassen sich auf- und zuklappen und + zeigen zugeklappt genau vier Zeilen. Geprueft an BEIDEN Stellen, die + den gemeinsamen Helfer aus kopf.js benutzen -- Personen und + Automationen. + =================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-klappe-")); +process.env.WORKSPACE_DB = join(ordner, "pruef.sqlite"); +process.env.PORT = "4180"; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; +process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-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 }); }; +await import("./index.js"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = "http://127.0.0.1:4180"; +setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.(); + +let fehler = 0; +const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const salt = randomBytes(16).toString("hex"); +const hash = scryptSync("CODE-DOGI-0001", 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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run("Dogfather", "admin", hash, salt, 32768, new Date().toISOString()); +/* Reichlich Eintraege, damit der Unterschied zwischen zu und auf + sichtbar wird. 'betreuung_gesetzt' zaehlt auch die Automationen-Seite + als Automation -- damit dort ebenfalls genug zusammenkommt. */ +for (let i = 0; i < 30; i++) { + d.prepare("INSERT INTO protokoll (zeitpunkt, aktion, detail, ip) VALUES (?,?,?,?)") + .run(new Date(Date.now() - i * 3600_000).toISOString(), "betreuung_gesetzt", "Nr. " + i, "1.2.3.4"); +} +d.close(); + +const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await chromium.launch(); + +async function anmelden(kontext) { + const seite = await kontext.newPage(); + await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); + await seite.click('.rolle[data-rolle="admin"]'); + await seite.fill("#code", "CODE-DOGI-0001"); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 15000 }); + return seite; +} + +/* seite, Liste, Knopf, Textfeld -> was gerade zu sehen ist */ +const lesen = (seite, liste, knopf, text) => seite.evaluate(([l, k, t]) => { + const box = document.querySelector(l); + const kn = document.querySelector(k); + const zeilen = box ? [...box.children] : []; + return { + sichtbar: zeilen.filter((z) => z.offsetParent !== null).length, + gesamt: zeilen.length, + text: document.querySelector(t)?.textContent, + auf: kn?.getAttribute("aria-expanded"), + knopfDa: kn ? !kn.hidden : false, + ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth, + pfeil: getComputedStyle(document.querySelector(k + " .klapp-pfeil")).transform, + }; +}, [liste, knopf, text]); + +async function pruefe(seite, name, pfad, liste, knopf, text) { + await seite.goto(BASIS + pfad, { waitUntil: "networkidle" }); + await seite.waitForTimeout(600); + console.log(`\n--- ${name}`); + + const zu = await lesen(seite, liste, knopf, text); + ok(zu.knopfDa, "der Knopf ist da"); + ok(zu.sichtbar === 4, `zugeklappt vier Zeilen sichtbar (${zu.sichtbar} von ${zu.gesamt})`); + ok(zu.auf === "false", `aria-expanded=false (${zu.auf})`); + ok(/^Alle \d+ zeigen$/.test(zu.text || ""), `Knopf nennt die Zahl ("${zu.text}")`); + ok(zu.ueber <= 0, `nichts steht über (${zu.ueber}px)`); + + await seite.click(knopf); + await seite.waitForTimeout(350); + const auf = await lesen(seite, liste, knopf, text); + ok(auf.sichtbar === auf.gesamt && auf.gesamt > 4, + `aufgeklappt alle Zeilen sichtbar (${auf.sichtbar} von ${auf.gesamt})`); + ok(auf.auf === "true", `aria-expanded=true (${auf.auf})`); + ok(auf.text === "Nur die letzten 4", `Knopf bietet das Zuklappen an ("${auf.text}")`); + ok(auf.pfeil !== zu.pfeil, "der Pfeil hat sich gedreht"); + ok(auf.ueber <= 0, `auch aufgeklappt steht nichts über (${auf.ueber}px)`); + + /* Zweimal klicken -- der haeufigste Fehler bei so etwas ist ein + doppelt gesetzter Zuhoerer, der beim zweiten Mal gar nichts tut. */ + await seite.click(knopf); + await seite.waitForTimeout(350); + const wiederZu = await lesen(seite, liste, knopf, text); + ok(wiederZu.sichtbar === 4, `wieder zugeklappt vier Zeilen (${wiederZu.sichtbar})`); + await seite.click(knopf); + await seite.waitForTimeout(350); + ok((await lesen(seite, liste, knopf, text)).sichtbar > 4, "und wieder auf"); + return zu.gesamt; +} + +for (const [breite, wie] of [[1440, "Computer"], [390, "Handy"]]) { + console.log(`\n=== ${wie}`); + const kontext = await browser.newContext({ viewport: { width: breite, height: 900 } }); + const seite = await anmelden(kontext); + const konsole = []; + seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); }); + seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message)); + + await pruefe(seite, "Personen · Letzte Ereignisse", "/workspace/personen.html", + "#protokoll", "#protokoll-schalter", "#protokoll-schalter-text"); + await pruefe(seite, "Automationen · Zuletzt automatisch passiert", "/workspace/automation.html", + "#verlauf .verlauf", "#verlauf-schalter", "#verlauf-schalter-text"); + + ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? " -> " + konsole.join(" | ") : ""}`); + + if (breite === 1440) { + await seite.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" }); + await seite.waitForTimeout(500); + await seite.screenshot({ path: "pruef-protokoll-zu.png" }); + await seite.click("#protokoll-schalter"); + await seite.waitForTimeout(400); + await seite.screenshot({ path: "pruef-protokoll-auf.png", fullPage: true }); + } + await kontext.close(); +} + +/* Gegenprobe: Bei hoechstens vier Eintraegen darf der Knopf nicht da + sein -- ein Knopf, der nichts verbirgt, verwirrt nur. */ +{ + const d2 = new DatabaseSync(process.env.WORKSPACE_DB); + d2.prepare("DELETE FROM protokoll WHERE aktion = 'betreuung_gesetzt'").run(); + d2.prepare("DELETE FROM protokoll WHERE id NOT IN (SELECT id FROM protokoll ORDER BY id DESC LIMIT 2)").run(); + d2.close(); + console.log("\n=== Wenige Einträge"); + const kontext = await browser.newContext({ viewport: { width: 1440, height: 900 } }); + const seite = await anmelden(kontext); + await seite.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" }); + await seite.waitForTimeout(500); + const r = await seite.evaluate(() => ({ + gesamt: document.querySelectorAll("#protokoll .protokoll-zeile").length, + knopfDa: !document.getElementById("protokoll-schalter")?.hidden, + klapp: document.getElementById("protokoll")?.dataset.klapp, + })); + ok(r.gesamt <= 4 && !r.knopfDa, `kein Knopf bei ${r.gesamt} Einträgen (Knopf da: ${r.knopfDa})`); + ok(!r.klapp, `und nichts bleibt versteckt (data-klapp=${r.klapp})`); + await kontext.close(); +} + +await browser.close(); +console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`); +try { rmSync(ordner, { recursive: true, force: true }); } catch {} +process.exit(fehler === 0 ? 0 : 1); diff --git a/workspace/assets/css/personen.css b/workspace/assets/css/personen.css index d639fc35..e8c638ca 100644 --- a/workspace/assets/css/personen.css +++ b/workspace/assets/css/personen.css @@ -55,6 +55,10 @@ .protokoll-block { margin-top: 44px; } +/* Das Auf- und Zuklappen selbst steht in start.css (.klapp-kopf, + .klapp-pfeil, [data-klapp="zu"]) -- es wird auch auf der + Automationen-Seite gebraucht. */ + .protokoll-zeile { display: grid; gap: 10px; grid-template-columns: 128px 190px 1fr auto; @@ -72,6 +76,7 @@ .protokoll-zeile { grid-template-columns: 1fr; gap: 2px; padding: 10px; } } + /* ---------- Zuständigkeit ------------------------------------------------ Wer betreut welchen Creator. Bewusst direkt in der Personenzeile und nicht auf einer eigenen Seite: Es ist eine Eigenschaft der Person, kein diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index 116eedde..3f9a4abc 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -159,6 +159,34 @@ body.start { .leise { color: var(--text-still); font-weight: 400; } +/* ---------- Auf- und Zuklappen ------------------------------------------ + Lange Listen (Protokoll, Verlauf) stehen zugeklappt da und zeigen nur + die neuesten Zeilen. Der Pfeil dreht sich beim Aufklappen -- er zeigt + die Richtung, der Text daneben sagt, was der Knopf TUT. + + Steht hier und nicht in einer Seiten-Datei: Er wird an mehreren + Stellen gebraucht, und genau so sind schon dreimal Regeln dort + gelandet, wo eine Seite sie gar nicht laedt. */ + +.klapp-kopf { + display: flex; align-items: center; justify-content: space-between; + gap: 12px; flex-wrap: wrap; margin-bottom: 8px; +} +.klapp-kopf > .feldschild, .klapp-kopf > .block__titel { margin: 0; } + +.klapp-pfeil { + width: 14px; height: 14px; flex: 0 0 auto; + fill: none; stroke: currentColor; + stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; + transition: transform var(--tempo); +} +[aria-expanded="true"] > .klapp-pfeil { transform: rotate(180deg); } + +/* Zugeklappt bleiben die vier neuesten Zeilen stehen. Bewusst ueber CSS + statt ueber das Entfernen von Zeilen: Das Aufklappen braucht dann + weder einen neuen Aufbau noch eine zweite Abfrage. */ +[data-klapp="zu"] > *:nth-child(n+5) { display: none; } + .willkommen { margin-bottom: 38px; } .willkommen .titel { margin-bottom: 6px; } diff --git a/workspace/assets/js/automation.js b/workspace/assets/js/automation.js index 32646661..303ce926 100644 --- a/workspace/assets/js/automation.js +++ b/workspace/assets/js/automation.js @@ -181,6 +181,9 @@ liste.append(z); } ziel.append(liste); + /* Geklappt wird die INNERE Liste, nicht #verlauf -- dessen einziges + Kind ist diese Liste, da gaebe es nichts zu verbergen. */ + window.klappe?.(liste, $('verlauf-schalter'), $('verlauf-schalter-text')); } /* ---------- Start --------------------------------------------------------- */ diff --git a/workspace/assets/js/kopf.js b/workspace/assets/js/kopf.js index 4a51be8f..81521e8e 100644 --- a/workspace/assets/js/kopf.js +++ b/workspace/assets/js/kopf.js @@ -10,6 +10,58 @@ const START = '/workspace/start.html'; + /* ---------- Auf- und Zuklappen langer Listen ------------------------- + + Lange Protokoll-Listen sind Nachschlagewerk, kein Startbild: Sie + stehen zugeklappt da und zeigen nur die neuesten Zeilen. + + Steht hier, weil es an mehreren Stellen gebraucht wird -- und weil + genau so schon dreimal Regeln in einer Seitendatei gelandet sind, + die eine andere Seite gar nicht laedt. + + `klappe(liste, knopf, text)` erwartet: die Liste (deren direkte + Kinder die Zeilen sind), den Knopf und das Textfeld darin. Das + Ausblenden macht CSS ueber [data-klapp="zu"] -- ohne Neuaufbau und + ohne zweite Abfrage. */ + const KLAPP_ZU = 4; /* dieselbe Zahl steht im CSS als nth-child(n+5) */ + + window.klappe = function (liste, knopf, textfeld) { + if (!liste || !knopf) return; + const anzahl = liste.children.length; + + /* Ein Knopf, der nichts zu verbergen hat, verwirrt nur. */ + knopf.hidden = anzahl <= KLAPP_ZU; + if (knopf.hidden) { liste.removeAttribute('data-klapp'); return; } + + /* Liest die Anzahl bei jedem Aufruf frisch aus der Liste, statt sie + von oben mitzunehmen. Der Zuhoerer unten wird nur einmal gesetzt + und haelt darum fuer immer die Fassung vom ersten Durchlauf fest + -- mit einer festgehaltenen Zahl stuende spaeter "Alle 3 zeigen" + an einer Liste mit dreissig Zeilen. */ + const stand = (offen) => { + if (offen) liste.removeAttribute('data-klapp'); + else liste.dataset.klapp = 'zu'; + knopf.setAttribute('aria-expanded', offen ? 'true' : 'false'); + /* Der Knopf sagt, was er TUT, nicht wo man gerade ist -- und nennt + die Zahl, damit man weiss, was dahintersteckt. */ + if (textfeld) { + textfeld.textContent = offen + ? `Nur die letzten ${KLAPP_ZU}` + : `Alle ${liste.children.length} zeigen`; + } + }; + + /* Die Liste wird bei jeder Aktualisierung neu aufgebaut, also laeuft + auch klappe() mehrfach. Der Zuhoerer wird deshalb nur EINMAL + gesetzt -- sonst haengten beim zweiten Mal zwei daran und ein + Klick haette doppelt umgeschaltet, also gar nichts bewirkt. */ + if (!knopf.dataset.klappBereit) { + knopf.dataset.klappBereit = 'ja'; + knopf.addEventListener('click', () => stand(liste.dataset.klapp === 'zu')); + } + stand(false); + }; + /* Gibt es eine sinnvolle vorherige Seite INNERHALB des Workspace? Blindes history.back() reicht nicht: Wer die Adresse direkt eingibt diff --git a/workspace/assets/js/personen.js b/workspace/assets/js/personen.js index 8a092a48..273d044b 100644 --- a/workspace/assets/js/personen.js +++ b/workspace/assets/js/personen.js @@ -245,6 +245,9 @@ z.append(el('span', 'protokoll-ip', e.ip || '')); ziel.append(z); } + /* Zugeklappt stehen nur die neuesten Zeilen da. Gemeinsamer + Helfer aus kopf.js -- die Automationen-Seite nutzt denselben. */ + window.klappe?.(ziel, $('protokoll-schalter'), $('protokoll-schalter-text')); } catch { /* umgeleitet */ } } diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 52513ecf..f8ec70d9 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -7,9 +7,9 @@ - - - + + + @@ -134,8 +134,8 @@ - - - + + + diff --git a/workspace/automation.html b/workspace/automation.html index f58fb474..35fda76b 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -84,8 +84,17 @@
-
+ +

Zuletzt automatisch passiert

+

Aus dem Protokoll – damit nachvollziehbar bleibt, was ohne Zutun geschah.

Wird geladen …

@@ -93,8 +102,8 @@ - - - + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 4c91a105..c6ce22d0 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -98,8 +98,8 @@ - - - + + + diff --git a/workspace/calls.html b/workspace/calls.html index ac6f4886..831b9d31 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -49,9 +49,9 @@ - - - - + + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index e84bea89..6edbc1fc 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -88,8 +88,8 @@ - - - + + + diff --git a/workspace/index.html b/workspace/index.html index bd06d3fc..99588258 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -25,7 +25,7 @@ type="image/webp" media="(max-width: 899px)" /> - + @@ -143,6 +143,6 @@

- + diff --git a/workspace/kalender.html b/workspace/kalender.html index 1e314461..e25642da 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -94,8 +94,8 @@ - - - + + + diff --git a/workspace/personen.html b/workspace/personen.html index e4b49e0e..bdd75594 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -86,15 +86,26 @@

Personen werden geladen …

+
-

Letzte Ereignisse

-

…

+
+

Letzte Ereignisse

+ +
+

…

- - - + + + diff --git a/workspace/profil.html b/workspace/profil.html index a1918b13..498544a6 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -127,8 +127,8 @@ - - - + + + diff --git a/workspace/report.html b/workspace/report.html index 30aca64b..5082d5dc 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -86,9 +86,9 @@ - - - - + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 13f81268..1c622878 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -124,8 +124,8 @@
- - - + + + diff --git a/workspace/start.html b/workspace/start.html index 3a41d078..7f7012ba 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -7,8 +7,8 @@ - - + + @@ -56,8 +56,8 @@

- - - + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index 5c474c26..78e87077 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -65,9 +65,9 @@ - - - - + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index c1310c3c..a1e99e51 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -7,9 +7,9 @@ - - - + + + @@ -52,7 +52,7 @@ - - + + diff --git a/workspace/wissen.html b/workspace/wissen.html index fb584c53..d28ca70b 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -170,8 +170,8 @@ - - - + + +