Files
dogfather-universe/server/pruef-verteilen.mjs
T
DogFatherGit 39c0b4dc1f Ein Modi sieht nur SEINE Aufgaben -- und gibt sich selbst keine
Filipe, unmissverstaendlich und mehrfach: "die modis sollen immer nur
ihre aufgaben auch sehen und nicht die von anderen, so wie bei den
daten ... damit wir endlich den modis aufgaben anstaendig verteilen
koennen und sie sich nicht selber aufgaben geben."

DAS DREHT DIE ENTSCHEIDUNG VOM 09.09. AUSDRUECKLICH UM. Damals: "ja,
sie sind untereinander ein team", damit ein Schichttausch ohne Umweg
geht. Beides steht jetzt im Code nebeneinander, damit niemand spaeter
die aeltere findet und fuer die gueltige haelt.

VIER AENDERUNGEN:

  Die Sicht. Ein Modi sieht nur `a.verantwortlich_id = ich`. Was ihm
  ueber aufgaben_zuteilung gegeben wurde, haengt mitZugeteilten() an --
  ein Pool, in dem er steht, bleibt also sichtbar, bis ihn jemand
  uebernimmt. Die rechte und die linke Hand behalten die Uebersicht.

  Das Anlegen. Im Team Dogi legt nur an, wer auch verteilen darf. In
  der AGENTUR bleibt es, wie es war -- dort ist eine Aufgabe eine
  Notiz an sich selbst, kein Auftrag von jemandem. Eine Regel, die
  beide Haeuser ueber einen Kamm schert, waere falsch.

  Der Knopf. "Neue Aufgabe" steht fuer einen Modi gar nicht mehr da.
  Ein Knopf, der mit 403 antwortet, ist schlimmer als keiner: Die
  Meldung erscheint ganz oben, und wer weiter unten steht, sieht nur,
  dass nichts passiert.

  Die Kacheln. Ein Modi sieht jetzt den Bereich "Entwicklung &
  Nachwuchs" mit denselben zwei Kacheln wie die Leitung -- nicht mehr
  zwei eigene mit anderem Namen. Zwei Namen fuer dieselbe Sache ist
  genau der Fehler, der am 19.09. zwei Kacheln "Chat" ergeben hat.
  "Talente" bleibt draussen: Dort stehen Notizen ueber Zuschauer, die
  nichts davon wissen.

UND DIE LINKE HAND SIEHT "DEIN TEAM" NICHT MEHR (Filipes Wunsch).
Abgeleitet, nicht nachgebaut: Ihre Liste ist die der rechten Hand
MINUS dieser einen Kachel, erkannt am ZIEL statt am Namen -- der Name
ist am 17.09. schon einmal gewandert.

Gemessen: admin 30 Kacheln, hand 30, linke 29 (ohne "Dein Team"),
modi 25 (mit dem Bereich, ohne Talente).

Zwei Pruefungen hielten die alte Regel fest und wurden dadurch rot --
genau ihre Aufgabe. Beide umgedreht, mit der alten Entscheidung im
Kommentar. pruef-zuteilung 65/0, pruef-verteilen 19/0 (war 15),
pruef-aufgabenbrett 49/0.
2026-09-22 01:17:11 +02:00

269 lines
13 KiB
JavaScript

