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]>
303 lines
13 KiB
JavaScript
303 lines
13 KiB
JavaScript
/* =====================================================================
|
||
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" });
|
||
}
|
||
});
|