/* ===================================================================== Wie geht's dir? (15.09.2026) Filipe, mit dem Bildschirmfoto der Kachel: *„ich will dass du diese seite perfektionnierst, denn gerade wenn ich drauf drücke geht die entwicklungsseite auf."* Er hat recht, und es war schlimmer als ein falscher Verweis: Die Kachel verspricht „sechs Fragen – die Antworten sieht nur du" und öffnete eine Seite mit Namen und Beobachtungen über andere Menschen. Wer ihr glaubt und draufdrückt, sieht im ersten Moment das Gegenteil dessen, was draufsteht. --------------------------------------------------------------------- EINE SEITE, EIN ZWECK `entwicklung.html` bediente zwei Fragen: „wie geht es dem Team?" und „wie geht es mir?". Das ist eine Seite zu wenig. Die sechs Fragen haben jetzt ihre eigene — und `entwicklung.html` wird dadurch selbst einfacher, weil sie nur noch eine Sache tut. --------------------------------------------------------------------- WAS DIE RECHERCHE ERGEBEN HAT (15.09.2026) Zu Puls-/Befindensabfragen in kleinen Teams sagen die Quellen übereinstimmend dreierlei: 1. KURZ. Fünf bis fünfzehn Fragen, unter zwei Minuten. Sechs sind also richtig — daran ändert sich nichts. 2. WIEDERHOLT. Monatlich oder zweiwöchentlich. Genau das fehlte: Bisher beantwortete man die sechs Fragen EINMAL, und die Antwort stand für immer. Ein Befinden von vor drei Monaten ist keine Auskunft mehr, sondern ein Andenken. 3. DER VERLAUF IST DIE AUSKUNFT, nicht der Einzelwert. „Niedrige Energie über mehrere Wochen" ist das Warnzeichen — ein einzelnes „da hakt es" ist ein schlechter Tag. Deshalb zwei Dinge neu: ein RHYTHMUS (nach 14 Tagen wird wieder gefragt) und ein VERLAUF, den nur die Person selbst sieht. Quellen: https://www.specific.app/blog/pulse-survey-questions-best-questions-for-burnout-to-detect-and-address-employee-engagement-risk https://formbricks.com/blog/pulse-survey-questions https://www.vantagecircle.com/en/blog/pulse-surveys/ --------------------------------------------------------------------- DER VERLAUF GEHÖRT IHR ALLEIN Er wird in einer eigenen Tabelle geführt, und kein anderer Weg im Haus liest sie. Die Ampel (wie viele es gerade schwer haben) zählt weiterhin nur den AKTUELLEN Stand und kennt keine Namen — daran ändert der Verlauf nichts. Das ist kein Nebensatz: Eine Verlaufsakte über das Befinden eines Menschen ist das Empfindlichste, was dieses Haus speichern kann. Sie existiert nur, weil sie IHM hilft, sein eigenes Muster zu sehen. ===================================================================== */ import express from "express"; import { db, sitzungLesen, protokolliere, echteIp } from "./workspace.js"; import { ENTWICKLUNG_BLOECKE, ENTWICKLUNG_PUNKTE, ENTWICKLUNG_STUFEN, } from "./workspace-entwicklungs-punkte.js"; import { tabellen as entwicklungTabellen } from "./workspace-entwicklung.js"; export const befindenRouter = express.Router(); /** Nach wie vielen Tagen wieder gefragt wird. * * 14 Tage — zweiwöchentlich, das untere Ende dessen, was die Quellen * empfehlen (monatlich bis zweiwöchentlich). Häufiger wäre * Umfragemüdigkeit: Wer jede Woche gefragt wird, klickt irgendwann * dasselbe an wie beim letzten Mal, ohne nachzudenken — und dann misst * die Abfrage nur noch sich selbst. */ export const RHYTHMUS_TAGE = 14; /** So viele frühere Antworten bekommt die Person zu sehen. * * Sechs Stände sind drei Monate — lang genug, um ein Muster zu sehen, * kurz genug, dass es kein Archiv wird. */ export const VERLAUF_MAX = 6; const jetzt = () => new Date().toISOString(); let bereit = false; function tabellen() { if (bereit) return; db().exec(` /* DER VERLAUF DES EIGENEN BEFINDENS. Eigene Tabelle und nicht „entwicklung_stand“: Dort steht der AKTUELLE Stand, einer je Punkt und Beurteiler, und der wird ueberschrieben. Genau das ist dort richtig -- eine Beobachtung ueber einen Menschen soll aktuell sein, nicht historisch. Hier ist es umgekehrt: Der Verlauf IST die Auskunft. Zwei verschiedene Beduerfnisse, zwei Tabellen. Sie in eine zu zwingen hiesse, eines von beiden falsch zu machen. KEIN von_id: Was hier steht, hat die Person ueber sich selbst gesagt. Eine Spalte fuer "wer hat das eingetragen" waere die Einladung, eines Tages auch jemand anderen eintragen zu lassen. */ CREATE TABLE IF NOT EXISTS befinden_verlauf ( id INTEGER PRIMARY KEY AUTOINCREMENT, person_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE, runde TEXT NOT NULL, -- Zeitpunkt der Runde, alle sechs teilen ihn schluessel TEXT NOT NULL, stufe TEXT NOT NULL, am TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_befinden_person ON befinden_verlauf (person_id, runde); `); /* UND DIE TABELLE, AUS DER DIE ANTWORTEN KOMMEN. Sie wird hier nicht angelegt, sondern angefordert: Die sechs Antworten stehen in `entwicklung_stand`, und die legt die Entwicklungsseite an -- beim ersten Aufruf, nicht beim Start. Wer die neue Seite oeffnete, bevor irgendjemand die Entwicklungsseite besucht hatte, bekam deshalb "no such table" und eine 503. Gefunden hat das die Pruefung, nicht das Lesen. */ entwicklungTabellen(); bereit = true; } function angemeldet(req, res, next) { const person = sitzungLesen(req); if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" }); req.person = person; try { tabellen(); } catch (f) { console.error("[befinden] Tabellen:", f?.message); return res.status(503).json({ fehler: "nicht_verfuegbar" }); } next(); } /** Der Block mit den sechs Fragen. Er ist als einziger `nurSelbst`. */ function block() { return ENTWICKLUNG_BLOECKE.find((b) => b.nurSelbst) || null; } /** Wann war die letzte vollständige Runde — und ist wieder Zeit? */ function rhythmus(personId) { const b = block(); const gesamt = (ENTWICKLUNG_PUNKTE[b.schluessel] || []).length; const letzte = db().prepare( `SELECT runde, COUNT(*) AS n FROM befinden_verlauf WHERE person_id = ? GROUP BY runde ORDER BY runde DESC LIMIT 1`).get(personId); if (!letzte) return { letzte: null, faellig: true, tage: null, vollstaendig: false }; const tage = Math.floor((Date.now() - Date.parse(letzte.runde)) / 86400000); return { letzte: letzte.runde, tage, vollstaendig: letzte.n >= gesamt, /* FAELLIG heisst: die letzte Runde ist alt genug -- ODER sie war nie fertig. Eine halb beantwortete Runde ist keine Runde. */ faellig: tage >= RHYTHMUS_TAGE || letzte.n < gesamt, }; } /* ===================================================================== DIE SEITE ===================================================================== */ befindenRouter.get("/workspace/api/befinden", angemeldet, (req, res) => { try { const b = block(); if (!b) return res.status(503).json({ fehler: "kein_block" }); const meine = new Map(db().prepare( `SELECT schluessel, stufe, anlass, am FROM entwicklung_stand WHERE person_id = ? AND von_id = ?`) .all(req.person.id, req.person.id).map((z) => [z.schluessel, z])); /* DER VERLAUF -- nach Runden gebuendelt, neueste zuerst. */ const runden = db().prepare( `SELECT runde FROM befinden_verlauf WHERE person_id = ? GROUP BY runde ORDER BY runde DESC LIMIT ?`).all(req.person.id, VERLAUF_MAX) .map((z) => z.runde); const zeilen = runden.length ? db().prepare( `SELECT runde, schluessel, stufe FROM befinden_verlauf WHERE person_id = ? AND runde IN (${runden.map(() => "?").join(",")})`) .all(req.person.id, ...runden) : []; const verlauf = runden.map((r) => ({ runde: r, antworten: Object.fromEntries( zeilen.filter((z) => z.runde === r).map((z) => [z.schluessel, z.stufe])), })); res.json({ block: b, stufen: ENTWICKLUNG_STUFEN, rhythmus_tage: RHYTHMUS_TAGE, rhythmus: rhythmus(req.person.id), punkte: (ENTWICKLUNG_PUNKTE[b.schluessel] || []).map((pt) => ({ ...pt, meins: meine.get(pt.schluessel) || null, })), verlauf, }); } catch (f) { console.error("[befinden] Lesen:", f?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); } }); /** Eine Runde abschließen: alle sechs Antworten wandern in den Verlauf. * * ALS RUNDE UND NICHT JE KLICK: Ein Verlauf, der jeden einzelnen Klick * festhält, zeigt kein Muster, sondern Rauschen — und er würde jedes * Zögern mitschreiben. Was zählt, ist der Stand, wenn jemand fertig * ist. */ befindenRouter.post("/workspace/api/befinden/runde", express.json({ limit: "8kb" }), angemeldet, (req, res) => { try { const b = block(); const punkte = ENTWICKLUNG_PUNKTE[b.schluessel] || []; const meine = new Map(db().prepare( `SELECT schluessel, stufe FROM entwicklung_stand WHERE person_id = ? AND von_id = ?`) .all(req.person.id, req.person.id).map((z) => [z.schluessel, z.stufe])); const fehlt = punkte.filter((p) => !meine.has(p.schluessel)); if (fehlt.length) { /* EINE HALBE RUNDE IST KEINE RUNDE. Sie wuerde im Verlauf wie ein vollstaendiger Stand aussehen und das Muster verfaelschen. */ return res.status(400).json({ fehler: "unvollstaendig", offen: fehlt.length, gesamt: punkte.length, }); } const runde = jetzt(); const rein = db().prepare( `INSERT INTO befinden_verlauf (person_id, runde, schluessel, stufe, am) VALUES (?,?,?,?,?)`); for (const p of punkte) rein.run(req.person.id, runde, p.schluessel, meine.get(p.schluessel), runde); /* IM PROTOKOLL STEHT NUR, DASS es passiert ist -- nicht was. Das Protokoll liest DogFather; die Antworten gehen ihn nichts an, und ein "detail" mit Stufen darin waere genau die Hintertuer, die es hier nicht geben darf. */ protokolliere("befinden_runde", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), detail: `${punkte.length} Fragen`, }); res.status(201).json({ runde, anzahl: punkte.length, rhythmus: rhythmus(req.person.id) }); } catch (f) { console.error("[befinden] Runde:", f?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); } });