diff --git a/server/pruef-schritt.mjs b/server/pruef-schritt.mjs new file mode 100644 index 00000000..f3c4deb6 --- /dev/null +++ b/server/pruef-schritt.mjs @@ -0,0 +1,388 @@ +/* ===================================================================== + Aus einer Beobachtung wird ein Schritt (15.09.2026) + + Filipe: „ich will dass du die 2 kategorien perfektionnierst […] es + soll so einfach wie möglich sein für jeden." + + --------------------------------------------------------------------- + WAS GEFEHLT HAT + + Die Entwicklungskarte konnte genau eines: festhalten, dass etwas + hakt — mit Anlass, das war schon richtig. Danach passierte nichts. + Beim nächsten Öffnen stand dieselbe Beobachtung da, nur älter. + + Ein Entwicklungsplan wirkt dann, wenn er an einer ECHTEN Aufgabe + hängt. Also wird aus „da hakt es" eine Aufgabe auf dem Brett — und + wenn sie erledigt ist, fragt die Karte dieselbe Frage noch einmal. + + --------------------------------------------------------------------- + UND GENAU DA SIND DIE RIEGEL + + Aus „läuft" einen Schritt machen? (darf nicht) + Aus der Beobachtung eines anderen? (darf nicht) + Zweimal zum selben Punkt? (darf nicht) + Ein Modi für sich selbst? (darf nicht) + Steht der Anlass hinterher im Protokoll? (darf nicht) + + Aufrufen mit: node server/pruef-schritt.mjs + ===================================================================== */ + +import { mkdtempSync, rmSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { request as httpAnfrage } from "node:http"; +import { portMussFreiSein } from "./helfer-port.mjs"; + +const PORT = await portMussFreiSein(4421, "pruef-schritt"); + +const ordner = mkdtempSync(join(tmpdir(), "ws-schritt-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(240_000, "pruef-schritt"); +await new Promise((r) => setTimeout(r, 700)); + +process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); +process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); + +let fehler = 0, geprueft = 0; +const melde = (t) => console.log(t); +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const CREW = "crew.dogfather-universe.com"; +const HAUS = "workspace.dogfather-universe.com"; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const kat = await import("./workspace-entwicklungs-punkte.js"); +const ent = await import("./workspace-entwicklung.js"); + +const oeffne = () => new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); + +let idDogi, idHand, idFrida; +{ + const d = oeffne(); + const s = randomBytes(32).toString("hex"); + d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", s, jetzt); + const anlegen = (name, rolle, code) => { + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)" + + " VALUES (?,?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, createHmac("sha256", s).update(code).digest("hex"), jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; + }; + idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); + idHand = anlegen("Rieke", "hand", "CODE-HAND-0001"); + idFrida = anlegen("Frida", "modi", "CODE-MODI-0001"); + d.close(); +} + +function ruf(pfad, host, methode, kopf = {}, koerper = null) { + return new Promise((fertig, schief) => { + const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: pfad, method: methode, + headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf } }, + (antwort) => { + let text = ""; + antwort.on("data", (s) => { text += s; }); + antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers })); + }); + a.on("error", schief); + if (koerper) a.write(JSON.stringify(koerper)); + a.end(); + }); +} +const json = (a) => { try { return JSON.parse(a.text); } catch { return {}; } }; +async function anmelden(rolle, code, host) { + const r = await ruf("/workspace/api/anmelden", host, "POST", {}, { rolle, code }); + return [].concat(r.kopf?.["set-cookie"] || []).map((c) => c.split(";")[0]).join("; "); +} +const kDogi = await anmelden("admin", "CODE-DOGI-0001", HAUS); +const kHand = await anmelden("hand", "CODE-HAND-0001", CREW); +const kFrida = await anmelden("modi", "CODE-MODI-0001", CREW); +ok(!!kDogi && !!kHand && !!kFrida, "DogFather, die rechte Hand und ein Modi sind angemeldet"); + +/* Zwei Punkte, die andere setzen dürfen. */ +const P1 = kat.ENTWICKLUNG_FREMD[0].schluessel; +const P2 = kat.ENTWICKLUNG_FREMD[1].schluessel; +const SELBST = kat.ENTWICKLUNG_SELBST[0].schluessel; + +/* DIE ADRESSE GEHÖRT ZUM KEKS. + + Ein Zugang ist an das Haus gebunden, über das er angemeldet wurde -- + die rechte Hand arbeitet auf crew., DogFather auf workspace. Mein + erster Entwurf schickte beide über dieselbe Adresse; die rechte Hand + bekam daraufhin 401, und WEIL sie nichts setzen konnte, ging ein + zweiter Test ("aus der Beobachtung eines anderen wird kein Schritt") + aus dem falschen Grund durch. Ein grüner Haken über einer Leere. */ +const wo = (keks) => (keks === kDogi ? HAUS : CREW); +const setzen = (keks, punkt, stufe, anlass) => + ruf(`/workspace/api/entwicklung/person/${idFrida}/${punkt}`, wo(keks), + "PUT", { cookie: keks }, { stufe, anlass }); +const schritt = (keks, punkt, person = idFrida) => + ruf(`/workspace/api/entwicklung/person/${person}/${punkt}/schritt`, wo(keks), + "POST", { cookie: keks }, {}); +const karte = async (keks = kDogi) => json(await ruf( + `/workspace/api/entwicklung/person/${idFrida}`, wo(keks), "GET", { cookie: keks })); +const punktVon = (k, s) => k.bloecke?.flatMap((b) => b.punkte).find((p) => p.schluessel === s); + +/* ======================================================================= + 1. EIN SCHRITT ENTSTEHT NUR AUS EINEM „DA HAKT ES" + ======================================================================= */ +melde(""); +melde("=== 1. Nur aus „Da hakt es“ ==="); +{ + const l = await setzen(kDogi, P1, "laeuft"); + ok(l.code === 200, `„Läuft“ ist gesetzt (${l.code})`); + const ohne = await schritt(kDogi, P1); + ok(ohne.code === 400 && json(ohne).fehler === "kein_hakt", + `daraus wird kein Schritt (${ohne.code}, ${json(ohne).fehler})`); + + /* Gar nichts gesetzt: auch nicht. */ + const nichts = await schritt(kDogi, P2); + ok(nichts.code === 400 && json(nichts).fehler === "kein_hakt", + "und aus gar nichts erst recht nicht"); + + const h = await setzen(kDogi, P1, "hakt", "Am Dienstag zweimal hintereinander gesehen."); + ok(h.code === 200, `mit „Da hakt es“ und Anlass geht es (${h.code})`); + const ja = await schritt(kDogi, P1); + ok(ja.code === 201, `und daraus wird ein Schritt (${ja.code})`); + ok(json(ja).titel === kat.ENTWICKLUNG_FREMD[0].titel, + `er heißt wie der Punkt: „${json(ja).titel}“`); + ok(/^\d{4}-\d{2}-\d{2}$/.test(json(ja).frist || ""), + `mit einer Frist in ${ent.SCHRITT_TAGE} Tagen (${json(ja).frist})`); +} + +/* ======================================================================= + 2. ES IST EINE ECHTE AUFGABE — keine Notiz + ======================================================================= */ +melde(""); +melde("=== 2. Eine echte Aufgabe ==="); +{ + const d = oeffne(); + const a = d.prepare("SELECT * FROM aufgaben WHERE aus_punkt = ?").get(P1); + d.close(); + ok(!!a, "sie steht in der Aufgabentabelle, nicht in einer eigenen Liste"); + ok(a.verantwortlich_id === idFrida, "Frida ist verantwortlich, nicht DogFather"); + ok(a.erstellt_von === idDogi, "und DogFather hat sie angelegt"); + ok(a.status === "offen", `sie ist offen (${a.status})`); + /* DER ANLASS WANDERT MIT -- ohne ihn stuende auf dem Brett ein Satz + ohne Grund, und die Aufgabe waere ein Vorwurf. */ + ok(/Am Dienstag zweimal/.test(a.beschreibung || ""), + "der Anlass steht in der Beschreibung"); + + /* UND SIE STEHT WIRKLICH AUF DEM BRETT, nicht nur in der Tabelle. */ + const brett = json(await ruf("/workspace/api/aufgaben", HAUS, "GET", { cookie: kDogi })); + const drauf = (brett.aufgaben || []).find((x) => x.id === a.id); + ok(!!drauf, "und sie taucht auf dem Aufgabenbrett auf"); + + /* AUCH FRIDA SIEHT SIE -- eine Aufgabe, die nur die Leitung sieht, + waere wieder eine Notiz. */ + const ihres = json(await ruf("/workspace/api/aufgaben", CREW, "GET", { cookie: kFrida })); + ok((ihres.aufgaben || []).some((x) => x.id === a.id), + "Frida sieht sie ebenfalls — sonst wäre sie keine Aufgabe"); +} + +/* ======================================================================= + 3. DIE KARTE ZEIGT IHN + ======================================================================= */ +melde(""); +melde("=== 3. Die Karte zeigt ihn ==="); +{ + const k = await karte(); + const p = punktVon(k, P1); + ok(!!p?.schritt, "an dem Punkt hängt der Schritt"); + ok(p.schritt.status === "offen", `er läuft (${p.schritt.status})`); + ok(punktVon(k, P2)?.schritt === null, "an einem Punkt ohne Schritt hängt nichts"); + + /* ZWEIMAL ZUM SELBEN PUNKT WAERE ZWEIMAL DIESELBE ERINNERUNG -- und + die zweite macht die erste wertlos. */ + const nochmal = await schritt(kDogi, P1); + ok(nochmal.code === 400 && json(nochmal).fehler === "schon_da", + `ein zweiter Schritt zum selben Punkt: abgelehnt (${json(nochmal).fehler})`); + ok(json(nochmal).aufgabe_id === p.schritt.id, + "und die Absage nennt den, der schon läuft"); +} + +/* ======================================================================= + 4. WER DARF — UND WER NICHT + ======================================================================= */ +melde(""); +melde("=== 4. Wer darf ==="); +{ + /* DIE RECHTE HAND FÜHRT DAS TEAM MIT -- sie darf. */ + await setzen(kHand, P2, "hakt", "Ist mir im Stream am Freitag aufgefallen."); + const hand = await schritt(kHand, P2); + ok(hand.code === 201, `die rechte Hand darf auch (${hand.code})`); + + /* EIN MODI NICHT -- nicht für sich und nicht für andere. */ + const modi = await ruf(`/workspace/api/entwicklung/person/${idFrida}/${P1}/schritt`, + CREW, "POST", { cookie: kFrida }, {}); + ok(json(modi).fehler === "nicht_gefunden", "mit der Auskunft „gibt es nicht“"); + ok(modi.code === 404, `ein Modi bekommt 404 — nicht „verboten“ (${modi.code})`); + + /* AUS DER BEOBACHTUNG EINES ANDEREN wird kein Schritt: DogFather hat + bei P2 nichts gesetzt, die rechte Hand schon. */ + const fremd = await schritt(kDogi, P2); + ok(fremd.code === 400 && json(fremd).fehler === "kein_hakt", + "und aus der Beobachtung eines anderen auch nicht"); + + /* EIN PUNKT AUS BLOCK 5 SIEHT VON AUSSEN AUS WIE EIN ERFUNDENER. */ + const geheim = await schritt(kDogi, SELBST); + ok(geheim.code === 404, `ein Punkt aus „Wie geht's dir?“ gibt 404 (${geheim.code})`); + const quatsch = await schritt(kDogi, "e-gibt-es-nicht"); + ok(quatsch.code === 404, "ein erfundener Punkt ebenfalls"); +} + +/* ======================================================================= + 5. IM PROTOKOLL STEHT, DASS — NICHT WORAN + ======================================================================= */ +melde(""); +melde("=== 5. Das Protokoll ==="); +{ + const d = oeffne(); + const zeilen = d.prepare( + "SELECT detail FROM protokoll WHERE aktion = 'entwicklung_schritt'").all(); + d.close(); + ok(zeilen.length === 2, `${zeilen.length} Schritte stehen im Protokoll`); + ok(zeilen.every((z) => /Frida/.test(z.detail || "")), + "mit Punkt und Name — das ist der Vorgang"); + /* WORAN JEMAND ETWAS GEMERKT HAT, GEHÖRT NICHT INS PROTOKOLL. Es + liest sich anders, wenn es zwischen Anmeldungen steht. */ + ok(!zeilen.some((z) => /Dienstag|Freitag/.test(z.detail || "")), + "aber ohne den Anlass"); +} + +/* ======================================================================= + 6. UND DANN SCHLIESST SICH DER KREIS + ======================================================================= */ +melde(""); +melde("=== 6. Der Kreis ==="); +{ + const d = oeffne(); + const a = d.prepare("SELECT id FROM aufgaben WHERE aus_punkt = ?").get(P1); + d.close(); + const fertig = await ruf(`/workspace/api/aufgaben/${a.id}`, HAUS, "PATCH", + { cookie: kDogi }, { status: "erledigt" }); + ok(fertig.code === 200, `die Aufgabe wird erledigt (${fertig.code})`); + + const k = await karte(); + const p = punktVon(k, P1); + ok(p.schritt?.status === "erledigt", `die Karte sieht das (${p.schritt?.status})`); + ok(!!p.schritt?.erledigt_am, "mit Datum"); + + /* UND DANACH GEHT ES WIEDER -- der Kreis ist ein Kreis, keine + Einbahnstrasse. Waere das gesperrt, koennte man denselben Punkt + nie wieder angehen. */ + const zweite = await schritt(kDogi, P1); + ok(zweite.code === 201, `zum selben Punkt geht danach ein neuer Schritt (${zweite.code})`); + const k2 = await karte(); + ok(punktVon(k2, P1).schritt?.id === json(zweite).id, + "und die Karte zeigt den NEUEN, nicht den alten"); +} + +/* ======================================================================= + 7. DER VERWEIS AUFS BRETT FÜHRT WIRKLICH HIN + ======================================================================= */ +melde(""); +melde("=== 7. Der Verweis ==="); +{ + /* `aufgaben.html#a12` stand an drei Stellen im Haus und wurde von + keiner gelesen -- das Brett kannte nur `?zeigen=`. Wer draufdrueckte, + landete auf dem Brett und suchte von Hand. */ + const brett = readFileSync(new URL("../workspace/assets/js/aufgaben.js", import.meta.url), "utf8"); + ok(/data-id=/.test(brett), "die Karten auf dem Brett tragen ihre Nummer"); + ok(/\^#a/.test(brett), "und das Brett liest `#a` aus der Adresse"); + + const e = readFileSync(new URL("../workspace/assets/js/entwicklung.js", import.meta.url), "utf8"); + ok(/aufgaben\.html#a/.test(e), "die Entwicklungskarte verweist in dieser Schreibweise"); + const b = readFileSync(new URL("../workspace/assets/js/bereich.js", import.meta.url), "utf8"); + const r = readFileSync(new URL("../workspace/assets/js/report.js", import.meta.url), "utf8"); + ok(/aufgaben\.html#a/.test(b) && /aufgaben\.html#a/.test(r), + "die beiden anderen Stellen auch — sie funktionieren jetzt mit"); +} + +/* ======================================================================= + 8. IM BROWSER + ======================================================================= */ +melde(""); +melde("=== 8. Im Browser ==="); +{ + const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); + const browser = await chromium.launch(); + const kontext = await browser.newContext({ viewport: { width: 1280, height: 1100 } }); + await kontext.addCookies(kDogi.split("; ").filter(Boolean).map((teil) => { + const i = teil.indexOf("="); + return { name: teil.slice(0, i), value: teil.slice(i + 1), domain: "127.0.0.1", path: "/" }; + })); + const seite = await kontext.newPage(); + const konsole = []; + seite.on("console", (m) => { if (m.type() === "error" && !/favicon|ERR_/i.test(m.text())) konsole.push(m.text()); }); + seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message)); + + await seite.goto(`http://127.0.0.1:${PORT}/workspace/entwicklung.html`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(800); + /* Erst die Person öffnen -- ohne Karte gibt es keine Punkte. */ + /* FRIDAS KARTE, nicht die erste in der Liste. Die steht dort nach + der Hierarchie des Hauses -- also die rechte Hand -- und die hat + keinen Schritt. Mein erster Entwurf klickte blind auf die erste + und maß danach eine leere Karte. */ + await seite.click('.e-person:has-text("Frida")'); + await seite.waitForTimeout(900); + + const stand = await seite.evaluate(() => ({ + punkte: document.querySelectorAll(".e-punkt").length, + laeuft: document.querySelectorAll('.e-schritt[data-lage="laeuft"]').length, + knoepfe: document.querySelectorAll(".e-schritt__knopf").length, + leere: document.querySelectorAll(".e-schritt:not([data-lage])").length, + verweis: document.querySelector(".e-schritt__hin")?.getAttribute("href") || "", + })); + ok(stand.punkte > 20, `${stand.punkte} Punkte stehen auf der Karte`); + ok(stand.laeuft >= 1, `${stand.laeuft} Schritt läuft sichtbar`); + ok(/aufgaben\.html#a\d+/.test(stand.verweis), + `und verweist auf die Aufgabe (${stand.verweis})`); + /* KEIN KNOPF, WO ER NICHTS TUN KANN. Bei über zwanzig Punkten wären + zwanzig Knöpfe, die fast alle „geht nicht" antworten, schlimmer + als keiner. */ + ok(stand.knoepfe === 0, + "an den übrigen Punkten steht kein Knopf — dort hakt nichts"); + ok(stand.leere > 20, `und dort steht auch kein leerer Kasten (${stand.leere})`); + + /* DER SPRUNG: die Adresse mit der Nummer, und das Brett zeigt genau + die eine Aufgabe. */ + const d = oeffne(); + const a = d.prepare("SELECT id FROM aufgaben WHERE aus_punkt IS NOT NULL ORDER BY id DESC").get(); + d.close(); + await seite.goto(`http://127.0.0.1:${PORT}/workspace/aufgaben.html#a${a.id}`, + { waitUntil: "networkidle" }); + await seite.waitForTimeout(1100); + const sprung = await seite.evaluate(() => ({ + leiste: document.querySelector(".ziel-leiste__text")?.textContent || "", + marke: document.querySelector(".ziel-leiste__marke")?.textContent || "", + })); + ok(/Diese eine Aufgabe/.test(sprung.leiste), + `das Brett sagt, was es zeigt: „${sprung.leiste}“`); + ok(sprung.marke === "1", `und es ist genau eine (${sprung.marke})`); + + ok(konsole.length === 0, + konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 90) : "keine Fehler im Browser"); + await browser.close(); +} + +/* ------------------------------------------------------------------- */ +melde(""); +melde(`${geprueft} Pruefungen, ${fehler} Fehler`); +melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } +process.exit(fehler ? 1 : 0); diff --git a/server/workspace-entwicklung.js b/server/workspace-entwicklung.js index 4b42e4cc..af54d703 100644 --- a/server/workspace-entwicklung.js +++ b/server/workspace-entwicklung.js @@ -326,6 +326,7 @@ entwicklungRouter.get("/workspace/api/entwicklung/person/:id", nurLeitung, (req, WHERE s.person_id = ? AND s.von_id <> ?`).all(id, req.person.id); const grenze = Date.now() - VERBLASST_NACH_TAGEN * TAG; + const schritte = schritteVon(p.id); const bloecke = katalogFuer(req.person, false).map((b) => ({ ...b, punkte: b.punkte.map((pt) => { @@ -344,6 +345,10 @@ entwicklungRouter.get("/workspace/api/entwicklung/person/:id", nurLeitung, (req, nicht" dasselbe Bild, und das erste ist eine Auskunft, das zweite eine Taeuschung. */ andere_offen: m ? 0 : fremde.filter((f) => f.schluessel === pt.schluessel).length, + /* DER SCHRITT ZU DIESEM PUNKT. Er steht an der Karte und + nicht auf einer eigenen Liste: Wer die Beobachtung sieht, + soll im selben Blick sehen, ob daraus etwas geworden ist. */ + schritt: schritte.get(pt.schluessel) || null, }; }), })); @@ -381,6 +386,124 @@ entwicklungRouter.get("/workspace/api/entwicklung/person/:id", nurLeitung, (req, } }); +/* ===================================================================== + AUS EINER BEOBACHTUNG WIRD EIN SCHRITT (15.09.2026) + + Filipe: „ich will die noch viel besser, perfektionniert […] so + einfach wie möglich für jeden." + + --------------------------------------------------------------------- + WAS GEFEHLT HAT + + Die Karte konnte bisher genau eines: festhalten, dass etwas hakt -- + mit Anlass, das war schon richtig. Danach passierte nichts. Beim + nächsten Öffnen stand dieselbe Beobachtung da, nur älter. + + Die Quellen zur laufenden Entwicklungsbegleitung sagen übereinstimmend + zweierlei: weg von der Bewertung, hin zum Gespräch -- und ein + Entwicklungsplan wirkt dann, wenn er an einer ECHTEN Aufgabe hängt. + Nicht „daran arbeiten wir", sondern etwas, das auf dem Aufgabenbrett + steht, einen Verantwortlichen hat und irgendwann erledigt ist. + + Genau dieses Muster gibt es im Haus schon: Aus einer Idee auf einem + Brett wird eine Aufgabe. Hier ist es dasselbe, nur mit einem anderen + Ausgangspunkt -- und deshalb sieht es auch gleich aus. + + --------------------------------------------------------------------- + UND DANN SCHLIESST SICH DER KREIS + + Ist der Schritt erledigt, sagt die Karte das und fragt dieselbe Frage + noch einmal. Das ist der Rhythmus, den die Quellen meinen: nicht + einmal im Jahr eine Bewertung, sondern eine Runde, die zu Ende geht. + + Quellen: + https://www.quantumworkplace.com/future-of-work/continuous-performance-management + https://www.aihr.com/blog/continuous-performance-management/ + https://www.deel.com/blog/implement-continuous-performance-management-system/ + ===================================================================== */ + +/** Wie lange ein Schritt Zeit bekommt. Vorschlag, kein Gesetz -- + * auf dem Aufgabenbrett lässt sich die Frist ändern wie bei jeder + * anderen Aufgabe auch. Zwei Wochen sind nah genug, dass man sich + * erinnert, und weit genug, dass sich etwas ändern kann. */ +export const SCHRITT_TAGE = 14; + +/** Die Schritte zu den Punkten einer Person — einer je Punkt. + * + * ES IST IMMER DER JÜNGSTE. Wer denselben Punkt ein zweites Mal + * angeht, soll den neuen Schritt sehen und nicht den von vor einem + * halben Jahr. */ +function schritteVon(personId) { + const zeilen = db().prepare( + `SELECT id, titel, status, frist, erledigt_am, aus_punkt, erstellt + FROM aufgaben + WHERE verantwortlich_id = ? AND aus_punkt IS NOT NULL + ORDER BY id DESC`).all(personId); + const nach = new Map(); + for (const z of zeilen) if (!nach.has(z.aus_punkt)) nach.set(z.aus_punkt, z); + return nach; +} + +entwicklungRouter.post("/workspace/api/entwicklung/person/:id/:schluessel/schritt", + gleicheHerkunft, nurLeitung, express.json({ limit: "8kb" }), (req, res) => { + try { + const id = nummer(req.params.id); + const schluessel = String(req.params.schluessel || ""); + const p = id && db().prepare("SELECT id, name, rolle FROM personen WHERE id = ?").get(id); + if (!p || !KARTEN_ROLLEN.includes(p.rolle)) { + return res.status(404).json({ fehler: "nicht_gefunden" }); + } + const punkt = ENTWICKLUNG_FREMD.find((x) => x.schluessel === schluessel); + if (!punkt) return res.status(404).json({ fehler: "nicht_gefunden" }); + + /* NUR AUS EINEM „DA HAKT ES" WIRD EIN SCHRITT -- und nur aus dem + EIGENEN. Aus „läuft" einen Schritt zu machen hieße, jemandem + Arbeit zu geben für etwas, das gut läuft; aus der Beobachtung + eines anderen hieße, in seinem Namen zu handeln. */ + const stand = db().prepare( + `SELECT stufe, anlass FROM entwicklung_stand + WHERE person_id = ? AND schluessel = ? AND von_id = ?`) + .get(id, schluessel, req.person.id); + if (!stand || stand.stufe !== "hakt") { + return res.status(400).json({ fehler: "kein_hakt" }); + } + + /* EIN OFFENER SCHRITT REICHT. Zwei Aufgaben zum selben Punkt + wären zwei Erinnerungen an dieselbe Sache -- und die zweite + macht die erste wertlos. */ + const offen = db().prepare( + `SELECT id FROM aufgaben + WHERE verantwortlich_id = ? AND aus_punkt = ? + AND status NOT IN ('erledigt','abgebrochen')`).get(id, schluessel); + if (offen) return res.status(400).json({ fehler: "schon_da", aufgabe_id: offen.id }); + + const frist = new Date(Date.now() + SCHRITT_TAGE * 86400000).toISOString().slice(0, 10); + const jetztIso = jetzt(); + db().prepare(`INSERT INTO aufgaben + (titel, beschreibung, status, prioritaet, verantwortlich_id, + frist, erstellt, erstellt_von, aus_punkt) + VALUES (?,?,'offen','mittel',?,?,?,?,?)`) + .run( + punkt.titel, + /* DER ANLASS WANDERT MIT. Ohne ihn stünde auf dem Brett ein + Satz ohne Anlass, und die Aufgabe wäre ein Vorwurf. */ + `${punkt.text}\n\nWoran es aufgefallen ist: ${stand.anlass || "–"}`, + id, frist, jetztIso, req.person.id, schluessel); + const neu = db().prepare("SELECT last_insert_rowid() AS id").get().id; + + protokolliere("entwicklung_schritt", { + personId: req.person.id, ip: echteIp(req), + /* KEIN ANLASS IM PROTOKOLL. Dass ein Schritt entstanden ist, + gehört hinein; woran jemand etwas gemerkt hat, nicht. */ + detail: `${punkt.titel} für ${p.name}`.slice(0, 120), + }); + res.status(201).json({ id: neu, titel: punkt.titel, frist }); + } catch (f) { + console.error("[entwicklung] Schritt:", f?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } + }); + const STUFEN_SATZ = new Set(ENTWICKLUNG_STUFEN.map((s) => s.schluessel)); entwicklungRouter.put("/workspace/api/entwicklung/person/:id/:schluessel", diff --git a/server/workspace.js b/server/workspace.js index d3694fb9..4b5b934c 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -2125,6 +2125,22 @@ function umstellungen(d) { im Projekt schon dreimal Spalten verlorengegangen sind. Geprueft wird beim Schreiben, an der Stelle, die die Liste kennt. */ ["aufgaben", "aufwand", "TEXT"], + + /* AUS WELCHER BEOBACHTUNG DIESE AUFGABE ENTSTANDEN IST (15.09.2026). + + Der Schluessel aus dem Entwicklungskatalog, nichts weiter. Zusammen + mit `verantwortlich_id` ergibt er das Paar, um das es geht: DIESER + Punkt bei DIESEM Menschen. + + WARUM ÜBERHAUPT: Eine Beobachtung, die nur auf einer Karte steht, + ist eine Notiz. Die Quellen zur laufenden Entwicklungsbegleitung + sagen dasselbe in einem Satz -- ein Entwicklungsplan wirkt dann, + wenn er an einer echten Aufgabe haengt, und sonst nicht. + + KEIN FREMDSCHLUESSEL: Die Punkte stehen in einer Datei, nicht in + einer Tabelle. Ein REFERENCES darauf gaebe es nicht; geprueft wird + beim Schreiben, an der Stelle, die den Katalog kennt. */ + ["aufgaben", "aus_punkt", "TEXT"], ]) { try { const vorhanden = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((s) => s.name); diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 3717c213..4c8f35f0 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609151355", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609151402", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609151355", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609151402", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609151355", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609151402", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609151355", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609151402", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/entwicklung.css b/workspace/assets/css/entwicklung.css index 479cceb9..d52672d0 100644 --- a/workspace/assets/css/entwicklung.css +++ b/workspace/assets/css/entwicklung.css @@ -578,3 +578,35 @@ display: flex; flex-wrap: wrap; gap: 9px; margin: 13px 0 0; } .t-uebergang__aendern { margin: 11px 0 0; } + +/* ===================================================================== + AUS DER BEOBACHTUNG WIRD EIN SCHRITT (15.09.2026) + + Die Zeile steht ganz unten an einem Punkt und ist absichtlich leise: + Sie ist kein zweites Ergebnis neben den vier Knöpfen, sondern das, + was aus einem von ihnen folgt. Bei einem Punkt, der läuft, ist sie + gar nicht da — leer und ohne Rand, nicht als leerer Kasten. + ===================================================================== */ +.e-schritt { + display: flex; flex-wrap: wrap; align-items: baseline; gap: 9px; + margin: 9px 0 0; +} +.e-schritt:not([data-lage]) { display: none; } +.e-schritt[data-lage] { + padding: 8px 11px; + background: rgba(0, 0, 0, .22); + border: 1px solid var(--rand); + border-left: 2px solid var(--rand-hell); +} +.e-schritt[data-lage="laeuft"] { border-left-color: color-mix(in srgb, #8ec9ff 50%, transparent); } +.e-schritt[data-lage="durch"] { border-left-color: color-mix(in srgb, #668e6e 60%, transparent); } + +.e-schritt__was { + font-size: .72rem; letter-spacing: .05em; text-transform: uppercase; + color: var(--text-still); +} +.e-schritt__hin { font-size: .84rem; color: var(--text); } +.e-schritt__frist, .e-schritt__satz { + font-size: .79rem; color: var(--text-leise); +} +.e-schritt__knopf { margin: 0; } diff --git a/workspace/assets/js/aufgaben.js b/workspace/assets/js/aufgaben.js index 41ab215f..df31edaf 100644 --- a/workspace/assets/js/aufgaben.js +++ b/workspace/assets/js/aufgaben.js @@ -171,6 +171,9 @@ function karte(a) { const k = el('article', 'karte'); k.dataset.prio = a.prioritaet; + /* DIE NUMMER AN DER KARTE. Ohne sie kann kein Verweis auf eine + EINZELNE Aufgabe zeigen -- und drei davon gibt es im Haus. */ + k.dataset.id = String(a.id); const kopf = el('div', 'karte__kopf'); kopf.append(el('h3', 'karte__titel', a.titel)); @@ -679,6 +682,21 @@ }; function sprungZeigen() { + /* EINE EINZELNE AUFGABE, ueber `#a` in der Adresse. + + Sie steht VOR der Auswertung von `?zeigen=`, weil sie genauer + ist: Wer eine bestimmte Aufgabe meint, meint nicht ihre Spalte. + + Die Nummer kommt aus dem Anker und nicht aus `?zeigen=`, weil + das Brett von drei Stellen aus so verlinkt wird -- und die + Schreibweise war schon da, nur gelesen hat sie niemand. */ + const einzeln = /^#a(\d+)$/.exec(location.hash || ""); + if (einzeln) { + const k = document.querySelector(`.karte[data-id="${einzeln[1]}"]`); + window.zielMarkieren?.(k ? [k] : [], 'Diese eine Aufgabe'); + return; + } + const was = window.zielSchluessel?.(); if (!was || !ZEIG_TEXT[was]) return; diff --git a/workspace/assets/js/entwicklung.js b/workspace/assets/js/entwicklung.js index 090fae80..e572476a 100644 --- a/workspace/assets/js/entwicklung.js +++ b/workspace/assets/js/entwicklung.js @@ -241,9 +241,89 @@ z.append(el('p', 'e-punkt__wann', `von dir am ${datum(pt.meins.am)}${pt.meins.verblasst ? ' – älter als 90 Tage' : ''}`)); } + + /* ==== AUS DER BEOBACHTUNG WIRD EIN SCHRITT (15.09.2026) ========== + + Bis heute konnte diese Zeile genau eines: festhalten, dass etwas + hakt. Danach passierte nichts — beim nächsten Öffnen stand + dieselbe Beobachtung da, nur älter. + + Ein Entwicklungsplan wirkt dann, wenn er an einer ECHTEN Aufgabe + hängt. Also wird aus „da hakt es" eine Aufgabe auf dem Brett, + mit Verantwortlichem und Frist — und wenn sie erledigt ist, + fragt die Karte dieselbe Frage noch einmal. Das ist der Kreis. + ============================================================== */ + z.append(schrittZeile(personId, pt)); return z; } + const SCHRITT_ABSAGE = { + kein_hakt: 'Ein Schritt entsteht aus „Da hakt es“ – setz das zuerst.', + schon_da: 'Dazu läuft schon ein Schritt.', + }; + + function schrittZeile(personId, pt) { + const s = pt.schritt; + const zeile = el('div', 'e-schritt'); + + /* ---- Es läuft schon etwas ---- */ + if (s && s.status !== 'erledigt' && s.status !== 'abgebrochen') { + zeile.dataset.lage = 'laeuft'; + zeile.append(el('span', 'e-schritt__was', 'Schritt läuft')); + const a = el('a', 'e-schritt__hin', s.titel); + /* DIE SCHREIBWEISE DES HAUSES: `#a`. Das Brett springt + damit wirklich zu der Aufgabe -- `?a=` hatte ich erfunden, und + das Brett liest es nicht. Ein Verweis, der nur die Seite + oeffnet und nichts zeigt, ist schlechter als keiner. */ + a.href = 'aufgaben.html#a' + s.id; + zeile.append(a); + if (s.frist) zeile.append(el('span', 'e-schritt__frist', `bis ${datum(s.frist)}`)); + return zeile; + } + + /* ---- Er ist durch: dieselbe Frage noch einmal ---- + DAS IST DER EIGENTLICHE GEWINN. Ohne diesen Satz wäre der Schritt + nur eine Aufgabe mehr; mit ihm wird aus der Beobachtung eine + Runde, die zu Ende geht. */ + if (s && s.status === 'erledigt') { + zeile.dataset.lage = 'durch'; + zeile.append(el('span', 'e-schritt__was', 'Schritt erledigt')); + zeile.append(el('span', 'e-schritt__satz', + s.erledigt_am + ? `am ${datum(s.erledigt_am)} – sieh noch einmal hin und setz neu.` + : 'Sieh noch einmal hin und setz neu.')); + return zeile; + } + + /* ---- Noch keiner: aber nur aus einem „da hakt es" ---- + Der Knopf steht NUR dort. Ein Knopf, der überall steht und fast + überall „geht nicht" antwortet, ist schlechter als keiner. */ + if (pt.meins?.stufe !== 'hakt') return zeile; + + zeile.dataset.lage = 'offen'; + const b = el('button', 'schritt e-schritt__knopf', 'Daraus einen Schritt machen'); + b.type = 'button'; + b.title = 'Legt eine Aufgabe auf dem Brett an – mit dieser Person als ' + + 'Verantwortlicher und einer Frist in zwei Wochen.'; + b.addEventListener('click', async () => { + b.disabled = true; + const a = await hole( + `/workspace/api/entwicklung/person/${personId}/${pt.schluessel}/schritt`, + { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); + b.disabled = false; + if (!a.ok) { + const j = await a.json().catch(() => ({})); + melde(SCHRITT_ABSAGE[j.fehler] || 'Ging nicht.'); + return; + } + const neu = await a.json(); + melde(`Steht auf dem Aufgabenbrett: „${neu.titel}“, bis ${datum(neu.frist)}.`); + await karte(personId); + }); + zeile.append(b); + return zeile; + } + /* ---- Deine eigene Karte ----------------------------------------------- WAS HIER STEHT UND WAS NICHT: die Antworten, nicht die Namen, und diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 67a9eb9e..f51fed94 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. --> - - - - - - + + + + + + - + @@ -282,10 +282,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 029b0f97..510187a3 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 dd9a41ab..0912ecb2 100644 --- a/workspace/befinden.html +++ b/workspace/befinden.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -84,9 +84,9 @@ - - - - + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index bcc763fa..39b377db 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -181,14 +181,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index cfda0ea4..a549215b 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 8f2fd619..ddbed14b 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 72b5768f..cb9c9676 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 013ea3d8..a5cbf4a0 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 b6af8388..7cba41f2 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -13,25 +13,25 @@ "theme_color": "#06090f", "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609151355", + "src": "/assets/img/app-symbole/crew-192.png?v=202609151402", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609151355", + "src": "/assets/img/app-symbole/crew-512.png?v=202609151402", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609151355", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609151402", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609151355", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609151402", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index 911c693d..5bf5351a 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 996af6c4..60963442 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 011ca9cb..98170c9a 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 3f4c1fe7..b10bfc70 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 06b902e3..d38ff978 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -423,10 +423,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index f97120fa..98c2374a 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 f0356e7f..202b58b6 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 5ccc2eaf..8207c667 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 b1899254..72dabb35 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 30600472..e7a2a7b2 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 0382e001..68690124 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 362d8a5e..76e66c90 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 b7f4a1ee..8cb2966e 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 4f41331b..fcd6746f 100644 --- a/workspace/talente.html +++ b/workspace/talente.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -94,9 +94,9 @@ - - - - + + + + diff --git a/workspace/team.html b/workspace/team.html index ac96465b..32824da3 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 d9eeeda6..e4b41297 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/treff-moderation.html b/workspace/treff-moderation.html index 1fd57d43..8401aa76 100644 --- a/workspace/treff-moderation.html +++ b/workspace/treff-moderation.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -110,9 +110,9 @@ - - - - + + + + diff --git a/workspace/treff-regeln.html b/workspace/treff-regeln.html index 2c74c3bc..c63089a3 100644 --- a/workspace/treff-regeln.html +++ b/workspace/treff-regeln.html @@ -7,18 +7,18 @@ - - - - - - - + + + + + + + - + @@ -210,10 +210,10 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index 8e7393b9..2708ddb1 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 0aa1c3b5..aa57c29f 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 @@ - - - - - + + + + +