From 2d5972f45fec6fd923cbd7df48983ee40f5173fc Mon Sep 17 00:00:00 2001 From: Dogfather Date: Tue, 15 Sep 2026 13:22:55 +0200 Subject: [PATCH] Wie lange geht das schon so -- Eskalation statt eines Schalters Stufe 9 aus dem Plan: "Eskalationsstufe sichtbar". Damit ist Stufe 9 bis auf einen Punkt durch. DER MANGEL, GEMESSEN "Ueberfaellig" war ein Schalter: an oder aus. Eine Aufgabe, deren Frist gestern lief, sah genauso aus wie eine, die seit drei Wochen liegt -- dasselbe Wort, dieselbe Farbe, derselbe Platz. Damit verliert das Wort seine Bedeutung. Stehen auf einem Brett zwanzig Karten "ueberfaellig", sagt keine davon mehr etwas; man ueberliest sie alle, auch die, die seit einem Monat blockiert. ZWEI VERSCHIEDENE FRAGEN, DIE NICHT VERMISCHT WERDEN DUERFEN FRIST UEBERSCHRITTEN Es war etwas versprochen, der Termin ist vorbei. Eine Tatsache. faellig (ab 1 Tag) -> liegt (ab 3) -> haengt (ab 8) NICHTS PASSIERT Keine Frist, aber seit 14 Tagen hat niemand die Karte angefasst. Eine Beobachtung. Die erste ist haerter und steht vorn. Eine Aufgabe ohne Frist ist nicht "zu spaet" -- sie ist nur still. Beides in einen Topf zu werfen hiesse, jemandem einen gebrochenen Termin vorzuwerfen, den er nie zugesagt hat. Deshalb ist "still" auch farblos gezeichnet, gestrichelt statt gefuellt. DAS IST KEINE MAHNUNG Dieselbe Haltung wie bei der Standzeit im Talente-Trichter: Die Zahl ist eine Auskunft ueber UNS, kein Vorwurf an eine Person. Deshalb steht nirgends ein Name, und die hoechste Stufe heisst "haengt", nicht "versaeumt". DIE SCHWELLEN SIND GESETZT, NICHT GEMESSEN -- und das steht so im Code: unter 3 Tagen reicht ein Wochenende; ab 3 ist es ein Zustand; ab 8 (ueber eine Woche) kommt es ohne Hilfe auch naechste Woche nicht dazu. Aendert sich eine Zahl, aendert sie sich an einer Stelle, und die Oberflaeche zieht mit -- sie bekommt das WORT vom Server. AM SERVER GERECHNET. Startseite, Brett und Uebersicht lesen dieselbe Liste. Drei Stellen, die dieselbe Frage selbst beantworten, geben irgendwann drei Antworten -- genau der Fehler, der im Projekt schon bei der Rollenreihenfolge und beim Wort "Agentur" passiert ist. DIE FARBE IST NICHT DIE AUSKUNFT: In jedem Kaestchen steht ein Wort ("liegt seit 5 Tagen"), nicht nur ein roterer Punkt. PRUEFUNGEN: pruef-eskalation neu mit 40, davon 6 im Browser. Gemessen wird an den GRENZEN, nicht in der Mitte -- jede Schwelle dreimal: einen Tag davor, genau darauf, einen Tag danach. Die wichtigste ist die erste: am Tag der Frist ist nichts ueberfaellig. Wer bis zum 15. Zeit hat, ist am 15. puenktlich; eine Eskalation, die einen Tag zu frueh anschlaegt, verliert genau das Vertrauen, das sie braucht. Die Pruefung benutzt einen FESTEN Zeitpunkt als Eingabe, keine Wanderuhr -- sonst misst sie irgendwann das Gegenteil. Co-Authored-By: Claude Opus 5 --- server/pruef-eskalation.mjs | 307 ++++++++++++++++++++++++++++++ server/workspace-aufgaben.js | 7 + server/workspace-eskalation.js | 151 +++++++++++++++ workspace/app.webmanifest | 8 +- workspace/assets/css/aufgaben.css | 43 +++++ workspace/assets/js/aufgaben.js | 26 ++- workspace/aufgaben.html | 24 +-- workspace/automation.html | 26 +-- workspace/bereich.html | 34 ++-- workspace/calls.html | 28 +-- workspace/chat.html | 26 +-- workspace/content.html | 30 +-- workspace/crew-index.html | 10 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 26 +-- workspace/entwicklung.html | 24 +-- workspace/index.html | 8 +- workspace/kalender.html | 28 +-- workspace/leistung.html | 26 +-- workspace/personen.html | 26 +-- workspace/profil.html | 28 +-- workspace/rechte.html | 24 +-- workspace/report.html | 28 +-- workspace/scouting.html | 26 +-- workspace/start.html | 24 +-- workspace/startcheck.html | 28 +-- workspace/steckbrief.html | 28 +-- workspace/talente.html | 24 +-- workspace/team.html | 26 +-- workspace/teamlage.html | 24 +-- workspace/treff-moderation.html | 24 +-- workspace/treff-regeln.html | 26 +-- workspace/uebersicht.html | 26 +-- workspace/wissen.html | 26 +-- 34 files changed, 879 insertions(+), 349 deletions(-) create mode 100644 server/pruef-eskalation.mjs create mode 100644 server/workspace-eskalation.js diff --git a/server/pruef-eskalation.mjs b/server/pruef-eskalation.mjs new file mode 100644 index 00000000..48b564a5 --- /dev/null +++ b/server/pruef-eskalation.mjs @@ -0,0 +1,307 @@ +/* ===================================================================== + Wie lange geht das schon so? (15.09.2026) + + Stufe 9: „Eskalationsstufe sichtbar“. + + --------------------------------------------------------------------- + GEPRÜFT WIRD AN DEN GRENZEN, NICHT IN DER MITTE + + Eine Stufenfunktion ist überall dort richtig oder falsch, wo sie + umspringt — dazwischen ist sie langweilig. Jede Schwelle wird deshalb + dreimal gemessen: einen Tag davor, genau darauf, einen Tag danach. + + Die wichtigste Grenze ist die erste: **am Tag der Frist ist nichts + überfällig.** Wer bis zum 15. Zeit hat, ist am 15. pünktlich. Eine + Eskalation, die einen Tag zu früh anschlägt, verliert genau das + Vertrauen, das sie braucht. + + --------------------------------------------------------------------- + UND ZWEI DINGE, DIE NICHT ESKALIEREN DÜRFEN + + Erledigtes und Abgebrochenes — es liegt niemandem mehr im Weg. + Eine Aufgabe mit Frist in der Zukunft — sie ist eingeplant, nicht + still. + + Aufrufen mit: node server/pruef-eskalation.mjs + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { request as httpAnfrage } from "node:http"; +import { portMussFreiSein } from "./helfer-port.mjs"; + +const PORT = await portMussFreiSein(4417, "pruef-eskalation"); + +const ordner = mkdtempSync(join(tmpdir(), "ws-eskal-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(180_000, "pruef-eskalation"); +await new Promise((r) => setTimeout(r, 700)); + +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); +process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); + +let fehler = 0, geprueft = 0; +const melde = (t) => console.log(t); +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const HAUS = "workspace.dogfather-universe.com"; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const { eskalation, STUFEN, STILL_TAGE } = await import("./workspace-eskalation.js"); + +const oeffne = () => new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); + +/* EIN FESTER ZEITPUNKT, keine Wanderuhr. Eine Pruefung, die "heute" + als Annahme benutzt, misst irgendwann das Gegenteil -- der Kalender + holt sie ein. Hier wird die Zeit uebergeben, also ist sie Eingabe. */ +const NUN = Date.parse("2026-09-15T12:00:00+02:00"); +const tagVor = (n) => new Date(NUN - n * 86400000).toISOString().slice(0, 10); + +let idDogi; +{ + const d = oeffne(); + const schluessel = randomBytes(32).toString("hex"); + d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", schluessel, jetzt); + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync("CODE-DOGI-0001", salt, 64, + { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + + " VALUES ('Filipe','admin',?,?,?,?,1,?)") + .run(hash, salt, 32768, createHmac("sha256", schluessel).update("CODE-DOGI-0001").digest("hex"), jetzt); + idDogi = d.prepare("SELECT last_insert_rowid() AS id").get().id; + d.close(); +} + +function ruf(pfad, methode, kopf = {}, koerper = null) { + return new Promise((fertig, schief) => { + const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode, + headers: { Host: HAUS, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf } }, + (antwort) => { + let text = ""; + antwort.on("data", (s) => { text += s; }); + antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers })); + }); + a.on("error", schief); + if (koerper) a.write(JSON.stringify(koerper)); + a.end(); + }); +} +const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } }; +const anm = await ruf("/workspace/api/anmelden", "POST", {}, + { rolle: "admin", code: "CODE-DOGI-0001" }); +const kDogi = [].concat(anm.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "); +ok(!!kDogi, "DogFather ist angemeldet"); + +/** Kurzform: eine gedachte Aufgabe einstufen. */ +const stufeVon = (a) => eskalation(a, NUN)?.stufe ?? null; +const tageVon = (a) => eskalation(a, NUN)?.tage ?? null; + +/* ======================================================================= + 1. DIE ERSTE GRENZE: AM TAG DER FRIST IST NICHTS ÜBERFÄLLIG + ======================================================================= */ +melde(""); +melde("=== 1. Der Tag der Frist ==="); +{ + const offen = (frist) => ({ status: "offen", frist }); + + ok(stufeVon(offen(tagVor(-1))) === null, "eine Frist von morgen eskaliert nicht"); + ok(stufeVon(offen(tagVor(0))) === null, + "am Tag der Frist selbst auch nicht — wer bis zum 15. Zeit hat, ist am 15. pünktlich"); + ok(stufeVon(offen(tagVor(1))) === "faellig", + `einen Tag danach schon (${stufeVon(offen(tagVor(1)))})`); + ok(tageVon(offen(tagVor(1))) === 1, `und zwar seit ${tageVon(offen(tagVor(1)))} Tag`); +} + +/* ======================================================================= + 2. JEDE SCHWELLE DREIMAL: DAVOR, DARAUF, DANACH + ======================================================================= */ +melde(""); +melde("=== 2. An den Grenzen ==="); +{ + const bei = (tage) => eskalation({ status: "offen", frist: tagVor(tage) }, NUN); + ok(STUFEN.length === 3, `${STUFEN.length} Stufen im Katalog`); + + for (const s of STUFEN) { + const davor = bei(s.ab - 1); + const drauf = bei(s.ab); + const danach = bei(s.ab + 1); + ok(drauf?.stufe === s.schluessel, + `bei ${s.ab} Tagen gilt „${s.schluessel}“ (${drauf?.stufe})`); + ok(danach?.rang >= drauf.rang, + `einen Tag danach nicht milder (${danach?.stufe})`); + /* Die Stufe DAVOR muss eine andere sein -- sonst waere die Schwelle + wirkungslos, und die Zeile darueber trotzdem gruen. */ + ok(davor === null || davor.stufe !== s.schluessel, + `und einen Tag davor noch nicht (${davor?.stufe ?? "gar nichts"})`); + } + + /* Die Zahl im Wort muss zur Zahl im Feld passen -- sonst liest jemand + "liegt seit 5 Tagen" an einer Karte, die seit 12 liegt. */ + const zwoelf = bei(12); + ok(zwoelf.wort.includes(String(zwoelf.tage)), + `das Wort nennt dieselbe Zahl wie das Feld (${zwoelf.wort} / ${zwoelf.tage})`); + ok(zwoelf.stufe === "haengt", `und zwölf Tage sind „${zwoelf.stufe}“`); +} + +/* ======================================================================= + 3. WAS NICHT ESKALIEREN DARF + ======================================================================= */ +melde(""); +melde("=== 3. Was ruhig bleibt ==="); +{ + ok(stufeVon({ status: "erledigt", frist: tagVor(60) }) === null, + "eine erledigte Aufgabe eskaliert nicht, auch nach 60 Tagen nicht"); + ok(stufeVon({ status: "abgebrochen", frist: tagVor(60) }) === null, + "eine abgebrochene ebenso wenig"); + ok(stufeVon({ status: "arbeit", frist: tagVor(-30) }) === null, + "und eine mit Frist in der Zukunft ist eingeplant, nicht still"); + + /* GEGENPROBE -- ohne sie beweisen die drei Zeilen nichts: Dieselbe + Frist bei offenem Status eskaliert sehr wohl. */ + ok(stufeVon({ status: "offen", frist: tagVor(60) }) === "haengt", + "dieselbe alte Frist bei einer offenen Aufgabe dagegen schon"); +} + +/* ======================================================================= + 4. OHNE FRIST: EINE BEOBACHTUNG, KEIN GEBROCHENES VERSPRECHEN + ======================================================================= */ +melde(""); +melde("=== 4. Ohne Frist ==="); +{ + const still = (tage) => ({ + status: "offen", frist: null, + geaendert: new Date(NUN - tage * 86400000).toISOString(), + }); + + ok(stufeVon(still(STILL_TAGE - 1)) === null, + `${STILL_TAGE - 1} Tage ohne Bewegung sind noch nichts`); + ok(stufeVon(still(STILL_TAGE)) === "still", + `ab ${STILL_TAGE} Tagen steht es da (${stufeVon(still(STILL_TAGE))})`); + ok(eskalation(still(20), NUN)?.grund === "keine_bewegung", + "und der Grund ist ein anderer als bei einer Frist"); + ok(/unberührt/.test(eskalation(still(20), NUN)?.wort || ""), + `das Wort wirft niemandem etwas vor: „${eskalation(still(20), NUN)?.wort}“`); + + /* Ohne `geaendert` zaehlt das Anlegedatum -- sonst waere eine nie + angefasste Aufgabe fuer immer unsichtbar. */ + const nie = { status: "offen", frist: null, geaendert: null, + erstellt: new Date(NUN - 30 * 86400000).toISOString() }; + ok(stufeVon(nie) === "still", "eine nie angefasste Aufgabe zählt ab dem Anlegen"); + + /* GEGENPROBE: Eine Aufgabe MIT Frist wird nicht zusaetzlich als + "still" gemeldet -- zwei Marken an einer Karte waeren Laerm. */ + const beides = { status: "offen", frist: tagVor(-5), + geaendert: new Date(NUN - 40 * 86400000).toISOString() }; + ok(stufeVon(beides) === null, + "eine eingeplante Aufgabe wird nicht zusätzlich als still gemeldet"); +} + +/* ======================================================================= + 5. DIE STUFE KOMMT MIT DER LISTE — GERECHNET, NICHT IM BROWSER + ======================================================================= */ +melde(""); +melde("=== 5. Über die Leitung ==="); +{ + const d = oeffne(); + const rein = d.prepare(`INSERT INTO aufgaben (titel, status, prioritaet, frist, erstellt, erstellt_von) + VALUES (?,?,'mittel',?,?,?)`); + const heute = new Date().toISOString().slice(0, 10); + const vor = (n) => new Date(Date.now() - n * 86400000).toISOString().slice(0, 10); + rein.run("Seit gestern faellig", "offen", vor(1), jetzt, idDogi); + rein.run("Liegt seit fuenf Tagen", "offen", vor(5), jetzt, idDogi); + rein.run("Haengt seit zwanzig", "offen", vor(20), jetzt, idDogi); + rein.run("Erst naechste Woche", "offen", new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10), jetzt, idDogi); + rein.run("Heute faellig", "offen", heute, jetzt, idDogi); + d.close(); + + const liste = json(await ruf("/workspace/api/aufgaben", "GET", { cookie: kDogi })).aufgaben || []; + ok(liste.length >= 5, `${liste.length} Aufgaben in der Liste`); + const nach = (t) => liste.find((a) => a.titel === t); + + ok(nach("Seit gestern faellig")?.eskalation?.stufe === "faellig", + `„seit gestern“ → ${nach("Seit gestern faellig")?.eskalation?.stufe}`); + ok(nach("Liegt seit fuenf Tagen")?.eskalation?.stufe === "liegt", + `„seit fünf Tagen“ → ${nach("Liegt seit fuenf Tagen")?.eskalation?.stufe}`); + ok(nach("Haengt seit zwanzig")?.eskalation?.stufe === "haengt", + `„seit zwanzig“ → ${nach("Haengt seit zwanzig")?.eskalation?.stufe}`); + ok(nach("Erst naechste Woche")?.eskalation === null, + "„erst nächste Woche“ trägt keine Stufe"); + ok(nach("Heute faellig")?.eskalation === null, + "und „heute fällig“ ebenfalls nicht — heute ist nicht zu spät"); + + /* Jede Stufe bringt ihr WORT mit. Rechnete der Browser es selbst, + waere das die zweite Wahrheit. */ + ok(/\d/.test(nach("Haengt seit zwanzig")?.eskalation?.wort || ""), + `das Wort kommt fertig mit: „${nach("Haengt seit zwanzig")?.eskalation?.wort}“`); +} + +/* ======================================================================= + 6. IM BROWSER + ======================================================================= */ +melde(""); +melde("=== 6. Im Browser ==="); +{ + const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); + const browser = await chromium.launch(); + const kontext = await browser.newContext({ viewport: { width: 1280, height: 900 } }); + const seite = await kontext.newPage(); + const konsole = []; + seite.on("console", (m) => { if (m.type() === "error" && !/favicon|ERR_/i.test(m.text())) konsole.push(m.text()); }); + seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message)); + + await seite.goto(`http://127.0.0.1:${PORT}/workspace/`, { waitUntil: "domcontentloaded" }); + await seite.click('.rolle[data-rolle="admin"]'); + await seite.fill("#code", "CODE-DOGI-0001"); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 20000 }); + await seite.goto(`http://127.0.0.1:${PORT}/workspace/aufgaben.html`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(900); + + const stand = await seite.evaluate(() => { + const m = [...document.querySelectorAll(".karte__eskal")]; + return { + anzahl: m.length, + stufen: [...new Set(m.map((x) => x.dataset.stufe))].sort(), + texte: m.map((x) => x.textContent.trim()), + /* Steht das Wort "ueberfaellig" noch zweimal an derselben Karte? */ + doppelt: [...document.querySelectorAll(".karte")] + .filter((k) => (k.innerText.match(/überfällig/gi) || []).length > 1).length, + }; + }); + ok(stand.anzahl >= 3, `${stand.anzahl} Karten tragen eine Stufe`); + ok(stand.stufen.length >= 3, + `und es sind verschiedene: ${stand.stufen.join(", ")}`); + ok(stand.texte.some((t) => /\d+ Tagen/.test(t)), + `die Zahl steht im Text: ${stand.texte.join(" | ").slice(0, 90)}`); + /* DIE FARBE IST NICHT DIE AUSKUNFT. Wer Farben schlecht sieht, muss + dasselbe lesen koennen -- also steht in jedem Kaestchen ein Wort. */ + ok(stand.texte.every((t) => t.length > 3), "jede Stufe steht als Wort da, nicht nur als Farbe"); + ok(stand.doppelt === 0, + `und „überfällig“ steht an keiner Karte zweimal (${stand.doppelt})`); + + ok(konsole.length === 0, + konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 90) : "keine Fehler im Browser"); + await browser.close(); +} + +/* ------------------------------------------------------------------- */ +melde(""); +melde(`${geprueft} Pruefungen, ${fehler} Fehler`); +melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } +process.exit(fehler ? 1 : 0); diff --git a/server/workspace-aufgaben.js b/server/workspace-aufgaben.js index 75d96278..50ae08bf 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -15,6 +15,7 @@ import { MODI_KATEGORIEN, kategorienFuer, kategoriePersonen, ohneTeamDogi, ohneAgentur, siehtModis, TEAM_DOGI_ROLLEN, } from "./workspace.js"; +import { eskalationSetzen } from "./workspace-eskalation.js"; export const aufgabenRouter = express.Router(); @@ -267,6 +268,12 @@ aufgabenRouter.get("/workspace/api/aufgaben", (req, res) => { CASE WHEN a.frist IS NULL THEN 1 ELSE 0 END, a.frist, CASE a.prioritaet WHEN 'hoch' THEN 0 WHEN 'mittel' THEN 1 ELSE 2 END, a.id DESC`).all(...werte, heuteLokal()); + /* WIE LANGE GEHT DAS SCHON SO? Am Server gerechnet, nicht im + Browser: Startseite, Brett und Uebersicht lesen dieselbe Liste, + und drei Stellen, die dieselbe Frage selbst beantworten, geben + irgendwann drei Antworten. */ + eskalationSetzen(reihen); + res.json({ aufgaben: reihen, /* Die Kategorien kommen mit der Liste, nicht ueber einen eigenen diff --git a/server/workspace-eskalation.js b/server/workspace-eskalation.js new file mode 100644 index 00000000..a17a630e --- /dev/null +++ b/server/workspace-eskalation.js @@ -0,0 +1,151 @@ +/* ===================================================================== + Wie lange geht das schon so? (15.09.2026) + + Stufe 9 aus dem Plan: „Eskalationsstufe sichtbar". + + --------------------------------------------------------------------- + DER MANGEL, GEMESSEN + + Überfällig war bisher ein Schalter: an oder aus. Eine Aufgabe, deren + Frist gestern lief, sah genauso aus wie eine, die seit drei Wochen + liegt — dasselbe Wort, dieselbe Farbe, derselbe Platz. + + Damit verliert das Wort seine Bedeutung. Wenn auf einem Brett zwanzig + Karten „überfällig" stehen, sagt keine davon mehr etwas; man + überliest sie alle, auch die, die seit einem Monat blockiert. + + --------------------------------------------------------------------- + ZWEI VERSCHIEDENE FRAGEN — UND SIE DÜRFEN NICHT VERMISCHT WERDEN + + FRIST ÜBERSCHRITTEN Es war etwas versprochen, und der Termin ist + vorbei. Eine Tatsache. + + NICHTS PASSIERT Es gibt keine Frist, aber seit Wochen hat + niemand die Karte angefasst. Eine Beobachtung. + + Die erste ist härter als die zweite, und sie steht deshalb vorn. Eine + Aufgabe ohne Frist ist nicht „zu spät" — sie ist nur still. Beides in + einen Topf zu werfen hieße, jemandem einen gebrochenen Termin + vorzuwerfen, den er nie zugesagt hat. + + --------------------------------------------------------------------- + DAS IST KEINE MAHNUNG + + Dieselbe Haltung wie bei der Standzeit im Talente-Trichter und bei + „Was auf dich wartet": Die Zahl ist eine Auskunft über UNS, nicht ein + Vorwurf an eine Person. Deshalb steht hier auch nirgends ein Name, + und deshalb heißt die höchste Stufe „hängt" und nicht „versäumt". + + --------------------------------------------------------------------- + DIE SCHWELLEN SIND GESETZT, NICHT GEMESSEN + + Ehrlich benannt: 3 und 8 Tage sind ein begründeter Anfang, kein + Messergebnis. Die Überlegung dahinter: + + unter 3 Tagen Ein Wochenende reicht dafür. Wer Freitag eine + Frist reißt und Montag liefert, hat nichts + versäumt — nur den Kalender nicht gelesen. + ab 3 Tagen Länger als ein Wochenende. Jetzt ist es keine + Verzögerung mehr, sondern ein Zustand. + ab 8 Tagen Über eine Woche. Wer so lange nicht dazu kommt, + kommt ohne Hilfe auch nächste Woche nicht dazu. + + Wenn sich zeigt, dass die Stufen zu früh oder zu spät greifen, wird + hier eine Zahl geändert — an einer Stelle, und die Oberfläche zieht + mit, weil sie das Wort vom Server bekommt. + ===================================================================== */ + +/** Die Stufen, von der mildesten zur härtesten. + * + * `ab` ist die Anzahl Tage, ab der die Stufe gilt. Die Liste wird von + * hinten durchsucht — die härteste zutreffende gewinnt. */ +export const STUFEN = [ + { schluessel: "faellig", ab: 1, rang: 1 }, + { schluessel: "liegt", ab: 3, rang: 2 }, + { schluessel: "haengt", ab: 8, rang: 3 }, +]; + +/** Ohne Frist: ab wie vielen Tagen ohne Bewegung es auffällt. + * + * 14 Tage — zwei Wochen. Kürzer wäre bei einer Aufgabe ohne Termin + * Panikmache: Nicht alles, was daliegt, ist liegengeblieben. */ +export const STILL_TAGE = 14; + +const TAG = 86400000; + +/** Ganze Tage zwischen zwei Datumsangaben (a später als b). */ +function tageSeit(zeitpunkt, jetzt) { + const t = Date.parse(zeitpunkt); + if (!Number.isFinite(t)) return null; + return Math.floor((jetzt - t) / TAG); +} + +/** Wie lange geht das mit dieser Aufgabe schon so? + * + * @param {object} a eine Zeile aus `aufgaben` + * @param {number} [jetzt] Zeitpunkt in ms (für die Prüfung setzbar) + * @returns {{stufe: string, tage: number, grund: string, wort: string}|null} + * null, wenn nichts anliegt + */ +export function eskalation(a, jetzt = Date.now()) { + if (!a) return null; + /* Erledigtes und Abgebrochenes eskaliert nicht. Eine Karte, die fertig + ist, war vielleicht zu spät fertig -- aber sie liegt niemandem mehr + im Weg, und eine Warnung daran wäre nur noch ein Vorwurf. */ + if (a.status === "erledigt" || a.status === "abgebrochen") return null; + + /* ---- 1. Die Frist. Eine Tatsache, und sie steht vorn. ---- */ + if (a.frist) { + /* Auf den ANFANG des Tages nach der Frist gerechnet: Wer bis zum + 15. Zeit hat, ist am 15. nicht zu spät. Die Grenze ist Mitternacht + danach, nicht der Moment des Nachsehens. */ + const ende = Date.parse(a.frist + "T23:59:59"); + if (Number.isFinite(ende) && jetzt > ende) { + const tage = Math.max(1, Math.floor((jetzt - ende) / TAG) + 1); + const treffer = [...STUFEN].reverse().find((s) => tage >= s.ab); + if (treffer) { + return { + stufe: treffer.schluessel, + rang: treffer.rang, + tage, + grund: "frist", + wort: treffer.schluessel === "faellig" + ? "überfällig" + : (treffer.schluessel === "liegt" + ? `liegt seit ${tage} Tagen` + : `hängt seit ${tage} Tagen`), + }; + } + } + /* MIT FRIST UND NOCH NICHT ÜBERSCHRITTEN: Hier endet es. Eine Karte, + deren Termin erst nächste Woche ist, ist nicht „still" -- sie ist + eingeplant. */ + return null; + } + + /* ---- 2. Ohne Frist: seit wann hat niemand sie angefasst? ---- */ + const zuletzt = a.geaendert || a.erstellt; + const still = zuletzt ? tageSeit(zuletzt, jetzt) : null; + if (still !== null && still >= STILL_TAGE) { + return { + stufe: "still", + rang: 1, + tage: still, + grund: "keine_bewegung", + wort: `seit ${still} Tagen unberührt`, + }; + } + return null; +} + +/** Setzt `eskalation` an jede Zeile einer Liste. + * + * AM SERVER UND NICHT IM BROWSER: Startseite, Aufgabenbrett und + * Übersicht lesen dieselbe Liste. Drei Stellen, die dieselbe Frage + * selbst beantworten, geben irgendwann drei Antworten -- genau der + * Fehler, der im Projekt schon bei der Rollenreihenfolge und beim Wort + * „Agentur" passiert ist. */ +export function eskalationSetzen(liste, jetzt = Date.now()) { + for (const a of liste || []) a.eskalation = eskalation(a, jetzt); + return liste; +} diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 62514ca8..cdd3e288 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609151311", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609151321", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609151311", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609151321", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609151311", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609151321", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609151311", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609151321", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/aufgaben.css b/workspace/assets/css/aufgaben.css index 01f870db..fe067f0c 100644 --- a/workspace/assets/css/aufgaben.css +++ b/workspace/assets/css/aufgaben.css @@ -1038,3 +1038,46 @@ } .anhaenge__zufuegen:hover .anhaenge__knopf { color: var(--text); border-color: var(--rand-hell); } .anhaenge__hinweis { margin: 6px 0 0; font-size: .78rem; color: var(--text-still); } + +/* ===================================================================== + WIE LANGE GEHT DAS SCHON SO? (15.09.2026) + + Drei Stufen, drei Toene -- aber die Farbe ist NICHT die Auskunft: Im + Text steht "liegt seit 5 Tagen", nicht nur ein roterer Punkt. Wer + Farben schlecht sieht, liest dasselbe. + + Und es bleibt gedaempft. Eine Karte, die schreit, wird weggeklickt; + eine, die ruhig sagt "seit 12 Tagen", wird gelesen. + ===================================================================== */ + +.karte__eskal { + font-size: .76rem; font-weight: 700; + padding: 2px 9px; border-radius: 999px; + letter-spacing: .01em; + color: var(--text-leise); + border: 1px solid var(--rand-hell); +} +/* faellig: es ist gerade passiert. Ein Hinweis, keine Warnung. */ +.karte__eskal[data-stufe="faellig"] { + color: #d8a76a; + border-color: color-mix(in srgb, #cc9451 40%, transparent); +} +/* liegt: laenger als ein Wochenende. Jetzt ist es ein Zustand. */ +.karte__eskal[data-stufe="liegt"] { + color: #e0955f; + border-color: color-mix(in srgb, #cc7a3f 50%, transparent); + background: color-mix(in srgb, #cc7a3f 10%, transparent); +} +/* haengt: ueber eine Woche. Ohne Hilfe wird das auch naechste Woche + nichts. */ +.karte__eskal[data-stufe="haengt"] { + color: #e8886f; + border-color: color-mix(in srgb, #c25c45 55%, transparent); + background: color-mix(in srgb, #c25c45 14%, transparent); +} +/* still: keine Frist gerissen, nur lange nichts passiert. Bewusst + farblos -- das ist eine Beobachtung, kein gebrochenes Versprechen. */ +.karte__eskal[data-stufe="still"] { + color: var(--text-still); + border-style: dashed; +} diff --git a/workspace/assets/js/aufgaben.js b/workspace/assets/js/aufgaben.js index b68b1443..82e817ea 100644 --- a/workspace/assets/js/aufgaben.js +++ b/workspace/assets/js/aufgaben.js @@ -194,10 +194,32 @@ if (a.prioritaet === 'hoch' && a.status !== 'erledigt') { zeile.append(el('span', 'karte__wichtig', 'wichtig')); } + /* ---- WIE LANGE GEHT DAS SCHON SO? (15.09.2026, Stufe 9) -------- + + "Ueberfaellig" war ein Schalter: an oder aus. Eine Karte, deren + Frist gestern lief, sah aus wie eine, die seit drei Wochen liegt. + Stehen auf einem Brett zwanzig Karten "ueberfaellig", sagt keine + davon mehr etwas -- man ueberliest sie alle, auch die, die seit + einem Monat blockiert. + + DAS WORT KOMMT VOM SERVER. Eine zweite Rechnung hier waere die, + die irgendwann anders antwortet als die Startseite. */ + if (a.eskalation) { + const e = el('span', 'karte__eskal', a.eskalation.wort); + e.dataset.stufe = a.eskalation.stufe; + e.title = a.eskalation.grund === 'frist' + ? 'Die Frist ist vorbei. Die Zahl sagt, seit wann.' + : 'Ohne Frist – seit so vielen Tagen hat niemand die Karte angefasst.'; + zeile.append(e); + } + if (a.frist) { - const faellig = a.status !== 'erledigt' && a.frist < heute; + /* Das Wort "ueberfaellig" steht jetzt oben in der Stufe. Hier + bleibt nur noch das Datum -- zweimal dasselbe nebeneinander + waere Laerm. */ + const faellig = !!a.eskalation && a.eskalation.grund === 'frist'; const f = el('span', 'karte__frist' + (faellig ? ' karte__frist--faellig' : ''), - (faellig ? 'überfällig ' : 'bis ') + datumHuebsch(a.frist)); + 'bis ' + datumHuebsch(a.frist)); zeile.append(f); } /* Die Kategorie als Wort in derselben Zeile wie Frist und Person. diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index e891bc86..b427f954 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - + @@ -267,10 +267,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 09e3e66e..f8ccd2ee 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -168,10 +168,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 5c774313..498cfd14 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -181,14 +181,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 1a788d78..35f9cac3 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -89,11 +89,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index 6e091b52..3ccda0ea 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -12,26 +12,26 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -341,10 +341,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index 37280f3e..a2c2d0c5 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -179,12 +179,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/crew-index.html b/workspace/crew-index.html index 292e3e3b..8dd57757 100644 --- a/workspace/crew-index.html +++ b/workspace/crew-index.html @@ -36,15 +36,15 @@ - - - + + + - + @@ -339,6 +339,6 @@ - + diff --git a/workspace/crew.webmanifest b/workspace/crew.webmanifest index 94708365..23fd4fb1 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609151311", + "src": "/assets/img/app-symbole/crew-192.png?v=202609151321", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609151311", + "src": "/assets/img/app-symbole/crew-512.png?v=202609151321", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609151311", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609151321", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609151311", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609151321", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index b46af05a..593369f0 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -108,10 +108,10 @@ - - - - - + + + + + diff --git a/workspace/entwicklung.html b/workspace/entwicklung.html index 947d8229..55448c62 100644 --- a/workspace/entwicklung.html +++ b/workspace/entwicklung.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -94,9 +94,9 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index 7e4e3b9e..494a010d 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -30,9 +30,9 @@ - - - + + + @@ -401,6 +401,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index 06331d0a..aad06ea0 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -377,11 +377,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index e8043dbc..ed79c6d1 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -423,10 +423,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index 223c711b..abdfd0c3 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -423,10 +423,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index fc79716f..f27b8d0b 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -214,11 +214,11 @@ - - - - - - + + + + + + diff --git a/workspace/rechte.html b/workspace/rechte.html index 4040461b..83a3b783 100644 --- a/workspace/rechte.html +++ b/workspace/rechte.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -134,9 +134,9 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - + + + + diff --git a/workspace/report.html b/workspace/report.html index 5d358850..420e29c2 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -106,11 +106,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index 24f303ad..bef5c18a 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -173,10 +173,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index 4653c6e4..146901ef 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -12,21 +12,21 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - + + - + @@ -674,10 +674,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index 43b6dec0..cfd6f06a 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -85,11 +85,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index b9604215..2832156c 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -146,11 +146,11 @@ - - - - - - + + + + + + diff --git a/workspace/talente.html b/workspace/talente.html index 829bed46..8d7edf27 100644 --- a/workspace/talente.html +++ b/workspace/talente.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -94,9 +94,9 @@ - - - - + + + + diff --git a/workspace/team.html b/workspace/team.html index 7824cef9..c5acfbe2 100644 --- a/workspace/team.html +++ b/workspace/team.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + @@ -110,10 +110,10 @@ - - - - - + + + + + diff --git a/workspace/teamlage.html b/workspace/teamlage.html index ab3d03be..12e6fda8 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/treff-moderation.html b/workspace/treff-moderation.html index b9ade640..e43bd332 100644 --- a/workspace/treff-moderation.html +++ b/workspace/treff-moderation.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -110,9 +110,9 @@ - - - - + + + + diff --git a/workspace/treff-regeln.html b/workspace/treff-regeln.html index c2e1bcc1..574b68d3 100644 --- a/workspace/treff-regeln.html +++ b/workspace/treff-regeln.html @@ -7,18 +7,18 @@ - - - - - - - + + + + + + + - + @@ -210,10 +210,10 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index 9e5e67d7..4c2ebfa2 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -12,22 +12,22 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -149,11 +149,11 @@ - + - - - - + + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index 630ef205..ac070ba7 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -190,10 +190,10 @@ - - - - - + + + + +