/* =====================================================================
pruef-verteilen.mjs — Wer darf Aufgaben an andere geben?
Filipe, 20.09.2026: "jeder soll sich selber aufgabe vergeben koennen
aber nur die rechte hand und dogfather aufgaben an andere verteilen."
---------------------------------------------------------------------
WARUM DAS EINE EIGENE PRUEFUNG BRAUCHT
Die Regel hat ZWEI Seiten, und beide muessen dasselbe sagen:
* Der Server lehnt ab, wer nicht darf.
* Die Seite zeigt die Felder nur dem, der darf.
Stimmt nur die erste, sieht die rechte Hand Felder, die nichts
bewirken. Stimmt nur die zweite, kommt jeder durch, der die
Schnittstelle direkt anspricht. Beides ist schon vorgekommen.
UND DIE GEFAEHRLICHE STELLE: `hand` steht bewusst NICHT in
`istLeitung` -- diese Menge wird an 57 Stellen gefragt, unter
anderem im vertraulichen Meldeweg. Die Pruefung haelt fest, dass
das Verteilen eine EIGENE Regel ist und nicht versehentlich zur
Leitung wird.
AUFRUF: node server/pruef-verteilen.mjs
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request as httpAnfrage } from "node:http";
import { eigenerPort } from "./helfer-port.mjs";
import { notbremse } from "./helfer-notbremse.mjs";
const PORT = await eigenerPort(import.meta, "pruef-verteilen");
const ordner = mkdtempSync(join(tmpdir(), "ws-verteilen-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
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");
notbremse(180_000, "pruef-verteilen");
await new Promise((r) => setTimeout(r, 900));
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 ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const melde = (t) => console.log(t);
console.log("\n=== Wer darf Aufgaben an andere geben? ===");
/* ---- 1. Die Regel selbst, ohne Server ----------------------------- */
melde("\n=== 1. Die Regel ===");
{
const { darfAufgabenVerteilen, istLeitung } = await import("./workspace.js");
const darf = ["admin", "hand", "spicy", "manager"];
const nicht = ["modi", "scout", "creator", "gast"];
ok(darf.every((r) => darfAufgabenVerteilen({ rolle: r })),
`verteilen duerfen: ${darf.join(", ")}`);
ok(nicht.every((r) => !darfAufgabenVerteilen({ rolle: r })),
`und nicht: ${nicht.join(", ")}`);
/* DIE WICHTIGE ABGRENZUNG: Verteilen ist NICHT Leitung. Waere die
rechte Hand in `istLeitung` gerutscht, saehe sie damit auch
vertrauliche Meldungen ueber Menschen -- eine Entscheidung, die
niemand getroffen hat. */
ok(!istLeitung({ rolle: "hand" }),
"die rechte Hand darf verteilen, ist aber NICHT „Leitung“ — das bleibt getrennt");
ok(darfAufgabenVerteilen({ rolle: undefined }) === false
&& darfAufgabenVerteilen(null) === false,
"Gegenprobe: ohne Rolle darf niemand");
}
/* ---- 2. Durch die Schnittstelle ----------------------------------- */
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const jetzt = new Date().toISOString();
let idDogi, idHand, idModi, idModi2;
{
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const k = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von) VALUES (?,?,?,NULL)"
+ " ON CONFLICT(schluessel) DO NOTHING").run("code_kennung_schluessel", k, jetzt);
const 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", k).update(code).digest("hex"), jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
};
idDogi = anlegen("Filipe", "admin", "CODE-A");
idHand = anlegen("VanVan", "hand", "CODE-H");
idModi = anlegen("Diene", "modi", "CODE-M");
idModi2 = anlegen("Ghost", "modi", "CODE-M2");
d.close();
}
const CREW = "crew.dogfather-universe.com";
function ruf(pfad, keks, methode = "GET", koerper = null) {
return new Promise((fertig, schief) => {
const rumpf = koerper ? JSON.stringify(koerper) : null;
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad, method: methode,
headers: {
Host: CREW, ...(keks ? { Cookie: keks } : {}),
...(rumpf ? { "Content-Type": "application/json", Origin: `https://${CREW}`,
"Content-Length": Buffer.byteLength(rumpf) } : {}),
},
}, (antwort) => {
let text = "";
antwort.on("data", (x) => { text += x; });
antwort.on("end", () => fertig({
code: antwort.statusCode, text,
keks: [].concat(antwort.headers["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "),
}));
});
a.on("error", schief);
if (rumpf) a.write(rumpf);
a.end();
});
}
const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } };
const anmelden = async (rolle, code) =>
(await ruf("/workspace/api/anmelden", null, "POST", { rolle, code })).keks;
melde("\n=== 2. Durch die Schnittstelle ===");
const kDogi = await anmelden("admin", "CODE-A");
const kHand = await anmelden("hand", "CODE-H");
const kModi = await anmelden("modi", "CODE-M");
ok(!!kDogi && !!kHand && !!kModi, "DogFather, rechte Hand und ein Modi sind angemeldet");
/* Sagt der Server es auch der Seite? */
for (const [name, keks, soll] of [["DogFather", kDogi, true], ["rechte Hand", kHand, true], ["Modi", kModi, false]]) {
const ich = json(await ruf("/workspace/api/ich", keks));
ok(ich.darf_verteilen === soll,
`„${name}“ bekommt darf_verteilen = ${ich.darf_verteilen} (erwartet ${soll})`);
}
/* Und die Tat: eine Aufgabe FUER JEMAND ANDEREN. */
async function anlegenFuer(keks, zielId, titel) {
const a = await ruf("/workspace/api/aufgaben", keks, "POST",
{ titel, verantwortlich_id: zielId, prioritaet: "mittel" });
if (a.code !== 201) return { code: a.code, verant: null };
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const z = d.prepare("SELECT verantwortlich_id FROM aufgaben WHERE id = ?").get(json(a).id);
d.close();
return { code: a.code, verant: z?.verantwortlich_id ?? null };
}
const vonHand = await anlegenFuer(kHand, idModi, "Von der rechten Hand an Diene");
ok(vonHand.code === 201 && vonHand.verant === idModi,
`die rechte Hand gibt eine Aufgabe an Diene (${vonHand.code}, liegt bei ${vonHand.verant})`);
const vonDogi = await anlegenFuer(kDogi, idModi2, "Von DogFather an Ghost");
ok(vonDogi.code === 201 && vonDogi.verant === idModi2,
`DogFather ebenso (${vonDogi.code}, liegt bei ${vonDogi.verant})`);
/* DIE ENTSCHEIDENDE GEGENPROBE: Ein Modi versucht dasselbe.
---------------------------------------------------------------------
BIS ZUM 22.09.2026 stand hier, die Aufgabe lande bei ihm selbst --
"jeder soll sich selber aufgabe vergeben koennen" (20.09.). Filipe
hat das ausdruecklich umgedreht: "damit wir endlich den modis
aufgaben anstaendig verteilen koennen und sie sich nicht selber
aufgaben geben."
Im Team Dogi kommen Aufgaben jetzt von der Leitung. Ein Modi
bekommt 403 -- und das ist der Unterschied, auf den es ankommt: Er
kann sich keine geben, weder sich selbst noch jemand anderem. */
const vonModi = await anlegenFuer(kModi, idModi2, "Versuch eines Modi");
ok(vonModi.code === 403,
`Gegenprobe: ein Modi legt gar keine Aufgabe mehr an (${vonModi.code})`);
/* UND IN DER AGENTUR BLEIBT ES, WIE ES WAR. Dort ist eine Aufgabe eine
Notiz an sich selbst, kein Auftrag von jemandem -- eine Regel, die
beide Haeuser ueber einen Kamm schert, waere falsch. */
{
const w = await import("./workspace.js");
ok(w.darfAufgabenAnlegen({ rolle: "creator" }) === true
&& w.darfAufgabenAnlegen({ rolle: "manager" }) === true,
"in der Agentur legt weiterhin jeder fuer sich selbst an");
ok(w.darfAufgabenAnlegen({ rolle: "modi" }) === false,
"ein Modi nicht");
ok(w.darfAufgabenAnlegen({ rolle: "hand" }) === true
&& w.darfAufgabenAnlegen({ rolle: "linke" }) === true
&& w.darfAufgabenAnlegen({ rolle: "admin" }) === true,
"die drei, die verteilen duerfen, schon");
}
/* =======================================================================
DIE FELDER MUESSEN AUCH ZU SEHEN SEIN
=======================================================================
Filipe am 21.09.2026: "ich sehe noch immer nicht dass dogfather oder
die rechte hand wenn sie aufgaben verteilen die spezifisch an leute
verteilen können. mach das endlich auch weil das ist seeehr wichtig
damit wir arbeiten können mit den leuten."
ALLES DARUEBER WAR GRUEN -- und er hatte trotzdem recht. Der Server
nahm die Zuteilung an, die Rechte stimmten, die Leute standen in der
Auskunft. Nur die rechte Hand BEKAM DAS FELD NIE ZU SEHEN: Das
Absenden fragte `ich.darf_verteilen` (Server: Leitung ODER rechte
Hand), das Anzeigen eine andere Menge aus bereiche.js (spicy, admin,
manager). Zwei Bedingungen fuer dieselbe Sache, und eine davon
veraltet.
EINE PRUEFUNG, DIE NUR DEN SERVER FRAGT, FINDET SO ETWAS NIE. Sie
sieht eine Erlaubnis, die es gibt -- und uebersieht, dass niemand
an sie herankommt. Deshalb wird hier der Quelltext der Oberflaeche
mitgelesen: Es muss an BEIDEN Stellen dieselbe Frage stehen.
======================================================================= */
{
const { readFileSync } = await import("node:fs");
/* KOMMENTARE ZAEHLEN NICHT MIT.
Beim ersten Lauf meldete diese Pruefung fuenf Stellen -- zwei
davon waren mein eigener ERKLAERTEXT, in dem die alte Bedingung
woertlich zitiert steht. Eine Pruefung, die ihre eigene
Begruendung als Befund zaehlt, treibt einen dazu, die Begruendung
zu loeschen. Der Zerleger aus helfer-ohne-kommentar.mjs trennt
beides sauber. */
const { ohneKommentar } = await import("./helfer-ohne-kommentar.mjs");
const js = ohneKommentar(readFileSync(
new URL("../workspace/assets/js/aufgaben.js", import.meta.url), "utf8"), ".js");
/* Wie oft wird ueberhaupt entschieden, ob jemand verteilen darf? */
const mitServer = (js.match(/ich\.darf_verteilen/g) || []).length;
ok(mitServer >= 2,
`die Oberflaeche fragt ${mitServer}-mal den Server (darf_verteilen)`);
/* UND NIRGENDS MEHR ALLEIN DIE ROLLENMENGE. `LEITUNG.has(ich.rolle)`
darf nur noch als Rueckfall hinter `??` stehen -- also fuer den
Fall, dass eine aeltere Serverfassung das Feld gar nicht schickt.
Steht es irgendwo als eigene Bedingung, ist die zweite Wahrheit
zurueck. */
const ohneRueckfall = [...js.matchAll(/LEITUNG\.has\(ich\.rolle\)/g)]
.filter((m) => !/\?\?\s*$/.test(js.slice(Math.max(0, m.index - 24), m.index)));
ok(ohneRueckfall.length === 0,
ohneRueckfall.length
? `${ohneRueckfall.length}-mal entscheidet die Rollenmenge allein -- `
+ "der Server sagt es besser"
: "und nirgends entscheidet die Rollenmenge allein");
/* DIE BESCHRIFTUNG IST TEIL DER FUNKTION. "Verantwortlich" mit einem
Strich daneben liest sich wie eine Eigenschaft; gefunden hat
Filipe das Feld damit nicht. "Wer macht es?" ist eine Frage. */
const html = readFileSync(
new URL("../workspace/aufgaben.html", import.meta.url), "utf8");
const schilder = [...html.matchAll(/for="(?:f|b)-verant">([^<]+)</g)].map((m) => m[1].trim());
ok(schilder.length === 2,
`beide Formulare haben das Feld (${schilder.length})`);
ok(schilder.every((t) => /wer\s+macht/i.test(t)),
`und beide fragen dasselbe ("${schilder.join('" / "')}")`);
/* Und der leere Wert sagt, was er bedeutet. Ein Strich ist kein
Zustand, sondern ein fehlendes Wort -- wer ihn sieht, weiss nicht,
ob er etwas versaeumt hat. */
ok(/— noch niemand —/.test(html),
"der leere Wert heisst „noch niemand“ und nicht nur „—“");
}
console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`);
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(fehler ? 1 : 0);