diff --git a/server/pruef-fremde-sicht.mjs b/server/pruef-fremde-sicht.mjs new file mode 100644 index 00000000..eb7305c6 --- /dev/null +++ b/server/pruef-fremde-sicht.mjs @@ -0,0 +1,202 @@ +/* ===================================================================== + DIE FREMDE SICHT — und die Regel, die sie zusammenhält. + + DogFather kann die Sicht einer anderen Person übernehmen. Dabei gilt + eine Regel, die in workspace.js ausdrücklich aufgeschrieben ist: + + "Die Oberflaeche braucht beides: WER BIN ICH (fuer alles, was + SCHREIBT) und WESSEN ARBEITSPLATZ SEHE ICH (fuer das, was gezeigt + wird). Die beiden zu vermischen waere der sichere Weg dazu, dass + irgendwann etwas unter fremdem Namen gespeichert wird." + + ANZEIGEN folgt also der Sicht, SCHREIBEN dem Angemeldeten. + + --------------------------------------------------------------------- + WARUM ES DIESE DATEI GIBT + + Am 11.09.2026 meldete Filipe, DogFather sehe "die Creator nicht + mehr". Nichts war kaputt -- er war seit dem Vortag in einer fremden + Sicht. Beim Nachmessen fiel aber etwas anderes auf: Die Sicht kam nur + auf der HALBEN Seite an. Die Zahlen einer Seite folgten ihr, die + Creator-Auswahl daneben nicht -- drei Endpunkte lasen `req.person`, + wo ihre eigenen Datenwege zwanzig Zeilen weiter `req.sicht || + req.person` lesen. + + Zwei Antworten auf eine Frage, und beide sahen für sich richtig aus. + Genau deshalb steht die Regel jetzt hier als Messung und nicht nur + als Absatz. + + Aufruf: node server/pruef-fremde-sicht.mjs + ===================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawn } from "node:child_process"; +import { DatabaseSync } from "node:sqlite"; +import { scryptSync, randomBytes } from "node:crypto"; +import { notbremse } from "./helfer-notbremse.mjs"; + +notbremse(240_000, "pruef-fremde-sicht"); + +const HIER = dirname(fileURLToPath(import.meta.url)); +const PORT = 4393; +const BASIS = `http://127.0.0.1:${PORT}`; +const ordner = mkdtempSync(join(tmpdir(), "ws-sicht-")); +const DB = join(ordner, "workspace.db"); +const jetzt = new Date().toISOString(); + +let gemacht = 0, fehler = 0; +const ok = (b, text) => { + gemacht++; + if (b) console.log(` ok ${text}`); + else { fehler++; console.log(` FEHL ${text}`); } +}; + +let kind = null; +async function starten() { + kind = spawn(process.execPath, [join(HIER, "index.js")], { + env: { ...process.env, WORKSPACE_DB: DB, PORT: String(PORT), + SITE_ACCESS_SECRET: "lokaler-test", + SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00" }, + stdio: ["ignore", "pipe", "pipe"] }); + let aus = ""; + kind.stdout.on("data", (d) => { aus += d; }); + kind.stderr.on("data", (d) => { aus += d; }); + for (let i = 0; i < 80; i++) { + await new Promise((r) => setTimeout(r, 250)); + if (kind.exitCode !== null) break; + try { const a = await fetch(BASIS + "/workspace/", { redirect: "manual" }); + if (a.status < 500) return; } catch { /* noch nicht oben */ } + } + throw new Error("Server startete nicht:\n" + aus.slice(-1500)); +} +await starten(); +kind.kill(); +await new Promise((r) => setTimeout(r, 600)); + +let idNova, idKiro, idLumi; +{ + const d = new DatabaseSync(DB); + const anlegen = (name, rolle, code) => { + const salz = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salz, 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, salz, 32768, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; + }; + anlegen("Filipe", "admin", "CODE-DOGI-0001"); + /* DREI Creator, nicht einer. Mit einem einzigen waere jede Liste + zufaellig richtig -- "alle" und "nur dieser" saehen gleich aus. */ + idNova = anlegen("Nova", "creator", "CODE-NOVA-0001"); + idKiro = anlegen("Kiro", "creator", "CODE-KIRO-0001"); + idLumi = anlegen("Lumi", "creator", "CODE-LUMI-0001"); + d.close(); +} +await starten(); + +const anmelden = async (rolle, code) => { + const a = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle, code }) }); + const keks = (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; "); + if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`); + return keks; +}; +const keksDogi = await anmelden("admin", "CODE-DOGI-0001"); + +/* Die Sicht kommt als Abfrageteil an den Weg -- genauso, wie der + Browser sie schickt (siehe index.js, `req.sicht`). */ +const hole = async (weg, sicht) => { + const trenner = weg.includes("?") ? "&" : "?"; + const a = await fetch(BASIS + weg + (sicht ? `${trenner}sicht=${sicht}` : ""), { + headers: { Cookie: keksDogi } }); + let daten = null; try { daten = await a.json(); } catch { /* leer erlaubt */ } + return { status: a.status, daten }; +}; +const namen = (d) => (d?.creator || d?.personen || []) + .map((x) => x.name).filter(Boolean).sort(); + +/* ======================================================================= + 1. In der EIGENEN Sicht sieht DogFather alle + ======================================================================= */ +console.log("=== Eigene Sicht: alle drei ==="); +for (const weg of ["/workspace/api/uebersicht/creator", "/workspace/api/report/creator", + "/workspace/api/startcheck/creator"]) { + const { daten } = await hole(weg); + const n = namen(daten); + ok(n.length === 3 && n.includes("Nova") && n.includes("Kiro") && n.includes("Lumi"), + `${weg.replace("/workspace/api/", "")}: ${n.join(", ") || "nichts"}`); +} + +/* ======================================================================= + 2. In der Sicht von Nova sieht er NUR Nova + ======================================================================= */ +console.log("\n=== Sicht von Nova: nur Nova ==="); +for (const weg of ["/workspace/api/uebersicht/creator", "/workspace/api/report/creator", + "/workspace/api/startcheck/creator"]) { + const { daten } = await hole(weg, idNova); + const n = namen(daten); + ok(n.length === 1 && n[0] === "Nova", + `${weg.replace("/workspace/api/", "")}: ${n.join(", ") || "nichts"}`); +} +{ + /* Der Start-Check sagt zusaetzlich, dass es die EIGENE Sicht der + angesehenen Person ist -- daran erkennt die Seite, dass sie den + Umschalter gar nicht erst zeigen muss. */ + const { daten } = await hole("/workspace/api/startcheck/creator", idNova); + ok(daten?.eigen === true, `startcheck meldet "eigen" (${daten?.eigen})`); +} + +/* ======================================================================= + 3. UND SCHREIBEN FOLGT NICHT DER SICHT + ======================================================================= */ +console.log("\n=== Was zum Schreiben da ist, bleibt beim Angemeldeten ==="); +{ + /* /api/personen fuellt die Auswahl "zu wem gehoert dieser Eintrag". + Das ist eine SCHREIB-Auswahl. Wuerde sie der Sicht folgen, koennte + DogFather in Novas Sicht nur noch Eintraege fuer Nova anlegen -- + und das waere keine Vereinfachung, sondern ein stiller Verlust von + Handlungsfaehigkeit an einer Stelle, an der man es nicht vermutet. + + Die Regel steht in workspace.js: "WER BIN ICH (fuer alles, was + SCHREIBT)". Diese Pruefung haelt sie fest. */ + const ohne = namen((await hole("/workspace/api/personen")).daten); + const mit = namen((await hole("/workspace/api/personen", idNova)).daten); + ok(ohne.length >= 4, `ohne Sicht stehen alle zur Auswahl (${ohne.length})`); + ok(JSON.stringify(ohne) === JSON.stringify(mit), + `mit Sicht steht dieselbe Auswahl da (${mit.length})`); +} + +/* ======================================================================= + 4. Gegenprobe: Die Messung erkennt den Unterschied wirklich + ======================================================================= */ +console.log("\n=== Gegenproben ==="); +{ + /* Ohne diese Probe hiesse "nur Nova" auch dann ok, wenn die Listen + grundsaetzlich einelementig waeren. */ + const drei = namen((await hole("/workspace/api/report/creator")).daten); + const eins = namen((await hole("/workspace/api/report/creator", idNova)).daten); + ok(drei.length !== eins.length, + `dieselbe Abfrage liefert mit und ohne Sicht Verschiedenes (${drei.length} gegen ${eins.length})`); +} +{ + /* Und eine Sicht auf jemand anderen muss auch jemand anderen zeigen + -- sonst waere "Nova" nur zufaellig der erste Eintrag. */ + const n = namen((await hole("/workspace/api/report/creator", idLumi)).daten); + ok(n.length === 1 && n[0] === "Lumi", `Sicht auf Lumi zeigt Lumi (${n.join(", ")})`); +} +{ + /* Eine erfundene Nummer darf keine Sicht oeffnen. */ + const n = namen((await hole("/workspace/api/report/creator", 999999)).daten); + ok(n.length === 3, `eine erfundene Nummer aendert nichts (${n.length})`); +} + +kind.kill(); +await new Promise((r) => setTimeout(r, 400)); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ } + +console.log(`\n${gemacht} Pruefungen`); +console.log(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`); +process.exit(fehler ? 1 : 0); diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index d57f0dbd..75d96278 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -494,7 +494,11 @@ function pruefeFelder(körper, { neu, person }) { aufgabenRouter.get("/workspace/api/uebersicht/creator", (req, res) => { try { - const person = req.person; + /* Die Auswahl folgt der SICHT, nicht dem Angemeldeten -- dieselbe + Regel wie bei den Daten dieses Moduls. Zwei Antworten auf eine + Frage waren der Grund, warum eine fremde Sicht nur auf der + halben Seite ankam (11.09.2026). */ + const person = req.sicht || req.person; let wo = "p.rolle = 'creator' AND p.aktiv = 1"; let werte = []; /* Zuteilung statt Rolle (03.09.2026): Ein Manager fiel hier durch diff --git a/server/workspace-reports.js b/server/workspace-reports.js index b9b499d0..218f8da3 100644 --- a/server/workspace-reports.js +++ b/server/workspace-reports.js @@ -236,7 +236,17 @@ reportRouter.get("/workspace/api/report/creator", (req, res) => { sichtbarePersonenIds/sichtbareCreatorIds liefern null fuer DogFather (= keine Einschraenkung) und sonst genau die erlaubten Nummern. */ - const ids = sichtbareCreatorIds(req.person); + /* DIE AUSWAHL FOLGT DER SICHT (11.09.2026). + + Hier stand `req.person` -- also der Angemeldete. Zwanzig Zeilen + weiter oben liest derselbe Router `req.sicht || req.person`. Zwei + Antworten auf eine Frage: Die SEITE zeigte die Zahlen der + angesehenen Person, die AUSWAHL daneben die eigenen Creator. + + Aufgefallen ist das nicht beim Lesen, sondern beim Nachmessen, + warum DogFather "die Creator nicht mehr sieht" -- er war in einer + fremden Sicht, und die halbe Seite wusste es nicht. */ + const ids = sichtbareCreatorIds(req.sicht || req.person); if (ids === null) { return res.json({ creator: db().prepare( diff --git a/server/workspace-startcheck.js b/server/workspace-startcheck.js index 3d5ab8af..71269dcd 100644 --- a/server/workspace-startcheck.js +++ b/server/workspace-startcheck.js @@ -115,7 +115,11 @@ const darfAendern = (person, creatorId) => startcheckRouter.get("/workspace/api/startcheck/creator", (req, res) => { try { - const person = req.person; + /* Die Auswahl folgt der SICHT, nicht dem Angemeldeten -- dieselbe + Regel wie bei den Daten dieses Moduls. Zwei Antworten auf eine + Frage waren der Grund, warum eine fremde Sicht nur auf der + halben Seite ankam (11.09.2026). */ + const person = req.sicht || req.person; if (person.rolle === "creator") { return res.json({ creator: [{ id: person.id, name: person.name }], eigen: true }); } diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 33d95ac3..0a325d8c 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609111201", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609111324", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609111201", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609111324", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609111201", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609111324", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609111201", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609111324", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 519ce99e..a9571985 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - + @@ -252,10 +252,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 7cb0a290..e743d401 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -147,10 +147,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 2a4c60dd..62aee9c1 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -181,14 +181,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 6a62b645..3400c81e 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -89,11 +89,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index de6b4c0d..807ed724 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -12,26 +12,26 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -289,10 +289,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index 72266fc3..d4404900 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -179,12 +179,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/crew-index.html b/workspace/crew-index.html index ace99cb3..fa6723cb 100644 --- a/workspace/crew-index.html +++ b/workspace/crew-index.html @@ -30,15 +30,15 @@ - - - + + + - + @@ -252,6 +252,6 @@ - + diff --git a/workspace/crew.webmanifest b/workspace/crew.webmanifest index ed61a754..d00a258d 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609111201", + "src": "/assets/img/app-symbole/crew-192.png?v=202609111324", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609111201", + "src": "/assets/img/app-symbole/crew-512.png?v=202609111324", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609111201", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609111324", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609111201", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609111324", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index 55c463b4..593c899f 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -108,10 +108,10 @@ - - - - - + + + + + diff --git a/workspace/index.html b/workspace/index.html index eea51bb2..e69d55a6 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -30,9 +30,9 @@ - - - + + + @@ -401,6 +401,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index 6ea7667d..d9f0b4ab 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -377,11 +377,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index f0459bba..1d29ca49 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -325,10 +325,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index 3293d2c4..21cd27aa 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -396,10 +396,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index b7df5bca..fabff1be 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -214,11 +214,11 @@ - - - - - - + + + + + + diff --git a/workspace/report.html b/workspace/report.html index 0b46b296..95e9dceb 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -106,11 +106,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index f5c5addb..a4707d1d 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -173,10 +173,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index 0ca3b056..073d3ea0 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -12,21 +12,21 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - + + - + @@ -674,10 +674,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index e817344c..cc73718b 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -85,11 +85,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index 61af8913..91595902 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -126,10 +126,10 @@ - - - - - + + + + + diff --git a/workspace/team.html b/workspace/team.html index bb8a49a9..6a4d09cc 100644 --- a/workspace/team.html +++ b/workspace/team.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + @@ -110,10 +110,10 @@ - - - - - + + + + + diff --git a/workspace/teamlage.html b/workspace/teamlage.html index 070194e7..ac936d20 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index 29999326..33d3aa78 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -12,22 +12,22 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -149,11 +149,11 @@ - + - - - - + + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index 6ec0e70b..4beedee5 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -190,10 +190,10 @@ - - - - - + + + + +