diff --git a/server/pruef-ampel.png b/server/pruef-ampel.png index 08818225..f5cb27a8 100644 Binary files a/server/pruef-ampel.png and b/server/pruef-ampel.png differ diff --git a/server/pruef-code.png b/server/pruef-code.png index 9464944e..4aeb8e2e 100644 Binary files a/server/pruef-code.png and b/server/pruef-code.png differ diff --git a/server/pruef-freie-namen.png b/server/pruef-freie-namen.png index e43dadaa..fdf1867e 100644 Binary files a/server/pruef-freie-namen.png and b/server/pruef-freie-namen.png differ diff --git a/server/pruef-handy.png b/server/pruef-handy.png index 557f7044..d6590c23 100644 Binary files a/server/pruef-handy.png and b/server/pruef-handy.png differ diff --git a/server/pruef-protokoll-auf.png b/server/pruef-protokoll-auf.png index 506a25c0..f8ef5699 100644 Binary files a/server/pruef-protokoll-auf.png and b/server/pruef-protokoll-auf.png differ diff --git a/server/pruef-protokoll-loeschen.mjs b/server/pruef-protokoll-loeschen.mjs new file mode 100644 index 00000000..1ec20fac --- /dev/null +++ b/server/pruef-protokoll-loeschen.mjs @@ -0,0 +1,255 @@ +/* ===================================================================== + Prüfung: Protokolle löschen, nur eins offen, und die Startseite lügt + nicht mehr (02.09.2026). + + Drei Wünsche aus einer Meldung: + + "wie du siehst hab ich es gemacht und wie du auf screen2 siehst wird + es immer noch angezeigt" — die Startseite meldete weiter ein + fehlendes Protokoll, obwohl es geschrieben war. + + "ich will auch dass ich die löschen kann. und nur ich kann die + löschen sonst keiner." + + "es soll immer automatisch nur 1 angezeigt werden." + + Der erste Punkt ist der lehrreiche: Der Server lag die ganze Zeit + richtig. Nachgestellt wurde, dass der Hinweis erscheint, sobald ein + vergangenes Gespräch kein Protokoll hat, und in dem Moment + verschwindet, in dem eines geschrieben ist. Falsch war der BILDSCHIRM + -- der Browser holte die verlassene Startseite beim Zurückgehen + unverändert aus seiner Schublade (bfcache), ohne ein Skript erneut + laufen zu lassen. + + Deshalb steht hier beides: dass die Zahl stimmt UND dass sie nach dem + Zurückgehen stimmt. Die erste Prüfung allein wäre grün gewesen, + während der Fehler weiter da war. + ===================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-prot-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = "4197"; +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:4197"; +setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.(); + +let fehler = 0; +let geprueft = 0; +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); + +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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} +const idDogi = anlegen("Dogfather", "admin", "CODE-DOGI-0001"); +const idCigdem = anlegen("Cigdem", "manager", "CODE-CIGDEM-01"); + +/* Drei vergangene Gespräche -- damit "nur eins offen" überhaupt etwas + zu zeigen hat und nicht zufällig stimmt. */ +const p = (n) => String(n).padStart(2, "0"); +const vorhin = (stunden) => { + const n = new Date(Date.now() - stunden * 3600_000); + return `${n.getFullYear()}-${p(n.getMonth() + 1)}-${p(n.getDate())}T${p(n.getHours())}:00`; +}; +for (const [titel, h] of [["Call - VanVan", 3], ["Call - Tili", 4], ["Review - September", 5]]) { + d.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, teilnehmer_id, erledigt, erstellt, erstellt_von) + VALUES (?,?,?,?,?,?,0,?,?)`) + .run(titel, "call", vorhin(h), 10, idDogi, idDogi, jetzt, idDogi); +} +const zahl = (sql, ...w) => d.prepare(sql).get(...w)?.n ?? 0; + +const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await chromium.launch(); + +async function anmelden(rolle, code) { + const kontext = await browser.newContext({ viewport: { width: 1440, height: 1000 } }); + const seite = await kontext.newPage(); + await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); + await seite.click(`.rolle[data-rolle="${rolle}"]`); + await seite.fill("#code", code); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 15000 }); + return seite; +} +const api = (seite, pfad, optionen) => seite.evaluate(async ([pf, o]) => { + const a = await fetch(pf, { + credentials: "same-origin", ...(o || {}), + headers: o?.koerper ? { "Content-Type": "application/json" } : undefined, + body: o?.koerper ? JSON.stringify(o.koerper) : undefined, + }); + return { status: a.status, daten: await a.json().catch(() => ({})) }; +}, [pfad, optionen]); + +const dogi = await anmelden("admin", "CODE-DOGI-0001"); + +/* =================================================================== + TEIL 1 — Der Hinweis stimmt, auch nach dem Zurückgehen + =================================================================== */ + +console.log("\n— Der Hinweis auf der Startseite —"); + +const hinweisZahl = async (seite) => { + const r = await api(seite, "/workspace/api/hinweise"); + return (r.daten.hinweise || []).find((h) => h.art === "protokoll_fehlt")?.anzahl ?? 0; +}; + +ok(await hinweisZahl(dogi) === 3, "drei Gespräche ohne Protokoll werden gemeldet"); + +const calls = await api(dogi, "/workspace/api/calls"); +const ersterCall = calls.daten.offen[0]; +ok(!!ersterCall, "sie stehen unter „Protokoll fehlt“"); + +await api(dogi, `/workspace/api/calls/${ersterCall.id}/protokoll`, { + method: "POST", koerper: { punkte: "War ein Test." }, +}); +ok(await hinweisZahl(dogi) === 2, + "nach einem geschriebenen Protokoll meldet der Server nur noch zwei"); + +/* DER EIGENTLICHE FEHLER: die Seite, nicht der Server. + Erst zur Calls-Seite, dann mit der Zurück-Taste auf die Startseite -- + genau der Weg, den Filipe gegangen ist. */ +console.log("\n— Zurück-Taste: die Startseite darf nicht lügen —"); + +await dogi.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" }); +await dogi.waitForTimeout(700); +await dogi.goto(BASIS + "/workspace/calls.html", { waitUntil: "networkidle" }); +await dogi.waitForTimeout(500); +const zweiterCall = calls.daten.offen[1]; +await api(dogi, `/workspace/api/calls/${zweiterCall.id}/protokoll`, { + method: "POST", koerper: { punkte: "Auch erledigt." }, +}); +await dogi.goBack({ waitUntil: "networkidle" }); + +/* Nachfassen statt einmal schauen: Wird die Seite aus der Schublade + geholt, stösst kopf.js ein Neuladen an -- und das braucht einen + Augenblick. Ein einzelner Blick direkt nach dem Zurückgehen erwischt + womöglich noch die alte Anzeige und würde einen Fehler melden, den es + nicht gibt. Deshalb bis zu fünf Sekunden lang hinsehen. */ +/* Gemessen wird an EINZAHL gegen MEHRZAHL, nicht an einer Ziffer: Die + Zahl steht in einem eigenen Feld und taucht im Fliesstext gar nicht + auf -- der Satz selbst verrät sie aber zuverlässig. + + "1 Gespräch HAT noch kein Protokoll" -> richtig (eins offen) + "2 Gespräche HABEN noch kein Protokoll" -> der alte Stand + + Genau diese Unterscheidung ist der ganze Fehler. */ +let text = ""; +let alterStand = true; +for (let i = 0; i < 25; i++) { + await dogi.waitForTimeout(200); + text = await dogi.evaluate(() => document.querySelector("main")?.innerText || ""); + alterStand = /Gespräche haben noch kein Protokoll/.test(text); + if (!alterStand) break; +} +ok(!alterStand, + "die zurückgeholte Startseite zeigt den NEUEN Stand (Einzahl), nicht den alten (Mehrzahl)"); +ok(/Gespräch hat noch kein Protokoll/.test(text), + "und meldet das eine verbliebene Gespräch weiterhin – die Zeile ist nicht bloss verschwunden"); + +/* GEGENPROBE: Die Zahl darf nicht einfach immer verschwinden -- sonst + wäre die Zeile darüber auch dann grün, wenn der Hinweis gar nicht + mehr funktioniert. Ein drittes Gespräch ist noch ohne Protokoll. */ +ok(await hinweisZahl(dogi) === 1, + "GEGENPROBE: das dritte Gespräch wird weiterhin gemeldet – der Hinweis lebt"); + +/* =================================================================== + TEIL 2 — Löschen: nur DogFather + =================================================================== */ + +console.log("\n— Protokoll löschen —"); + +const cigdem = await anmelden("manager", "CODE-CIGDEM-01"); +const verboten = await api(cigdem, `/workspace/api/calls/${ersterCall.id}/protokoll`, { method: "DELETE" }); +ok(verboten.status === 403, `ein Manager darf NICHT löschen (${verboten.status})`); +ok(zahl("SELECT COUNT(*) AS n FROM protokolle WHERE termin_id = ?", ersterCall.id) === 1, + "und das Protokoll steht danach unverändert da"); + +const geloescht = await api(dogi, `/workspace/api/calls/${ersterCall.id}/protokoll`, { method: "DELETE" }); +ok(geloescht.status === 200, "DogFather darf löschen"); +ok(zahl("SELECT COUNT(*) AS n FROM protokolle WHERE termin_id = ?", ersterCall.id) === 0, + "das Protokoll ist weg"); +ok(zahl("SELECT COUNT(*) AS n FROM termine WHERE id = ?", ersterCall.id) === 1, + "das GESPRÄCH steht weiterhin im Kalender – nur das Protokoll war gemeint"); + +const danach = await api(dogi, "/workspace/api/calls"); +ok(danach.daten.offen.some((c) => c.id === ersterCall.id), + "es steht wieder unter „Protokoll fehlt“ – die Handlung ist umkehrbar"); +ok(await hinweisZahl(dogi) === 2, "und der Hinweis zählt es wieder mit"); + +const nochmal = await api(dogi, `/workspace/api/calls/${ersterCall.id}/protokoll`, { method: "DELETE" }); +ok(nochmal.status === 404, "ein zweites Löschen läuft sauber ins Leere, statt etwas kaputtzumachen"); + +/* =================================================================== + TEIL 3 — Immer nur eins offen + =================================================================== */ + +console.log("\n— Immer nur ein Protokoll aufgeklappt —"); + +await dogi.goto(BASIS + "/workspace/calls.html", { waitUntil: "networkidle" }); +await dogi.waitForTimeout(900); + +const knoepfe = dogi.locator(".call__karte button, .kk__knoepfe button"); +const zustand = async () => dogi.evaluate(() => ({ + offen: document.querySelectorAll(".call__detail").length, + loeschen: [...document.querySelectorAll("button")] + .filter((b) => /Protokoll löschen/.test(b.textContent)).length, +})); + +ok((await zustand()).offen === 0, "beim Laden ist alles zugeklappt"); + +const alleAuf = dogi.locator("button", { hasText: /Protokoll (ansehen|schreiben)/ }); +const wieviele = await alleAuf.count(); +ok(wieviele >= 2, `es gibt mindestens zwei Gespräche zum Aufklappen (${wieviele})`); + +await alleAuf.nth(0).click(); +await dogi.waitForTimeout(400); +ok((await zustand()).offen === 1, "nach dem ersten Klick ist genau eins offen"); + +await dogi.locator("button", { hasText: /Protokoll (ansehen|schreiben)/ }).nth(0).click(); +await dogi.waitForTimeout(400); +const nachZweitem = await zustand(); +ok(nachZweitem.offen <= 1, + `auch nach dem zweiten Klick ist höchstens eins offen (${nachZweitem.offen})`); + +await dogi.screenshot({ path: "pruef-protokoll-loeschen.png", fullPage: false }); + +/* Und der Löschknopf steht NUR bei DogFather. */ +await cigdem.goto(BASIS + "/workspace/calls.html", { waitUntil: "networkidle" }); +await cigdem.waitForTimeout(900); +await cigdem.locator("button", { hasText: /Protokoll ansehen/ }).first().click().catch(() => {}); +await cigdem.waitForTimeout(400); +const beiCigdem = await cigdem.evaluate(() => [...document.querySelectorAll("button")] + .filter((b) => /Protokoll löschen/.test(b.textContent)).length); +ok(beiCigdem === 0, + "GEGENPROBE: bei einer Managerin steht der Löschknopf gar nicht erst da"); + +const ERWARTET = 19; +console.log(""); +ok(geprueft >= ERWARTET, `es sind ${geprueft} Prüfungen gelaufen (erwartet mindestens ${ERWARTET})`); + +await browser.close(); +d.close(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +console.log(`\n${fehler ? "FEHLER: " + fehler : "Alles in Ordnung"} — ${geprueft} Prüfungen`); +process.exit(fehler ? 1 : 0); diff --git a/server/pruef-protokoll-loeschen.png b/server/pruef-protokoll-loeschen.png new file mode 100644 index 00000000..90451740 Binary files /dev/null and b/server/pruef-protokoll-loeschen.png differ diff --git a/server/pruef-protokoll-zu.png b/server/pruef-protokoll-zu.png index e43469c4..50201905 100644 Binary files a/server/pruef-protokoll-zu.png and b/server/pruef-protokoll-zu.png differ diff --git a/server/pruef-rollen-creator.png b/server/pruef-rollen-creator.png index 4df57f82..27786159 100644 Binary files a/server/pruef-rollen-creator.png and b/server/pruef-rollen-creator.png differ diff --git a/server/pruef-serien-computer.png b/server/pruef-serien-computer.png index 5841916c..049d774a 100644 Binary files a/server/pruef-serien-computer.png and b/server/pruef-serien-computer.png differ diff --git a/server/pruef-start-computer.png b/server/pruef-start-computer.png index c5fcf0e7..100dd72d 100644 Binary files a/server/pruef-start-computer.png and b/server/pruef-start-computer.png differ diff --git a/server/pruef-start-creator.png b/server/pruef-start-creator.png index 2c7110b5..1f2f93fb 100644 Binary files a/server/pruef-start-creator.png and b/server/pruef-start-creator.png differ diff --git a/server/pruef-start-handy.png b/server/pruef-start-handy.png index c0719430..845a511a 100644 Binary files a/server/pruef-start-handy.png and b/server/pruef-start-handy.png differ diff --git a/server/workspace-calls.js b/server/workspace-calls.js index 13756637..7765c3a3 100644 --- a/server/workspace-calls.js +++ b/server/workspace-calls.js @@ -18,7 +18,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, einstellung, einstellungSetzen, istLeitung, + db, protokolliere, echteIp, sitzungLesen, einstellung, einstellungSetzen, istLeitung, istDogFather, } from "./workspace.js"; import { sichtbar } from "./workspace-kalender.js"; @@ -182,6 +182,58 @@ callRouter.put("/workspace/api/call-link", gleicheHerkunft, (req, res) => { } }); +/* ---------- Protokoll löschen ------------------------------------------- + + NUR DogFather (Wunsch 02.09.2026: "ich will auch dass ich die löschen + kann. und nur ich kann die löschen sonst keiner"). + + Bewusst istDogFather und nicht istLeitung: Ein Manager hat sonst + überall dieselben Rechte, hier ausdrücklich nicht. Ein Protokoll ist + die Gedächtnisstütze eines Gesprächs -- wer es entfernen darf, kann + nachträglich bestimmen, was besprochen wurde. + + Gelöscht wird NUR das Protokoll, nicht das Gespräch. Der Termin bleibt + im Kalender und erscheint danach wieder unter "Protokoll fehlt" -- die + Handlung ist damit umkehrbar: neu schreiben, und alles ist wie vorher. + + Die daraus entstandenen AUFGABEN bleiben ebenfalls stehen. Sie sind + echte Arbeit, die jemand übernommen hat; sie mit einem Klick auf ein + Protokoll verschwinden zu lassen wäre ein stiller Datenverlust an + einer ganz anderen Stelle. Die Verknüpfung fällt weg (ON DELETE + CASCADE auf der Zwischentabelle), die Aufgaben selbst nicht. */ +callRouter.delete("/workspace/api/calls/:id/protokoll", gleicheHerkunft, (req, res) => { + try { + if (!istDogFather(req.person)) { + return res.status(403).json({ fehler: "Nur DogFather darf ein Protokoll löschen." }); + } + const id = Number(req.params.id); + if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" }); + + /* Auch DogFather kommt nur an Gespräche, die er sehen darf -- die + Sichtbarkeitsregel gilt hier wie überall. Bei ihm ist das "alles", + aber die Abfrage steht trotzdem da: Ändert sich die Regel einmal, + ändert sie sich hier mit. */ + const { wo, werte } = sichtbar(req.person); + const call = db().prepare( + `SELECT t.id, t.titel, pr.id AS protokoll_id + FROM termine t LEFT JOIN protokolle pr ON pr.termin_id = t.id + WHERE ${wo} AND t.id = ?`).get(...werte, id); + if (!call) return res.status(404).json({ fehler: "nicht_gefunden" }); + if (!call.protokoll_id) return res.status(404).json({ fehler: "kein_protokoll" }); + + db().prepare("DELETE FROM protokolle WHERE id = ?").run(call.protokoll_id); + + protokolliere("protokoll_geloescht", { + personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), + detail: `Call #${id} ${call.titel}`.slice(0, 120), + }); + res.json({ ok: true }); + } catch (fehler) { + console.error("[workspace] Protokoll löschen:", fehler?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } +}); + callRouter.post("/workspace/api/calls/:id/protokoll", gleicheHerkunft, (req, res) => { const d = db(); try { diff --git a/workspace/assets/js/calls.js b/workspace/assets/js/calls.js index c9a85cf0..967c21e0 100644 --- a/workspace/assets/js/calls.js +++ b/workspace/assets/js/calls.js @@ -49,6 +49,22 @@ } catch { return false; } } + /** Ein Knopf, der etwas tut und sich dabei selbst sperrt. Ohne das + * Sperren löst ein doppelter Klick die Aktion zweimal aus -- beim + * Löschen scheitert der zweite Versuch dann mit einer Fehlermeldung, + * obwohl alles geklappt hat. */ + function tuKnopf(text, tu) { + const b = el('button', 'abmelden', text); + b.type = 'button'; + b.addEventListener('click', async (ev) => { + ev.stopPropagation(); + b.disabled = true; + melde(''); + try { await tu(); } catch { /* umgeleitet */ } finally { b.disabled = false; } + }); + return b; + } + /** Läuft dieses Gespräch gerade noch? Ab dem geplanten Beginn bis eine * Stunde nach dem geplanten Ende. Gerechnet wird auf den Textformen * ('JJJJ-MM-TTThh:mm'), die der Server ohnehin schickt -- sie @@ -285,7 +301,20 @@ : c.protokoll_id ? 'Protokoll ansehen' : 'Protokoll schreiben'); b.type = 'button'; b.addEventListener('click', () => { - if (offen.has(c.id)) offen.delete(c.id); else offen.add(c.id); + /* IMMER NUR EINS OFFEN (Wunsch 02.09.2026: "es soll immer + automatisch nur 1 angezeigt werden"). + + Vorher konnte man beliebig viele Protokolle gleichzeitig + aufklappen -- die Seite wurde dabei so lang, dass die Liste + darunter aus dem Blick geriet und man nicht mehr sah, was + sonst noch offen ist. Ein neu geöffnetes Gespräch schliesst + jetzt das vorherige. + + `clear()` VOR dem Merken: Sonst löschte es den gerade + gesetzten Eintrag gleich wieder mit. */ + const warOffen = offen.has(c.id); + offen.clear(); + if (!warOffen) offen.add(c.id); zeichne(); }); knöpfe.append(b); @@ -323,6 +352,34 @@ } auf.append(el('p', 'call__quelle', `Festgehalten von ${c.protokoll_von || '—'} am ${datum(c.protokoll_erstellt)}`)); + + /* Löschen darf NUR DogFather (Wunsch 02.09.2026: "nur ich kann die + löschen sonst keiner"). Der Knopf steht deshalb auch nur bei ihm + da -- ein Knopf, der bei anderen erscheint und dann abgewiesen + wird, ist eine Einladung zum Ärgernis. Der Server prüft es + trotzdem noch einmal; die Anzeige ist keine Sicherung. + + Gelöscht wird NUR das Protokoll. Das Gespräch bleibt im + Kalender stehen und rutscht wieder zu "Protokoll fehlt" -- so + ist die Wirkung umkehrbar: neu schreiben, fertig. */ + if (ich?.rolle === 'admin') { + const weg = el('div', 'kk__knoepfe'); + weg.append(tuKnopf('Protokoll löschen', async () => { + if (!confirm('Protokoll zu „' + c.titel + '" löschen?\n\n' + + 'Der Text ist danach weg und lässt sich nicht wiederholen.\n\n' + + 'Das Gespräch selbst bleibt im Kalender und steht danach ' + + 'wieder unter „Protokoll fehlt".\n\n' + + 'Bereits daraus entstandene Aufgaben bleiben ebenfalls stehen.')) return; + const a = await hole('/workspace/api/calls/' + c.id + '/protokoll', { method: 'DELETE' }); + if (!a.ok) { + melde((await a.json().catch(() => ({}))).fehler || 'Löschen ging nicht.'); + return; + } + offen.delete(c.id); + await laden(); + })); + auf.append(weg); + } if (c.naechster_termin_id) { const p = el('p', 'call__quelle', 'Folgetermin liegt im Kalender. '); const a = el('a', 'call__verweis', 'Kalender öffnen'); diff --git a/workspace/assets/js/kopf.js b/workspace/assets/js/kopf.js index 8bebf74c..2f7cef70 100644 --- a/workspace/assets/js/kopf.js +++ b/workspace/assets/js/kopf.js @@ -898,4 +898,34 @@ if (a.ok) window.werZeigen(await a.json()); } catch { /* nicht angemeldet -- die Seite leitet ohnehin um */ } }, 1200); + + /* ===================================================================== + ZURÜCK-TASTE: die Seite neu holen statt aus der Schublade nehmen + (02.09.2026) + + Gemeldet: "wie du siehst hab ich es gemacht und wird es immer noch + angezeigt" -- ein Protokoll war geschrieben, die Startseite meldete + trotzdem weiter "1 Gespräch hat noch kein Protokoll". + + Nachgestellt: Der Server lag richtig. Der Hinweis erscheint, sobald + ein Gespräch ohne Protokoll vorbei ist, und verschwindet in dem + Moment, in dem das Protokoll steht. Falsch war der BILDSCHIRM. + + Browser legen eine verlassene Seite komplett beiseite (bfcache) und + holen sie beim Zurückgehen unverändert wieder hervor -- mitsamt + allen Zahlen, die beim ersten Laden geholt wurden. Kein Skript läuft + dabei erneut. Wer also ein Protokoll schreibt und dann auf "Zurück" + tippt, sieht zwangsläufig den Stand von vorher. + + Das ist kein Schönheitsfehler: Eine Zahl, die etwas Falsches + behauptet, ist schlimmer als gar keine -- man glaubt ihr ja. Genau + diese Sorte Fehler kostet danach Vertrauen in ALLE Zahlen. + + `persisted` unterscheidet die beiden Fälle sauber: true heisst + "aus der Schublade geholt", false heisst "ganz normal geladen". + Nur im ersten Fall wird neu geladen -- ein pauschales Neuladen bei + jedem Anzeigen wäre eine Endlosschleife. */ + addEventListener('pageshow', (e) => { + if (e.persisted) location.reload(); + }); })(); diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index a2bef728..76fe0ff2 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -7,9 +7,9 @@ - - - + + +
@@ -152,9 +152,9 @@ - - - - + + + +