Files
dogfather-universe/server/workspace-startcheck.js
DogFatherGitandClaude Opus 5 5487b94f73 Die fremde Sicht kam nur auf der halben Seite an
Gefunden beim Nachmessen von Filipes Meldung "die dogfather rolle sieht
die creator nicht mehr". Nichts war kaputt -- er war in einer fremden
Sicht. Dabei fiel aber etwas anderes auf:

DREI ENDPUNKTE LASEN `req.person`, WO IHRE NACHBARN `req.sicht ||
req.person` LESEN.

  /workspace/api/report/creator      (workspace-reports.js)
  /workspace/api/startcheck/creator  (workspace-startcheck.js)
  /workspace/api/uebersicht/creator  (workspace-aufgaben.js)

Folge: Die Zahlen einer Seite folgten der angesehenen Person, die
Creator-Auswahl daneben zeigte die EIGENEN. Zwei Antworten auf eine
Frage, und beide sahen fuer sich richtig aus. In reports.js stehen die
beiden Zeilen keine zwanzig auseinander.

Gemessen vorher/nachher in der Sicht von Miesmuschel:
  report/creator      6 Eintraege  ->  2 (Alle Creator + sie)
  startcheck/creator  5 Eintraege  ->  kein Umschalter, ihr eigener
  uebersicht/creator  alle         ->  nur sie

DER START-CHECK ZEIGT JETZT GAR KEINEN UMSCHALTER MEHR, und das ist
richtig: Bei `eigen: true` blendet die Seite ihn aus und zeigt den
Start-Check der angesehenen Person direkt -- genau das, was sie selbst
saehe. Mein erster Messwert las "0 Eintraege" und sah nach Fehler aus;
er hiess "kein Umschalter". Eine Zaehlung, die Verstecktes und Leeres
nicht unterscheidet, misst hier das Falsche.

WAS ABSICHTLICH NICHT MITGEAENDERT WURDE
/workspace/api/personen bleibt beim Angemeldeten. Diese Liste fuellt
die Auswahl "zu wem gehoert dieser Eintrag" -- also eine SCHREIB-
Auswahl. Die Regel steht seit dem 02.09. in workspace.js:

  "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."

Wuerde sie der Sicht folgen, koennte DogFather in einer fremden Sicht
nur noch Eintraege fuer diese eine Person anlegen -- ein stiller
Verlust von Handlungsfaehigkeit an einer Stelle, an der man ihn nicht
vermutet. Damit sind auch content.html und bereich.html zu Recht
unveraendert: Das sind Eingabeformulare, keine Ansichten.

NEUE PRUEFUNG (pruef-fremde-sicht, 12 Pruefungen)
Sie haelt beide Haelften der Regel fest -- Anzeigen folgt der Sicht,
Schreiben nicht -- und hat drei Gegenproben: dass dieselbe Abfrage mit
und ohne Sicht wirklich Verschiedenes liefert, dass eine Sicht auf
jemand anderen auch jemand anderen zeigt (sonst waere "Nova" nur
zufaellig der erste Eintrag), und dass eine erfundene Nummer keine
Sicht oeffnet. Drei Creator statt einem: Mit einem einzigen saehen
"alle" und "nur dieser" gleich aus.

Nebenbei geprueft: pruef-verwaltung-app, das im Sommer mit
MODULE_NOT_FOUND scheiterte, gibt es nicht mehr -- der Punkt ist
erledigt. Alle 132 Pruef- und Werkzeugdateien sind syntaktisch heil.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-11 13:24:17 +02:00

