diff --git a/server/pruef-bewerbung-aufgaben.mjs b/server/pruef-bewerbung-aufgaben.mjs
index fd7fc487..8278da92 100644
--- a/server/pruef-bewerbung-aufgaben.mjs
+++ b/server/pruef-bewerbung-aufgaben.mjs
@@ -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");
diff --git a/server/workspace-vorlagen.js b/server/workspace-vorlagen.js
index 42655be7..41cf40ae 100644
--- a/server/workspace-vorlagen.js
+++ b/server/workspace-vorlagen.js
@@ -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 {
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 60292bd2..9e64136a 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,15 +16,15 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+