Der Eingang: Mitglieder-Kachel, drei Stufen -- und lesbar statt leer
Zwei Auftraege in einem Zug: "mach weiter" (Blueprint Kapitel 4/4.1)
und, zum Bildschirmfoto der Seite, "das muss viel krasser sein".
KAPITEL 4 -- DIE MITGLIEDER-KACHEL
Der Blueprint nennt: "Name & Foto · Rolle(n)/Kategorie(n) · Status
(aktiv/pausiert) · Anzahl offener Aufgaben · Datum 'Modi seit' ·
Schnellaktionen". Name, Foto und Zahlen standen schon; Status, "dabei
seit", Stufe und ein Knopf zum Schreiben kommen dazu.
KAPITEL 4.1 -- DIE DREI STUFEN
Probe, Standard, Senior. Sie sind eine ARBEITSEINTEILUNG, keine
Rechtegrenze -- was ein Modi darf, haengt an der Rolle; die Stufe sagt,
wo er im Team steht. Gesetzt werden sie nur von DogFather: Der
Blueprint gibt der rechten Hand den gleichen UEBERBLICK, aber
"Verwaltungsrechte optional durch Owner freischaltbar", also aus.
NULL heisst "Probe" und nicht "unbekannt" -- ein dritter Zustand waere
eine Frage, die niemand beantworten kann.
ZWEI ECHTE FEHLER, BEIDE VON DER NEUEN PRUEFUNG GEFUNDEN
1. PAUSIERTE VERSCHWANDEN KOMPLETT. In der Abfrage stand `AND aktiv =
1`. Wer jemanden pausierte, bei dem verschwand er samt seiner
OFFENEN RUECKMELDUNGEN aus dem Eingang -- die warteten weiter auf
eine Antwort, nur sah sie niemand mehr.
2. DER EINGANG HAETTE AUF FRISCHER ANLAGE 503 GELIEFERT. Die
Checklisten-Tabellen entstehen beim ersten Aufruf einer Checkliste;
diese Seite liest sie aber auch. Wer sie oeffnete, bevor je jemand
eine Checkliste angesehen hatte, bekam einen Fehler ohne Erklaerung
-- auf einer neuen Anlage also beim allerersten Blick. Das Schema
hat jetzt einen Besitzer, der es herausgibt; ein zweites CREATE
TABLE waere der Anfang von zwei Schemata gewesen.
"VIEL KRASSER" -- UND ZWAR MIT INFORMATION, NICHT MIT LAERM
* ZWEI REIHEN STATT EINER. Alle fuenf Kaesten lagen in EINEM Raster
mit 150 px Mindestbreite: Im Bildschirmfoto stand "Community 12
von" -- abgeschnitten mitten in der Zahl -- und die laengste Liste
machte die ganze Reihe so hoch wie sich selbst. Jetzt oben, was
eine Antwort braucht, darunter das Team; 300 px Mindestbreite.
* DIE KARTE WAR FAHL, und das war ein Fehler: `--r` fiel auf ein
helles Grau zurueck, aus dem das Kantenlicht einen Nebel ueber die
ganze Karte legte. Sie traegt jetzt die Farbe der Stufe -- kein
Nebel, und man sieht am Rand, wer wo steht.
* SECHS ZAHLENKAESTEN WURDEN DREI BALKEN. "40 offen" beantwortet
nicht, wie weit man ist: 40 von 40 ist etwas anderes als 40 von
100. Gruen (in Ordnung) und Bernstein (zu besprechen) fuellen den
Balken; die ganze Zeile ist der Weg dorthin.
* DIE DREI ZAHLEN DER SEITE stehen oben als Zahlen statt in einem
Satz. Die Warnfarbe erscheint NUR, wenn wirklich etwas wartet --
eine Null in Bernstein waere ein Alarm ohne Anlass, und ab dem
dritten Mal sieht man ihn nicht mehr.
GEMESSEN
pruef-team-stufen (neu) 24, mit Gegenprobe: hoch- und zurueckstufen,
und dieselbe Stufe zweimal zu setzen darf
keine zweite Protokollzeile erzeugen
pruef-rollen 274 pruef-modi-checkliste 59
pruef-zwischenspeicher 21
Ansicht: Rechner 1440 und Handy 412 -- keine Skriptfehler, nichts
ragt seitlich heraus (0 px).
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
/* =====================================================================
|
||||
DIE MITGLIEDER-KACHEL UND DIE DREI STUFEN (Blueprint Kapitel 4/4.1)
|
||||
|
||||
Vier Fragen, und die dritte ist die, an der es schiefgeht:
|
||||
|
||||
1. Stehen die Angaben aus Kapitel 4 in der Antwort -- Status,
|
||||
"dabei seit", Stufe?
|
||||
2. Erscheinen auch PAUSIERTE? Vorher stand in der Abfrage
|
||||
`AND aktiv = 1`: Wer jemanden pausierte, bei dem verschwand er
|
||||
samt seiner offenen Rueckmeldungen aus dem Eingang. Die
|
||||
warteten weiter auf eine Antwort, nur sah sie niemand mehr.
|
||||
3. WER DARF DIE STUFE SETZEN? Der Blueprint trennt das
|
||||
ausdruecklich: gleicher Ueberblick fuer die rechte Hand,
|
||||
"Verwaltungsrechte optional durch Owner freischaltbar" -- also
|
||||
standardmaessig aus. Eine Rechtegrenze, die man nicht misst,
|
||||
ist eine Absichtserklaerung.
|
||||
4. Ueberlebt die Stufe den naechsten Aufruf? Ein Wert, der nur im
|
||||
Browser steht, sieht bis zum Neuladen richtig aus.
|
||||
|
||||
MIT GEGENPROBE: Am Ende wird dieselbe Person auf eine andere Stufe
|
||||
gesetzt und wieder zurueck. Ohne diesen Teil waere nicht bewiesen,
|
||||
dass die Pruefung ueberhaupt einen Unterschied bemerkt.
|
||||
===================================================================== */
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-stufen-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
process.env.PORT = "4375";
|
||||
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 }); };
|
||||
import { notbremse } from "./helfer-notbremse.mjs";
|
||||
await import("./index.js");
|
||||
notbremse(180_000, "pruef-team-stufen");
|
||||
await new Promise((r) => setTimeout(r, 700));
|
||||
const BASIS = "http://127.0.0.1:4375";
|
||||
|
||||
let fehler = 0, geprueft = 0, offen = 0;
|
||||
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
const unklar = (t) => { offen++; console.log(" -- " + t); };
|
||||
|
||||
/* ---------- Personen ------------------------------------------------- */
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
||||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const jetzt = new Date().toISOString();
|
||||
|
||||
const schluessel = randomBytes(32).toString("hex");
|
||||
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
||||
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
||||
.run("code_kennung_schluessel", schluessel, jetzt);
|
||||
|
||||
function anlegen(name, rolle, code, aktiv = 1, erstellt = jetzt) {
|
||||
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 (?,?,?,?,?,?,?,?)")
|
||||
.run(name, rolle, hash, salt, 32768,
|
||||
createHmac("sha256", schluessel).update(code).digest("hex"), aktiv, erstellt);
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
|
||||
/* Ein Datum weit genug zurueck, damit "seit ... Tagen dabei" etwas
|
||||
anderes ergibt als "seit heute" -- und RELATIV gerechnet, nicht fest
|
||||
hingeschrieben: Ein festes Datum waere in einem halben Jahr eine
|
||||
andere Aussage (Lehre vom 06.09.2026). */
|
||||
const vor40Tagen = new Date(Date.now() - 40 * 86400000).toISOString();
|
||||
|
||||
anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||||
anlegen("Rieke", "hand", "CODE-HAND-0001");
|
||||
const idMarina = anlegen("Marina", "modi", "CODE-MODI-0001", 1, vor40Tagen);
|
||||
const idKim = anlegen("Kim", "modi", "CODE-MODI-0002", 0); // pausiert
|
||||
const idCreator = anlegen("Tili", "creator", "CODE-TILI-0001");
|
||||
|
||||
async function anmelden(rolle, code) {
|
||||
const a = await fetch(`${BASIS}/workspace/api/anmelden`, {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rolle, code }),
|
||||
});
|
||||
return (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
||||
}
|
||||
const holeLage = (keks) => fetch(`${BASIS}/workspace/api/teamlage`, { headers: { Cookie: keks } });
|
||||
const setzeStufe = (keks, person_id, stufe) =>
|
||||
fetch(`${BASIS}/workspace/api/teamlage/stufe`, {
|
||||
method: "PUT",
|
||||
headers: { Cookie: keks, "Content-Type": "application/json", Origin: BASIS },
|
||||
body: JSON.stringify({ person_id, stufe }),
|
||||
});
|
||||
|
||||
const keksDogi = await anmelden("admin", "CODE-DOGI-0001");
|
||||
const keksHand = await anmelden("creator", "CODE-HAND-0001");
|
||||
const keksModi = await anmelden("creator", "CODE-MODI-0001");
|
||||
if (!keksDogi || !keksHand || !keksModi) {
|
||||
console.error("FEHLER: Anmeldung misslang -- ohne sie misst diese Datei nichts.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
1. WAS IN DER ANTWORT STEHT
|
||||
===================================================================== */
|
||||
console.log("");
|
||||
console.log("--- Die Mitglieder-Kachel (Kapitel 4) ---");
|
||||
|
||||
const a1 = await holeLage(keksDogi);
|
||||
ok(a1.status === 200, `DogFather bekommt die Lage (${a1.status})`);
|
||||
const lage = await a1.json();
|
||||
const leute = lage.modis || [];
|
||||
|
||||
ok(leute.length === 2, `beide Modis stehen da, auch der pausierte (${leute.length})`);
|
||||
|
||||
const marina = leute.find((m) => m.id === idMarina);
|
||||
const kim = leute.find((m) => m.id === idKim);
|
||||
ok(!!marina && !!kim, "beide sind namentlich wiederzufinden");
|
||||
|
||||
ok(marina?.aktiv === true && kim?.aktiv === false,
|
||||
`Status stimmt (Marina aktiv: ${marina?.aktiv}, Kim aktiv: ${kim?.aktiv})`);
|
||||
/* AKTIVE ZUERST -- wer pausiert ist, steht unten und nicht mittendrin. */
|
||||
ok(leute[0]?.id === idMarina, "die Aktiven stehen oben");
|
||||
|
||||
ok(typeof marina?.seit === "string" && marina.seit.startsWith(vor40Tagen.slice(0, 10)),
|
||||
`"dabei seit" kommt mit (${(marina?.seit || "").slice(0, 10)})`);
|
||||
|
||||
/* NULL HEISST PROBE, nicht "unbekannt" -- an EINER Stelle im Server
|
||||
abgebildet. Beide Modis wurden ohne Stufe angelegt. */
|
||||
ok(marina?.stufe === "probe" && kim?.stufe === "probe",
|
||||
`ohne Eintrag ist die Stufe "Probe" (${marina?.stufe} / ${kim?.stufe})`);
|
||||
|
||||
/* Die Liste der Stufen kommt VOM SERVER -- eine zweite im Browser
|
||||
waere die Stelle, an der beim naechsten Mal eine fehlt. */
|
||||
const stufen = (lage.stufen || []).map((s) => s.wert);
|
||||
ok(JSON.stringify(stufen) === JSON.stringify(["probe", "standard", "senior"]),
|
||||
`die drei Stufen kommen mit, in ihrer Reihenfolge (${stufen.join(", ")})`);
|
||||
ok((lage.stufen || []).every((s) => s.name && s.name !== s.wert),
|
||||
"und jede hat einen lesbaren Namen");
|
||||
|
||||
/* =====================================================================
|
||||
2. WER DARF SIE SETZEN
|
||||
===================================================================== */
|
||||
console.log("");
|
||||
console.log("--- Wer die Stufe setzen darf ---");
|
||||
|
||||
const gesetzt = await setzeStufe(keksDogi, idMarina, "senior");
|
||||
ok(gesetzt.status === 200, `DogFather darf (${gesetzt.status})`);
|
||||
|
||||
/* DIE RECHTE HAND SIEHT ALLES UND SETZT NICHTS. Blueprint 3.1:
|
||||
"Verwaltungsrechte optional durch Owner freischaltbar" -- also aus.
|
||||
404 und nicht 403: Wer nicht darf, muss nicht erfahren, dass es den
|
||||
Weg gibt. */
|
||||
const handSieht = await holeLage(keksHand);
|
||||
ok(handSieht.status === 200, `die rechte Hand sieht die Lage (${handSieht.status})`);
|
||||
const handSetzt = await setzeStufe(keksHand, idMarina, "probe");
|
||||
ok(handSetzt.status === 404, `aber sie setzt keine Stufe (${handSetzt.status})`);
|
||||
|
||||
const modiSetzt = await setzeStufe(keksModi, idMarina, "probe");
|
||||
ok(modiSetzt.status === 404, `ein Modi erst recht nicht (${modiSetzt.status})`);
|
||||
const modiSieht = await holeLage(keksModi);
|
||||
ok(modiSieht.status === 404, `und er sieht die Seite gar nicht (${modiSieht.status})`);
|
||||
|
||||
/* =====================================================================
|
||||
3. WAS ABGEWIESEN WIRD
|
||||
===================================================================== */
|
||||
console.log("");
|
||||
console.log("--- Was nicht durchgeht ---");
|
||||
|
||||
const erfunden = await setzeStufe(keksDogi, idMarina, "chef");
|
||||
ok(erfunden.status === 400, `eine erfundene Stufe wird abgewiesen (${erfunden.status})`);
|
||||
|
||||
/* An einem Creator hat die Stufe nichts verloren -- sie stuende in der
|
||||
Datenbank und wuerde nirgends angezeigt. */
|
||||
const anCreator = await setzeStufe(keksDogi, idCreator, "senior");
|
||||
ok(anCreator.status === 404, `an einem Creator geht sie nicht (${anCreator.status})`);
|
||||
const nachCreator = d.prepare("SELECT stufe FROM personen WHERE id = ?").get(idCreator);
|
||||
ok(!nachCreator?.stufe, "und es steht auch nichts in der Datenbank");
|
||||
|
||||
const anNiemand = await setzeStufe(keksDogi, 99999, "senior");
|
||||
ok(anNiemand.status === 404, `an einer unbekannten Nummer auch nicht (${anNiemand.status})`);
|
||||
|
||||
/* =====================================================================
|
||||
4. UEBERLEBT SIE DEN NAECHSTEN AUFRUF?
|
||||
===================================================================== */
|
||||
console.log("");
|
||||
console.log("--- Beim naechsten Laden ---");
|
||||
|
||||
const a2 = await holeLage(keksDogi);
|
||||
const marina2 = ((await a2.json()).modis || []).find((m) => m.id === idMarina);
|
||||
ok(marina2?.stufe === "senior", `die neue Stufe kommt zurueck (${marina2?.stufe})`);
|
||||
const inDb = d.prepare("SELECT stufe FROM personen WHERE id = ?").get(idMarina);
|
||||
ok(inDb?.stufe === "senior", `und steht in der Datenbank (${inDb?.stufe})`);
|
||||
|
||||
/* Eine Personalentscheidung gehoert ins Protokoll -- gerade wenn
|
||||
jemand zurueckgestuft wird, muss spaeter nachvollziehbar sein, wer
|
||||
das wann getan hat. */
|
||||
const spur = d.prepare(
|
||||
"SELECT COUNT(*) AS n FROM protokoll WHERE aktion = 'stufe_gesetzt'").get()?.n || 0;
|
||||
ok(spur >= 1, `die Entscheidung steht im Protokoll (${spur})`);
|
||||
|
||||
/* =====================================================================
|
||||
DIE GEGENPROBE
|
||||
|
||||
Bis hierhin koennte alles auch dann gruen sein, wenn der Server
|
||||
jeden Wert einfach annimmt und die Anzeige ihn nie wieder aendert.
|
||||
Also zurueckstufen und noch einmal nachsehen.
|
||||
===================================================================== */
|
||||
console.log("");
|
||||
console.log("--- Gegenprobe: wieder zurueck ---");
|
||||
|
||||
const zurueck = await setzeStufe(keksDogi, idMarina, "standard");
|
||||
ok(zurueck.status === 200, `zurueckstufen geht (${zurueck.status})`);
|
||||
const a3 = await holeLage(keksDogi);
|
||||
const marina3 = ((await a3.json()).modis || []).find((m) => m.id === idMarina);
|
||||
ok(marina3?.stufe === "standard",
|
||||
`und die Anzeige folgt (${marina3?.stufe}) -- sie bleibt also nicht auf "senior" stehen`);
|
||||
|
||||
/* Dieselbe Stufe noch einmal: kein Fehler, aber auch keine zweite
|
||||
Protokollzeile -- sonst waere das Protokoll nach einem Tag Klicken
|
||||
unlesbar. */
|
||||
const vorher = d.prepare(
|
||||
"SELECT COUNT(*) AS n FROM protokoll WHERE aktion = 'stufe_gesetzt'").get()?.n || 0;
|
||||
const nochmal = await setzeStufe(keksDogi, idMarina, "standard");
|
||||
const nachher = d.prepare(
|
||||
"SELECT COUNT(*) AS n FROM protokoll WHERE aktion = 'stufe_gesetzt'").get()?.n || 0;
|
||||
ok(nochmal.status === 200 && nachher === vorher,
|
||||
`dieselbe Stufe noch einmal aendert nichts (${vorher} -> ${nachher} Zeilen)`);
|
||||
|
||||
/* ---------- Ende ------------------------------------------------------ */
|
||||
d.close();
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Windows haelt manchmal */ }
|
||||
|
||||
console.log("");
|
||||
console.log(`${fehler === 0 ? "BESTANDEN" : "FEHLGESCHLAGEN"}`
|
||||
+ ` -- ${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`);
|
||||
process.exit(fehler === 0 ? 0 : 1);
|
||||
@@ -174,6 +174,22 @@ for (const [bereich, liste] of Object.entries(LISTEN)) {
|
||||
}
|
||||
|
||||
let bereit = false;
|
||||
/* AUSGEGEBEN, seit die Eingang-Seite dieselben Tabellen liest
|
||||
(10.09.2026).
|
||||
|
||||
Sie entstehen beim ersten Aufruf einer Checkliste. Wer die
|
||||
Eingang-Seite oeffnete, BEVOR das je passiert war, bekam dort eine
|
||||
503 ohne Erklaerung -- auf einer frischen Anlage also beim ersten
|
||||
Blick ueberhaupt. Gefunden hat das nicht der Betrieb, sondern eine
|
||||
neue Pruefung, die genau diesen Zustand herstellt: Server frisch,
|
||||
Checkliste nie geoeffnet.
|
||||
|
||||
Die Alternative waere ein zweites CREATE TABLE in der Eingang-Seite
|
||||
gewesen. Genau daraus werden zwei Schemata, die sich langsam
|
||||
auseinanderentwickeln. Das Schema hat einen Besitzer; er gibt es
|
||||
heraus. */
|
||||
export function checklistenTabellen() { tabellen(); }
|
||||
|
||||
function tabellen() {
|
||||
if (bereit) return;
|
||||
db().exec(`
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
db, sitzungLesen, istDogFather, heuteLokal, protokolliere, echteIp,
|
||||
} from "./workspace.js";
|
||||
import { MODI_PUNKTE } from "./workspace-modi-punkte.js";
|
||||
import { checklistenTabellen } from "./workspace-checkliste.js";
|
||||
|
||||
/* Titel und Bereich zu einem Schluessel -- damit im Eingang steht,
|
||||
WORUM es geht, und nicht nur ein Schluessel. */
|
||||
@@ -74,6 +75,43 @@ const GESAMT = Object.fromEntries(
|
||||
* vierte kommt mit der naechsten Route. Wer eine davon vergisst,
|
||||
* merkt es nicht -- eine vergessene Schranke faellt nur dem auf, der
|
||||
* hindurchgeht. */
|
||||
/* =====================================================================
|
||||
DIE DREI STUFEN (Blueprint V3.0, Kapitel 4.1)
|
||||
|
||||
Probe -- in der Einarbeitung, wird eingeschaetzt.
|
||||
Standard -- der Normalfall: moderiert, arbeitet eigene Aufgaben ab.
|
||||
Senior -- kann zusaetzlich neue Probe-Modis einschaetzen, mehrere
|
||||
Kategorien gleichzeitig tragen und die rechte Hand bei
|
||||
Abwesenheit vertreten.
|
||||
|
||||
SIE SIND EINE ARBEITSEINTEILUNG, KEINE RECHTEGRENZE -- jedenfalls
|
||||
heute noch nicht. Das gehoert ausgesprochen, damit niemand sie
|
||||
spaeter fuer eine haelt: Was ein Modi in dieser App darf, haengt an
|
||||
der ROLLE. Die Stufe sagt, wo er im Team steht, und macht den
|
||||
naechsten Schritt sichtbar. Die Rechte, die der Blueprint dort
|
||||
nennt, liegen ausserdem groesstenteils gar nicht hier, sondern im
|
||||
Stream (Timeout, Bann).
|
||||
|
||||
Die Reihenfolge ist die Steigerung -- sie steht hier einmal, damit
|
||||
keine Liste im Browser eine eigene erfindet. */
|
||||
export const STUFEN = ["probe", "standard", "senior"];
|
||||
|
||||
export const STUFEN_NAME = {
|
||||
probe: "Probe",
|
||||
standard: "Standard",
|
||||
senior: "Senior",
|
||||
};
|
||||
|
||||
/** NULL heisst "Probe", nicht "unbekannt".
|
||||
*
|
||||
* Ein dritter Zustand waere eine Frage, die niemand beantworten kann,
|
||||
* und ein neuer Mensch faengt ohnehin in der Einarbeitung an. Die
|
||||
* Abbildung steht an EINER Stelle -- stuende sie in jeder Abfrage,
|
||||
* waere sie beim naechsten Mal in einer davon anders. */
|
||||
function stufeVon(wert) {
|
||||
return STUFEN.includes(String(wert || "")) ? String(wert) : "probe";
|
||||
}
|
||||
|
||||
function darfEingang(person) {
|
||||
return istDogFather(person) || person?.rolle === "hand";
|
||||
}
|
||||
@@ -85,9 +123,30 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => {
|
||||
/* 404 und nicht 403 -- siehe Kopf. */
|
||||
if (!darfEingang(person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
/* DIE CHECKLISTEN-TABELLEN ENTSTEHEN BEIM ERSTEN AUFRUF EINER
|
||||
CHECKLISTE -- diese Seite liest sie aber auch.
|
||||
|
||||
Wer den Eingang oeffnete, BEVOR je jemand eine Checkliste
|
||||
angesehen hatte, bekam hier eine 503 ohne Erklaerung. Auf einer
|
||||
frischen Anlage also beim allerersten Blick. Gefunden hat das
|
||||
nicht der Betrieb, sondern pruef-team-stufen.mjs, die genau
|
||||
diesen Zustand herstellt. */
|
||||
checklistenTabellen();
|
||||
|
||||
/* AUCH DIE PAUSIERTEN (10.09.2026, Blueprint Kapitel 4).
|
||||
|
||||
Die Mitglieder-Kachel dort nennt ausdruecklich "Status
|
||||
(aktiv/pausiert)" -- also muss man Pausierte auch SEHEN. Vorher
|
||||
stand hier `AND aktiv = 1`: Wer jemanden pausierte, bei dem
|
||||
verschwand er samt seiner offenen Rueckmeldungen aus dem
|
||||
Eingang. Die warteten weiter auf eine Antwort, nur sah sie
|
||||
niemand mehr.
|
||||
|
||||
Aktive zuerst, dann nach Namen -- wer pausiert ist, steht unten
|
||||
und nicht mittendrin. */
|
||||
const modis = db().prepare(
|
||||
`SELECT id, name, bild, letzter_login FROM personen
|
||||
WHERE rolle = 'modi' AND aktiv = 1 ORDER BY name`).all();
|
||||
`SELECT id, name, bild, letzter_login, aktiv, erstellt, stufe FROM personen
|
||||
WHERE rolle = 'modi' ORDER BY aktiv DESC, name`).all();
|
||||
|
||||
if (!modis.length) {
|
||||
/* `eingang` auch hier -- die Oberflaeche darf nicht raten
|
||||
@@ -187,6 +246,14 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => {
|
||||
bereiche: GESAMT,
|
||||
eingang,
|
||||
angebote,
|
||||
/* DIE STUFEN KOMMEN VOM SERVER, nicht aus einer Liste im Browser.
|
||||
|
||||
Eine zweite Aufzaehlung dort waere die Stelle, an der beim
|
||||
naechsten Mal eine Stufe fehlt oder anders heisst -- genau so
|
||||
stand am 07.09. unter dem Codefeld "PERSOENLICHER ZUGANGSCODE ·
|
||||
undefined", weil eine Rolle in der Browser-Liste vergessen
|
||||
worden war. Einmal geschrieben, einmal geaendert. */
|
||||
stufen: STUFEN.map((wert) => ({ wert, name: STUFEN_NAME[wert] })),
|
||||
modis: modis.map((m) => {
|
||||
const s = stand.get(m.id) || {};
|
||||
const je = {};
|
||||
@@ -200,6 +267,10 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => {
|
||||
name: m.name,
|
||||
bild: m.bild ? `/workspace/api/steckbrief/bild/${m.bild}` : null,
|
||||
letzter_login: m.letzter_login,
|
||||
/* Die drei Felder aus Kapitel 4: Status, "dabei seit", Stufe. */
|
||||
aktiv: !!m.aktiv,
|
||||
seit: m.erstellt || null,
|
||||
stufe: stufeVon(m.stufe),
|
||||
aufgaben: aufgaben.get(m.id) || { offen: 0, ueberfaellig: 0 },
|
||||
ideen: ideen.get(m.id) || 0,
|
||||
nachrichten: nachrichten.get(m.id) || 0,
|
||||
@@ -232,6 +303,59 @@ teamlageRouter.get("/workspace/api/teamlage", (req, res) => {
|
||||
annimmt, nimmt sie sich. Sie einem Dritten zuzuweisen waere eine
|
||||
Entscheidung ueber jemand anderen, ohne ihn zu fragen.
|
||||
===================================================================== */
|
||||
/* =====================================================================
|
||||
DIE STUFE SETZEN (Blueprint Kapitel 4.1, Punkt 3: "Rechte anpassen")
|
||||
|
||||
NUR DOGFATHER, nicht die rechte Hand. Der Blueprint trennt das
|
||||
ausdruecklich: Sie hat den gleichen UEBERBLICK, aber
|
||||
"Verwaltungsrechte optional durch Owner freischaltbar" -- also
|
||||
standardmaessig aus. Wer im Team auf welcher Stufe steht, ist eine
|
||||
Personalentscheidung; sie gehoert dem, der die Leute holt.
|
||||
|
||||
404 statt 403, wie ueberall auf dieser Seite: Wer nicht darf, muss
|
||||
nicht erfahren, dass es den Weg gibt.
|
||||
===================================================================== */
|
||||
teamlageRouter.put("/workspace/api/teamlage/stufe", gleicheHerkunft,
|
||||
express.json({ limit: "2kb" }), (req, res) => {
|
||||
try {
|
||||
const person = sitzungLesen(req);
|
||||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||||
if (!istDogFather(person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
const personId = Number(req.body?.person_id) || 0;
|
||||
const stufe = String(req.body?.stufe || "");
|
||||
if (!STUFEN.includes(stufe)) {
|
||||
return res.status(400).json({ fehler: "Unbekannte Stufe." });
|
||||
}
|
||||
|
||||
/* NUR AN EINEM MODI. Ein Manager oder Creator hat keine Stufe --
|
||||
sie stuende in der Datenbank und wuerde nirgends angezeigt,
|
||||
also waere sie ein stiller Datensatz, den niemand mehr
|
||||
zuordnen kann. Und die rechte Hand hat auch keine: Sie ist
|
||||
eine Rolle, keine Stufe darin. */
|
||||
const ziel = db().prepare(
|
||||
"SELECT id, name, rolle, stufe FROM personen WHERE id = ?").get(personId);
|
||||
if (!ziel || ziel.rolle !== "modi") {
|
||||
return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
}
|
||||
const vorher = STUFEN.includes(String(ziel.stufe || "")) ? String(ziel.stufe) : "probe";
|
||||
if (vorher === stufe) return res.json({ ok: true, stufe, geaendert: false });
|
||||
|
||||
db().prepare("UPDATE personen SET stufe = ? WHERE id = ?").run(stufe, ziel.id);
|
||||
/* Ins Protokoll, weil es eine Personalentscheidung ist: Wer sie
|
||||
wann getroffen hat, muss spaeter nachvollziehbar sein -- gerade
|
||||
wenn jemand zurueckgestuft wird. */
|
||||
protokolliere("stufe_gesetzt", {
|
||||
personId: ziel.id, rolle: ziel.rolle, ip: echteIp(req),
|
||||
detail: `${ziel.name}: ${vorher} -> ${stufe}`,
|
||||
});
|
||||
return res.json({ ok: true, stufe, geaendert: true });
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Stufe setzen:", fehler?.message);
|
||||
return res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
teamlageRouter.post("/workspace/api/teamlage/behandeln", gleicheHerkunft,
|
||||
express.json({ limit: "16kb" }), (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -1037,6 +1037,32 @@ function umstellungen(d) {
|
||||
allein scrypt. Ein Treffer hier allein laesst niemanden herein. */
|
||||
["personen", "code_kennung", "TEXT"],
|
||||
|
||||
/* DIE STUFE IM TEAM (10.09.2026).
|
||||
|
||||
Blueprint V3.0, Kapitel 4.1: Probe, Standard, Senior. Ein
|
||||
Probe-Modi ist in der Einarbeitung, ein Senior kann die rechte
|
||||
Hand bei Abwesenheit vertreten.
|
||||
|
||||
WARUM OHNE CHECK-REGEL, anders als bei `rolle`: Eine CHECK-Liste
|
||||
laesst sich in SQLite nur ueber einen Tabellenneubau erweitern
|
||||
(siehe checkListeErweitern) -- und die Stufen sind eine
|
||||
ARBEITSEINTEILUNG, keine Rechtegrenze. Kaeme morgen eine vierte
|
||||
dazu, waere ein Tabellenneubau auf einer Live-Datenbank ein
|
||||
hoher Preis fuer eine Beschriftung. Geprueft wird deshalb im
|
||||
Code, an genau einer Stelle (STUFEN), und was dort nicht
|
||||
draufsteht, wird abgewiesen.
|
||||
|
||||
NULL HEISST "Probe" und nicht "unbekannt". Ein dritter Zustand
|
||||
waere eine Frage, die niemand beantworten kann ("was ist er
|
||||
denn nun?"); und ein neuer Mensch faengt ohnehin in der
|
||||
Einarbeitung an. Beim Lesen wird NULL deshalb auf 'probe'
|
||||
abgebildet -- an einer Stelle, nicht in jeder Abfrage.
|
||||
|
||||
Die Spalte heisst wie `punkt_stand.stufe`, meint aber etwas
|
||||
anderes (dort: gut/verbessern). Zwei Tabellen, kein Konflikt --
|
||||
aber wer beides im selben Kopf hat, sollte es wissen. */
|
||||
["personen", "stufe", "TEXT"],
|
||||
|
||||
/* DIE KATEGORIE EINER AUFGABE (10.09.2026).
|
||||
|
||||
Kapitel 6.1 des Anforderungsdokuments verlangt sie an jeder
|
||||
|
||||
Reference in New Issue
Block a user