303 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
workspace-startcheck.js — Erstanalyse (Konzept, Seite 5, Block 03).
"Die Betreuung startet strukturiert: Ausgangslage verstehen, Ziele
festlegen und daraus konkrete Arbeitspakete bauen."
Der letzte Satz ist der entscheidende. Ein Check, der mit einer Note
endet, ist eine Beurteilung. Ein Check, der mit Aufgaben endet, ist
Betreuung. Deshalb laesst sich jeder Punkt mit Handlungsbedarf direkt
in eine Aufgabe verwandeln -- dieselbe Linie wie beim Review
("endet mit einer Entscheidung") und beim Call ("endet mit To-dos").
Die Punkte stehen fest im Code, nicht in der Datenbank. Eine
Erstanalyse, bei der jeder seine eigenen Punkte anlegt, ist keine
Erstanalyse mehr -- man koennte zwei Creator nicht mehr vergleichen,
und genau das ist ihr Zweck.
===================================================================== */
import express from "express";
import {
sichtbareCreatorIds, db, protokolliere, echteIp, sitzungLesen, darfCreator, betreuteIds } from "./workspace.js";
export const startcheckRouter = express.Router();
/* Die vier Felder aus dem Deck, mit je vier Punkten. Bewusst knapp
gehalten: Eine Liste, die man in einem Gespraech durchgehen kann. */
export const FELDER = [
{
schluessel: "profil",
name: "Profil & Auftritt",
punkte: [
{ schluessel: "bild", name: "Profilbild und Name", hinweis: "Erkennt man auf Anhieb, wer das ist?" },
{ schluessel: "bio", name: "Beschreibung / Bio", hinweis: "Steht dort, was einen erwartet?" },
{ schluessel: "kanaele", name: "Verlinkte Kanäle", hinweis: "Führen die Verweise irgendwohin – und stimmen sie noch?" },
{ schluessel: "wiedererkennung", name: "Wiedererkennbarkeit", hinweis: "Farben, Bilder, Sprache – passt das zusammen?" },
],
},
{
schluessel: "live",
name: "LIVE-Struktur",
punkte: [
{ schluessel: "zeiten", name: "Feste Sendezeiten", hinweis: "Weiß die Community, wann sie kommen kann?" },
{ schluessel: "ablauf", name: "Roter Faden im Stream", hinweis: "Gibt es einen erkennbaren Ablauf, oder passiert es einfach?" },
{ schluessel: "begruessung", name: "Umgang mit neuen Zuschauern", hinweis: "Werden Neue angesprochen oder gehen sie unter?" },
{ schluessel: "laenge", name: "Länge und Rhythmus", hinweis: "Passt die Dauer zur Energie – und wird sie durchgehalten?" },
],
},
{
schluessel: "content",
name: "Content-Muster",
punkte: [
{ schluessel: "formate", name: "Wiederkehrende Formate", hinweis: "Gibt es etwas, worauf man sich freuen kann?" },
{ schluessel: "rhythmus", name: "Veröffentlichungsrhythmus", hinweis: "Regelmäßig oder in Schüben?" },
{ schluessel: "einstiege", name: "Einstiege der ersten Sekunden", hinweis: "Bleibt man hängen oder scrollt man weiter?" },
{ schluessel: "verwertung", name: "LIVE wird zu Content", hinweis: "Werden gute Momente aus dem Stream weiterverwendet?" },
],
},
{
schluessel: "community",
name: "Community & Modis",
punkte: [
{ schluessel: "stamm", name: "Stammzuschauer", hinweis: "Gibt es Namen, die immer wieder auftauchen?" },
{ schluessel: "moderation", name: "Moderation besetzt", hinweis: "Ist jemand da, wenn es voll oder ungemütlich wird?" },
{ schluessel: "regeln", name: "Regeln sichtbar", hinweis: "Weiß man, was gilt, bevor etwas passiert?" },
{ schluessel: "stoerer", name: "Umgang mit Störern", hinweis: "Gibt es ein geübtes Vorgehen – oder wird improvisiert?" },
],
},
];
const BEWERTUNGEN = new Set(["ok", "mittel", "handlung"]);
const NOTIZ_MAX = 2000;
/* Nachschlagewerk, damit ein Aufruf nicht irgendeinen Punkt erfinden kann. */
const GUELTIG = new Map();
for (const f of FELDER) {
for (const p of f.punkte) GUELTIG.set(`${f.schluessel}:${p.schluessel}`, { feld: f, punkt: p });
}
const PUNKTE_GESAMT = GUELTIG.size;
const jetzt = () => new Date().toISOString();
function angemeldet(req, res, next) {
const person = sitzungLesen(req);
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
req.person = person;
next();
}
function gleicheHerkunft(req, res, next) {
const herkunft = req.get("origin");
if (!herkunft) return next();
let erlaubt;
try { erlaubt = new URL(herkunft).host === req.get("host"); } catch { erlaubt = false; }
if (!erlaubt) return res.status(403).json({ fehler: "fremde_herkunft" });
next();
}
startcheckRouter.use("/workspace/api/startcheck", angemeldet);
/* Lesen darf: das Management, der zustaendige Scout -- und der Creator
selbst. Der Check ist Teil SEINES Entwicklungsplans ("jeder Creator
bekommt einen eigenen, lebenden Entwicklungsplan"), nicht ein Urteil
hinter seinem Ruecken. Wer etwas festhalten will, das der Creator
nicht sehen soll, hat dafuer die interne Notiz im Profil.
Aendern duerfen ihn nur Management und Scout: Es ist eine
Fremdeinschaetzung, sonst waere es eine Selbsteinschaetzung. */
function darfLesen(person, creatorId) {
if (person.rolle === "creator") return person.id === creatorId;
return darfCreator(person, creatorId);
}
const darfAendern = (person, creatorId) =>
person.rolle !== "creator" && darfCreator(person, creatorId);
/* ---------- Auswahl der Creator ----------------------------------------- */
startcheckRouter.get("/workspace/api/startcheck/creator", (req, res) => {
try {
/* 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 });
}
/* Nicht die ROLLE, sondern die ZUTEILUNG entscheidet (03.09.2026).
Hier stand eine Abfrage ueber ALLE -- eine Managerin bekam damit
auch Namen und Daten von Creators, die ihr nie zugeteilt waren.
sichtbarePersonenIds/sichtbareCreatorIds liefern null fuer
DogFather (= keine Einschraenkung) und sonst genau die erlaubten
Nummern. */
let nur = "";
let werte = [];
const erlaubt = sichtbareCreatorIds(person);
if (erlaubt !== null) {
if (!erlaubt.length) return res.json({ creator: [], eigen: false });
nur = ` AND id IN (${erlaubt.map(() => "?").join(",")})`;
werte = erlaubt;
}
res.json({
creator: db().prepare(
`SELECT id, name FROM personen WHERE rolle = 'creator' AND aktiv = 1${nur}
ORDER BY name`).all(...werte),
eigen: false,
});
} catch (fehler) {
console.error("[workspace] Start-Check Creator:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---------- Lesen -------------------------------------------------------- */
startcheckRouter.get("/workspace/api/startcheck/:id", (req, res) => {
try {
const id = Number(req.params.id);
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
if (!darfLesen(req.person, id)) return res.status(404).json({ fehler: "nicht_gefunden" });
const person = db().prepare(
"SELECT id, name, rolle FROM personen WHERE id = ?").get(id);
if (!person || person.rolle !== "creator") {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
const zeilen = db().prepare(`
SELECT s.feld, s.punkt, s.bewertung, s.notiz, s.geaendert, p.name AS von
FROM startcheck s LEFT JOIN personen p ON p.id = s.geaendert_von
WHERE s.creator_id = ?`).all(id);
const karte = new Map(zeilen.map((z) => [`${z.feld}:${z.punkt}`, z]));
const felder = FELDER.map((f) => ({
schluessel: f.schluessel,
name: f.name,
punkte: f.punkte.map((p) => {
const z = karte.get(`${f.schluessel}:${p.schluessel}`);
return {
schluessel: p.schluessel, name: p.name, hinweis: p.hinweis,
bewertung: z?.bewertung ?? null, notiz: z?.notiz ?? null,
geaendert: z?.geaendert ?? null, von: z?.von ?? null,
};
}),
}));
const geprueft = zeilen.filter((z) => BEWERTUNGEN.has(z.bewertung)).length;
res.json({
creator: { id: person.id, name: person.name },
felder,
geprueft,
gesamt: PUNKTE_GESAMT,
handlungsbedarf: zeilen.filter((z) => z.bewertung === "handlung").length,
darf_aendern: darfAendern(req.person, id),
});
} catch (fehler) {
console.error("[workspace] Start-Check lesen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---------- Einen Punkt setzen ------------------------------------------- */
startcheckRouter.put("/workspace/api/startcheck/:id", gleicheHerkunft, (req, res) => {
try {
const id = Number(req.params.id);
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
/* 404 statt 403 auch hier: Wer nicht lesen darf, soll nicht ueber
die Fehlermeldung erfahren, dass es die Person gibt. */
if (!darfLesen(req.person, id)) return res.status(404).json({ fehler: "nicht_gefunden" });
if (!darfAendern(req.person, id)) {
return res.status(403).json({ fehler: "Den Start-Check füllt die Betreuung aus." });
}
const feld = String(req.body?.feld ?? "");
const punkt = String(req.body?.punkt ?? "");
if (!GUELTIG.has(`${feld}:${punkt}`)) {
return res.status(400).json({ fehler: "Diesen Punkt gibt es nicht." });
}
const b = req.body?.bewertung;
const bewertung = b === null || b === "" ? null : String(b);
if (bewertung !== null && !BEWERTUNGEN.has(bewertung)) {
return res.status(400).json({ fehler: "Unbekannte Bewertung." });
}
const notiz = String(req.body?.notiz ?? "").trim().slice(0, NOTIZ_MAX) || null;
/* Ein Punkt ohne Bewertung und ohne Notiz ist kein Eintrag, sondern
ein zurueckgenommener -- der verschwindet, statt als leere Zeile
stehen zu bleiben. */
if (bewertung === null && !notiz) {
db().prepare("DELETE FROM startcheck WHERE creator_id = ? AND feld = ? AND punkt = ?")
.run(id, feld, punkt);
} else {
db().prepare(`
INSERT INTO startcheck (creator_id, feld, punkt, bewertung, notiz, geaendert, geaendert_von)
VALUES (?,?,?,?,?,?,?)
ON CONFLICT(creator_id, feld, punkt) DO UPDATE SET
bewertung = excluded.bewertung, notiz = excluded.notiz,
geaendert = excluded.geaendert, geaendert_von = excluded.geaendert_von`).run(
id, feld, punkt, bewertung, notiz, jetzt(), req.person.id);
}
protokolliere("startcheck_gesetzt", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
detail: `Creator #${id} ${feld}/${punkt} = ${bewertung ?? "offen"}`.slice(0, 120),
});
res.json({ ok: true });
} catch (fehler) {
console.error("[workspace] Start-Check setzen:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---------- Aus einem Befund eine Aufgabe machen -------------------------
"... und daraus konkrete Arbeitspakete bauen." Ohne diesen Schritt
waere der Check eine Beurteilung statt Betreuung. */
startcheckRouter.post("/workspace/api/startcheck/:id/aufgabe", gleicheHerkunft, (req, res) => {
try {
const id = Number(req.params.id);
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
if (!darfLesen(req.person, id)) return res.status(404).json({ fehler: "nicht_gefunden" });
if (!darfAendern(req.person, id)) {
return res.status(403).json({ fehler: "nicht_erlaubt" });
}
const feld = String(req.body?.feld ?? "");
const punkt = String(req.body?.punkt ?? "");
const eintrag = GUELTIG.get(`${feld}:${punkt}`);
if (!eintrag) return res.status(400).json({ fehler: "Diesen Punkt gibt es nicht." });
const titel = String(req.body?.titel ?? "").trim().slice(0, 160);
if (titel.length < 3) return res.status(400).json({ fehler: "Was genau ist zu tun?" });
const frist = String(req.body?.frist ?? "").trim();
if (frist && (!/^\d{4}-\d{2}-\d{2}$/.test(frist) || Number.isNaN(Date.parse(frist)))) {
return res.status(400).json({ fehler: "Frist ist kein gültiges Datum." });
}
const gespeichert = db().prepare(
"SELECT bewertung FROM startcheck WHERE creator_id = ? AND feld = ? AND punkt = ?")
.get(id, feld, punkt);
const prio = gespeichert?.bewertung === "handlung" ? "hoch" : "mittel";
const { lastInsertRowid } = db().prepare(`
INSERT INTO aufgaben
(titel, beschreibung, status, prioritaet, creator_id, verantwortlich_id,
frist, erstellt, erstellt_von)
VALUES (?,?,'offen',?,?,?,?,?,?)`).run(
titel,
`Aus dem Start-Check: ${eintrag.feld.name} – ${eintrag.punkt.name}`,
prio, id, id, frist || null, jetzt(), req.person.id);
protokolliere("startcheck_aufgabe", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
detail: `#${lastInsertRowid} ${titel}`.slice(0, 120),
});
res.status(201).json({ id: Number(lastInsertRowid), prioritaet: prio });
} catch (fehler) {
console.error("[workspace] Start-Check Aufgabe:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});