Bewerbungen der Modis bekommen eine eigene Spalte im Aufgabenbrett

Filipe: "ich will in dieser seite auch eine eigene kategorie fuer die
aufgaben wo die modis sich selbst bewerben. ich will dass man die
getrennt sieht. mach es richtig uebersichtlich."

WO SIE VORHER STANDEN, und warum das nicht reichte: nur auf dem
Vorlagenbrett, an der jeweiligen Karte. Das ist der richtige Ort zum
ENTSCHEIDEN -- man liest Aufgabe und Name nebeneinander. Es ist der
falsche Ort zum SEHEN: Wer morgens das Aufgabenbrett oeffnet, sieht
nicht, dass drei Leute auf eine Antwort warten, und das Vorlagenbrett
ist eine Seite weiter. Fuer den Modi gab es ueberhaupt keine Stelle,
an der stand "dein Wunsch ist angekommen".

DIE SPALTE STEHT GANZ VORN. Sie ist die einzige, in der jemand auf eine
ANTWORT wartet -- alle anderen zeigen Arbeit, die laeuft. Und sie
VERSCHWINDET, wenn nichts wartet: Eine leere Spalte "Bewerbungen"
stuende 360 Tage im Jahr im Weg, um an fuenf Tagen etwas zu sagen. Die
vier festen Spalten haben auch leer eine Aussage ("hier landet, was
fertig ist"); diese nicht.

WAS AUF DER KARTE STEHT: der TITEL der Vorlage (nicht ihr Schluessel),
wer sie uebernehmen moechte, und sein eigener Satz dazu -- als Zitat
gesetzt, mit Strich davor. Er gehoert ihm, nicht dem Brett. Ohne ihn
entscheidet man ueber einen Namen.

DER TITEL WIRD NACHGESCHLAGEN, NICHT MITGESPEICHERT. Er gehoert dem
Katalog; stuende er in der Bewerbungszeile, gaebe es zwei Wahrheiten,
und die aeltere gewinnt still, sobald jemand eine Vorlage umbenennt.

EIN EIGENER, KLEINER WEG statt eines Mitschleppens:
`/workspace/api/vorlagen/bewerbungen` liefert nur die Bewerbungen --
nicht den ganzen Katalog, der an `/workspace/api/vorlagen` haengt. Das
Brett zeichnet sich bei jedem Statuswechsel neu; der Katalog ist um ein
Vielfaches groesser als die Handvoll Bewerbungen.

WER ENTSCHEIDEN DARF, SAGT DER SERVER (`darf_entscheiden`) -- nicht der
Rollenname im Browser. `assets/js` bekommt jeder, der die Seite
oeffnet, und ein Rollenvergleich dort ist in diesem Haus allein diese
Woche dreimal veraltet. Bis die Antwort da ist, gilt `false`: lieber
einen Knopf zu spaet zeigen als einen, der eine Absage holt.

DIE FARBE IST NEU IM BRETT. Die vier vorhandenen stehen fuer einen
Arbeitsstand (grau, blau, gelb, gruen); hier wartet niemand auf Arbeit,
sondern auf eine Entscheidung. Kein Rot ("kaputt"), kein Gelb (heisst
schon "zur Freigabe") -- Violett, das im Chat seit gestern fuer "an
dich gerichtet" steht. Eine Sprache im Haus.

Geprueft: pruef-bewerbung-aufgaben 101 -> 111 Punkte. Darunter die drei
Gegenproben, ohne die "die Spalte ist da" nichts bewiese: ohne
Bewerbung gibt es sie NICHT, der Modi bekommt KEINE
Entscheidungsknoepfe (sondern "wartet auf Antwort"), und die Spalte
steht wirklich an erster Stelle. Dazu pruef-aufgabenbrett und
pruef-vorlagen (24), beide unveraendert gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-25 03:29:33 +02:00
co-authored by Claude Opus 5
parent 1a14eb7449
commit 8bf2f440d0
45 changed files with 1034 additions and 636 deletions
+115
View File
@@ -843,6 +843,121 @@ melde("=== 6. Am echten Bildschirm ===");
} finally { await m.ctx.close(); }
}
/* =================================================================
DIE SPALTE „BEWERBUNGEN" IM AUFGABENBRETT (25.09.2026)
=================================================================
Filipe: „ich will in dieser seite auch eine eigene kategorie fuer
die aufgaben wo die modis sich selbst bewerben. ich will dass man
die getrennt sieht. mach es richtig uebersichtlich."
BISHER STANDEN BEWERBUNGEN NUR AUF DEM VORLAGENBRETT, an der
jeweiligen Karte. Wer morgens das Aufgabenbrett oeffnete, sah
nicht, dass jemand auf eine Antwort wartet.
DREI DINGE WERDEN GEMESSEN, und das dritte ist das, was ohne
Pruefung irgendwann kippt:
1. Wer entscheidet, sieht die Spalte MIT den zwei Knoepfen.
2. Der Modi sieht seine eigene -- ohne Knoepfe, mit dem Satz
„wartet auf Antwort". Ein Knopf, der eine Absage holt,
waere schlimmer als keiner.
3. Ohne Bewerbung gibt es die Spalte GAR NICHT. Eine leere
Spalte stuende 360 Tage im Jahr im Weg.
================================================================= */
{
/* ERST DER ZUSTAND OHNE: Ohne diese Messung waere „die Spalte ist
da" auch dann gruen, wenn sie immer da waere. */
const vorher = await seiteAls("admin", "CODE-DOGI-0001", 1400);
let ohne = null;
try {
await vorher.s.goto(`${SEITE}/workspace/aufgaben.html`, { waitUntil: "networkidle" });
await vorher.s.waitForTimeout(1700);
ohne = await vorher.s.evaluate(() =>
document.querySelectorAll(".spalte--bewerbung").length);
} finally { await vorher.ctx.close(); }
ok(ohne === 0, `ohne Bewerbung gibt es die Spalte nicht (${ohne})`);
/* JETZT BEWIRBT SICH EIN MODI -- ueber die echte Schnittstelle
der Seite, damit Sitzung und Herkunftspruefung mitlaufen. */
const m = await seiteAls("modi", "CODE-MODI-0001", 1400);
let beworben = null, beiModi = null;
try {
await m.s.goto(`${SEITE}/workspace/entwicklung.html`, { waitUntil: "networkidle" });
await m.s.waitForTimeout(1500);
beworben = await m.s.evaluate(async () => {
const k = await fetch("/workspace/api/vorlagen", { credentials: "same-origin" });
const d = await k.json();
const v = (d.katalog?.aufgaben || [])[0];
if (!v) return { fehler: "kein Katalog" };
const a = await fetch("/workspace/api/vorlagen/bewerben", {
method: "POST", credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ vorlage: v.schluessel, text: "ich habe Freitag Zeit" }),
});
return { code: a.status, titel: v.titel };
});
/* UND DER MODI SIEHT SEINE EIGENE -- auf dem Aufgabenbrett. */
await m.s.goto(`${SEITE}/workspace/aufgaben.html`, { waitUntil: "networkidle" });
await m.s.waitForTimeout(1800);
beiModi = await m.s.evaluate(() => {
const sp = document.querySelector(".spalte--bewerbung");
if (!sp) return { spalte: false };
return {
spalte: true,
karten: sp.querySelectorAll(".karte--bewerbung").length,
knoepfe: sp.querySelectorAll(".karte--bewerbung button").length,
warte: !!sp.querySelector(".karte__warte"),
titel: sp.querySelector(".karte__titel")?.textContent || "",
wort: sp.querySelector(".karte__bwort")?.textContent || "",
};
});
} finally { await m.ctx.close(); }
ok(beworben?.code === 201,
`ein Modi bewirbt sich (HTTP ${beworben?.code} ${beworben?.fehler || ""})`);
ok(beiModi?.spalte === true && beiModi.karten === 1,
`er sieht sie im Aufgabenbrett in einer eigenen Spalte (${beiModi?.karten})`);
ok(beiModi?.titel === beworben?.titel,
`mit dem TITEL der Aufgabe, nicht dem Schluessel ("${beiModi?.titel}")`);
ok((beiModi?.wort || "").includes("Freitag"),
`und mit seinem eigenen Satz dazu ("${beiModi?.wort}")`);
ok(beiModi?.knoepfe === 0 && beiModi?.warte === true,
`Gegenprobe: er bekommt KEINE Entscheidungsknoepfe (${beiModi?.knoepfe}), `
+ `sondern „wartet auf Antwort" (${beiModi?.warte})`);
/* UND DER, DER ENTSCHEIDET. */
const e2 = await seiteAls("admin", "CODE-DOGI-0001", 1400);
let beiDogi = null;
try {
await e2.s.goto(`${SEITE}/workspace/aufgaben.html`, { waitUntil: "networkidle" });
await e2.s.waitForTimeout(1800);
beiDogi = await e2.s.evaluate(() => {
const sp = document.querySelector(".spalte--bewerbung");
if (!sp) return { spalte: false };
return {
spalte: true,
karten: sp.querySelectorAll(".karte--bewerbung").length,
knoepfe: [...sp.querySelectorAll(".karte--bewerbung button")]
.map((b) => b.textContent.trim()),
/* Die Spalte muss die ERSTE sein: Was auf eine Antwort
wartet, gehoert an die Stelle, die man zuerst ansieht. */
erste: document.querySelector("#brett .spalte")?.classList
.contains("spalte--bewerbung"),
wer: sp.querySelector(".karte__wer")?.textContent || "",
};
});
} finally { await e2.ctx.close(); }
ok(beiDogi?.spalte === true && beiDogi.karten === 1,
`DogFather sieht dieselbe Bewerbung (${beiDogi?.karten})`);
ok(beiDogi?.erste === true,
`und die Spalte steht ganz vorn (${beiDogi?.erste})`);
ok(JSON.stringify(beiDogi?.knoepfe) === JSON.stringify(["Annehmen", "Ablehnen"]),
`mit beiden Knoepfen (${JSON.stringify(beiDogi?.knoepfe)})`);
ok((beiDogi?.wer || "").includes("möchte das übernehmen"),
`und dem Namen dessen, der wartet ("${beiDogi?.wer}")`);
}
ok(meckern.length === 0, meckern.length
? `der Browser meldet: ${meckern.slice(0, 3).join(" | ")}`
: "kein Absturz und keine Fehlermeldung im Browser");
+57 -6
View File
@@ -1542,12 +1542,30 @@ function vorlagenBewerbungenFuer(person) {
LEFT JOIN personen p ON p.id = b.person_id
WHERE b.zustand = 'beworben' ${darf ? "" : "AND b.person_id = ?"}
ORDER BY b.id`).all(...(darf ? [] : [person.id]));
return zeilen.map((z) => ({
id: z.id, vorlage: z.vorlage, kategorie: z.kategorie,
person_id: z.person_id, person_name: z.person_name || "Jemand",
text: z.text || null, erstellt: z.erstellt,
selbst: z.person_id === person.id,
}));
return zeilen.map((z) => {
/* DER TITEL KOMMT MIT (25.09.2026).
Bis heute reichte der Schluessel: Die Bewerbung stand auf dem
Vorlagenbrett AN der Karte, und die Karte trug den Titel. Seit
sie auch im Aufgabenbrett steht (Filipes Wunsch, eigene
Kategorie), gibt es dort keine Karte daneben -- und ein
Schluessel wie "regeln_lesen" ist kein Satz, den jemand liest.
NACHGESCHLAGEN, NICHT MITGESPEICHERT: Der Titel gehoert dem
Katalog. Wuerde er in der Bewerbungszeile stehen, haette man
zwei Wahrheiten, und die aeltere gewinnt still, sobald jemand
eine Vorlage umbenennt. */
const v = vorlageZuSchluessel(z.vorlage);
return {
id: z.id, vorlage: z.vorlage, kategorie: z.kategorie,
titel: v?.titel || z.vorlage,
text_vorlage: v?.text || null,
tage: v?.tage ?? null,
person_id: z.person_id, person_name: z.person_name || "Jemand",
text: z.text || null, erstellt: z.erstellt,
selbst: z.person_id === person.id,
};
});
}
/* =====================================================================
@@ -1589,6 +1607,39 @@ function vorlageZuSchluessel(schluessel) {
return MODI_AUFGABEN_FLACH.find((v) => v.schluessel === schluessel) || null;
}
/* ==== NUR DIE BEWERBUNGEN, OHNE DEN GANZEN KATALOG (25.09.2026) ====
Filipe: „ich will in dieser seite auch eine eigene kategorie fuer
die aufgaben wo die modis sich selbst bewerben. ich will dass man
die getrennt sieht."
Das Aufgabenbrett braucht die Bewerbungen -- aber nicht den Katalog
mit allen Vorlagen, der an `/workspace/api/vorlagen` haengt. Ein
eigener, kleiner Weg statt eines Mitschleppens: Das Brett laedt bei
jedem Zeichnen, und der Katalog ist um ein Vielfaches groesser als
die Handvoll Bewerbungen.
WER WAS SIEHT, ENTSCHEIDET UNVERAENDERT `vorlagenBewerbungenFuer`:
Wer entscheidet, sieht alle offenen; alle anderen nur die eigenen.
Diese Route waehlt nichts selbst aus -- sonst gaebe es eine zweite
Stelle, an der die Sichtbarkeit falsch werden kann. */
vorlagenRouter.get("/workspace/api/vorlagen/bewerbungen", (req, res) => {
try {
/* WER ENTSCHEIDET, KOMMT MIT. Die Oberflaeche soll die zwei
Knoepfe nur dort zeigen, wo der Server sie auch annimmt -- und
sie darf es nicht am Rollennamen ausrechnen: `assets/js` bekommt
jeder, der die Seite oeffnet, und ein Rollenvergleich im Browser
ist in diesem Haus diese Woche dreimal veraltet. */
res.json({
bewerbungen: vorlagenBewerbungenFuer(req.person),
darf_entscheiden: entscheidetUeberAufgaben(req.person),
});
} catch (f) {
console.error("[vorlagen] Bewerbungen lesen:", f?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
vorlagenRouter.post("/workspace/api/vorlagen/bewerben", gleicheHerkunft,
express.json({ limit: "4kb" }), (req, res) => {
try {