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 <[email protected]>
This commit is contained in:
2026-09-23 11:16:30 +02:00
co-authored by Claude Opus 5
parent 6862bba437
commit 1f2a4d335e
6 changed files with 438 additions and 4 deletions
+123
View File
@@ -860,6 +860,129 @@ console.log("");
} finally { await browser.close(); } } 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 -------------------------------------------- */ /* ---------- 7. Gegenprobe -------------------------------------------- */
console.log("\n=== 7. Gegenprobe: kann das hier ueberhaupt scheitern? ===\n"); console.log("\n=== 7. Gegenprobe: kann das hier ueberhaupt scheitern? ===\n");
{ {
+27 -4
View File
@@ -115,19 +115,42 @@ d.close();
melde(""); melde("");
melde("=== 1. Das Team ==="); 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", const a = await push.benachrichtige(idModi, "test",
{ titel: "Call gleich", text: "In zehn Minuten", ziel: "/workspace/calls.html" }); { titel: "Call gleich", text: "In zehn Minuten", ziel: "/workspace/calls.html" });
ok(a.verschickt === 1, `die Benachrichtigung geht raus (${a.verschickt})`); ok(a.verschickt === 1, `die Benachrichtigung geht raus (${a.verschickt})`);
/* DAS IST DER PUNKT: Sie wird NICHT unterdrueckt. Ein Modi, der in ok(a.ziel === "/workspace/calls.html",
einem Call steht, soll erfahren, dass er gleich anfaengt. */ `und fuehrt auf die Calls-Seite, die es fuer ihn seit dem 22.09. gibt (${a.ziel})`);
ok(a.ziel === "/workspace/start.html",
`aber nicht auf eine Seite, die es fuer ihn nicht 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", const b = await push.benachrichtige(idHand, "test",
{ titel: "Scouting", text: "Follow-up faellig", ziel: "/workspace/scouting.html" }); { titel: "Scouting", text: "Follow-up faellig", ziel: "/workspace/scouting.html" });
ok(b.ziel === "/workspace/start.html", ok(b.ziel === "/workspace/start.html",
`auch die rechte Hand landet nicht im Scouting (${b.ziel})`); `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 /* UND DIE GEGENPROBE, ohne die die zwei Zeilen darueber nichts
beweisen: Ein Ziel, das es auf crew. GIBT, bleibt stehen. Sonst beweisen: Ein Ziel, das es auf crew. GIBT, bleibt stehen. Sonst
hiesse "wird zur Startseite" nur, dass jedes Ziel dorthin faellt. */ hiesse "wird zur Startseite" nur, dass jedes Ziel dorthin faellt. */
+185
View File
@@ -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 });
}
+34
View File
@@ -107,6 +107,40 @@ export const ARTEN = [
liest es um sieben -- das reicht. */ liest es um sieben -- das reicht. */
{ schluessel: "chat_erwaehnung", name: "Jemand erwähnt dich im Chat (@)", vorgabe: true }, { 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). /* ER IST LIVE (22.09.2026).
Filipe: "die leute sollen auch automatisch eine benarichtigung Filipe: "die leute sollen auch automatisch eine benarichtigung
+31
View File
@@ -50,6 +50,13 @@ import {
darfAufgabenVerteilen, darfAufgabenVerteilen,
} from "./workspace.js"; } 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(); export const vorlagenRouter = express.Router();
const jetzt = () => new Date().toISOString(); const jetzt = () => new Date().toISOString();
@@ -1621,6 +1628,16 @@ vorlagenRouter.post("/workspace/api/vorlagen/bewerben", gleicheHerkunft,
VALUES (?,?,?,?,'beworben',?)`) VALUES (?,?,?,?,'beworben',?)`)
.run(v.schluessel, v.kategorie, req.person.id, text, jetzt()); .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", { protokolliere("vorlage_beworben", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
detail: `${v.schluessel} ${v.titel}`.slice(0, 120), 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, .run(ja ? "angenommen" : "abgelehnt", kommentar, req.person.id,
nun, aufgabeId, 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", { protokolliere(ja ? "vorlage_bewerbung_angenommen" : "vorlage_bewerbung_abgelehnt", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
detail: `${b.vorlage} fuer ${wer.name}`.slice(0, 120), detail: `${b.vorlage} fuer ${wer.name}`.slice(0, 120),
+38
View File
@@ -58,6 +58,11 @@ import {
sitzungLesen, sitzungLesen,
} from "./workspace.js"; } from "./workspace.js";
import { TEAM_DOGI_ROLLEN } from "./crew-adresse.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(); 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); .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", { protokolliere("aufgabe_beworben", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
detail: `#${a.id} ${a.titel}`.slice(0, 120), 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); 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", { protokolliere(entscheidung === "ja" ? "bewerbung_angenommen" : "bewerbung_abgelehnt", {
personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req),
detail: `#${a.id} fuer ${wer}`.slice(0, 120), detail: `#${a.id} fuer ${wer}`.slice(0, 120),