diff --git a/server/pruef-manager-ziele.mjs b/server/pruef-manager-ziele.mjs index f8309d8c..7badb48f 100644 --- a/server/pruef-manager-ziele.mjs +++ b/server/pruef-manager-ziele.mjs @@ -471,6 +471,27 @@ const altNochmal = await schick("scout", `${API}/eintrag/${altId}`, "PATCH", ok(altNochmal.status === 409, `der Scout kommt auch danach nicht hinein (${altNochmal.status}) — die Freigabe wirkte nur einmal`); +/* SPICY MEDIA DARF SEIT DEM 02.10.2026 DASSELBE WIE DOGFATHER. + Filipe auf die Rueckfrage, wer korrigieren darf: „ja spicy und + dogfather". Geprueft wird beides -- dass Spicy hineinkommt UND + dass die Freigabe danach wieder zu ist. Nur das erste zu pruefen + hiesse, eine geoeffnete Tuer fuer einen Erfolg zu halten. */ +const altSpicy = await schick("spicy", `${API}/eintrag/${altId}`, "PATCH", + { datum: `${VORMONAT}-09`, notiz: "Korrektur durch Spicy Media" }); +ok(altSpicy.status === 200, + `Spicy Media darf im Vormonat ebenfalls korrigieren (${altSpicy.status})`); +const db2b = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true }); +const freigabeNachSpicy = db2b.prepare("SELECT offen FROM mz_freigabe WHERE eins = 1").get(); +db2b.close(); +ok(freigabeNachSpicy.offen === 0, + `…und die Freigabe ist auch danach wieder zu (${freigabeNachSpicy.offen})`); + +/* UND DIE GRENZE HAELT WEITERHIN: Ein Manager ist keine Leitung. */ +const altManager = await schick("manager", `${API}/eintrag/${altId}`, "PATCH", + { datum: `${VORMONAT}-10` }); +ok(altManager.status === 403 || altManager.status === 409, + `ein Manager kommt dort weiterhin nicht hinein (${altManager.status})`); + /* ===================================================================== 7 · DIE ZIELZAHLEN ===================================================================== */ @@ -1070,31 +1091,86 @@ ok(mitNamen === gesamtEintraege, /* ===================================================================== 16 · KEIN KNOPF, DER NICHTS TUT ===================================================================== */ -melde("\n=== Bearbeitbar heisst wirklich bearbeitbar ==="); -const spicySicht = await (await hol("spicy", - `${API}/eintraege?monat=${MONAT}&aufgabe=creator&person=${IDS.scout}`)).json(); -ok(spicySicht.bearbeitbar === false, - "Spicy Media bekommt an einer FREMDEN Liste keine Bearbeiten-Knoepfe angeboten"); -const dogiSicht = await (await hol("admin", - `${API}/eintraege?monat=${MONAT}&aufgabe=creator&person=${IDS.scout}`)).json(); -ok(dogiSicht.bearbeitbar === true, "DogFather schon"); +melde("\n=== Wer korrigieren darf (seit 02.10.2026: Spicy UND DogFather) ==="); + +/* DIE ANGEZEIGTE ERLAUBNIS UND DIE TATSAECHLICHE MUESSEN DIESELBE + SEIN. Eine Liste, die „darfst du" sagt und danach 403 liefert, ist + schlimmer als eine ohne Knoepfe -- man klickt zweimal, bevor man + es glaubt. Deshalb wird hier zu JEDER Anzeige auch der Vollzug + gemessen. */ +const sichten = {}; +for (const r of ["spicy", "admin", "manager"]) { + sichten[r] = await (await hol(r, + `${API}/eintraege?monat=${MONAT}&aufgabe=creator&person=${IDS.scout}`)).json(); +} +ok(sichten.spicy.bearbeitbar === true, + "Spicy Media bekommt an einer fremden Liste Bearbeiten-Knoepfe"); +ok(sichten.admin.bearbeitbar === true, "DogFather ebenfalls"); +/* BEIM MANAGER WAR MEINE ERSTE PRUEFUNG FALSCH, nicht der Code: Er + bekommt `bearbeitbar: true` -- weil er gar keine fremde Liste + bekommt, sondern seine EIGENE. Die Nummer in der Adresse wird fuer + ihn schlicht nicht beachtet. Das ist das richtige Verhalten und + die falsche Frage; gemessen gehoert, dass bei ihm nichts Fremdes + ankommt. */ +const fremdeIds = new Set(sichten.admin.eintraege.map((e) => e.id)); +ok(fremdeIds.size > 0, + `es gibt ueberhaupt fremde Eintraege, die durchsickern koennten (${fremdeIds.size})`); +ok(!sichten.manager.eintraege.some((e) => fremdeIds.has(e.id)), + `ein Manager sieht keinen einzigen davon (${sichten.manager.eintraege.length} eigene)`); + const eigeneSicht = await (await hol("scout", `${API}/eintraege?monat=${MONAT}&aufgabe=creator`)).json(); ok(eigeneSicht.bearbeitbar === true, "und jeder an seiner eigenen"); -/* GEGENPROBE: Die Angabe muss mit dem uebereinstimmen, was der Server - dann WIRKLICH tut. Eine Anzeige, die „darfst du" sagt und danach - 403 liefert, ist schlimmer als gar keine. */ -/* EIN EINTRAG, DEN ES WIRKLICH NOCH GIBT. Beim ersten Lauf stand - hier `einId` -- der war weiter oben geloescht worden, und die - Antwort lautete 404 statt 403. Das haette wie ein bestandener - Riegel ausgesehen und war nur „gibt es nicht". */ +/* EIN EINTRAG, DEN ES WIRKLICH NOCH GIBT. Beim ersten Lauf stand hier + ein geloeschter -- die Antwort lautete 404 statt 403, und das sah + aus wie ein haltender Riegel. */ const lebtNoch = eigeneSicht.eintraege[0]?.id; -braucht(!!lebtNoch, "es gibt keinen Eintrag, an dem sich die Absage messen liesse"); +braucht(!!lebtNoch, "es gibt keinen Eintrag, an dem sich das messen liesse"); + const spicyVersuch = await schick("spicy", `${API}/eintrag/${lebtNoch}`, "PATCH", - { datum: HEUTE, name: "@vonSpicy" }); -ok(spicyVersuch.status === 403, - `…und der Server weist Spicy auch wirklich ab, mit 403 (${spicyVersuch.status})`); + { datum: HEUTE, name: "@vonSpicyKorrigiert" }); +ok(spicyVersuch.status === 200, + `Spicy Media korrigiert einen fremden Eintrag wirklich (${spicyVersuch.status})`); + +const dbK = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true }); +const angekommen = dbK.prepare("SELECT name FROM mz_eintrag WHERE id = ?").get(lebtNoch); +const vonSpicy = dbK.prepare( + `SELECT COUNT(*) n FROM protokoll + WHERE aktion = 'manager_ziele_korrigiert' AND rolle = 'spicy'`).get().n; +dbK.close(); +ok(angekommen?.name === "@vonSpicyKorrigiert", + "…und die Aenderung steht wirklich in der Datenbank"); +ok(vonSpicy >= 1, + `…und sie ist unter ihrem Namen protokolliert (${vonSpicy} Eintraege)`); + +/* GEGENPROBE: Die Grenze liegt bei der Leitung, nicht bei „irgendwer + mit einem Zugang". Ohne diese beiden Zeilen hiesse „Spicy darf" + moeglicherweise „jeder darf". */ +const managerVersuch = await schick("manager", `${API}/eintrag/${lebtNoch}`, "PATCH", + { datum: HEUTE, name: "@vonManager" }); +ok(managerVersuch.status === 403, + `ein Manager wird weiterhin abgewiesen (${managerVersuch.status})`); +const scout2Versuch = await schick("schnell", `${API}/eintrag/${lebtNoch}`, "PATCH", + { datum: HEUTE, name: "@vonFremdemScout" }); +ok(scout2Versuch.status === 403, + `ein fremder Scout ebenfalls (${scout2Versuch.status})`); + +const dbK2 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true }); +const unveraendert = dbK2.prepare("SELECT name FROM mz_eintrag WHERE id = ?").get(lebtNoch); +dbK2.close(); +ok(unveraendert?.name === "@vonSpicyKorrigiert", + "…und nach beiden Absagen steht immer noch der Wert von Spicy da"); + +/* Und Loeschen folgt derselben Grenze -- geprueft, nicht angenommen: + PATCH und DELETE sind zwei Routen, und zwei Routen koennen + auseinanderlaufen. */ +const managerLoeschen = await hol("manager", `${API}/eintrag/${lebtNoch}`, { method: "DELETE" }); +ok(managerLoeschen.status === 403, + `ein Manager kann einen fremden Eintrag auch nicht loeschen (${managerLoeschen.status})`); +const spicyLoeschen = await hol("spicy", `${API}/eintrag/${lebtNoch}`, { method: "DELETE" }); +ok(spicyLoeschen.status === 200, + `Spicy Media kann ihn loeschen (${spicyLoeschen.status})`); /* ===================================================================== SCHLUSS diff --git a/server/workspace-manager-ziele.js b/server/workspace-manager-ziele.js index 037011b2..a1dd84ed 100644 --- a/server/workspace-manager-ziele.js +++ b/server/workspace-manager-ziele.js @@ -19,6 +19,12 @@ gleichzeitig es immer ändern können für alle"). Ein Scout muss nicht dieselbe Zahl schaffen wie DogFather. + DASSELBE GILT SEIT DEM 02.10.2026 FÜRS KORRIGIEREN. Die Vorlage + kannte dort nur DogFather; auf die Rückfrage: „ja spicy und + dogfather". Beides liegt jetzt auf derselben Menge + (`siehtAlles`) — wer alle Zahlen sieht und die Ziele setzt, muss + auch einen Zahlendreher gerade rücken können. + 3. DIE PIPELINE IST ANGEBUNDEN, in beide Richtungen. Die Vorlage kennt nur die Handeingabe. Das Haus hat aber ein vollständiges Scout-CRM (`leads`), und zwei Listen derselben Rekrutierungen @@ -151,6 +157,29 @@ export function stufenText(stufe, bisEnde) { return ""; } +/** Wer fremde Eintraege korrigieren und abgeschlossene Monate oeffnen + * darf. + * + * BEIDE ROLLEN, NICHT NUR DOGFATHER (02.10.2026). Die Vorlage sagte + * „Ausnahme: DogFather"; Filipe auf die Rueckfrage: „ja spicy und + * dogfather". Damit gilt hier dieselbe Menge wie bei den Zielzahlen + * und bei der Team-Uebersicht -- und das ist der eigentliche Grund, + * es so zu machen: Wer die Zahlen aller sieht und die Ziele setzt, + * muss auch einen Zahlendreher gerade ruecken koennen. Zwei + * verschiedene Grenzen fuer „darf alles sehen" und „darf etwas + * richtigstellen" waeren die Stelle, an der sich spaeter niemand + * mehr erklaeren kann, warum ein Knopf fehlt. + * + * ES IST `siehtAlles` UND KEINE EIGENE LISTE. Eine zweite Aufzaehlung + * derselben zwei Rollen waere die, die beim naechsten Umbau + * auseinanderlaeuft. + * + * UND ES BLEIBT NACHVOLLZIEHBAR: Jede Korrektur an einem fremden + * Eintrag und jede Aenderung an einem abgeschlossenen Monat steht + * mit Name und Zeit im Protokoll -- unveraendert, auch jetzt, wo es + * zwei Menschen tun koennen statt einem. */ +const darfKorrigieren = (person) => siehtAlles(person); + const RANG = { rot: 0, orange: 1, neutral: 2, gruen: 3, aus: 4 }; /** Die schlechteste Stufe einer Liste — das ist die Farbe der Kachel. */ export function schlechteste(stufen) { @@ -528,9 +557,10 @@ function dublette(personId, monat, werte, ausser = 0) { DIE FREIGABE FÜR ABGESCHLOSSENE MONATE ===================================================================== - Nur DogFather, nur für eine einzelne Handlung, nur zwei Minuten. - Die Vorlage sagt „Vergangene Monate sind gesperrt und nicht mehr - bearbeitbar (Ausnahme: DogFather)" — und der Trigger in der + Nur die Leitung (DogFather und Spicy Media), nur für eine einzelne + Handlung, nur zwei Minuten. Die Vorlage sagt „Vergangene Monate + sind gesperrt und nicht mehr bearbeitbar (Ausnahme: DogFather)"; + seit dem 02.10.2026 gilt sie für beide — und der Trigger in der Datenbank kann keine Sitzung lesen. Also wird die Ausnahme ein sichtbarer Vorgang: Schloss auf, Handlung, Schloss zu, protokolliert. @@ -577,7 +607,7 @@ managerZieleRouter.get(`${BASIS}/stand`, (req, res) => { rollenName: ROLLEN_NAME[person.rolle] || person.rolle }, eigen: person.id === req.person.id, darfZiele: siehtAlles(req.person), - darfKorrigieren: req.person.rolle === "admin", + darfKorrigieren: darfKorrigieren(req.person), erster: ERSTER_MONAT, jetztMonat: monatJetzt(), }); @@ -618,8 +648,8 @@ managerZieleRouter.get(`${BASIS}/eintraege`, (req, res) => { Spicy Media an einer fremden Liste „Bearbeiten" und „Löschen" stehen, und der Server antwortete mit 403 — ein Knopf, der nichts tut, ist schlimmer als kein Knopf. */ - bearbeitbar: (personId === req.person.id || req.person.rolle === "admin") - && (monat === monatJetzt() || req.person.rolle === "admin"), + bearbeitbar: (personId === req.person.id || darfKorrigieren(req.person)) + && (monat === monatJetzt() || darfKorrigieren(req.person)), }); } catch (fehler) { console.error("[manager-ziele] Eintraege:", fehler?.message); @@ -814,8 +844,8 @@ managerZieleRouter.patch(`${BASIS}/eintrag/:id`, (req, res) => { if (!alt) return res.status(404).json({ fehler: "unbekannt" }); const fremd = alt.person_id !== req.person.id; - const istAdmin = req.person.rolle === "admin"; - if (fremd && !istAdmin) return res.status(403).json({ fehler: "nicht_deins" }); + const darfEr = darfKorrigieren(req.person); + if (fremd && !darfEr) return res.status(403).json({ fehler: "nicht_deins" }); const heute = tagBerlin(); const gepruefte = eintragPruefen({ ...req.body, aufgabe: alt.aufgabe }, alt.monat, heute); @@ -834,7 +864,7 @@ managerZieleRouter.patch(`${BASIS}/eintrag/:id`, (req, res) => { jetzt(), req.person.id, id); const altMonat = alt.monat !== monatJetzt(); - if (altMonat && !istAdmin) return res.status(409).json({ fehler: "monat_gesperrt" }); + if (altMonat && !darfEr) return res.status(409).json({ fehler: "monat_gesperrt" }); if (altMonat) mitFreigabe(schreiben); else schreiben(); if (fremd || altMonat) { @@ -864,11 +894,11 @@ managerZieleRouter.delete(`${BASIS}/eintrag/:id`, (req, res) => { if (!alt) return res.status(404).json({ fehler: "unbekannt" }); const fremd = alt.person_id !== req.person.id; - const istAdmin = req.person.rolle === "admin"; - if (fremd && !istAdmin) return res.status(403).json({ fehler: "nicht_deins" }); + const darfEr = darfKorrigieren(req.person); + if (fremd && !darfEr) return res.status(403).json({ fehler: "nicht_deins" }); const altMonat = alt.monat !== monatJetzt(); - if (altMonat && !istAdmin) return res.status(409).json({ fehler: "monat_gesperrt" }); + if (altMonat && !darfEr) return res.status(409).json({ fehler: "monat_gesperrt" }); const loeschen = () => db().prepare("DELETE FROM mz_eintrag WHERE id = ?").run(id); if (altMonat) mitFreigabe(loeschen); else loeschen(); diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 4a0d9f8f..11069850 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,8 +16,8 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +