diff --git a/server/pruef-nachruesten.mjs b/server/pruef-nachruesten.mjs index 6f3842d0..87d18645 100644 --- a/server/pruef-nachruesten.mjs +++ b/server/pruef-nachruesten.mjs @@ -70,7 +70,12 @@ const { scryptSync, randomBytes } = await import("node:crypto"); wirklich gab. ======================================================================= */ const ALT = ["eintrag_id", "stufe", "seit", "von_id", "notiz", "buddy_id", "erste_schicht"]; -const NEU = ["beendet_am", "beendet_grund", "beendet_von"]; +/* Die Spalten, die auf einer bestehenden Datenbank nachkommen muessen. + `person_id` kam am Abend des 17.09. dazu (die Verknuepfung zwischen + Talentkarte und angelegtem Zugang) und wird hier mitgefuehrt: Jede + neue Spalte gehoert in diese Liste, sonst prueft die Datei den Weg + von gestern. */ +const NEU = ["beendet_am", "beendet_grund", "beendet_von", "person_id"]; { const d = new DatabaseSync(process.env.WORKSPACE_DB); diff --git a/server/pruef-nachwuchs.mjs b/server/pruef-nachwuchs.mjs index c22ddb10..d0e37257 100644 --- a/server/pruef-nachwuchs.mjs +++ b/server/pruef-nachwuchs.mjs @@ -1235,6 +1235,296 @@ melde("=== 15. Im echten Browser ==="); } } +/* ======================================================================= + 16. AUS DEM TALENT WIRD EIN MENSCH MIT AUFGABEN (17.09.2026) + + Filipe: „wie talente bewertet werden, aufgaben bekommen, analysiert + werden von vanvan und mir, alles moegliche." + + Gemessen vor dem Bau: Der letzte Schritt des Trichters legt einen + Zugang an -- und die Verbindung wurde NIRGENDS gespeichert. Die + Karte wusste hinterher nicht, wer aus ihr geworden ist. Damit ging + weder der Sprung zur Person noch die Frage, aus welchem Kandidaten + welches Teammitglied wurde -- und erst recht nicht, ihm aus der + Karte heraus seine ersten Aufgaben zu geben. + ======================================================================= */ +melde(""); +melde("=== 16. Vom Talent zur Person mit Aufgaben ==="); +{ + /* Ein eigener, frischer Kandidat. Der aus Abschnitt 13/14 ist durch + den Ausgang und wieder hinein gelaufen -- an ihm waere „hat noch + keine Person" nicht falsch, sondern unpruefbar. */ + const neuer = await roh("/workspace/api/bereich/talente", HAUS, { cookie: kDogi }, + { art: "ruhig", titel: "Marek aus dem Stream", text: "Faellt auf." }); + ok(neuer.code === 201 || neuer.code === 200, `ein frischer Kandidat (${neuer.code})`); + + const finde = async (titel) => (json(await roh("/workspace/api/talente/lage", HAUS, + { cookie: kDogi })).kandidaten || []).find((k) => k.titel === titel); + + let k = await finde("Marek aus dem Stream"); + ok(!!k && k.person === null, + `am Anfang haengt kein Mensch an der Karte (${JSON.stringify(k?.person)})`); + + /* ---- Die Nummer wird geprueft, nicht geglaubt ---- */ + { + const r = await roh(`/workspace/api/talente/${k.id}/stufe`, HAUS, { cookie: kDogi }, + { stufe: "beobachtet", person_id: 999999 }, "PUT"); + ok(r.code === 400, `eine erfundene Zugangsnummer wird abgewiesen (${r.code})`); + /* GEGENPROBE zur Wirkung: Die Stufe darf sich dabei NICHT geaendert + haben -- eine abgewiesene Anfrage, die trotzdem die Haelfte tut, + waere schlimmer als eine, die durchgeht. */ + k = await finde("Marek aus dem Stream"); + ok(k.stufe === "aufgefallen", `und die Stufe bleibt stehen (${k.stufe})`); + } + + /* ---- Der Weg bis ins Team ---- */ + { + const morgen = tagLokal(1); + await roh(`/workspace/api/talente/${k.id}/stufe`, HAUS, { cookie: kDogi }, + { stufe: "beobachtet" }, "PUT"); + await roh(`/workspace/api/talente/${k.id}/stufe`, HAUS, { cookie: kDogi }, + { stufe: "angesprochen" }, "PUT"); + const hand = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })) + .buddys?.[0]; + await roh(`/workspace/api/talente/${k.id}/stufe`, HAUS, { cookie: kDogi }, + /* Die Felder heissen `buddy_id` und `erste_schicht` -- beim + ersten Anlauf stand hier `buddy`/`datum`, und die Route wies + den Uebergang ab. Der Fehler lag in dieser Datei, nicht im + Code; nachgesehen in uebergangPruefen(). */ + { stufe: "probe", buddy_id: hand?.id, erste_schicht: morgen }, "PUT"); + k = await finde("Marek aus dem Stream"); + ok(k.stufe === "probe", `er steht in der Probe (${k.stufe})`); + } + + /* ---- Der Zugang entsteht und wird VERKNUEPFT ---- */ + let personId = null; + { + const p = await roh("/workspace/api/verwaltung/personen", HAUS, { cookie: kDogi }, + { name: "Marek", rolle: "modi" }); + ok(p.code === 200 || p.code === 201, `der Zugang entsteht (${p.code})`); + personId = json(p).id; + ok(!!personId, `und hat eine Nummer (${personId})`); + + const r = await roh(`/workspace/api/talente/${k.id}/stufe`, HAUS, { cookie: kDogi }, + { stufe: "imteam", person_id: personId }, "PUT"); + ok(r.code === 200, `die Karte geht ins Team (${r.code})`); + + k = await finde("Marek aus dem Stream"); + ok(k.person?.id === personId, + `und weiss jetzt, wer daraus wurde (${k.person?.name})`); + ok(k.person?.aktiv === true, "der Zugang laeuft"); + } + + /* ---- Die Verknuepfung ueberlebt einen Stufenwechsel ---- + + `person_id` wird nur gesetzt, nie geloescht. Ohne diese Regel + verloere die Karte ihren Menschen, sobald jemand sie zurueckstellt + -- und niemand wuesste mehr, wer das war. */ + { + /* ZURUECK AUF "angesprochen", NICHT AUF "probe". + + Beim ersten Anlauf stand hier `probe` -- und dieser Uebergang + verlangt Buddy und Datum (uebergangPruefen mit pflicht: true). + Ohne sie antwortete die Route mit 400, die Stufe blieb stehen, + und die Zeile darunter bestaetigte, dass sich nichts geaendert + hat: eine Pruefung, die immer gruen ist. + + Aufgefallen ist das erst an der GEGENPROBE -- mit ausgebautem + COALESCE blieb sie gruen, obwohl sie genau das absichern soll. + Eine Gegenprobe ist deshalb keine Kuer: Sie prueft die Pruefung. */ + const r = await roh(`/workspace/api/talente/${k.id}/stufe`, HAUS, { cookie: kDogi }, + { stufe: "angesprochen" }, "PUT"); + ok(r.code === 200, `ein Schritt zurueck geht durch (${r.code})`); + const zurueck = await finde("Marek aus dem Stream"); + ok(zurueck.stufe === "angesprochen", + `und er steht wirklich dort (${zurueck.stufe})`); + ok(zurueck.person?.id === personId, + "der Schritt zurueck nimmt ihr den Menschen nicht weg"); + await roh(`/workspace/api/talente/${k.id}/stufe`, HAUS, { cookie: kDogi }, + { stufe: "imteam" }, "PUT"); + } + + /* ---- Womit faengt er an? ---- */ + { + const lage2 = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })); + ok(Array.isArray(lage2.start_kategorien) && lage2.start_kategorien.length > 0, + `es gibt ${lage2.start_kategorien?.length} Bereiche zum Anfangen`); + /* JEDER BEREICH HAT AUCH WIRKLICH AUFGABEN. Ein Knopf, der „0 + Aufgaben" anlegt, ist schlimmer als keiner. */ + ok((lage2.start_kategorien || []).every((g) => g.anzahl > 0), + "und in jedem steht mindestens eine"); + ok((lage2.start_kategorien || []).every((g) => g.name && g.wert), + "jeder hat Namen und Schluessel"); + + const g = lage2.start_kategorien[0]; + const vorher = lage2.schon_vergeben?.[personId] || 0; + ok(vorher === 0, `er hat noch nichts (${vorher})`); + + const r = await roh("/workspace/api/vorlagen/uebernehmen", HAUS, { cookie: kDogi }, + { art: "katalog_alle", gruppe: g.wert, stufe: "neu", person_id: personId }); + ok(r.code === 200, `die Aufgaben lassen sich geben (${r.code})`); + ok(json(r).angelegt === g.anzahl, + `und es sind genau so viele, wie der Knopf versprochen hat ` + + `(${json(r).angelegt} von ${g.anzahl})`); + + const lage3 = json(await roh("/workspace/api/talente/lage", HAUS, { cookie: kDogi })); + ok((lage3.schon_vergeben?.[personId] || 0) === g.anzahl, + `die Karte zeigt jetzt ${lage3.schon_vergeben?.[personId]} offene`); + } + + /* ---- Und die Community kommt an nichts davon ---- */ + { + const l = await roh("/workspace/api/talente/lage", CREW, { cookie: kModi }); + ok(l.code === 404, `ein Modi sieht den Trichter nicht (${l.code})`); + } +} + +/* ======================================================================= + 17. DER ANSCHLUSS IM BROWSER (17.09.2026) + + Abschnitt 16 weist nach, dass die Verknuepfung in der Datenbank steht + und die Aufgaben sich geben lassen. Hier geht es darum, ob man es + SIEHT und ob der Weg mit einem Klick geht -- der ganze Zweck war ja, + die vier Schritte auf einer anderen Seite loszuwerden. + + EIGENER BROWSER, NICHT DER AUS ABSCHNITT 15. Der lief, bevor es + „Marek" ueberhaupt gab. Einen Browserstand von zwei Abschnitten + vorher weiterzubenutzen waere keine Messung, sondern eine Annahme. + ======================================================================= */ +melde(""); +melde("=== 17. Der Anschluss im 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 -- der Anschluss wurde nicht angesehen"); + } else { + const browser = await chromium.launch(); + try { + const sicht = await browser.newContext({ viewport: { width: 1280, height: 900 } }); + 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(1600); + + const leute = await seite.evaluate(() => + [...document.querySelectorAll(".t-person")].map((p2) => ({ + name: p2.querySelector(".t-person__name")?.textContent || "", + ziel: p2.querySelector(".t-person__name")?.getAttribute("href") || "", + sichtbar: p2.offsetHeight > 0, + schon: p2.querySelector(".t-person__schon")?.textContent || "", + }))); + ok(leute.length > 0, `an ${leute.length} Karte(n) steht, wer daraus wurde`); + ok(leute.length > 0 && leute.every((x) => x.sichtbar), "und man sieht es auch"); + ok(leute.some((x) => x.ziel === "personen.html"), + `mit dem Weg zu den Zugaengen (${leute[0]?.ziel})`); + ok(leute.some((x) => /[0-9]/.test(x.schon)), + `und mit der Zahl dessen, was schon offen ist ("${leute.map((x) => x.schon).join("|")}")`); + + /* GEGENPROBE: NICHT an jeder Karte. Wer nicht im Team ist, hat + auch keinen Menschen -- sonst hiesse „daraus wurde" nur, dass + ueberall etwas steht. */ + const karten = await seite.locator(".t-karte").count(); + ok(karten > leute.length, `aber nicht an allen (${leute.length} von ${karten})`); + + /* ---- Der Kasten mit den ersten Aufgaben ---- */ + const kaesten = await seite.locator(".t-start").count(); + ok(kaesten > 0, `der Kasten fuer die ersten Aufgaben steht da (${kaesten})`); + + await seite.locator(".t-start__auf").first().click(); + await seite.waitForTimeout(500); + const knoepfe = await seite.evaluate(() => + [...document.querySelectorAll(".t-start__kat")].map((b) => ({ + name: b.querySelector(".t-start__kat-name")?.textContent || "", + zahl: b.querySelector(".t-start__kat-zahl")?.textContent || "", + hoehe: b.offsetHeight, + }))); + ok(knoepfe.length > 5, `aufgeklappt stehen ${knoepfe.length} Bereiche zur Wahl`); + ok(knoepfe.length > 0 && knoepfe.every((b) => b.name && /[0-9]/.test(b.zahl)), + `jeder nennt Namen und Anzahl ("${knoepfe[0]?.name}", "${knoepfe[0]?.zahl}")`); + /* Mit einem Finger treffbar -- die Seite wird am Handy benutzt. */ + ok(knoepfe.length > 0 && knoepfe.every((b) => b.hoehe >= 44), + `und treffbar (kleinster ${Math.min(...knoepfe.map((b) => b.hoehe))} px)`); + + /* WAS HIER ZU PRUEFEN IST -- und was nicht (17.09.2026). + + Der erste Anlauf verlangte, dass das Wort NIRGENDS auf der + Seite steht. Das war zu scharf und hat prompt einen Treffer + gemeldet: „Die Modis, die mit ihm gearbeitet haben" -- ein Satz + aus dem Probeplan, der ueber /api/talente/lage kommt und damit + nur bei jemandem ankommt, der ohnehin alles sieht (nurLeitung). + Er gehoert genau dorthin. + + Die Regel, um die es wirklich geht, lautet anders: Das Wort darf + nicht in einer DATEI stehen, die der Server jedem ausliefert, + der die Adresse kennt. Darueber wacht pruef-modi-wortleck, und + zwar ueber alle Dateien statt ueber eine Seite. + + HIER wird deshalb das geprueft, was HIER neu ist: Die Bausteine + fuer den Anschluss duerfen die Rolle nicht nennen -- weder an + einem Knopf noch in einem Schild. Sie werden im Browser gebaut, + also faellt es nur hier auf. */ + const neueTexte = await seite.evaluate(() => + [...document.querySelectorAll(".t-start, .t-person")] + .map((e) => e.textContent || "").join(" ")); + ok(!/modis?/i.test(neueTexte), + `die neuen Bausteine nennen die Rolle nicht (${neueTexte.length} Zeichen geprueft)`); + /* GEGENPROBE zur Voraussetzung: Es wurde ueberhaupt etwas + angesehen. Ohne Text waere die Zeile darueber auch dann gruen, + wenn die Kaesten gar nicht da sind. */ + /* GEMESSEN WIRD AUFGEKLAPPT. Zugeklappt steht dort ein + Schaltersatz von 64 Zeichen -- daran ist nichts zu sehen. + Die Zahl haengt an den Kategorien, die der Server liefert, + nicht an einer geratenen Untergrenze. */ + ok(neueTexte.length > 200 && knoepfe.length > 5, + `und es stand wirklich etwas da (${neueTexte.length} Zeichen, ` + + `${knoepfe.length} Bereiche)`); + + /* ---- EIN KLICK LEGT SIE AN ---- */ + { + const vorher = Number((leute[0]?.schon.match(/\d+/) || [0])[0]); + const name = knoepfe[1]?.name; + const menge = Number((knoepfe[1]?.zahl.match(/\d+/) || [0])[0]); + await seite.locator(".t-start__kat").nth(1).click(); + await seite.waitForTimeout(1400); + const meldung = await seite.evaluate(() => + document.querySelector(".t-start__fertig")?.textContent || ""); + ok(/bereit/.test(meldung), `ein Klick meldet, was entstanden ist ("${meldung.trim()}")`); + ok(/Aufgaben ansehen/.test(meldung), "und nennt den Weg dorthin"); + const nachher = await seite.evaluate(() => + document.querySelector(".t-person__schon")?.textContent || ""); + const n2 = Number((nachher.match(/\d+/) || [0])[0]); + ok(n2 === vorher + menge, + `die Zahl an der Karte zieht mit (${vorher} + ${menge} = ${n2}), ` + + `Bereich „${name}"`); + } + + /* KEIN ROLLENNAME IM AUSGELIEFERTEN ZUSTAND. Die Seite bekommt + jeder, der die Adresse kennt -- am 16.09. stand das Wort schon + einmal im Quelltext. */ + + + ok(konsole.length === 0, + konsole.length ? `Konsolenfehler: ${konsole[0]}` : "kein Konsolenfehler"); + + await seite.screenshot({ path: "server/pruef-nachwuchs-anschluss.png", fullPage: true }); + melde(" -- Bildschirmfoto: server/pruef-nachwuchs-anschluss.png"); + } finally { + await browser.close(); + } + } +} + /* ------------------------------------------------------------------- */ melde(""); melde(`${geprueft} Pruefungen, ${fehler} Fehler` diff --git a/server/workspace-entwicklung.js b/server/workspace-entwicklung.js index bbaed146..4cb0c1ce 100644 --- a/server/workspace-entwicklung.js +++ b/server/workspace-entwicklung.js @@ -61,8 +61,12 @@ import express from "express"; import { db, sitzungLesen, protokolliere, echteIp, fuehrtTeamDogi, ROLLEN_NAME, - ROLLEN_SORTIERUNG, + ROLLEN_SORTIERUNG, MODI_KATEGORIEN, kategorienFuer, } from "./workspace.js"; +/* Die Aufgaben, die ein Neuer bekommen kann -- dieselbe Liste, aus der + auch die Aufgabenseite ihre Vorschlaege baut. Eine zweite waere die, + die beim naechsten Nachschaerfen auseinanderlaeuft. */ +import { MODI_AUFGABEN_FLACH } from "./workspace-vorlagen.js"; import { ENTWICKLUNG_BLOECKE, ENTWICKLUNG_PUNKTE, ENTWICKLUNG_STUFEN, ENTWICKLUNG_ERWARTUNG, ENTWICKLUNG_FREMD, ENTWICKLUNG_SELBST, @@ -262,6 +266,25 @@ export function tabellen() { ["beendet_am", "TEXT"], ["beendet_grund", "TEXT"], ["beendet_von", "INTEGER"], + + /* WER AUS DEM TALENT GEWORDEN IST (17.09.2026). + + Der letzte Schritt des Trichters legt einen Zugang an -- und bis + heute war das eine Einbahnstrasse: Die Person entstand, der Code + wurde einmal gezeigt, und die Karte wusste hinterher NICHT, wer + daraus geworden ist. + + Was dadurch nicht ging: + - von der Karte zur Person springen + - dem Neuen seine ersten Aufgaben aus der Karte heraus geben + - in einem halben Jahr nachsehen, aus welchem Kandidaten + eigentlich welches Teammitglied wurde + + KEIN FREMDSCHLUESSEL AUF personen. Wird ein Zugang geloescht, + soll die Karte stehen bleiben -- sie erzaehlt, wie jemand + gekommen ist, und das bleibt wahr, auch wenn er wieder geht. Ein + CASCADE haette genau diesen Verlauf mitgeloescht. */ + ["person_id", "INTEGER"], ]) { try { const da = db().prepare("PRAGMA table_info(talent_stufe)").all().map((s) => s.name); @@ -923,9 +946,11 @@ function uebergangVon(stand) { function talentKarte(e, merkmale) { const stand = db().prepare( `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 + s.beendet_am, s.beendet_grund, p.name AS beendet_von_name, + s.person_id, w.name AS person_name, w.aktiv AS person_aktiv FROM talent_stufe s LEFT JOIN personen p ON p.id = s.beendet_von + LEFT JOIN personen w ON w.id = s.person_id WHERE s.eintrag_id = ?`) .get(e.id); const stufe = talentStufe(stand?.stufe) || TALENT_STUFEN[0]; @@ -986,6 +1011,17 @@ function talentKarte(e, merkmale) { 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. */ + /* WER DARAUS GEWORDEN IST (17.09.2026). + + `aktiv` kommt mit: Ein Zugang, der abgeschaltet wurde, soll an + der Karte nicht aussehen wie einer, der laeuft. Die Karte bleibt + trotzdem stehen -- sie erzaehlt, wie jemand gekommen ist, und das + bleibt wahr, auch wenn er wieder geht. */ + person: stand?.person_id && stand?.person_name ? { + id: stand.person_id, + name: stand.person_name, + aktiv: stand.person_aktiv === 1, + } : null, beendet: stand?.beendet_am ? { am: stand.beendet_am, grund: stand.beendet_grund || null, @@ -1073,6 +1109,57 @@ entwicklungRouter.get("/workspace/api/talente/lage", (req, res) => { Wort. Beides kommt von hier, damit im Browser kein fester Text dazu steht; siehe ZUGANG_ROLLE oben. */ zugang: { rolle: ZUGANG_ROLLE, name: ROLLEN_NAME[ZUGANG_ROLLE] ?? ZUGANG_ROLLE }, + /* WOMIT EIN NEUER ANFAENGT (17.09.2026). + + Filipe: „wie talente bewertet werden, aufgaben bekommen, + analysiert werden von vanvan und mir, alles moegliche." + + Der Trichter endete bisher beim Zugang. Wer im Team ist, hat + einen Code -- und keine einzige Aufgabe. Um ihm welche zu + geben, musste man auf eine andere Seite, die Person dort + heraussuchen, Kategorie und Stufe waehlen und uebernehmen. + Vier Schritte, die niemand macht, waehrend er gerade jemanden + aufnimmt. Genau dieses Muster hat heute schon einmal zu einer + Karte auf „Im Team" ohne Menschen darin gefuehrt. + + ES BLEIBT EIN ANGEBOT, KEINE AUTOMATIK. Filipes Entscheidung + bei der Live-Checkliste gilt hier genauso: „vierzehn Aufgaben, + die von selbst erscheinen, ueberrumpeln, und was ungefragt + Dinge anlegt, ist schwer wieder loszuwerden." Deshalb steht + hier nur, WAS es gaebe -- angelegt wird auf Klick, eine + Kategorie auf einmal. + + KEIN ROLLENNAME, nirgends. Die Kategorien heissen „chat", + „team", „technik" -- aus keiner davon laesst sich schliessen, + fuer wen sie gedacht sind. Wer den Katalog nicht bekommen + darf, bekommt `null` und die Seite baut nichts; ein leeres + Feld sagt nichts, ein Feld mit sprechendem Namen saehe jeder + im Netzwerkprotokoll seines eigenen Browsers. */ + start_kategorien: kategorienFuer(req.person).length + ? MODI_KATEGORIEN.map((k) => ({ + wert: k.wert, name: k.name, text: k.text, + anzahl: MODI_AUFGABEN_FLACH + .filter((v) => v.kategorie === k.wert && v.stufe === "neu").length, + })).filter((k) => k.anzahl > 0) + : null, + /* WAS SIE SCHON HABEN -- je Person, in EINER Abfrage. + + Ohne diese Angabe legt man beim zweiten Hinsehen dasselbe noch + einmal an; der Knopf sagt dann „6 Aufgaben" und meint die + siebte bis zwoelfte. Der Server verhindert Doppelte nicht -- + er soll es auch nicht, denn dieselbe Aufgabe ein zweites Mal + kann gewollt sein. Sichtbar muss es trotzdem sein. */ + schon_vergeben: (() => { + const ids = karten.map((k) => k.person?.id).filter(Boolean); + if (!ids.length) return {}; + const platz = ids.map(() => "?").join(", "); + const zeilen = db().prepare( + `SELECT verantwortlich_id AS wer, COUNT(*) AS n + FROM aufgaben + WHERE verantwortlich_id IN (${platz}) AND status NOT IN ('erledigt','abgebrochen') + GROUP BY verantwortlich_id`).all(...ids); + return Object.fromEntries(zeilen.map((z) => [z.wer, z.n])); + })(), kandidaten: karten, /* Nach Ausgangsdatum, das juengste zuerst -- wer gerade abgesagt hat, ist der, an den man sich erinnert. */ @@ -1390,15 +1477,39 @@ entwicklungRouter.put("/workspace/api/talente/:id/stufe", gleicheHerkunft, (req, schickt Buddy und Datum nicht noch einmal mit -- und die beiden sollen bleiben, wo sie sind. Sie sind die Geschichte dieser Probe, nicht ein Formularfeld. */ + /* WER AUS DEM TALENT GEWORDEN IST (17.09.2026). + + Der letzte Schritt legt einen Zugang an. Bis heute war das eine + Einbahnstrasse: Die Person entstand, ihr Code wurde einmal + gezeigt -- und die Karte wusste danach nicht, wer daraus wurde. + + GEPRUEFT WIRD, OB ES DIE PERSON GIBT, und nicht nur, ob eine + Zahl ankam. Eine erfundene Nummer wuerde die Karte sonst auf + jemanden zeigen lassen, den es nicht gibt -- und das faellt erst + auf, wenn jemand darauf klickt. + + NUR SETZEN, NIE LOESCHEN: Kommt kein `person_id` mit (jeder + andere Stufenwechsel), bleibt die vorhandene stehen. Sonst + verloere die Karte ihre Person, sobald jemand sie auf eine + fruehere Stufe zuruecksetzt. */ + let personId = null; + if (nummer(req.body?.person_id)) { + const wer = db().prepare("SELECT id FROM personen WHERE id = ? AND aktiv = 1") + .get(nummer(req.body.person_id)); + if (!wer) return res.status(400).json({ fehler: "Diesen Zugang gibt es nicht." }); + personId = wer.id; + } + db().prepare(`INSERT INTO talent_stufe - (eintrag_id, stufe, seit, von_id, notiz, buddy_id, erste_schicht) - VALUES (?,?,?,?,?,?,?) + (eintrag_id, stufe, seit, von_id, notiz, buddy_id, erste_schicht, person_id) + VALUES (?,?,?,?,?,?,?,?) ON CONFLICT(eintrag_id) DO UPDATE SET stufe = excluded.stufe, seit = excluded.seit, von_id = excluded.von_id, notiz = excluded.notiz, buddy_id = COALESCE(excluded.buddy_id, talent_stufe.buddy_id), - erste_schicht = COALESCE(excluded.erste_schicht, talent_stufe.erste_schicht)`) - .run(id, stufe, jetzt(), req.person.id, notiz || null, p.buddyId, p.schicht); + erste_schicht = COALESCE(excluded.erste_schicht, talent_stufe.erste_schicht), + person_id = COALESCE(excluded.person_id, talent_stufe.person_id)`) + .run(id, stufe, jetzt(), req.person.id, notiz || null, p.buddyId, p.schicht, personId); protokolliere("talent_stufe", { personId: req.person.id, ip: echteIp(req), diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index b3ce9357..85e9cc20 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609172352", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609180011", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609172352", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609180011", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609172352", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609180011", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609172352", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609180011", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/entwicklung.css b/workspace/assets/css/entwicklung.css index ccb21cb4..7c2fc859 100644 --- a/workspace/assets/css/entwicklung.css +++ b/workspace/assets/css/entwicklung.css @@ -1248,3 +1248,104 @@ .t-beendet__wieder:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; } .t-beendet__wieder[disabled] { opacity: .5; cursor: default; } +/* ===================================================================== + WER AUS DEM TALENT GEWORDEN IST (17.09.2026) + + Der letzte Schritt des Trichters legt einen Zugang an. Bis heute war + das eine Einbahnstrasse -- die Karte wusste hinterher nicht, wer + daraus wurde. Jetzt steht es da, mit dem Weg zu den Zugaengen. + ===================================================================== */ +.t-person { + display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px; + margin-top: 12px; + padding: 9px 13px; + border: 1px solid color-mix(in srgb, var(--gut, #79d1a2) 30%, var(--rand)); + border-radius: 10px; + background: color-mix(in srgb, var(--tinte) 92%, #ffffff 8%); +} +.t-person__schild { + font-size: .72rem; font-weight: 700; letter-spacing: .03em; + text-transform: uppercase; + color: var(--text-still); +} +.t-person__name { + font-size: .95rem; font-weight: 700; + color: color-mix(in srgb, var(--gut, #79d1a2) 32%, #ffffff); + text-decoration: none; + border-bottom: 1px solid transparent; +} +.t-person__name:hover { border-bottom-color: currentColor; } +.t-person__name:focus-visible { outline: 2px solid var(--akzent); outline-offset: 3px; } +.t-person__schon { font-size: .78rem; color: var(--text-leise); margin-inline-start: auto; } + +/* Ein abgeschalteter Zugang sieht anders aus als ein laufender -- die + Karte bleibt trotzdem stehen, sie erzaehlt ja, wie jemand gekommen + ist. Gedaempft wird ueber die FARBE, nicht ueber `opacity`: Das + macht den Kasten durchsichtig, und das Wasserzeichen der Seite + scheint hindurch. */ +.t-person[data-still="ja"] { border-color: var(--rand); } +.t-person[data-still="ja"] .t-person__name { color: var(--text-still); } +.t-person__still { + font-size: .74rem; + padding: 1px 7px; border-radius: 999px; + border: 1px solid var(--rand); + color: var(--text-still); +} + +/* ===================================================================== + WOMIT ER ANFAENGT + + Ein Angebot, keine Automatik: Was ungefragt Dinge anlegt, ist schwer + wieder loszuwerden (Filipes Entscheidung bei der Live-Checkliste, + und sie gilt hier genauso). + ===================================================================== */ +.t-start { margin-top: 10px; } + +.t-start__auf { + background: none; border: 0; padding: 2px 0; + color: var(--text-leise); + font: inherit; font-size: .84rem; font-weight: 600; + cursor: pointer; + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-color: color-mix(in srgb, var(--text-leise) 45%, transparent); +} +.t-start__auf:hover { color: var(--text); } +.t-start__auf:focus-visible { outline: 2px solid var(--akzent); outline-offset: 3px; border-radius: 4px; } + +.t-start__satz { + margin: 8px 0 10px; font-size: .84rem; line-height: 1.55; + color: var(--text-leise); +} + +.t-start__reihe { display: flex; flex-wrap: wrap; gap: 8px; } + +.t-start__kat { + display: flex; flex-direction: column; gap: 2px; + min-height: 44px; + padding: 7px 13px; + border: 1px solid var(--rand-hell); + border-radius: 10px; + background: color-mix(in srgb, var(--tinte) 94%, #ffffff 6%); + color: var(--text); + font: inherit; + text-align: start; + cursor: pointer; + transition: border-color .16s ease; +} +.t-start__kat:hover { border-color: var(--akzent); } +.t-start__kat:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; } +.t-start__kat[disabled] { opacity: .5; cursor: default; } +.t-start__kat-name { font-size: .86rem; font-weight: 600; } +.t-start__kat-zahl { font-size: .74rem; color: var(--text-still); } + +.t-start__fertig { + display: flex; flex-wrap: wrap; align-items: center; gap: 10px; + margin: 0 0 14px; + padding: 11px 15px; + border: 1px solid color-mix(in srgb, var(--gut, #79d1a2) 40%, var(--rand)); + border-radius: 10px; + background: color-mix(in srgb, var(--tinte) 92%, #ffffff 8%); + font-size: .88rem; +} + diff --git a/workspace/assets/js/talente.js b/workspace/assets/js/talente.js index 1cb0ac29..a655e268 100644 --- a/workspace/assets/js/talente.js +++ b/workspace/assets/js/talente.js @@ -64,6 +64,7 @@ const darfAnlegen = () => Array.isArray(darfListe) && !!lage?.zugang?.rolle && darfListe.includes(lage.zugang.rolle); const offen = new Set(); // welche Karten aufgeklappt sind + const offenStart = new Set(); // wo der Kasten mit den ersten Aufgaben offen ist /* ---- Der Trichter oben ---------------------------------------------- */ function trichter() { @@ -148,7 +149,18 @@ const r = await hole(`/workspace/api/talente/${k.id}/stufe`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ stufe: k.naechste, ...uebergang }), + /* DIE NUMMER DES NEUEN ZUGANGS WANDERT MIT (17.09.2026). + + Bis heute entstand hier ein Mensch, sein Code wurde einmal + gezeigt -- und danach wusste die Karte nicht mehr, wer aus + ihr geworden ist. Damit ging weder der Sprung zur Person + noch die Frage, aus welchem Kandidaten eigentlich welches + Teammitglied wurde. + + `zugang?.id` und nicht `zugang.id`: Bei jedem anderen + Schritt ist `zugang` null, und der Server laesst ein + fehlendes Feld ausdruecklich stehen, statt zu loeschen. */ + body: JSON.stringify({ stufe: k.naechste, ...uebergang, person_id: zugang?.id }), }); b.disabled = false; if (!r.ok) { @@ -247,6 +259,37 @@ a.append(u); } + /* ---- WER DARAUS GEWORDEN IST (17.09.2026) ------------------------- + + Steht die Karte auf „Im Team", gehoert der Mensch dazu, der + daraus entstanden ist -- mit dem Weg zu seinen Zugaengen. Ohne + ihn ist die Karte ein Verlauf, der genau an der Stelle aufhoert, + an der es interessant wird. + + IST DER ZUGANG ABGESCHALTET, steht das dabei. Eine Karte auf „Im + Team" mit einem stillgelegten Zugang daneben ist die ehrliche + Auskunft; eine, die so tut, als liefe alles, ist es nicht. */ + if (k.person) { + const wer = el('div', 't-person'); + if (!k.person.aktiv) wer.dataset.still = 'ja'; + wer.append(el('span', 't-person__schild', 'Daraus wurde')); + const name = el('a', 't-person__name', k.person.name); + name.href = 'personen.html'; + wer.append(name); + if (!k.person.aktiv) { + wer.append(el('span', 't-person__still', 'Zugang abgeschaltet')); + } + /* WAS SIE SCHON HAT. Ohne diese Zahl legt man beim zweiten + Hinsehen dasselbe noch einmal an. */ + const schon = lage.schon_vergeben?.[k.person.id] || 0; + if (schon) { + wer.append(el('span', 't-person__schon', + `${schon} ${schon === 1 ? 'offene Aufgabe' : 'offene Aufgaben'}`)); + } + a.append(wer); + if (k.person.aktiv) startAufgaben(a, k, schon); + } + /* ---- Die Probezeit -------------------------------------------------- Sie steht direkt unter dem Uebergang: waehrend einer Probe ist sie @@ -415,6 +458,111 @@ return kasten; } + /* ---- WOMIT FAENGT ER AN? (17.09.2026) ------------------------------- + + Filipe: „wie talente bewertet werden, aufgaben bekommen, analysiert + werden von vanvan und mir, alles moegliche." + + Bis heute endete der Trichter beim Zugang: ein Code und keine + einzige Aufgabe. Um ihm welche zu geben, musste man auf eine andere + Seite, die Person heraussuchen, Kategorie und Stufe waehlen. Vier + Schritte, die niemand macht, waehrend er gerade jemanden aufnimmt. + + EIN ANGEBOT, KEINE AUTOMATIK -- dieselbe Entscheidung wie bei der + Live-Checkliste: Was ungefragt Dinge anlegt, ist schwer wieder + loszuwerden. Deshalb steht hier, WAS es gaebe; angelegt wird auf + Klick, eine Kategorie auf einmal. + + ZUGEKLAPPT, WENN SCHON ETWAS DA IST. Wer bereits Aufgaben hat, + braucht den Kasten nicht offen -- er wuerde dann zum Nachlegen + einladen, und das ist selten gemeint. */ + function startAufgaben(ziel, k, schon) { + const kategorien = lage.start_kategorien; + if (!Array.isArray(kategorien) || !kategorien.length) return; + + const kasten = el('section', 't-start'); + const auf = offenStart.has(k.id) || !schon; + const schalter = el('button', 't-start__auf', + auf ? 'Aufgaben zuklappen' : 'Aufgaben für den Anfang geben'); + schalter.type = 'button'; + schalter.setAttribute('aria-expanded', String(auf)); + schalter.addEventListener('click', () => { + if (offenStart.has(k.id)) offenStart.delete(k.id); else offenStart.add(k.id); + zeichne(); + }); + kasten.append(schalter); + if (!auf) { ziel.append(kasten); return; } + + kasten.append(el('p', 't-start__satz', + 'Such aus, womit er anfangen soll. Ein Klick legt die Aufgaben ' + + 'dieses Bereichs an – mit Frist, und änderbar wie jede andere.')); + + const reihe = el('div', 't-start__reihe'); + for (const g of kategorien) { + const knopf = el('button', 't-start__kat'); + knopf.type = 'button'; + knopf.title = g.text || ''; + knopf.append(el('span', 't-start__kat-name', g.name)); + knopf.append(el('span', 't-start__kat-zahl', + `${g.anzahl} ${g.anzahl === 1 ? 'Aufgabe' : 'Aufgaben'}`)); + knopf.addEventListener('click', async () => { + melde(''); + knopf.disabled = true; + try { + const r = await hole('/workspace/api/vorlagen/uebernehmen', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + art: 'katalog_alle', gruppe: g.wert, stufe: 'neu', + person_id: k.person.id, + }), + }); + if (!r.ok) { + melde(absage(await r.json().catch(() => ({})))); + knopf.disabled = false; + return; + } + const d = await r.json().catch(() => ({})); + /* DIE ZAHL AUS DER ANTWORT, nicht die erwartete. Legt der + Server weniger an, als der Knopf versprach, soll das + dastehen und nicht verschwiegen werden. + + `d.angelegt` ist eine ZAHL (workspace-vorlagen.js: + `res.json({ angelegt: angelegt.length, titel: angelegt })`). + Der erste Anlauf stand auf `d.angelegt?.length` -- das ist + bei einer Zahl `undefined` und waere still auf die + erwartete Menge zurueckgefallen. Der Knopf haette dann „6 + Aufgaben stehen bereit" gemeldet, auch wenn null entstanden + sind. */ + const n = Number.isFinite(d.angelegt) ? d.angelegt : g.anzahl; + melde(''); + offenStart.delete(k.id); + await laden(); + if (!n) { + melde('Dabei ist nichts Neues entstanden – vermutlich steht es schon da.'); + return; + } + const hin = el('p', 't-start__fertig'); + hin.append(document.createTextNode( + `${n} ${n === 1 ? 'Aufgabe steht' : 'Aufgaben stehen'} für ` + + `${k.person.name} bereit. `)); + /* DER WEG DORTHIN GEHOERT DAZU. Eine Meldung „sechs Aufgaben + stehen bereit" ohne Link laesst einen raten, wo. */ + const zu = el('a', 'schritt', 'Aufgaben ansehen'); + zu.href = 'aufgaben.html'; + hin.append(zu); + $('liste').prepend(hin); + } catch { + melde('Ging nicht.'); + knopf.disabled = false; + } + }); + reihe.append(knopf); + } + kasten.append(reihe); + ziel.append(kasten); + } + /* ---- Aus der Probe wird ein Zugang --------------------------------- DERSELBE WEG WIE IN "PERSONEN & ZUGAENGE", nicht ein zweiter. diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 4adc0396..6bb0fe32 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 dca3a65f..aad44ca7 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 dac5809e..d04ea3df 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 ca87fa48..e7c83422 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. --> - - - - - - + + + + + + - - + + - + @@ -243,14 +243,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/bewerben.html b/workspace/bewerben.html index 4be79fc1..4381ca3b 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 5855b1ce..201d215e 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 138f79ec..23d477d1 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 a259ca6d..c354c113 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 c1db5f51..75ecc73e 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 11af162a..ed595544 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 0d5f40e4..c7dc4abb 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609172352", + "src": "/assets/img/app-symbole/crew-192.png?v=202609180011", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609172352", + "src": "/assets/img/app-symbole/crew-512.png?v=202609180011", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609172352", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609180011", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609172352", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609180011", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index d94a598d..770cb7d6 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 f062f18a..de2df388 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 a093cd9d..40803724 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 b9342f5e..0bf2a9ad 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 1e7f39e3..e14520c5 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 45105e8d..2362f8e5 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 a0292f5e..062e8385 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 fba52113..f8724314 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 57cbf681..852471de 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 bb951278..7a64ffeb 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 1ce0be22..f35b2a08 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 a53eded3..c05da2a5 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 db965b28..a77414ab 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 46f4897c..5e2cb9ed 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 b0a26483..58fbf0bf 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 6afa7ec7..d529e2b2 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/teilen.html b/workspace/teilen.html index f243cad7..c53ad29a 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 2fba8bcd..cba4dc74 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 a7c5782b..f401ca77 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 18dd6b68..c110d0ab 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 3f369355..16a9cf8e 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 @@ - - - - - + + + + +