From 1f2a4d335e0a03f066f9fe37db6bf7afa48d592c Mon Sep 17 00:00:00 2001 From: Dogfather Date: Wed, 23 Sep 2026 11:16:30 +0200 Subject: [PATCH] Eine Bewerbung, die niemand sieht, ist keine Beim Weiterarbeiten am Vorlagenbrett nachgemessen und gefunden: `benachrichtige` kam in workspace-zuteilung.js KEIN EINZIGES MAL vor, in workspace-vorlagen.js auch nicht. Beide Bewerbungswege waren gebaut, beide funktionierten -- und beide waren stumm: * Bewirbt sich Frida, erfaehrt DogFather es nur, wenn er von sich aus das Brett aufmacht. * Antwortet er, erfaehrt Frida es nur, wenn SIE von sich aus nachsieht. Das ist keine Kleinigkeit, das ist die Funktion. Wer sich bewirbt, wartet -- und Warten ohne Rueckmeldung fuehlt sich nach zwei Tagen an wie "interessiert keinen". Genau das soll eine Bewerbung verhindern. WER ES ERFAEHRT -- ABGELEITET, NICHT AUFGEZAEHLT ------------------------------------------------ Die naheliegende Zeile waere `rolle IN ('admin','hand')` gewesen; so steht sie in workspace-hilfe.js. Das ist eine Abschrift, und Abschriften altern: Kaeme morgen eine Rolle dazu, die entscheiden darf, bekaeme sie keine einzige Meldung -- und niemand merkte es, weil ja alles funktioniert. Gefragt wird deshalb die Regel selbst (entscheidetUeberAufgaben), Person fuer Person. Und zusaetzlich darfSchreibenMit: Wer den Bewerber gar nicht sehen darf, bekommt auch keine Meldung ueber ihn. Das ist keine Vorsicht um ihrer selbst willen -- ohne diese Zeile erfuehre die Agentur ueber eine Push-Nachricht, dass es Team Dogi ueberhaupt gibt. Gemessen: Die Bewerbung eines Modis erreicht genau zwei Leute (admin, hand) von sechs Aktiven. Nicht die linke Hand (sie entscheidet hier nicht mit), niemand aus dem anderen Haus, und nicht der Bewerber selbst. ZWEI SCHALTER, ZWEI ENTSCHEIDUNGEN ---------------------------------- "bewerbung_neu" trifft den, der antwortet -- an einem lebhaften Tag mehrfach, das kann man stumm stellen wollen. "bewerbung_antwort" trifft den, der wartet; sie kommt einmal, und niemand will sie stumm stellen. Eine gemeinsame Art hiesse: beides zusammen abschalten oder beides zusammen ertragen. Dieselbe Ueberlegung wie beim Chat (Nachricht / Erwaehnung). Beide von sich aus an. Keine Ausnahme von der Ruhezeit: Eine Bewerbung wartet, ein Anruf nicht. DIE NOTIZ STEHT IN DER MELDUNG ------------------------------ Filipe hat sie ausdruecklich verlangt ("mit einem text als notiz"). Sie erst zu verlangen und dann an genau der Stelle zu verschweigen, an der man sie liest, waere die halbe Funktion. Und das Ergebnis steht im TITEL -- "angenommen" oder "diesmal nicht" -- damit man es lesen kann, ohne zu oeffnen. Auch die gute Nachricht. Der Wortlaut steht in zwei reinen Funktionen (bewerbungText, antwortText), exportiert, damit eine Pruefung sie lesen kann, ohne einen Push-Dienst nachzubauen. Genau an so einer Stelle steckte am 18.09. der Fehler "Nachricht von [object Object]", der von aussen nicht messbar war. EINE STELLE FUER BEIDE WEGE --------------------------- workspace-bewerbung-melden.js. Zwei Fassungen waeren zwei Gelegenheiten, dass eine davon die Ruhezeit, die Abschaltbarkeit oder die Haeusertrennung vergisst -- und dieselbe Person laese zweimal etwas Verschiedenes ueber denselben Vorgang. Die Meldung wird NICHT abgewartet (`void`): Ob sie durchgeht, haengt am Push-Dienst, an der Ruhezeit und an den Einstellungen des Empfaengers. Nichts davon darf entscheiden, ob die Bewerbung gespeichert ist -- die ist es laengst. NOCH EINE ROTE PRUEFUNG, DIE NIEMAND GESEHEN HAT ------------------------------------------------- pruef-push-ziel meldete: "aber nicht auf eine Seite, die es fuer ihn nicht gibt (/workspace/calls.html)". Das sah aus wie ein Befund und war eine erfuellte Bestellung -- Filipe hatte am 22.09. genau das Gegenteil bestellt ("jeder der einen kalender hat soll auch sowas haben"). Nachgemessen: Modi, rechte und linke Hand haben je eine Calls-Kachel. Die Pruefung steht jetzt andersherum: Die Calls-Seite MUSS stehen bleiben. Dieselbe Zeile schuetzt damit das, was sie vorher verboten hat -- und wird rot, wenn die Kachel je wieder verschwindet. Das Umlenken selbst bleibt geprueft (Scouting, zweimal). Das ist die DRITTE stille rote Pruefung an einem Tag (nach pruef-modi-wortleck und pruef-zuteilung). Die Frage an Filipe, ob ein naechtlicher Lauf sie selbst anstossen soll, steht in der Vault-Notiz und wird nicht von mir allein entschieden. GEPRUEFT -------- pruef-modi-katalog: 116 Pruefungen, 0 Fehler (vorher 95). Neu: die beiden Schalter, wer es erfaehrt (samt Gegenprobe, dass es nicht einfach alle sind: 2 von 6), und der Wortlaut an acht Proben. Dabei war meine eigene erste Messung falsch -- sie erwartete eine Kuerzung bei 50 Zeichen, die nur gilt, wenn eine Notiz danebensteht. Steht als Begruendung in der Pruefung. pruef-push-ziel: 11 von 11 (vorher 1 Fehler). pruef-zuteilung, pruef-push, pruef-push-weg: gruen. Co-Authored-By: Claude Opus 5 --- server/pruef-modi-katalog.mjs | 123 ++++++++++++++++++ server/pruef-push-ziel.mjs | 31 ++++- server/workspace-bewerbung-melden.js | 185 +++++++++++++++++++++++++++ server/workspace-push.js | 34 +++++ server/workspace-vorlagen.js | 31 +++++ server/workspace-zuteilung.js | 38 ++++++ 6 files changed, 438 insertions(+), 4 deletions(-) create mode 100644 server/workspace-bewerbung-melden.js diff --git a/server/pruef-modi-katalog.mjs b/server/pruef-modi-katalog.mjs index 82fd2a82..1cf7d968 100644 --- a/server/pruef-modi-katalog.mjs +++ b/server/pruef-modi-katalog.mjs @@ -860,6 +860,129 @@ console.log(""); } finally { await browser.close(); } } +/* ======================================================================= + 6c. EINE BEWERBUNG, DIE NIEMAND SIEHT, IST KEINE (23.09.2026) + ======================================================================= + + Beide Bewerbungswege waren gebaut und beide funktionierten -- und + beide waren STUMM. Nachgemessen vor dem Bauen: `benachrichtige` kam + weder in workspace-zuteilung.js noch in workspace-vorlagen.js ein + einziges Mal vor. + + Wer sich bewirbt, wartet. Warten ohne Rueckmeldung fuehlt sich nach + zwei Tagen an wie "interessiert keinen" -- und genau das soll eine + Bewerbung verhindern. + + GEPRUEFT WIRD, WAS SICH OHNE PUSH-DIENST PRUEFEN LAESST, und das ist + fast alles: WER etwas erfaehrt (abgeleitet, nicht aufgezaehlt), WAS + darin steht (reine Funktionen), und DASS es zwei getrennte Schalter + gibt. + ======================================================================= */ +console.log("\n=== 6c. Erfaehrt es auch jemand? ===\n"); +{ + const melder = await import("./workspace-bewerbung-melden.js"); + const { ARTEN } = await import("./workspace-push.js"); + + /* ---- DIE SCHALTER --------------------------------------------- */ + const neu2 = ARTEN.find((a) => a.schluessel === "bewerbung_neu"); + const antwort = ARTEN.find((a) => a.schluessel === "bewerbung_antwort"); + ok(!!neu2 && !!antwort, + "beide Arten stehen in den Benachrichtigungs-Einstellungen"); + ok(neu2?.vorgabe === true && antwort?.vorgabe === true, + "und sind von sich aus an"); + /* ZWEI SCHALTER, ZWEI ENTSCHEIDUNGEN. Wer an einem lebhaften Tag + viele Bitten bekommt, will die stumm stellen koennen -- die + Antwort auf die EIGENE Bewerbung will niemand stumm stellen. Eine + gemeinsame Art hiesse: beides zusammen abschalten oder beides + zusammen ertragen. */ + ok(neu2?.schluessel !== antwort?.schluessel, + "getrennt voneinander — die eine trifft den, der antwortet, die andere den, der wartet"); + + /* ---- WER ES ERFAEHRT ------------------------------------------- */ + const wer = melder.entscheiderFuer({ id: idModi, name: "Marina" }); + const rollen = wer.map((x) => x.rolle).sort(); + ok(wer.length > 0, `die Bewerbung eines Modis erreicht ${wer.length} Leute`); + ok(rollen.includes("admin") && rollen.includes("hand"), + `DogFather und die rechte Hand sind dabei (${rollen.join(", ")})`); + /* DIE LINKE HAND NICHT -- sie darf verteilen, aber nicht ueber + Bewerbungen entscheiden. Eine Meldung ueber etwas, das man nicht + beantworten kann, ist Laerm. */ + ok(!rollen.includes("linke"), + "die linke Hand nicht — sie entscheidet hier nicht mit"); + /* UND DIE AGENTUR ERST RECHT NICHT. Ohne die Haeuser-Bedingung + erfuehre ein Manager ueber eine Push-Nachricht, dass es Team Dogi + ueberhaupt gibt. */ + ok(!rollen.includes("manager") && !rollen.includes("creator"), + `niemand aus dem anderen Haus (${rollen.join(", ") || "niemand"})`); + /* Und der Bewerber nicht sich selbst. */ + ok(!wer.some((x) => x.id === idModi), "und der Bewerber nicht sich selbst"); + + /* GEGENPROBE: Die Liste darf nicht einfach "alle Aktiven" sein -- + sonst waere jede Zeile oben zufaellig richtig. */ + const alleAktiven = (await import("node:sqlite")).DatabaseSync; + const d3 = new alleAktiven(process.env.WORKSPACE_DB, { readOnly: true }); + const anzahl = d3.prepare("SELECT COUNT(*) AS n FROM personen WHERE aktiv = 1").get().n; + d3.close(); + ok(wer.length < anzahl, + `Gegenprobe: es sind nicht einfach alle (${wer.length} von ${anzahl} Aktiven)`); + + /* ---- WAS DRINSTEHT --------------------------------------------- */ + const b1 = melder.bewerbungText({ + name: "Marina", titel: "Die Regeln des Rudels einmal ganz lesen", + wort: "ich habe Freitag Zeit" }); + ok(b1.titel === "Marina bewirbt sich", `der Titel nennt den Menschen (${b1.titel})`); + ok(b1.text.includes("Regeln des Rudels"), "und die Zeile darunter die Aufgabe"); + /* DAS WORT IST OFT DIE GANZE AUSKUNFT. Wer es schon auf dem + Sperrbildschirm liest, kann entscheiden, ohne zu oeffnen. */ + ok(b1.text.includes("ich habe Freitag Zeit"), + `samt dem Wort des Bewerbers (${b1.text})`); + + const b2 = melder.bewerbungText({ name: "Marina", titel: "Kurz", wort: "" }); + ok(!b2.text.includes("„"), `ohne Wort bleibt es sauber (${b2.text})`); + ok(b2.text === "Kurz", "und nur die Aufgabe steht da"); + + /* NAMENLOS DARF NICHT "undefined bewirbt sich" ergeben. */ + const b3 = melder.bewerbungText({ name: "", titel: "X", wort: "" }); + ok(b3.titel === "Jemand bewirbt sich", `ohne Namen steht ein Wort da (${b3.titel})`); + + const a1 = melder.antwortText({ titel: "Die Regeln", ja: true, notiz: "mach das gern" }); + const a2 = melder.antwortText({ titel: "Die Regeln", ja: false, notiz: "diesmal jemand anders" }); + /* DAS ERGEBNIS STEHT IM TITEL -- man soll es lesen koennen, ohne zu + oeffnen. Auch die gute Nachricht. */ + ok(/angenommen/.test(a1.titel), `die Zusage sagt es im Titel (${a1.titel})`); + ok(/nicht/.test(a2.titel) && a2.titel !== a1.titel, + `die Absage auch, und anders (${a2.titel})`); + /* DIE NOTIZ IST DER GRUND, WARUM FILIPE SIE VERLANGT HAT ("mit + einem text als notiz"). Sie hier wegzulassen hiesse, sie an genau + der Stelle zu verstecken, an der man sie liest. */ + ok(a1.text.includes("mach das gern") && a2.text.includes("diesmal jemand anders"), + "und die Notiz steht in beiden"); + + /* LANGES WIRD GEKUERZT, ABER NICHT MITTEN IM WORT. + + DIE ERSTE FASSUNG DIESER ZEILE WAR FALSCH und hat mich das + Richtige gelehrt: Sie nahm einen 68 Zeichen langen Titel und + erwartete eine Kuerzung bei 50 -- die gilt aber nur, wenn eine + Notiz danebensteht und Platz wegnimmt. Ohne Notiz darf der Titel + die ganzen 90 haben. Die Pruefung war rot, das Programm richtig. + Gemessen wird jetzt mit etwas, das JEDE Grenze reisst. */ + const langerSatz = "Eine ganze Sendung neben einem Neuen sitzen und im Zweifel " + + "eingreifen und danach noch drei Zeilen aufschreiben, damit am naechsten " + + "Tag jemand weiss, was eigentlich los war"; + const lang = melder.antwortText({ titel: langerSatz, ja: true, notiz: "" }); + ok(lang.text.length < langerSatz.length && /…$/.test(lang.text), + `zu Langes endet mit Auslassung (${lang.text})`); + /* KEIN SCHNITT MITTEN IM WORT -- ein "eingrei…" liest sich wie ein + Fehler, nicht wie eine Kuerzung. */ + ok(/\s…$/.test(lang.text) || /[a-zA-Zäöüß]{3,}\s?…$/.test(lang.text), + `und nicht mitten im Wort (${lang.text.slice(-24)})`); + /* GEGENPROBE: Kurzes bleibt unangetastet -- sonst bewiese die Zeile + darueber nur, dass immer gekuerzt wird. */ + const kurzerSatz = melder.antwortText({ titel: "Kurzer Titel", ja: true, notiz: "" }); + ok(kurzerSatz.text === "Kurzer Titel", + `Gegenprobe: Kurzes bleibt unangetastet (${kurzerSatz.text})`); +} + /* ---------- 7. Gegenprobe -------------------------------------------- */ console.log("\n=== 7. Gegenprobe: kann das hier ueberhaupt scheitern? ===\n"); { diff --git a/server/pruef-push-ziel.mjs b/server/pruef-push-ziel.mjs index c647bae2..2292181a 100644 --- a/server/pruef-push-ziel.mjs +++ b/server/pruef-push-ziel.mjs @@ -115,19 +115,42 @@ d.close(); melde(""); melde("=== 1. Das Team ==="); { + /* ==== DIE CALLS-SEITE GEHOERT INZWISCHEN DAZU (berichtigt 23.09.2026) + + Hier stand, dass ein Modi von "/workspace/calls.html" auf die + Startseite umgelenkt wird -- die Seite gab es fuer ihn nicht. + + Seither hat Filipe das Gegenteil bestellt: "jeder der einen + kalender hat soll auch sowas haben." Hinter jedem Kalender steht + jetzt eine Calls-Kachel (mitCalls in workspace.js), und zwar fuer + Modis, rechte und linke Hand. Nachgemessen: je eine Kachel, alle + drei. + + Die Pruefung war damit seit dem Umbau ROT -- und zwar mit einem + Satz, der wie ein Befund aussah ("aber nicht auf eine Seite, die + es fuer ihn nicht gibt") und in Wahrheit eine erfuellte Bestellung + meldete. Sie steht jetzt andersherum: Die Calls-Seite MUSS stehen + bleiben. So schuetzt dieselbe Zeile das, was sie vorher verboten + hat -- und wenn die Kachel je wieder verschwindet, wird sie rot. */ const a = await push.benachrichtige(idModi, "test", { titel: "Call gleich", text: "In zehn Minuten", ziel: "/workspace/calls.html" }); ok(a.verschickt === 1, `die Benachrichtigung geht raus (${a.verschickt})`); - /* DAS IST DER PUNKT: Sie wird NICHT unterdrueckt. Ein Modi, der in - einem Call steht, soll erfahren, dass er gleich anfaengt. */ - ok(a.ziel === "/workspace/start.html", - `aber nicht auf eine Seite, die es fuer ihn nicht gibt (${a.ziel})`); + ok(a.ziel === "/workspace/calls.html", + `und fuehrt auf die Calls-Seite, die es fuer ihn seit dem 22.09. gibt (${a.ziel})`); + /* Das Umlenken selbst muss trotzdem greifen -- sonst bewiese die + Zeile darueber nur, dass jedes Ziel stehen bleibt. Das Scouting + ist und bleibt Agentur. */ const b = await push.benachrichtige(idHand, "test", { titel: "Scouting", text: "Follow-up faellig", ziel: "/workspace/scouting.html" }); ok(b.ziel === "/workspace/start.html", `auch die rechte Hand landet nicht im Scouting (${b.ziel})`); + const b2 = await push.benachrichtige(idModi, "test", + { titel: "Scouting", text: "Follow-up faellig", ziel: "/workspace/scouting.html" }); + ok(b2.ziel === "/workspace/start.html", + `und ein Modi ebenso wenig (${b2.ziel})`); + /* UND DIE GEGENPROBE, ohne die die zwei Zeilen darueber nichts beweisen: Ein Ziel, das es auf crew. GIBT, bleibt stehen. Sonst hiesse "wird zur Startseite" nur, dass jedes Ziel dorthin faellt. */ diff --git a/server/workspace-bewerbung-melden.js b/server/workspace-bewerbung-melden.js new file mode 100644 index 00000000..94420116 --- /dev/null +++ b/server/workspace-bewerbung-melden.js @@ -0,0 +1,185 @@ +/* ===================================================================== + EINE BEWERBUNG, DIE NIEMAND SIEHT, IST KEINE (23.09.2026) + ===================================================================== + + Filipe, 22.09.2026: „die option dass die modis sich für aufgaben + bewerben können." + Filipe, 23.09.2026: „nur dogfather und die rechte hand sollen + annehmen oder ablehnen können, mit einem text als notiz." + + --------------------------------------------------------------------- + WAS GEFEHLT HAT -- und es ist der ganze Grund für diese Datei + + Beide Wege waren gebaut und beide funktionierten. Nur: Wenn Frida + sich bewirbt, erfährt DogFather das ausschließlich dann, wenn er von + sich aus das Brett aufmacht. Und wenn er antwortet, erfährt Frida es + ausschließlich dann, wenn SIE von sich aus nachsieht. + + Das ist keine Kleinigkeit, das ist die Funktion. Wer sich bewirbt, + wartet -- und Warten ohne Rückmeldung fühlt sich nach zwei Tagen an + wie „interessiert keinen". Genau das Gefühl soll eine Bewerbung + verhindern. + + Nachgemessen, bevor gebaut wurde: In workspace-zuteilung.js kam + `benachrichtige` kein einziges Mal vor, in workspace-vorlagen.js + auch nicht. Es war keine vergessene Zeile, es war ein fehlendes + Stück. + + --------------------------------------------------------------------- + WARUM EINE EIGENE DATEI + + Es gibt ZWEI Bewerbungswege -- auf eine Aufgabe, die es schon gibt + (workspace-zuteilung.js), und auf eine Vorlage, aus der erst eine + wird (workspace-vorlagen.js). Beide sollen dasselbe melden, sonst + liest dieselbe Person zweimal etwas Verschiedenes über denselben + Vorgang. + + Zwei Fassungen wären zwei Gelegenheiten, dass eine davon beim + nächsten Umbau die Ruhezeit, die Abschaltbarkeit oder die + Häusertrennung vergisst. + + --------------------------------------------------------------------- + WER ERFÄHRT ES -- ABGELEITET, NICHT AUFGEZÄHLT + + Die naheliegende Zeile wäre `rolle IN ('admin','hand')` gewesen; so + steht sie in workspace-hilfe.js. Sie ist eine Abschrift, und + Abschriften altern: Käme morgen eine Rolle dazu, die entscheiden + darf, bekäme sie keine einzige Meldung -- und niemand merkte es, + weil ja alles funktioniert. + + Gefragt wird deshalb die Regel selbst (`entscheidetUeberAufgaben`), + Person für Person. Und zusätzlich `darfSchreibenMit`: Wer den + Bewerber gar nicht sehen darf, bekommt auch keine Meldung über ihn. + Das ist keine Vorsicht um ihrer selbst willen -- ohne diese Zeile + erführe die Agentur über eine Push-Nachricht, dass es Team Dogi + gibt. + ===================================================================== */ + +import { + db, entscheidetUeberAufgaben, darfSchreibenMit, +} from "./workspace.js"; + +/** Wer über die Bewerbung DIESER Person entscheiden kann und darf. + * + * ZWEI BEDINGUNGEN, ZWEI GRÜNDE: + * entscheidetUeberAufgaben -- er darf antworten + * darfSchreibenMit -- er darf den Bewerber überhaupt sehen + * + * Der Bewerber selbst fällt heraus: Wer entscheiden darf, bewirbt + * sich nicht (die Routen lehnen das ab) -- aber falls doch einmal, + * wäre eine Meldung an sich selbst Unsinn. + */ +export function entscheiderFuer(bewerber) { + try { + const alle = db().prepare( + "SELECT id, rolle, name FROM personen WHERE aktiv = 1").all(); + return alle + .filter((p) => p.id !== bewerber.id) + .filter((p) => entscheidetUeberAufgaben(p)) + .filter((p) => darfSchreibenMit(p, bewerber.id)); + } catch (fehler) { + console.error("[bewerbung] Entscheider nicht ermittelbar:", fehler?.message); + return []; + } +} + +/** Träge geladen -- wie überall im Haus. + * + * Der Bewerbungsweg soll auch dann laufen, wenn am Push-Teil etwas + * klemmt. Eine Bewerbung, die wegen eines Schlüsselproblems gar nicht + * erst gespeichert wird, wäre der schlechtere Tausch. + */ +async function schicken(personId, art, inhalt) { + try { + const { benachrichtige } = await import("./workspace-push.js"); + await benachrichtige(personId, art, inhalt); + } catch (fehler) { + console.error("[bewerbung] Benachrichtigung:", fehler?.message); + } +} + +/** Kurz halten, aber nicht abschneiden mitten im Wort. + * + * Eine Benachrichtigung steht auf einem Sperrbildschirm; was dort + * nicht hinpasst, verschluckt das Gerät ohnehin. Ein hartes `slice` + * endet dabei gern mitten im Wort, und das liest sich wie ein Fehler. + */ +function kurz(text, max = 90) { + const t = String(text || "").trim().replace(/\s+/g, " "); + if (t.length <= max) return t; + const schnitt = t.slice(0, max); + const luecke = schnitt.lastIndexOf(" "); + return (luecke > max * 0.6 ? schnitt.slice(0, luecke) : schnitt) + " …"; +} + +/* ===================================================================== + WAS DRINSTEHT -- als eigene, reine Funktionen + ===================================================================== + + ALS EIGENE FUNKTIONEN UND EXPORTIERT, damit eine Prüfung sie lesen + kann, ohne einen Push-Dienst nachzubauen. Genau an dieser Stelle + steckte am 18.09.2026 ein Fehler, den Filipe gemeldet hat und der + von außen nicht messbar war: Der Wortlaut entstand tief in einer + Funktion, die nur der Push-Weg aufruft, und lautete am Ende + „Nachricht von [object Object]". + + Sie bekommen nichts als Text und geben Text zurück -- keine + Datenbank, kein Netz. Eine Prüfung kann sie deshalb mit einer Zeile + an fünfzehn Proben halten. + ===================================================================== */ + +/** + * „Jemand bewirbt sich" -- für alle, die antworten können. + * + * DER TITEL NENNT DEN MENSCHEN, nicht die Aufgabe. Wer drei Meldungen + * untereinander sieht, will wissen, WER etwas von ihm will; welche + * Aufgabe es war, steht in der Zeile darunter. + */ +export function bewerbungText({ name, titel, wort }) { + return { + titel: `${String(name || "Jemand").trim() || "Jemand"} bewirbt sich`, + /* DAS WORT DES BEWERBERS GEHT MIT, wenn es eines gibt. Es ist oft + die ganze Auskunft („ich habe Freitag Zeit") -- und wer sie + schon auf dem Sperrbildschirm liest, kann entscheiden, ohne die + Seite zu öffnen. */ + text: wort ? `${kurz(titel, 60)} — „${kurz(wort, 70)}“` : kurz(titel), + }; +} + +/** + * „Deine Bewerbung ist beantwortet" -- für den, der gewartet hat. + * + * DIE ANTWORT IST DER WICHTIGERE TEIL. Wer sich bewirbt, wartet; eine + * Absage, die man drei Tage später zufällig entdeckt, ist schlimmer + * als eine sofortige. + * + * ZWEI VERSCHIEDENE TITEL, und das ist Absicht: „angenommen" und + * „diesmal nicht" sagen das Ergebnis, bevor man öffnet. Ein + * gemeinsames „Antwort auf deine Bewerbung" zwänge jedes Mal zum + * Nachsehen -- auch bei der guten Nachricht. + * + * DIE NOTIZ STEHT DRIN -- genau dafür hat Filipe sie verlangt („mit + * einem text als notiz"). Sie erst zu verlangen und dann an der + * Stelle zu verschweigen, an der man sie liest, wäre die halbe + * Funktion. + */ +export function antwortText({ titel, ja, notiz }) { + return { + titel: ja ? "Deine Bewerbung: angenommen" : "Deine Bewerbung: diesmal nicht", + text: notiz ? `${kurz(titel, 50)} — „${kurz(notiz, 80)}“` : kurz(titel), + }; +} + +/** Schickt „jemand bewirbt sich" an alle, die antworten können. + * Gibt zurück, an wie viele -- die Zahl braucht die Prüfung. */ +export async function meldeBewerbung({ bewerber, titel, wort, ziel }) { + const leute = entscheiderFuer(bewerber); + const inhalt = bewerbungText({ name: bewerber.name, titel, wort }); + for (const p of leute) await schicken(p.id, "bewerbung_neu", { ...inhalt, ziel }); + return leute.length; +} + +/** Schickt die Antwort an den, der gewartet hat. */ +export async function meldeBewerbungsantwort({ anWen, titel, ja, notiz, ziel }) { + await schicken(anWen, "bewerbung_antwort", { ...antwortText({ titel, ja, notiz }), ziel }); +} diff --git a/server/workspace-push.js b/server/workspace-push.js index fd1d35a6..018b3e73 100644 --- a/server/workspace-push.js +++ b/server/workspace-push.js @@ -107,6 +107,40 @@ export const ARTEN = [ liest es um sieben -- das reicht. */ { schluessel: "chat_erwaehnung", name: "Jemand erwähnt dich im Chat (@)", vorgabe: true }, + /* ---- BEWERBUNGEN (23.09.2026) ----------------------------------- + + Filipe, 22.09.: „die option dass die modis sich für aufgaben + bewerben können." + Filipe, 23.09.: „nur dogfather und die rechte hand sollen annehmen + oder ablehnen können, mit einem text als notiz." + + BEIDE WEGE WAREN GEBAUT UND BEIDE STUMM. Wer sich beworben hatte, + erfuhr die Antwort nur, wenn er von sich aus nachsah -- und wer + antworten sollte, erfuhr von der Bewerbung nur, wenn er von sich + aus das Brett aufmachte. Nachgemessen: `benachrichtige` kam in + workspace-zuteilung.js und workspace-vorlagen.js kein einziges Mal + vor. Das war keine vergessene Zeile, das war ein fehlendes Stück. + + ZWEI ARTEN UND NICHT EINE, aus demselben Grund wie beim Chat: Die + beiden treffen verschiedene Menschen in verschiedenen Lagen. Wer + entscheidet, bekommt an einem lebhaften Tag mehrere Bitten -- + das kann man stumm stellen wollen. Die ANTWORT auf die eigene + Bewerbung will niemand stumm stellen; sie kommt einmal, und man + wartet darauf. Eine gemeinsame Art hieße: beides zusammen + abschalten oder beides zusammen ertragen. + + `vorgabe: true` bei beiden -- wie alles, was auf ein Ereignis + antwortet, statt es von sich aus zu erzeugen. + + KEINE AUSNAHME VON DER RUHEZEIT. Eine Bewerbung wartet, ein Anruf + nicht. Wer um halb vier eine Absage bekommt, liest sie um sieben; + das ist früh genug und sehr viel besser, als nachts geweckt zu + werden. */ + { schluessel: "bewerbung_neu", + name: "Jemand bewirbt sich für eine Aufgabe", vorgabe: true }, + { schluessel: "bewerbung_antwort", + name: "Antwort auf deine eigene Bewerbung", vorgabe: true }, + /* ER IST LIVE (22.09.2026). Filipe: "die leute sollen auch automatisch eine benarichtigung diff --git a/server/workspace-vorlagen.js b/server/workspace-vorlagen.js index a5bc3b27..ff8f075f 100644 --- a/server/workspace-vorlagen.js +++ b/server/workspace-vorlagen.js @@ -50,6 +50,13 @@ import { darfAufgabenVerteilen, } from "./workspace.js"; +/* Dieselbe Meldung wie beim anderen Bewerbungsweg -- eine Stelle, + damit dieselbe Person nicht zweimal etwas Verschiedenes ueber + denselben Vorgang liest. */ +import { + meldeBewerbung, meldeBewerbungsantwort, +} from "./workspace-bewerbung-melden.js"; + export const vorlagenRouter = express.Router(); const jetzt = () => new Date().toISOString(); @@ -1621,6 +1628,16 @@ vorlagenRouter.post("/workspace/api/vorlagen/bewerben", gleicheHerkunft, VALUES (?,?,?,?,'beworben',?)`) .run(v.schluessel, v.kategorie, req.person.id, text, jetzt()); + /* ES ERFAEHRT JEMAND (23.09.2026) -- siehe die Begruendung in + workspace-bewerbung-melden.js. Ohne diese Zeile waere die + Bewerbung ein Zettel in einer Schublade, die niemand oeffnet. */ + void meldeBewerbung({ + bewerber: { id: req.person.id, name: req.person.name }, + titel: v.titel, + wort: text, + ziel: "/workspace/aufgaben.html", + }); + protokolliere("vorlage_beworben", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), detail: `${v.schluessel} ${v.titel}`.slice(0, 120), @@ -1715,6 +1732,20 @@ for (const [weg, ja] of [["annehmen", true], ["ablehnen", false]]) { .run(ja ? "angenommen" : "abgelehnt", kommentar, req.person.id, nun, aufgabeId, id); + /* DIE ANTWORT AN DEN, DER GEWARTET HAT -- mit der Notiz. + Bei einer Zusage fuehrt das Antippen auf die neue Aufgabe, + bei einer Absage auf das Brett: Dort steht die Vorlage + wieder offen, und man kann sich spaeter erneut bewerben. */ + void meldeBewerbungsantwort({ + anWen: wer.id, + titel: v.titel, + ja, + notiz: kommentar, + ziel: aufgabeId + ? `/workspace/aufgaben.html?aufgabe=${aufgabeId}` + : "/workspace/aufgaben.html", + }); + protokolliere(ja ? "vorlage_bewerbung_angenommen" : "vorlage_bewerbung_abgelehnt", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), detail: `${b.vorlage} fuer ${wer.name}`.slice(0, 120), diff --git a/server/workspace-zuteilung.js b/server/workspace-zuteilung.js index b741e41d..7d6b9465 100644 --- a/server/workspace-zuteilung.js +++ b/server/workspace-zuteilung.js @@ -58,6 +58,11 @@ import { sitzungLesen, } from "./workspace.js"; import { TEAM_DOGI_ROLLEN } from "./crew-adresse.js"; +/* WER ES ERFAEHRT -- an einer Stelle fuer beide Bewerbungswege. + Begruendung samt Messung steht in der Datei selbst. */ +import { + meldeBewerbung, meldeBewerbungsantwort, +} from "./workspace-bewerbung-melden.js"; export const zuteilungRouter = express.Router(); @@ -577,6 +582,25 @@ zuteilungRouter.post("/workspace/api/aufgaben/:id/bewerben", (req, res) => { .run(a.id, req.person.id, text || null, nun, nun); } + /* ==== UND JETZT ERFAEHRT ES AUCH JEMAND (23.09.2026) ========= + + Bis heute endete eine Bewerbung hier: gespeichert, protokolliert + -- und still. DogFather erfuhr davon nur, wenn er von sich aus + das Brett aufmachte. Wer sich bewirbt, wartet aber; Warten ohne + Rueckmeldung fuehlt sich nach zwei Tagen an wie "interessiert + keinen", und genau das soll eine Bewerbung verhindern. + + NICHT ABWARTEN (`void`): Ob die Meldung durchgeht, haengt am + Push-Dienst, an der Ruhezeit und an den Einstellungen des + Empfaengers. Nichts davon darf entscheiden, ob die Bewerbung + selbst gespeichert ist -- die ist es laengst. */ + void meldeBewerbung({ + bewerber: { id: req.person.id, name: req.person.name }, + titel: a.titel, + wort: text, + ziel: `/workspace/aufgaben.html?aufgabe=${a.id}`, + }); + protokolliere("aufgabe_beworben", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), detail: `#${a.id} ${a.titel}`.slice(0, 120), @@ -664,6 +688,20 @@ for (const [weg, entscheidung] of [["annehmen", "ja"], ["ablehnen", "nein"]]) { WHERE id = ?`).run(nun, kommentar, req.person.id, nun, z.id); } + /* DIE ANTWORT IST DER WICHTIGERE TEIL. Eine Absage, die man + drei Tage spaeter zufaellig entdeckt, ist schlimmer als eine + sofortige -- und die NOTIZ geht mit, denn genau dafuer hat + Filipe sie verlangt ("mit einem text als notiz"). Sie erst zu + verlangen und dann an der Stelle zu verschweigen, an der man + sie liest, waere die halbe Funktion. */ + void meldeBewerbungsantwort({ + anWen: wer, + titel: a.titel, + ja: entscheidung === "ja", + notiz: kommentar, + ziel: `/workspace/aufgaben.html?aufgabe=${a.id}`, + }); + protokolliere(entscheidung === "ja" ? "bewerbung_angenommen" : "bewerbung_abgelehnt", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), detail: `#${a.id} fuer ${wer}`.slice(0, 120),