diff --git a/server/pruef-nachwuchs.mjs b/server/pruef-nachwuchs.mjs index f048175c..c22ddb10 100644 --- a/server/pruef-nachwuchs.mjs +++ b/server/pruef-nachwuchs.mjs @@ -49,6 +49,13 @@ process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):" 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++; }; +/* DER DRITTE AUSGANG: nicht in Ordnung ist etwas anderes als "konnte + nicht nachsehen". Ohne ihn wuerde ein fehlendes Playwright entweder + als Fehler gemeldet (falscher Alarm) oder stillschweigend + uebersprungen (schlimmer -- die Zahl saenke, und niemand wuesste + warum). */ +let offen = 0; +const unklar = (t) => { offen++; console.log(" -- " + t); }; const CREW = "crew.dogfather-universe.com"; const HAUS = "workspace.dogfather-universe.com"; @@ -920,9 +927,318 @@ melde("=== 13. Die Probezeit ==="); } } +/* ======================================================================= + 14. DER AUSGANG AUS DEM TRICHTER (17.09.2026) + + Filipe: „alles moegliche, wie gesagt ich will hoch profissionel + arbeiten." + + Gemessen vor dem Bau: talentNaechste kettet aufgefallen -> beobachtet + -> angesprochen -> probe -> imteam, und danach kommt nichts. Ein + Kandidat, der nicht passt, blieb auf seiner Stufe stehen -- mit + wachsender Standzeit und rotem "liegt"-Zeichen -- obwohl der Text der + Stufe woertlich verspricht: "uebernehmen ... oder sauber beenden". + + Geprueft wird hier DREIERLEI, und der dritte Punkt ist der, wegen dem + es die Spalten und nicht eine neue Stufe geworden ist: + 1. Der Satz ist Pflicht -- und die Grenze beisst wirklich. + 2. Beendete verschwinden aus der Arbeitsliste, nicht aus dem Haus. + 3. Die STUFE bleibt stehen: Man sieht, WO es geendet hat. + ======================================================================= */ +melde(""); +melde("=== 14. Der Ausgang ==="); +{ + /* Jonas steht aus Abschnitt 13 in der Probe -- genau der interessante + Fall: Wer in der Probe aussteigt, hat eine andere Geschichte als + wer nach dem ersten Hinsehen aussteigt. */ + const vorher = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })); + const jonas = (vorher.kandidaten || []).find((k) => k.titel === "Jonas aus dem Chat"); + ok(!!jonas && jonas.stufe === "probe", + `Jonas steht in der Arbeitsliste, Stufe ${jonas?.stufe}`); + const stufeVorher = (vorher.trichter || []).find((x) => x.schluessel === "probe")?.anzahl; + + /* ---- Der Satz ist Pflicht ---- */ + { + const r = await roh(`/workspace/api/talente/${jonas.id}/beenden`, HAUS, + { cookie: kDogi }, { grund: "" }, "PUT"); + ok(r.code === 400, `ohne Satz geht es nicht (${r.code})`); + ok((json(r).fehler || "").length > 20, + "und es steht dabei, warum der Satz verlangt wird"); + } + { + /* DIE GRENZE BEISST WIRKLICH. "Passt nicht" hat neun Zeichen -- die + haeufigste Nicht-Begruendung ueberhaupt, und genau die soll + auffallen. Ohne diese Zeile wuesste man nur, dass ein LEERES Feld + abgewiesen wird, und das ist eine andere, viel schwaechere + Aussage. */ + const r = await roh(`/workspace/api/talente/${jonas.id}/beenden`, HAUS, + { cookie: kDogi }, { grund: "passt nix" }, "PUT"); + ok(r.code === 400, `neun Zeichen sind noch kein Satz (${r.code})`); + } + + /* ---- Beenden ---- */ + const GRUND = "Hat sich nach der zweiten Schicht nicht mehr gemeldet."; + { + const r = await roh(`/workspace/api/talente/${jonas.id}/beenden`, HAUS, + { cookie: kDogi }, { grund: GRUND }, "PUT"); + ok(r.code === 200 && json(r).beendet === true, `mit Satz geht es (${r.code})`); + } + { + const l = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })); + ok(!(l.kandidaten || []).some((k) => k.id === jonas.id), + "er steht nicht mehr in der Arbeitsliste"); + const b = (l.beendete || []).find((k) => k.id === jonas.id); + ok(!!b, "aber er ist da -- unter den Beendeten"); + ok(b?.beendet?.grund === GRUND, `der Satz steht dabei (${b?.beendet?.grund})`); + ok(b?.beendet?.von === "Filipe", `und wer es war (${b?.beendet?.von})`); + /* DER EIGENTLICHE PUNKT: Die Stufe blieb stehen. Waere daraus ein + eigener Stufenwert "beendet" geworden, stuende hier "beendet" -- + und die Frage "wie weit war er gekommen?" waere fuer immer + unbeantwortbar. */ + ok(b?.stufe === "probe", + `und WO es geendet hat: ${b?.stufe_name} -- nicht bloss "beendet"`); + ok(b?.liegt === false, + "er liegt nicht mehr -- eine beendete Karte wird nicht rot"); + const nachher = (l.trichter || []).find((x) => x.schluessel === "probe")?.anzahl; + ok(nachher === stufeVorher - 1, + `der Trichter zaehlt ihn nicht mehr mit (${stufeVorher} -> ${nachher})`); + } + + /* ---- Wieder aufnehmen ---- */ + { + const r = await roh(`/workspace/api/talente/${jonas.id}/wieder`, HAUS, + { cookie: kDogi }, null, "PUT"); + ok(r.code === 200, `wieder aufnehmen geht (${r.code})`); + const l = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })); + const k = (l.kandidaten || []).find((x) => x.id === jonas.id); + ok(!!k, "er steht wieder in der Arbeitsliste"); + ok(k?.stufe === "aufgefallen", + `und faengt vorne an (${k?.stufe}) -- ein Jahr altes Urteil gilt nicht weiter`); + /* DER SATZ VON DAMALS BLEIBT. Er ist beim zweiten Anlauf das + Wertvollste, was man hat -- und genau er wuerde beim Zuruecksetzen + am leichtesten verschwinden. */ + ok((k?.notiz || "").includes(GRUND), + "der Grund von damals steht in der Notiz"); + ok(!k?.beendet, "und die Karte ist nicht mehr als beendet gezeichnet"); + /* Die Probe faengt neu an -- alte Haken wuerden eine Probezeit + beschreiben, die vier Wochen zurueckliegt. */ + ok(!(l.beendete || []).some((x) => x.id === jonas.id), + "aus der Sammlung der Beendeten ist er heraus"); + } + { + const r = await roh(`/workspace/api/talente/${jonas.id}/wieder`, HAUS, + { cookie: kDogi }, null, "PUT"); + ok(r.code === 409, `wer laeuft, kann nicht wieder aufgenommen werden (${r.code})`); + } + + /* ---- Wer im Team ist, wird nicht hier beendet ---- + + Gefunden am Bildschirmfoto: Auf einer Karte auf "Im Team" stand + "Nichts mehr zu tun -- der Zugang steht" und direkt darunter + "Passt doch nicht?". Ein Beenden dort haette die KARTE stillgelegt + und den ZUGANG offengelassen -- und das ist schlimmer als gar kein + Ausgang, weil man glaubt, gehandelt zu haben. */ + { + const l = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })); + const drin = (l.kandidaten || []).find((k) => k.stufe === "imteam"); + ok(!!drin, `es gibt einen im Team zum Gegenprobieren (${drin?.titel})`); + const r = await roh(`/workspace/api/talente/${drin.id}/beenden`, HAUS, + { cookie: kDogi }, { grund: GRUND }, "PUT"); + ok(r.code === 409, `wer im Team ist, wird nicht hier beendet (${r.code})`); + ok((json(r).fehler || "").includes("Zug"), + `und es steht dabei, wo stattdessen (${(json(r).fehler || "").slice(0, 40)}...)`); + const l2 = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })); + ok((l2.kandidaten || []).some((k) => k.id === drin.id), + "er steht unveraendert in der Liste -- nichts halb passiert"); + } + + /* ---- Und die Gegenproben ---- */ + { + const r = await roh("/workspace/api/talente/999999/beenden", HAUS, + { cookie: kDogi }, { grund: GRUND }, "PUT"); + ok(r.code === 404, `eine erfundene Nummer wird abgewiesen (${r.code})`); + } + { + const r = await roh(`/workspace/api/talente/${jonas.id}/beenden`, CREW, + { cookie: kModi }, { grund: GRUND }, "PUT"); + ok(r.code === 404, + `ein Modi kann niemanden beenden -- und erfaehrt nicht, dass es das gibt (${r.code})`); + } + { + const r = await roh(`/workspace/api/talente/${jonas.id}/wieder`, CREW, + { cookie: kModi }, null, "PUT"); + ok(r.code === 404, `dasselbe beim Wiederaufnehmen (${r.code})`); + } +} + +/* ======================================================================= + 15. DIE TALENTE-SEITE IM ECHTEN BROWSER (17.09.2026) + + Der erste Browserblick auf diese Seite ueberhaupt. Er ist faellig: + + Heute Mittag hat ein Bildschirmfoto zwei Dinge gefunden, die keine + Zahl gefunden haette -- ein Wasserzeichen, das durch vertraulichen + Text hindurchschien (color-mix mit DURCHSICHTIG statt mit Weiss), + und eine Klassenkollision, bei der .t-schritt fuer zwei verschiedene + Dinge stand. Beides war gruen in jeder Prueflinie und falsch auf dem + Schirm. + + GEKLICKT WIRD WIRKLICH, nicht ueber die Schnittstelle. Der ganze Weg + -- aufklappen, tippen, absenden -- ist der, der kaputtgehen kann; was + darunter liegt, ist in Abschnitt 14 schon bewiesen. + ======================================================================= */ +melde(""); +melde("=== 15. Im echten Browser ==="); +{ + let chromium; + try { + ({ chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs")); + } catch { chromium = null; } + + if (!chromium) { + unklar("kein Playwright gefunden -- die Oberflaeche wurde nicht angesehen"); + } else { + const browser = await chromium.launch(); + try { + /* 390 x 844 -- ein Handy. Filipe arbeitet am Handy, und die + Kopfleiste ist genau dort im September zerbrochen. */ + const sicht = await browser.newContext({ viewport: { width: 390, height: 844 } }); + await sicht.addCookies(kDogi.split("; ").map((c) => { + const [name, ...rest] = c.split("="); + return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" }; + })); + const seite = await sicht.newPage(); + const konsole = []; + seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); }); + await seite.goto(`http://127.0.0.1:${PORT}/workspace/talente.html`, + { waitUntil: "domcontentloaded" }); + await seite.waitForTimeout(1200); + + const karten = await seite.locator(".t-karte").count(); + ok(karten > 0, `die Seite zeigt ${karten} Karten`); + /* ABGELEITET, NICHT ABGESCHRIEBEN: An jeder Karte IM TRICHTER + steht ein Ausgang -- an denen im Team ausdruecklich nicht. + Feste Zahlen waeren hier die Rechnung von gestern: Kommt ein + Kandidat dazu, muesste man sie nachziehen. */ + const imTeam = await seite.locator('.t-karte[data-stufe="imteam"]').count(); + const ausgaenge = await seite.locator(".t-ausgang__auf").count(); + ok(ausgaenge === karten - imTeam, + `an jeder Karte im Trichter ein Ausgang (${ausgaenge} von ${karten} ` + + `- ${imTeam} im Team)`); + ok(imTeam > 0 && await seite.locator(".t-ausgang__anderswo").count() === imTeam, + `und an denen im Team steht stattdessen der Weg zum Zugang (${imTeam})`); + ok(await seite.locator('.t-karte[data-stufe="imteam"] .t-ausgang__auf').count() === 0, + "dort gibt es das Feld nicht einmal zum Aufklappen"); + + /* ---- Der Weg: aufklappen, zu kurz, dann richtig ---- + Gezielt eine Karte AUS DEM TRICHTER, nicht einfach die erste: + Welche oben steht, entscheidet die Sortierung, und die haengt + an Standzeiten. */ + const erste = seite.locator('.t-karte:not([data-stufe="imteam"])').first(); + const titel = (await erste.locator(".t-karte__titel").textContent())?.trim(); + ok(await erste.locator(".t-ausgang__form").isHidden(), + "das Feld ist zugeklappt -- der Ausgang draengt sich nicht auf"); + await erste.locator(".t-ausgang__auf").click(); + await seite.waitForTimeout(200); + ok(await erste.locator(".t-ausgang__form").isVisible(), + "nach einem Klick steht es da"); + + { + /* ZU KURZ -- und zwar im Browser, nicht nur am Server. Was der + Browser prueft, ist Hoeflichkeit; faellt sie aus, sieht man + statt eines Satzes eine rohe Fehlermeldung. */ + await erste.locator(".t-ausgang__feld").fill("passt nix"); + await erste.locator(".t-ausgang__knopf").click(); + await seite.waitForTimeout(400); + const meldung = (await seite.locator("#fehler").textContent())?.trim() || ""; + ok(meldung.length > 10, `ein zu kurzer Satz wird abgewiesen ("${meldung}")`); + ok(await seite.locator(".t-karte").count() === karten, + "und die Karte bleibt stehen"); + } + + const GRUND_B = "Kam dreimal nicht zur Schicht, ohne Bescheid zu sagen."; + { + await erste.locator(".t-ausgang__feld").fill(GRUND_B); + await erste.locator(".t-ausgang__knopf").click(); + await seite.waitForTimeout(900); + const uebrig = await seite.locator(".t-karte").count(); + ok(uebrig === karten - 1, + `mit Satz verschwindet sie aus der Arbeitsliste (${karten} -> ${uebrig})`); + ok(await seite.locator(".t-beendet__auf").count() === 1, + "und unten steht die Sammlung der Beendeten"); + const wort = (await seite.locator(".t-beendet__auf").textContent())?.trim() || ""; + ok(/[0-9]/.test(wort), `zugeklappt mit Zahl ("${wort}")`); + } + + { + await seite.locator(".t-beendet__auf").click(); + await seite.waitForTimeout(300); + const karte = seite.locator(".t-beendet__karte").first(); + ok(await karte.isVisible(), "aufgeklappt steht die Karte da"); + ok((await karte.locator(".t-beendet__titel").textContent())?.trim() === titel, + "mit dem richtigen Namen"); + ok((await karte.locator(".t-beendet__grund").textContent())?.includes(GRUND_B), + "und dem Satz von damals"); + const stufe = (await karte.locator(".t-beendet__stufe").textContent())?.trim() || ""; + ok(stufe.length > 3, `und WO es geendet hat (${stufe})`); + /* GEGENPROBE ZUR SICHTBARKEIT: An einer beendeten Karte steht + KEIN zweiter Ausgang -- sonst koennte man beenden, was schon + beendet ist, und der Grund wuerde ueberschrieben. */ + ok(await karte.locator(".t-ausgang__auf").count() === 0, + "an einer beendeten Karte steht kein zweiter Ausgang"); + ok(await karte.locator(".t-beendet__wieder").count() === 1, + "sondern der Weg zurueck"); + } + + { + /* DIE KOPFZEILE BEI 390 px. margin-inline-start: auto schiebt + das Datum nach rechts -- genau die Bauart, die im September + in der Kopfleiste zwei Knoepfe uebereinandergelegt hat. Eine + Messung statt einer Zahl im CSS. */ + const k = seite.locator(".t-beendet__karte").first(); + const a = await k.locator(".t-beendet__titel").boundingBox(); + const b = await k.locator(".t-beendet__zeit").boundingBox(); + const ueber = a && b + && a.x < b.x + b.width && b.x < a.x + a.width + && a.y < b.y + b.height && b.y < a.y + a.height; + ok(!!a && !!b && !ueber, + `Name und Datum liegen nicht uebereinander (${Math.round(a?.x ?? -1)}+` + + `${Math.round(a?.width ?? 0)} | ${Math.round(b?.x ?? -1)})`); + /* Und der Weg zurueck muss mit einem Finger zu treffen sein. */ + const w = await k.locator(".t-beendet__wieder").boundingBox(); + ok(!!w && w.height >= 28 && w.width >= 100, + `der Weg zurueck ist treffbar (${Math.round(w?.width ?? 0)} x ${Math.round(w?.height ?? 0)})`); + } + + { + await seite.locator(".t-beendet__wieder").first().click(); + await seite.waitForTimeout(900); + const zurueck = await seite.locator(".t-karte").count(); + ok(zurueck === karten, `wieder aufnehmen bringt sie zurueck (${zurueck})`); + ok(await seite.locator(".t-beendet__auf").count() === 0, + "und die Sammlung verschwindet, wenn nichts mehr drin ist"); + } + + ok(konsole.length === 0, + konsole.length ? `Konsolenfehler: ${konsole[0]}` : "kein einziger Konsolenfehler"); + + /* Das Bildschirmfoto -- der Teil, den keine Zahl ersetzt. */ + await seite.locator('.t-karte:not([data-stufe="imteam"]) .t-ausgang__auf') + .first().click().catch(() => {}); + await seite.waitForTimeout(300); + await seite.screenshot({ path: "server/pruef-nachwuchs-ausgang.png", fullPage: true }); + melde(" -- Bildschirmfoto: server/pruef-nachwuchs-ausgang.png"); + } finally { + await browser.close(); + } + } +} + /* ------------------------------------------------------------------- */ melde(""); -melde(`${geprueft} Pruefungen, ${fehler} Fehler`); +melde(`${geprueft} Pruefungen, ${fehler} Fehler` + + (offen ? `, ${offen} nicht nachsehbar` : "")); 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-entwicklung.js b/server/workspace-entwicklung.js index 684244a6..bbaed146 100644 --- a/server/workspace-entwicklung.js +++ b/server/workspace-entwicklung.js @@ -85,6 +85,13 @@ const jetzt = () => new Date().toISOString(); const TAG = 86400000; const ANLASS_MAX = 1200; +/* WIE KURZ EIN ABSCHIEDSSATZ SEIN DARF. + Zehn Zeichen -- dieselbe Grenze wie bei einer abgelehnten Anfrage + (NACHRICHT_MIN, workspace-bewerbung-fragen.js), und aus demselben + Grund: Sie verhindert nicht das schlechte Argument, sie verhindert + das leere Feld. "Passt nicht" hat neun Zeichen. */ +const BEENDEN_GRUND_MIN = 10; + /* WANN EINE EINSCHAETZUNG VERBLASST. Nach 90 Tagen zaehlt ein Stand nicht mehr als aktuell. Er wird NICHT geloescht -- man soll sehen, dass etwas alt ist, statt dass es @@ -230,6 +237,31 @@ export function tabellen() { for (const [spalte, typ] of [ ["buddy_id", "INTEGER REFERENCES personen(id) ON DELETE SET NULL"], ["erste_schicht", "TEXT"], + /* DER AUSGANG (17.09.2026). + + Der Trichter hatte bis heute genau EINEN: "Ins Team holen". Die + Stufenbeschreibung versprach daneben "oder sauber beenden" -- + den Weg dazu gab es nicht. Ein Kandidat, bei dem es nicht passt, + blieb damit fuer immer auf seiner Stufe stehen, mit wachsender + Standzeit und rotem Hinweis. Das ist schlimmer als keine + Funktion: Die Liste fuellt sich mit Leuten, bei denen laengst + entschieden ist, und irgendwann liest niemand mehr die + Standzeiten. + + ZWEI SPALTEN UND KEINE NEUE STUFE, und das ist Absicht: + + Eine Stufe "beendet" haette die Regel in talent_stufe umbauen + muessen (CHECK) -- ein Tabellenneubau an lebenden Daten fuer + etwas, das sich mit zwei Spalten sagen laesst. + + Und sie haette eine Auskunft VERNICHTET: So bleibt stehen, WO + es geendet hat. "Beendet in der Probe" und "beendet nach dem + ersten Hinsehen" sind zwei verschiedene Saetze ueber dieselbe + Person -- und wenn sie in einem halben Jahr wieder auffaellt, + ist genau das die Frage. */ + ["beendet_am", "TEXT"], + ["beendet_grund", "TEXT"], + ["beendet_von", "INTEGER"], ]) { try { const da = db().prepare("PRAGMA table_info(talent_stufe)").all().map((s) => s.name); @@ -890,7 +922,11 @@ function uebergangVon(stand) { /** Die Karte eines Kandidaten -- Stufe, Standzeit, was zu tun ist. */ function talentKarte(e, merkmale) { const stand = db().prepare( - "SELECT stufe, seit, notiz, buddy_id, erste_schicht FROM talent_stufe WHERE eintrag_id = ?") + `SELECT s.stufe, s.seit, s.notiz, s.buddy_id, s.erste_schicht, + s.beendet_am, s.beendet_grund, p.name AS beendet_von_name + FROM talent_stufe s + LEFT JOIN personen p ON p.id = s.beendet_von + WHERE s.eintrag_id = ?`) .get(e.id); const stufe = talentStufe(stand?.stufe) || TALENT_STUFEN[0]; const seit = stand?.seit || e.erstellt; @@ -906,7 +942,11 @@ function talentKarte(e, merkmale) { /* LIEGENGEBLIEBEN IST EINE AUSKUNFT UEBER UNS, nicht ueber den Kandidaten. Deshalb steht sie an der Karte und nicht in einer Erinnerungsmail, die man wegklickt. */ - liegt: stufe.frist !== null && tage !== null && tage > stufe.frist, + /* Ein beendeter Kandidat liegt nicht -- er ist fertig. Ohne diese + Ausnahme wuerde die Standzeit weiterlaufen und die Karte nach + einer Woche rot werden, obwohl niemand mehr etwas zu tun hat. */ + liegt: !stand?.beendet_am && stufe.frist !== null + && tage !== null && tage > stufe.frist, merkmale: gesetzt.filter((s) => !TALENT_NACH_SCHLUESSEL.get(s)?.warnung), warnzeichen: warn, notiz: stand?.notiz || null, @@ -935,6 +975,23 @@ function talentKarte(e, merkmale) { Abruf -- sonst holt man sie erst, wenn man schon entschieden hat. */ probe: probeVon(e.id, stufe.schluessel), + /* DER AUSGANG (17.09.2026). + + Die Stufe bleibt stehen -- dass jemand raus ist, steht daneben, + nicht anstelle. "Beendet in der Probe" und "beendet nach dem + ersten Hinsehen" sind zwei verschiedene Auskuenfte, und bei + einem zweiten Anlauf in einem halben Jahr ist genau der + Unterschied die Frage. + + DER GRUND WIRD MITGELIEFERT, NICHT NUR DAS DATUM. Ein Ausgang + ohne Satz waere dieselbe Sackgasse, gegen die der Satz beim + Ablehnen einer Anfrage verlangt wird. */ + beendet: stand?.beendet_am ? { + am: stand.beendet_am, + grund: stand.beendet_grund || null, + von: stand.beendet_von_name || null, + tage: tageSeit(stand.beendet_am), + } : null, }; } @@ -983,7 +1040,22 @@ entwicklungRouter.get("/workspace/api/talente/lage", (req, res) => { .all(...eintraege.map((e) => e.id)) : []; - const karten = eintraege.map((e) => talentKarte(e, merkmale)); + /* BEENDETE KOMMEN AUS DER ARBEITSLISTE HERAUS -- aber nicht aus + dem Haus (17.09.2026). + + Wer einmal abgesagt hat, steht sonst fuer immer im Trichter, + mit wachsender Standzeit und rotem "liegt"-Zeichen. Nach einem + halben Jahr sind das mehr als die echten Kandidaten, und dann + schaut niemand mehr hin -- eine Liste, in der 20 von 25 Eintraege + erledigt sind, ist keine Liste mehr. + + SIE WERDEN TROTZDEM MITGELIEFERT, in einem eigenen Feld. Das + Loeschen waere die bequeme Variante und die, nach der in sechs + Monaten niemand mehr weiss, dass es schon einmal einen Anlauf + gab. */ + const alle = eintraege.map((e) => talentKarte(e, merkmale)); + const karten = alle.filter((k) => !k.beendet); + const beendete = alle.filter((k) => k.beendet); res.json({ stufen: TALENT_STUFEN, gruppen: TALENT_GRUPPEN.map((g) => ({ ...g, merkmale: TALENT_MERKMALE[g.schluessel] || [] })), @@ -1002,6 +1074,9 @@ entwicklungRouter.get("/workspace/api/talente/lage", (req, res) => { dazu steht; siehe ZUGANG_ROLLE oben. */ zugang: { rolle: ZUGANG_ROLLE, name: ROLLEN_NAME[ZUGANG_ROLLE] ?? ZUGANG_ROLLE }, kandidaten: karten, + /* Nach Ausgangsdatum, das juengste zuerst -- wer gerade abgesagt + hat, ist der, an den man sich erinnert. */ + beendete: beendete.sort((x, y) => (y.beendet.am > x.beendet.am ? 1 : -1)), /* Wie viele auf jeder Stufe liegen -- der Trichter als Zahlen. Abgeleitet, damit er nicht danebenliegen kann. */ trichter: TALENT_STUFEN.map((s) => ({ @@ -1098,6 +1173,133 @@ entwicklungRouter.put("/workspace/api/talente/:id/probe/:schluessel", gleicheHer } }); +/* ===================================================================== + DEN TRICHTER SAUBER VERLASSEN (17.09.2026) + + Filipe: „wie talente bewertet werden, aufgaben bekommen, analysiert + werden ... alles moegliche, wie gesagt ich will hoch profissionel + arbeiten." + + EIN GRUND IST PFLICHT, und zwar aus zwei Richtungen: + + Nach vorn -- wenn dieselbe Person in einem halben Jahr wieder + auffaellt, ist die erste Frage: Was war damals? Ohne Satz + beantwortet sie niemand, und man faengt bei null an. + + Nach innen -- wer einen Satz schreiben muss, entscheidet anders + als wer einen Knopf drueckt. "Passt nicht" laesst sich nicht + aufschreiben, ohne dass auffaellt, dass es keine Begruendung ist. + + DER EINTRAG BLEIBT. Er wird nicht geloescht, sondern bekommt ein + Datum -- und verschwindet damit aus der Arbeitsliste, nicht aus dem + Gedaechtnis. Loeschen waere die bequeme Variante und die, nach der + in sechs Monaten niemand mehr weiss, dass es schon einmal einen + Anlauf gab. + ===================================================================== */ +entwicklungRouter.put("/workspace/api/talente/:id/beenden", gleicheHerkunft, + express.json({ limit: "8kb" }), (req, res) => { + try { + const id = nummer(req.params.id); + const e = id && db().prepare( + "SELECT id, bereich FROM eintraege WHERE id = ?").get(id); + if (!e || e.bereich !== "talente") return res.status(404).json({ fehler: "nicht_gefunden" }); + + const grund = text(req.body?.grund, 600); + if (grund.length < BEENDEN_GRUND_MIN) { + return res.status(400).json({ + fehler: "Schreib kurz dazu, woran es lag – in einem halben Jahr " + + "weiß es sonst niemand mehr.", + }); + } + /* WER IM TEAM IST, WIRD NICHT HIER BEENDET (17.09.2026). + + Gefunden am Bildschirmfoto, nicht an einer Zahl: Auf einer + Karte auf "Im Team" stand woertlich "Nichts mehr zu tun -- der + Zugang steht" und direkt darunter "Passt doch nicht?". + + Ein Beenden haette dort die KARTE stillgelegt und den ZUGANG + bestehen lassen -- die Person koennte sich weiter anmelden, + waehrend im Trichter steht, sie sei raus. Das ist schlimmer + als gar kein Ausgang: Man glaubt, gehandelt zu haben. + + Der Riegel steht hier und nicht nur im Browser. Was der Browser + prueft, ist Hoeflichkeit; was der Server prueft, ist die Regel. */ + const wo = db().prepare("SELECT stufe FROM talent_stufe WHERE eintrag_id = ?").get(id); + if (wo?.stufe === "imteam") { + return res.status(409).json({ + fehler: "Der ist schon im Team. Wer aufhört, wird unter " + + "„Personen & Zugänge“ abgeschaltet – sonst bleibt der " + + "Zugang offen, während hier steht, er sei raus.", + }); + } + const jetztZeit = jetzt(); + /* Die STUFE bleibt stehen. Wo es geendet hat, ist die Auskunft -- + "beendet in der Probe" und "beendet nach dem ersten Hinsehen" + sind zwei verschiedene Saetze. */ + const da = db().prepare("SELECT eintrag_id FROM talent_stufe WHERE eintrag_id = ?").get(id); + if (da) { + db().prepare(`UPDATE talent_stufe + SET beendet_am = ?, beendet_grund = ?, beendet_von = ? + WHERE eintrag_id = ?`).run(jetztZeit, grund, req.person.id, id); + } else { + db().prepare(`INSERT INTO talent_stufe + (eintrag_id, stufe, seit, von_id, beendet_am, beendet_grund, beendet_von) + VALUES (?, 'aufgefallen', ?, ?, ?, ?, ?)`) + .run(id, jetztZeit, req.person.id, jetztZeit, grund, req.person.id); + } + protokolliere("talent_beendet", { + personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), + detail: `#${id}`, + }); + res.json({ ok: true, beendet: true }); + } catch (f) { + console.error("[talente] Beenden:", f?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } + }); + +/* WIEDER AUFNEHMEN -- und der alte Grund bleibt stehen. + + Menschen aendern sich; ein "damals zu wenig Zeit" ist in einem Jahr + keine Aussage mehr. Wer wieder aufgenommen wird, startet deshalb + wieder bei "aufgefallen" -- aber der Satz von damals wandert in die + Notiz, statt geloescht zu werden. Genau er ist beim zweiten Anlauf + das Wertvollste, was man hat. */ +entwicklungRouter.put("/workspace/api/talente/:id/wieder", gleicheHerkunft, + (req, res) => { + try { + const id = nummer(req.params.id); + const stand = id && db().prepare( + `SELECT s.eintrag_id, s.beendet_am, s.beendet_grund, s.notiz + FROM talent_stufe s JOIN eintraege e ON e.id = s.eintrag_id + WHERE s.eintrag_id = ? AND e.bereich = 'talente'`).get(id); + if (!stand) return res.status(404).json({ fehler: "nicht_gefunden" }); + if (!stand.beendet_am) { + return res.status(409).json({ fehler: "Der läuft doch noch." }); + } + const alt = stand.beendet_grund + ? `Beim ersten Anlauf beendet: ${stand.beendet_grund}` : null; + const notiz = [stand.notiz, alt].filter(Boolean).join("\n\n").slice(0, 2000) || null; + db().prepare(`UPDATE talent_stufe + SET stufe = 'aufgefallen', seit = ?, von_id = ?, notiz = ?, + beendet_am = NULL, beendet_grund = NULL, beendet_von = NULL, + buddy_id = NULL, erste_schicht = NULL + WHERE eintrag_id = ?`) + .run(jetzt(), req.person.id, notiz, id); + /* Die Probe faengt neu an -- alte Haken wuerden sonst eine + Probezeit beschreiben, die vier Wochen zurueckliegt. */ + db().prepare("DELETE FROM talent_probe WHERE eintrag_id = ?").run(id); + protokolliere("talent_wieder", { + personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), + detail: `#${id}`, + }); + res.json({ ok: true }); + } catch (f) { + console.error("[talente] Wieder:", f?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } + }); + /** Buddy und Datum prüfen. Gibt `{ fehlt: [...] }` oder `{ buddyId, schicht }`. * * EINE STELLE, ZWEI AUFRUFER: der Stufenwechsel auf „Probe" und das diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 6e1117e4..a8c8d1eb 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609171614", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609171838", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609171614", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609171838", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609171614", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609171838", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609171614", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609171838", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/entwicklung.css b/workspace/assets/css/entwicklung.css index 8e4ae84c..ccb21cb4 100644 --- a/workspace/assets/css/entwicklung.css +++ b/workspace/assets/css/entwicklung.css @@ -1111,3 +1111,140 @@ /* Wer ein Merkmal gesehen hat. Steht am Knopf, klein -- 27 Knoepfe nebeneinander vertragen keine zweite Zeile in Normalgroesse. */ .t-merkmal__wer { font-size: .72rem; opacity: .6; margin-inline-start: 6px; } + +/* ===================================================================== + DER AUSGANG AUS DEM TRICHTER (17.09.2026) + + LEISE, UNTEN, OHNE ROT. Ein "Beenden" in Warnfarbe waere eine + Drohung -- gemeint ist das Gegenteil: Eine Absage ist ein normaler, + ordentlicher Ausgang, kein Schaden. Rot gehoert an Dinge, die + schiefgehen, nicht an Entscheidungen, die getroffen werden. + + Erst der Satz als Knopf, dann das Feld. Zugeklappt nimmt der ganze + Block eine Zeile. + ===================================================================== */ +.t-ausgang { + margin-top: 14px; + padding-top: 12px; + border-top: 1px solid color-mix(in srgb, var(--rand) 60%, transparent); +} + +.t-ausgang__auf { + background: none; border: 0; padding: 2px 0; + color: var(--text-still); + font: inherit; font-size: .82rem; + cursor: pointer; + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-color: color-mix(in srgb, var(--text-still) 45%, transparent); +} +.t-ausgang__auf:hover { color: var(--text-leise); } +.t-ausgang__auf:focus-visible { + outline: 2px solid var(--akzent); outline-offset: 3px; border-radius: 4px; +} + +.t-ausgang__form { margin-top: 10px; } + +/* Der Hinweis anstelle des Ausgangs -- an Karten, die schon im Team + sind. Gleiche Groesse wie der leise Knopf daneben, damit er nicht + wie eine Warnung wirkt. */ +.t-ausgang__anderswo { + margin: 0; font-size: .82rem; line-height: 1.55; + color: var(--text-still); +} +.t-ausgang__anderswo a { color: var(--text-leise); } + +.t-ausgang__satz { + margin: 0 0 8px; font-size: .82rem; line-height: 1.55; + color: var(--text-leise); +} + +.t-ausgang__schild { + display: block; margin-bottom: 4px; + font-size: .82rem; font-weight: 600; +} + +.t-ausgang__feld { + width: 100%; box-sizing: border-box; + /* Undurchsichtig gemischt, nicht mit `transparent` (17.09.2026): + `color-mix(..., transparent)` laesst den Hintergrund durch -- auf + einer Seite mit Wasserzeichen steht er dann im Text. */ + background: color-mix(in srgb, var(--tinte) 88%, #ffffff 12%); + color: var(--text); + border: 1px solid var(--rand-hell); + border-radius: 8px; + padding: 8px 10px; + font: inherit; font-size: .88rem; line-height: 1.5; + resize: vertical; +} +.t-ausgang__feld:focus-visible { + outline: 2px solid var(--akzent); outline-offset: 2px; + border-color: var(--akzent); +} + +.t-ausgang__knopf { margin-top: 8px; } + +/* ===================================================================== + WAS BEENDET WURDE + + Eine eigene Sammlung unter der Arbeitsliste. Zugeklappt eine Zeile + mit einer Zahl; aufgeklappt Karten, die deutlich stiller sind als + die oben -- sie sind Gedaechtnis, keine Arbeit. + ===================================================================== */ +.t-beendet { margin-top: 26px; } + +.t-beendet__auf { + width: 100%; + background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%); + border: 1px solid var(--rand); + border-radius: 10px; + padding: 10px 14px; + color: var(--text-leise); + font: inherit; font-size: .86rem; + text-align: start; + cursor: pointer; +} +.t-beendet__auf:hover { border-color: var(--rand-hell); color: var(--text); } +.t-beendet__auf:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; } + +.t-beendet__karte { + margin-top: 10px; + padding: 12px 14px; + background: color-mix(in srgb, var(--tinte) 96%, #ffffff 4%); + border: 1px solid color-mix(in srgb, var(--rand) 70%, transparent); + border-radius: 10px; +} + +.t-beendet__kopf { + display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px; +} +.t-beendet__titel { + margin: 0; font-size: .96rem; font-weight: 600; + /* Nicht per `opacity` gedaempft -- das macht den Kasten durchsichtig + und laesst den Hintergrund durch den Text. Eine leisere FARBE tut + dasselbe, ohne die Flaeche anzufassen. */ + color: var(--text-leise); +} +.t-beendet__stufe { font-size: .8rem; color: var(--text-still); } +.t-beendet__zeit { font-size: .78rem; color: var(--text-still); margin-inline-start: auto; } + +.t-beendet__grund { + margin: 6px 0 0; font-size: .86rem; line-height: 1.55; + color: var(--text); +} +.t-beendet__wer { margin: 4px 0 0; font-size: .76rem; color: var(--text-still); } + +.t-beendet__wieder { + margin-top: 10px; + background: none; + border: 1px solid var(--rand-hell); + border-radius: 999px; + padding: 5px 12px; + color: var(--text-leise); + font: inherit; font-size: .8rem; + cursor: pointer; +} +.t-beendet__wieder:hover { color: var(--text); border-color: var(--akzent); } +.t-beendet__wieder:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; } +.t-beendet__wieder[disabled] { opacity: .5; cursor: default; } + diff --git a/workspace/assets/js/talente.js b/workspace/assets/js/talente.js index b163ce44..1cb0ac29 100644 --- a/workspace/assets/js/talente.js +++ b/workspace/assets/js/talente.js @@ -294,9 +294,127 @@ a.append(warn); } + /* ---- Der Ausgang ---------------------------------------------------- + + Filipe: „alles mögliche, wie gesagt ich will hoch profissionel + arbeiten." + + ER IST LEISE UND ER IST DA. Bis heute hatte der Trichter fünf + Stufen und genau einen Ausgang: ins Team. Wer nicht passte, blieb + auf seiner Stufe stehen, mit wachsender Standzeit und rotem + Zeichen -- obwohl die Stufe selbst verspricht: „übernehmen oder + sauber beenden". + + ER STEHT UNTEN UND SIEHT NICHT AUS WIE EIN KNOPF. Oben steht der + eine nächste Schritt; wenn daneben gleich groß „Beenden" stünde, + wären es wieder zwei Möglichkeiten und keine Anweisung. + + UND ES GIBT KEINE ZUSÄTZLICHE RÜCKFRAGE. Der Satz IST die + Rückfrage -- wer einen Grund tippen muss, hat länger nachgedacht + als jemand, der „Wirklich beenden?" wegklickt. Ein Dialog obendrauf + wäre ein Klick mehr und würde genau das trainieren: wegklicken. */ + a.append(ausgangZeigen(k)); + return a; } + /* Der Ausgang einer Karte: erst ein leiser Satz, dann das Feld. */ + function ausgangZeigen(k) { + const kasten = el('div', 't-ausgang'); + + /* WER IM TEAM IST, HAT EINEN ZUGANG -- und der wird woanders + abgeschaltet (17.09.2026, am Bildschirmfoto gefunden). + + Auf einer Karte auf „Im Team" stand „Nichts mehr zu tun – der + Zugang steht" und direkt darunter „Passt doch nicht?". Wer das + geklickt hätte, hätte die Karte stillgelegt und den Zugang + offengelassen: eine halbe Handlung, die sich wie eine ganze + anfühlt. + + Statt des Ausgangs steht deshalb der Weg, der wirklich hilft. + Nicht weglassen -- wer nichts findet, sucht, und wer nichts + findet und aufgibt, lässt den Zugang offen. */ + if (k.stufe === 'imteam') { + const hin = el('p', 't-ausgang__anderswo'); + hin.append(document.createTextNode('Hört jemand auf? Der Zugang wird unter ')); + const a = el('a', null, 'Personen & Zugänge'); + a.href = 'personen.html'; + hin.append(a); + hin.append(document.createTextNode(' abgeschaltet – diese Karte bleibt als ' + + 'Verlauf stehen.')); + kasten.append(hin); + return kasten; + } + + const oeffnen = el('button', 't-ausgang__auf', 'Passt doch nicht?'); + oeffnen.type = 'button'; + oeffnen.setAttribute('aria-expanded', 'false'); + + const form = el('form', 't-ausgang__form'); + form.hidden = true; + form.append(el('p', 't-ausgang__satz', + 'Schreib auf, woran es lag. Der Satz bleibt an der Karte – ' + + 'wenn dieselbe Person in einem halben Jahr wieder auffällt, ' + + 'ist er das Einzige, was dann noch hilft.')); + + const id = 'aus-' + k.id; + const schild = el('label', 't-ausgang__schild', 'Woran lag es?'); + schild.htmlFor = id; + const feld = document.createElement('textarea'); + feld.id = id; + feld.className = 't-ausgang__feld'; + feld.rows = 3; + feld.maxLength = 600; + feld.required = true; + feld.placeholder = 'z. B. hat sich nach der zweiten Schicht nicht mehr gemeldet'; + form.append(schild, feld); + + const knopf = el('button', 'schritt t-ausgang__knopf', 'Sauber beenden'); + knopf.type = 'submit'; + form.append(knopf); + + oeffnen.addEventListener('click', () => { + const zu = form.hidden; + form.hidden = !zu; + oeffnen.setAttribute('aria-expanded', String(zu)); + if (zu) feld.focus(); + }); + + form.addEventListener('submit', async (e) => { + e.preventDefault(); + melde(''); + const grund = feld.value.trim(); + /* DIESELBE GRENZE WIE AUF DEM SERVER -- und sie steht dort, nicht + hier: Was der Browser prüft, ist Höflichkeit, was der Server + prüft, ist die Regel. */ + if (grund.length < 10) { + melde('Ein Satz gehört dazu – „passt nicht" ist keiner.'); + feld.focus(); + return; + } + knopf.disabled = true; + try { + const r = await hole(`/workspace/api/talente/${k.id}/beenden`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ grund }), + }); + if (!r.ok) { + melde(absage(await r.json().catch(() => ({})))); + knopf.disabled = false; + return; + } + await laden(); + } catch { + melde('Ging nicht.'); + knopf.disabled = false; + } + }); + + kasten.append(oeffnen, form); + return kasten; + } + /* ---- Aus der Probe wird ein Zugang --------------------------------- DERSELBE WEG WIE IN "PERSONEN & ZUGAENGE", nicht ein zweiter. @@ -535,7 +653,12 @@ datum_zu_alt: 'Das Datum liegt zu weit zurück.', datum_zu_weit: 'Das Datum liegt zu weit in der Zukunft – das ist kein Termin mehr.', }; - const absage = (antwort) => UEBERGANG_ABSAGE[(antwort?.fehlt || [])[0]] || 'Ging nicht.'; + /* Erst die bekannten Fehlstellen, dann der Satz des Servers, dann + erst der allgemeine. Bis heute fiel ein erklärender Satz vom Server + hier lautlos auf „Ging nicht." zurück -- die Auskunft war da und + wurde weggeworfen. */ + const absage = (antwort) => UEBERGANG_ABSAGE[(antwort?.fehlt || [])[0]] + || antwort?.fehler || 'Ging nicht.'; function merkmal(k, m, istWarnung) { const gesetzt = (istWarnung ? k.warnzeichen : k.merkmale).includes(m.schluessel); @@ -740,6 +863,10 @@ knoepfe.append(t); leer.append(knoepfe); ziel.append(leer); + /* AUCH AUF DER LEEREN SEITE. Wer gerade den letzten Kandidaten + beendet hat, sähe sonst „Noch niemand auf der Liste" und + müsste glauben, seine Arbeit sei verschwunden. */ + beendeteZeigen(ziel); return; } /* LIEGENGEBLIEBENE ZUERST. Die Reihenfolge ist die eigentliche @@ -755,6 +882,87 @@ fuss.append(a); fuss.append('. Von dort landet es hier automatisch auf „Aufgefallen".'); ziel.append(fuss); + + beendeteZeigen(ziel); + } + + /* ---- Was beendet wurde ------------------------------------------------ + + ZUGEKLAPPT, UNTEN, MIT ZAHL. Nicht gelöscht und nicht oben. + + Gelöscht wäre bequem -- und in sechs Monaten wüsste niemand mehr, + dass es schon einmal einen Anlauf gab. Oben wäre es die Liste, die + die Arbeitsliste erdrückt: nach einem Jahr stehen hier mehr + Beendete als Kandidaten, und dann schaut niemand mehr auf die + Seite. + + Aufgeklappt steht an jedem, WO er ausgestiegen ist und WARUM. + Beides zusammen -- „beendet in der Probe, hat sich nicht mehr + gemeldet" ist eine Auskunft, „beendet" ist keine. */ + const beendeteOffen = { ja: false }; + + function beendeteZeigen(ziel) { + const liste = lage.beendete || []; + if (!liste.length) return; + + const kasten = el('section', 't-beendet'); + const schalter = el('button', 't-beendet__auf', + beendeteOffen.ja + ? 'Beendete zuklappen' + : `Beendet: ${liste.length} ${liste.length === 1 ? 'Kandidat' : 'Kandidaten'}`); + schalter.type = 'button'; + schalter.setAttribute('aria-expanded', String(beendeteOffen.ja)); + schalter.addEventListener('click', () => { + beendeteOffen.ja = !beendeteOffen.ja; + zeichne(); + }); + kasten.append(schalter); + + if (beendeteOffen.ja) { + for (const k of liste) kasten.append(beendetKarte(k)); + } + ziel.append(kasten); + } + + function beendetKarte(k) { + const a = el('article', 't-beendet__karte'); + const kopf = el('div', 't-beendet__kopf'); + kopf.append(el('h3', 't-beendet__titel', k.titel)); + /* WO ES GEENDET HAT, nicht nur DASS. Die Stufe blieb deshalb + stehen, statt auf einen eigenen Wert „beendet" umgeschrieben zu + werden -- „nach dem ersten Hinsehen" und „in der Probe" sind + zwei verschiedene Sätze. */ + kopf.append(el('span', 't-beendet__stufe', 'in „' + k.stufe_name + '"')); + kopf.append(el('span', 't-beendet__zeit', kurzDatum(k.beendet.am))); + a.append(kopf); + + if (k.beendet.grund) a.append(el('p', 't-beendet__grund', k.beendet.grund)); + if (k.beendet.von) { + a.append(el('p', 't-beendet__wer', 'beendet von ' + k.beendet.von)); + } + + const wieder = el('button', 't-beendet__wieder', 'Wieder aufnehmen'); + wieder.type = 'button'; + wieder.title = 'Beginnt neu bei „Aufgefallen". Der Satz von damals ' + + 'wandert in die Notiz.'; + wieder.addEventListener('click', async () => { + wieder.disabled = true; + melde(''); + try { + const r = await hole(`/workspace/api/talente/${k.id}/wieder`, { method: 'PUT' }); + if (!r.ok) { + melde(absage(await r.json().catch(() => ({})))); + wieder.disabled = false; + return; + } + await laden(); + } catch { + melde('Ging nicht.'); + wieder.disabled = false; + } + }); + a.append(wieder); + return a; } async function laden() { diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index c85b0b55..9a939535 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. --> - - - - - - + + + + + + - + @@ -291,10 +291,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index f797c94d..47ac4bcc 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/befinden.html b/workspace/befinden.html index 4d7e5e66..da5f5351 100644 --- a/workspace/befinden.html +++ b/workspace/befinden.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -105,9 +105,9 @@ - - - - + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 788ba515..da12fe24 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,24 +12,24 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - - + + - + @@ -237,14 +237,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/bewerben.html b/workspace/bewerben.html index a6c34ed0..1e5f8af3 100644 --- a/workspace/bewerben.html +++ b/workspace/bewerben.html @@ -7,17 +7,17 @@ - - - - - - - - - + + + + + + + + + - + @@ -107,10 +107,10 @@ - - - - - + + + + + diff --git a/workspace/bewerbungen.html b/workspace/bewerbungen.html index 5458f9d0..b59a1831 100644 --- a/workspace/bewerbungen.html +++ b/workspace/bewerbungen.html @@ -7,16 +7,16 @@ - - - - - - - - + + + + + + + + - + @@ -80,10 +80,10 @@ - - - - - + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index c61c6555..39322709 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 ee5859a4..e8ba1bce 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 5f88637c..57ec962d 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 cdc5861d..3ba1527d 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 ad76ffef..9aa7db06 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609171614", + "src": "/assets/img/app-symbole/crew-192.png?v=202609171838", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609171614", + "src": "/assets/img/app-symbole/crew-512.png?v=202609171838", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609171614", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609171838", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609171614", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609171838", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index ad50f7ae..676f8889 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 ca14e58d..433145ed 100644 --- a/workspace/entwicklung.html +++ b/workspace/entwicklung.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -102,9 +102,9 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index dc55d869..7c5088dc 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 e382913b..4bab1650 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 12c45f71..6d1b9ad7 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. --> - - - - - - - + + + + + + + - + @@ -444,10 +444,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index 3d2745c3..a754e345 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 10f29216..04159e11 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 0bd32402..764abfb6 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 291ca2b3..8b796c60 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 0197955d..4649e36d 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 0c1726ca..3c7c055d 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 a848e7a0..0b932d81 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 bac542da..8dfadeb7 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 f21ae29f..194a88ba 100644 --- a/workspace/talente.html +++ b/workspace/talente.html @@ -7,18 +7,18 @@ - - - - - - + + + + + + - - - + + + @@ -111,9 +111,9 @@ - - - - + + + + diff --git a/workspace/team.html b/workspace/team.html index e77e8f4a..56b116aa 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 8eaad048..5ec81a61 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/teilen.html b/workspace/teilen.html index c111afe2..3e1168c9 100644 --- a/workspace/teilen.html +++ b/workspace/teilen.html @@ -7,13 +7,13 @@ - - - - - - - + + + + + + + @@ -112,9 +112,9 @@ - - - - + + + + diff --git a/workspace/treff-moderation.html b/workspace/treff-moderation.html index a1487f2c..c005bacf 100644 --- a/workspace/treff-moderation.html +++ b/workspace/treff-moderation.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -126,9 +126,9 @@ - - - - + + + + diff --git a/workspace/treff-regeln.html b/workspace/treff-regeln.html index fbd638b4..c9be0f94 100644 --- a/workspace/treff-regeln.html +++ b/workspace/treff-regeln.html @@ -7,18 +7,18 @@ - - - - - - - + + + + + + + - + @@ -211,10 +211,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 c03f8932..056cd2b5 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 58093d00..d22450a7 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 @@ - - - - - + + + + +