diff --git a/server/pruef-entwicklung.mjs b/server/pruef-entwicklung.mjs new file mode 100644 index 00000000..c06429a0 --- /dev/null +++ b/server/pruef-entwicklung.mjs @@ -0,0 +1,267 @@ +/* ENTWICKLUNG UND TALENTE (11.09.2026) + + Filipe: "ich will dass ich genau so eine kategorie habe in der + dogfather und rechten hand rollen, wo wir aufgaben oder bewertungen + über modis eingeben können. auch für zuschauer die vielleicht modis + werden könnten wäre auch geil." + + --------------------------------------------------------------------- + DIE PRÜFUNG, DERENTWEGEN ES DIESE DATEI GIBT + + In „Entwicklung" steht, was über Menschen aufgeschrieben wird. Wenn + diese beiden Bereiche irgendwo durchsickern, ist der Schaden nicht + technisch, sondern menschlich -- und er lässt sich nicht zurücknehmen. + + Zwei Schlösser sichern das: der ZUGANG (fuehrtTeamDogi) und die + SICHTBARKEIT der Zeilen. Geprüft werden beide, und zwar von der + falschen Seite aus: Ein Modi klopft an die Liste, an eine einzelne + Zeile, ans Ändern und ans Löschen -- und muss überall dieselbe + Antwort bekommen wie jemand, für den es diese Bereiche nicht gibt. + + Dazu die Brücke: Ein Eintrag lässt sich EINMAL an die Person + schicken. Gemessen wird, dass die Nachricht wirklich in ihrem Chat + ankommt (sonst wäre „geschickt" nur ein Datum) und dass ein zweiter + Versuch abgelehnt wird. + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { request } from "node:http"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-entw-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); + +const { portMussFreiSein } = await import("./helfer-port.mjs"); +await portMussFreiSein(4389, "die Entwicklungspruefung"); + +process.env.PORT = "4389"; +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 }); }; +await import("./index.js"); +await new Promise((r) => setTimeout(r, 1000)); + +for (const art of ["uncaughtException", "unhandledRejection"]) { + process.on(art, (f) => { console.error(""); console.error("ABBRUCH:", f?.stack || f); process.exit(1); }); +} +setTimeout(() => { console.error(""); console.error("ABBRUCH: 150 s vorbei."); process.exit(1); }, + 150000).unref?.(); + +let fehler = 0, geprueft = 0; +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const CREW = "crew.dogfather-universe.com"; +const WS = "workspace.dogfather-universe.com"; + +function hole(pfad, { host = WS, methode = "GET", rumpf = null, keks = "" } = {}) { + return new Promise((fertig, schief) => { + const kopf = { Host: host }; + if (rumpf) { kopf["Content-Type"] = "application/json"; kopf["Content-Length"] = Buffer.byteLength(rumpf); } + if (keks) kopf.Cookie = keks; + const a = request({ host: "127.0.0.1", port: 4389, path: pfad, method: methode, headers: kopf }, + (antwort) => { + let text = ""; + antwort.setEncoding("utf8"); + antwort.on("data", (t) => { text += t; }); + antwort.on("end", () => { + let daten = null; + try { daten = JSON.parse(text); } catch { /* kein JSON */ } + fertig({ status: antwort.statusCode, text, daten, + keks: (antwort.headers["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ") }); + }); + }); + a.on("error", schief); + a.setTimeout(15000, () => { a.destroy(new Error("Zeitueberschreitung")); }); + if (rumpf) a.write(rumpf); + a.end(); + }); +} + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +await hole("/workspace/api/anmelden", { methode: "POST", + rumpf: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); +for (let i = 0; i < 60; i++) { + try { + const p = new DatabaseSync(process.env.WORKSPACE_DB); + try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); } + } catch { await new Promise((r) => setTimeout(r, 250)); } +} +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +function 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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} +const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); +anlegen("VanVan", "hand", "CODE-HAND-0001"); +const idModi = anlegen("Ayla", "modi", "CODE-TEAM-0001"); +anlegen("Cigdem", "manager", "CODE-MANA-0001"); + +async function anmelden(rolle, code, wand) { + const a = await hole("/workspace/api/anmelden", { host: wand, methode: "POST", + rumpf: JSON.stringify({ rolle, code }) }); + return { ok: a.status === 200, keks: a.keks }; +} +const dogi = await anmelden("admin", "CODE-DOGI-0001", CREW); +const hand = await anmelden("hand", "CODE-HAND-0001", CREW); +const ayla = await anmelden("modi", "CODE-TEAM-0001", CREW); +const mana = await anmelden("manager", "CODE-MANA-0001", WS); +ok(dogi.ok && hand.ok && ayla.ok && mana.ok, "alle vier sind angemeldet"); + +const ENTW = "/workspace/api/bereich/entwicklung"; +const TAL = "/workspace/api/bereich/talente"; + +/* ======================================================================= + 1. Es gibt die beiden Bereiche -- und ihre Felder + ======================================================================= */ +console.log(""); +console.log("=== Die beiden Bereiche ==="); +{ + const a = await hole(ENTW, { host: CREW, keks: dogi.keks }); + ok(a.daten?.einstellung?.name === "Entwicklung", + `DogFather kann Entwicklung oeffnen (${a.daten?.einstellung?.name})`); + const arten = Object.keys(a.daten?.einstellung?.arten || {}); + ok(arten.length === 5, `fuenf Arten (${arten.join(", ")})`); + /* DIE REIHENFOLGE IST ABSICHT: Wer ein Formular oeffnet, dessen + erstes Feld "Problem" heisst, schreibt Probleme auf. */ + ok(arten[0] === "staerke" && arten[1] === "dank", + "und die ersten beiden sind die guten"); + ok(arten.includes("last"), + "„Zu viel gerade“ ist dabei -- der haeufigste Grund aufzuhoeren"); + + const b = await hole(TAL, { host: CREW, keks: hand.keks }); + ok(b.daten?.einstellung?.name === "Talente", + `die rechte Hand kann Talente oeffnen (${b.daten?.einstellung?.name})`); + const tArten = Object.keys(b.daten?.einstellung?.arten || {}); + ok(tArten.includes("empfohlen"), + `die Empfehlung aus dem Team ist dabei (${tArten.join(", ")})`); +} + +/* ======================================================================= + 2. DIE EIGENTLICHE PRUEFUNG: Wer NICHT hineinkommt + ======================================================================= */ +console.log(""); +console.log("=== Wer nicht hineinkommt ==="); +let eintragId = 0; +{ + /* Erst ein Eintrag, damit es ueberhaupt etwas zu verbergen gibt -- + sonst waere jede Sperre unten gruen ueber einer leeren Liste. */ + const a = await hole(ENTW, { host: CREW, methode: "POST", keks: dogi.keks, + rumpf: JSON.stringify({ + art: "staerke", titel: "Ruhig geblieben, als es kippte", + text: "Hat den Streit im Chat gedreht, ohne jemanden bloßzustellen.", + datum: "2026-09-11", creator_id: idModi, einsatz: "Beim nächsten Live wieder ihr überlassen", + }) }); + eintragId = a.daten?.id; + ok(a.status === 201 && eintragId > 0, + `ein Eintrag ueber Ayla (${a.status}) ${a.status === 201 ? "" : a.text.slice(0, 160)}`); + + for (const [wer, keks, wand] of [["ein Teammitglied", ayla.keks, CREW], + ["ein Manager", mana.keks, WS]]) { + const liste = await hole(ENTW, { host: wand, keks }); + ok(liste.status === 404, `${wer}: die Liste gibt es nicht (${liste.status})`); + const aendern = await hole(`${ENTW}/${eintragId}`, { host: wand, methode: "PATCH", + keks, rumpf: JSON.stringify({ titel: "umgeschrieben" }) }); + ok(aendern.status === 404, `${wer}: aendern geht nicht (${aendern.status})`); + const weg = await hole(`${ENTW}/${eintragId}`, { host: wand, methode: "DELETE", keks }); + ok(weg.status === 404, `${wer}: loeschen auch nicht (${weg.status})`); + const tal = await hole(TAL, { host: wand, keks }); + ok(tal.status === 404, `${wer}: Talente ebenfalls nicht (${tal.status})`); + } + + /* Und die Zeile steht unveraendert da -- sonst waeren die 404 oben + Antworten auf Anfragen, die trotzdem gewirkt haben. */ + const heil = d.prepare("SELECT titel FROM eintraege WHERE id = ?").get(eintragId); + ok(heil?.titel === "Ruhig geblieben, als es kippte", "der Eintrag ist unveraendert"); + + /* GEGENPROBE: Die beiden, die duerfen, sehen ihn sehr wohl. Ohne sie + hiesse "niemand sieht ihn" auch dann gruen, wenn er gar nicht + angekommen waere. */ + for (const [wer, keks] of [["DogFather", dogi.keks], ["die rechte Hand", hand.keks]]) { + const a = await hole(ENTW, { host: CREW, keks }); + ok((a.daten?.eintraege || []).some((e) => e.id === eintragId), + `${wer} sieht ihn`); + } +} + +/* ======================================================================= + 3. Die Bruecke: einmal an die Person schicken + ======================================================================= */ +console.log(""); +console.log("=== An die Person schicken ==="); +{ + const a = await hole(`${ENTW}/${eintragId}/senden`, { host: CREW, methode: "POST", + keks: dogi.keks, rumpf: "{}" }); + ok(a.status === 200, `geschickt (${a.status})`); + + /* KOMMT SIE WIRKLICH AN? Ohne diese Zeile waere "geschickt" nur ein + Datum in einer Spalte. Nachgesehen wird in AYLAS Chat, nicht in + der Antwort der Route. */ + const raeume = await hole("/workspace/api/chat/raeume", { host: CREW, keks: ayla.keks }); + const raum = (raeume.daten?.raeume || [])[0]; + ok(!!raum, `Ayla hat ein Gespraech (${(raeume.daten?.raeume || []).length})`); + const verlauf = await hole(`/workspace/api/chat/raeume/${raum?.id}/nachrichten`, + { host: CREW, keks: ayla.keks }); + const texte = (verlauf.daten?.nachrichten || []).map((n) => n.text || "").join(" "); + ok(/Ruhig geblieben, als es kippte/.test(texte), "und die Nachricht steht darin"); + ok(/Das läuft gut/.test(texte), + "mit der Art davor -- sonst waere es ein Fetzen ohne Zusammenhang"); + ok(/Beim nächsten Live wieder ihr überlassen/.test(texte), + "und dem, was daraus folgen soll"); + + /* EINMAL IST EINMAL. Zweimal dasselbe Lob ist kein doppeltes Lob. */ + const b = await hole(`${ENTW}/${eintragId}/senden`, { host: CREW, methode: "POST", + keks: dogi.keks, rumpf: "{}" }); + ok(b.status === 409, `ein zweites Mal wird abgelehnt (${b.status})`); + const nachher = await hole(`/workspace/api/chat/raeume/${raum?.id}/nachrichten`, + { host: CREW, keks: ayla.keks }); + ok((nachher.daten?.nachrichten || []).length === (verlauf.daten?.nachrichten || []).length, + "und es kam keine zweite Nachricht an"); + + /* Ein Eintrag ohne Person laesst sich nicht schicken -- an wen auch. */ + const ohne = await hole(ENTW, { host: CREW, methode: "POST", keks: dogi.keks, + rumpf: JSON.stringify({ art: "wachsen", titel: "Allgemeine Notiz", datum: "2026-09-11" }) }); + const c = await hole(`${ENTW}/${ohne.daten?.id}/senden`, { host: CREW, methode: "POST", + keks: dogi.keks, rumpf: "{}" }); + ok(c.status === 400, `ein Eintrag ohne Person geht nicht raus (${c.status})`); +} + +/* ======================================================================= + 4. Die Kacheln + ======================================================================= */ +console.log(""); +console.log("=== Die Kacheln ==="); +{ + for (const [wer, keks] of [["DogFather", dogi.keks], ["die rechte Hand", hand.keks]]) { + const a = await hole("/workspace/api/ich", { host: CREW, keks }); + const alle = [...(a.daten?.bereiche || []), ...(a.daten?.bereiche_zusatz || [])]; + const beide = alle.filter((k) => /b=(entwicklung|talente)/.test(k.ziel || "")); + ok(beide.length === 2, `${wer}: beide Kacheln (${beide.map((k) => k.name).join(", ") || "keine"})`); + ok(beide.every((k) => k.gruppe === "Entwicklung & Nachwuchs"), + `${wer}: in einer eigenen Gruppe (${[...new Set(beide.map((k) => k.gruppe))].join(", ")})`); + } + const a = await hole("/workspace/api/ich", { host: CREW, keks: ayla.keks }); + const alle = [...(a.daten?.bereiche || []), ...(a.daten?.bereiche_zusatz || [])]; + ok(!alle.some((k) => /b=(entwicklung|talente)/.test(k.ziel || "")), + `ein Teammitglied bekommt sie nicht angeboten (${alle.length} Kacheln)`); + + /* Kein Farbton doppelt -- zwei Kacheln in derselben Farbe sind ein + Fehler, den man sonst erst am Bildschirm findet. */ + const dogiIch = await hole("/workspace/api/ich", { host: CREW, keks: dogi.keks }); + const toene = [...(dogiIch.daten?.bereiche || [])].map((k) => k.ton).filter((t) => t !== undefined); + ok(new Set(toene).size === toene.length, `kein Farbton doppelt (${toene.length} Kacheln)`); +} + +d.close(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +console.log(""); +console.log(`${fehler === 0 ? "BESTANDEN" : "FEHLGESCHLAGEN"} -- ${geprueft} geprueft, ${fehler} Fehler`); +process.exit(fehler ? 1 : 0); diff --git a/server/pruef-modi-checkliste.mjs b/server/pruef-modi-checkliste.mjs index fc3e243e..a43cf987 100644 --- a/server/pruef-modi-checkliste.mjs +++ b/server/pruef-modi-checkliste.mjs @@ -370,12 +370,28 @@ for (const [name, an, wenigstens] of [ ]) { const a = await hole(an.keks, "/workspace/api/ich"); const zusatz = a.daten?.bereiche_zusatz || []; - const falsch = zusatz.filter((k) => k.gruppe !== "Team Dogi").map((k) => k.name); + /* ZWEI GRUPPEN SIND ERLAUBT, und die Liste steht ausgeschrieben da + (11.09.2026). + + Hier stand nur "Team Dogi". Seit es "Entwicklung & Nachwuchs" gibt, + waere die Pruefung rot geworden -- und die naheliegende Reparatur + ("prueft eben gar nicht mehr, in welcher Gruppe") haette die + Aussage weggeworfen statt sie nachzuziehen. + + Die Aussage ist: Eine Zusatzkachel darf NICHT in einer Gruppe + landen, die alle sehen. Steht sie in "Taeglich" oder "Rund um den + Creator", sitzt sie zwischen Kacheln, die jeder hat -- und dort + faellt niemandem auf, dass sie nur bei zweien steht. + + Kommt eine dritte Gruppe, wird diese Zeile rot. Das ist Absicht: + Wo Kacheln ueber Menschen stehen, ist etwas Reibung richtig. */ + const ERLAUBTE_GRUPPEN = new Set(["Team Dogi", "Entwicklung & Nachwuchs"]); + const falsch = zusatz.filter((k) => !ERLAUBTE_GRUPPEN.has(k.gruppe)).map((k) => k.name); ok(wenigstens ? zusatz.length >= 1 : zusatz.length === 0, `${name.padEnd(12)} bekommt ${zusatz.length} Zusatzkachel(n)` + `, erwartet ${wenigstens ? "mindestens eine" : "keine"}`); ok(falsch.length === 0, - `${name.padEnd(12)} und alle stehen in der Gruppe Team Dogi` + `${name.padEnd(12)} und alle stehen in einer Gruppe fuer euch beide` + (falsch.length ? ` -- ausser: ${falsch.join(", ")}` : "")); } diff --git a/server/pruef-start-ansicht.mjs b/server/pruef-start-ansicht.mjs index 0771c78a..6979ae98 100644 --- a/server/pruef-start-ansicht.mjs +++ b/server/pruef-start-ansicht.mjs @@ -204,10 +204,20 @@ for (const [breite, wie] of [[1440, "Computer"], [390, "Handy"]]) { bereiche.js -- sie steht in keiner Datei, die jeder herunterladen kann. Die Kachelzahl blieb bei 24: der Eingang ist umgezogen, das Ideen-Board aus bereiche.js hierher gewandert, keine Kachel ist neu. */ - const ERWARTETE_GRUPPEN = ["Täglich", "Team Dogi", "Rund um den Creator", - "Rund um das Team", "Team & System"]; + /* NACHTRAG 11.09.2026: SECHS -- "Entwicklung & Nachwuchs" an dritter + Stelle. + + Sie steht dort nicht zufaellig: `gruppeNach: "Team Dogi"` setzt sie + direkt hinter das Team. Was ueber Menschen aufgeschrieben wird, + gehoert neben das Team und nicht zwischen die taeglichen + Werkzeuge -- und schon gar nicht ans Ende, wo es niemand ansieht. + + Die Kachelzahl steigt von 25 auf 27; beide Kacheln sind neu + (Entwicklung, Talente), keine ist umgezogen. */ + const ERWARTETE_GRUPPEN = ["Täglich", "Team Dogi", "Entwicklung & Nachwuchs", + "Rund um den Creator", "Rund um das Team", "Team & System"]; ok(JSON.stringify(r.gruppen.map((g) => g.name)) === JSON.stringify(ERWARTETE_GRUPPEN), - `fünf Gruppen in dieser Reihenfolge (${r.gruppen.map((g) => g.name).join(" · ")})`); + `sechs Gruppen in dieser Reihenfolge (${r.gruppen.map((g) => g.name).join(" · ")})`); /* WIE VIELE KACHELN? Nicht abgeschrieben, sondern aus bereiche.js gezaehlt (06.09.2026). diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index b859b595..f87647ab 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -18,8 +18,9 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, betreutWo, darfCreator, betreuteIds, istLeitung, istDogFather, siehtAlles, istSpicy, ohneDogFather, externPruefen, externSql, - ohneTeamDogi, ohneAgentur, siehtModis, MODI_BEREICHE_ERLAUBT, TEAM_DOGI_ROLLEN, + ohneTeamDogi, ohneAgentur, siehtModis, fuehrtTeamDogi, MODI_BEREICHE_ERLAUBT, TEAM_DOGI_ROLLEN, } from "./workspace.js"; +import { nachrichtSchicken } from "./workspace-chat.js"; export const bereicheRouter = express.Router(); @@ -173,6 +174,104 @@ export const BEREICHE = { Saetzen erkennt jeder jeden. Ein Versprechen, das nicht haelt, ist schlimmer als keines. Was es stattdessen gibt, ist die Wahl zwischen "fuers Team" und "nur an DogFather". */ + /* ===================================================================== + ENTWICKLUNG (11.09.2026) + + Filipe: "wo wir aufgaben oder bewertungen ueber modis eingeben + koennen." + + ES IST KEINE NOTE, UND DAS IST KEINE ZIMPERLICHKEIT, SONDERN DAS + ERGEBNIS DER RECHERCHE. Die groesste Untersuchung dazu (Schoepke- + Gonzalez u. a., New Media & Society 2024, ueber 500 befragte + Freiwillige) nennt zwei Hauptgruende, warum Moderatoren aufhoeren: + zu wenig Zeit -- und Konflikte im Team beziehungsweise schaedliches + Verhalten der Leitung. Eine Bewertungsfunktion ist damit genau das + Werkzeug, mit dem man ein Team verliert, wenn sie als Ueberwachung + gebaut ist. + + Was HAELT, ist ebenso gut belegt: Anerkennung. Dank und + Rueckmeldung erhoehen die Verweildauer messbar; Rollenklarheit + senkt Burnout. + + DARAUS FOLGEN DIE FUENF ARTEN, und ihre Reihenfolge ist Absicht: + Das Erste, was man sieht, ist "Das laeuft gut". Wer ein Formular + oeffnet, dessen erstes Feld "Problem" heisst, schreibt Probleme + auf. + + "ZU VIEL GERADE" IST DIE WICHTIGSTE ART und die, die es sonst + nirgends gibt. Der haeufigste Grund zu gehen ist Zeitmangel, und + der zeigt sich frueh -- nur schreibt ihn niemand auf, weil es + kein Feld dafuer gibt. Jetzt gibt es eins. + + DIE PERSON SIEHT DEN BEREICH NICHT. Eine halb sichtbare Akte ist + schlimmer als eine geschlossene: Niemand weiss mehr, was der + andere gerade liest. Damit Anerkennung trotzdem ankommt, laesst + sich jeder Eintrag als Nachricht an die Person schicken -- ein + ausdruecklicher Schritt, und im Eintrag steht danach, wann er + getan wurde. + ===================================================================== */ + entwicklung: { + name: "Entwicklung", + ober: "Über die Zeit", + arten: { + staerke: "Das läuft gut", + dank: "Danke dafür", + wachsen: "Daran arbeiten wir", + last: "Zu viel gerade", + absprache: "So haben wir es vereinbart", + }, + bewertung: false, + dringlichkeit: true, + dringlichkeitName: "Wie dringend", + /* Der Personenbezug ist hier der SINN des Eintrags und nicht eine + Nebenangabe -- deshalb ausdruecklich KEIN `ohneCreatorBezug`. */ + einsatzFeld: true, + einsatzName: "Was daraus folgen soll", + }, + + /* ===================================================================== + TALENTE (11.09.2026) + + Filipe: "auch fuer zuschauer die vielleicht modis werden koennten + waere auch geil." + + DIE VIER MERKMALE SIND NICHT AUSGEDACHT. Sie sind das, was sich in + der Literatur zu Moderatorenauswahl immer wieder findet (ModSquad, + Kitfox Games, Discord-Leitfaeden): ruhig bleiben, wenn es hitzig + wird; von selbst helfen; die Regeln UND die Leute kennen; und + regelmaessig da sein. Ausdruecklich NICHT dabei: "schreibt viel" + -- angenehm im Chat zu sein ist nachweislich etwas anderes, als + ein guter Moderator zu sein. + + Und der treffsicherste Weg ueberhaupt ist der fuenfte: die + EMPFEHLUNG aus dem Team. Wer schon dabei ist, hat den Menschen im + Alltag erlebt, nicht in einem Bewerbungsformular. + + DER STATUS IST DIE PROBEZEIT. `offen` heisst beobachtet, + `angenommen` heisst angesprochen -- und was daraus wird, + entscheidet sich in "Personen & Zugaenge" mit einem Zugang auf + Stufe "Probe". Eine eigene Kandidatentabelle waere ein zweiter Ort + fuer dieselbe Frage. + ===================================================================== */ + talente: { + name: "Talente", + ober: "Nachwuchs", + arten: { + ruhig: "Bleibt ruhig, wenn es hitzig wird", + hilft: "Hilft anderen von selbst", + kennt: "Kennt die Regeln und die Leute", + da: "Ist regelmäßig da", + empfohlen: "Aus dem Team empfohlen", + }, + bewertung: true, + bewertungName: "Wie sicher bin ich", + dringlichkeit: true, + dringlichkeitName: "Wie eilig", + ohneCreatorBezug: true, + einsatzFeld: true, + einsatzName: "Wo ich ihn oder sie gesehen habe", + }, + rueckmeldung: { name: "Rückmeldung", /* Wie beim Ideen-Board: Das hier ist keine Akte ueber jemanden, @@ -339,6 +438,31 @@ for (const geschuetzt of ["ideen", "angebote", "rueckmeldung"]) { }); } +/* --------------------------------------------------------------------- + ZWEI BEREICHE GEHEN NUR DOGFATHER UND DIE RECHTE HAND AN (11.09.2026) + + Filipe: "wo WIR aufgaben oder bewertungen ueber modis eingeben + koennen." Wir heisst hier: die beiden. + + `siehtModis` waere zu weit -- darin sind die Modis selbst enthalten, + und in "Entwicklung" steht, was ueber sie aufgeschrieben wird. + `fuehrtTeamDogi` ist die engere Menge und dieselbe, die schon den + Eingang und die Kanaele traegt. + + 404 UND NICHT 403: Ein Modi soll nicht erfahren, dass es diese + Bereiche gibt. Ein "das darfst du nicht" waere die Auskunft, die + genau die Frage aufwirft, die niemand stellen soll. + + ZWEI SCHLOESSER WIE OBEN: Hier haengt der ZUGANG, die Sichtbarkeit + der Zeilen haengt an sichtbarEintrag(). Faellt eines weg, haelt das + andere. */ +for (const nurLeitung of ["entwicklung", "talente"]) { + bereicheRouter.use(`/workspace/api/bereich/${nurLeitung}`, (req, res, next) => { + if (fuehrtTeamDogi(req.person)) return next(); + return res.status(404).json({ fehler: "nicht_gefunden" }); + }); +} + /* --------------------------------------------------------------------- UND UMGEKEHRT: Ein Modi kommt nur in SEINE Bereiche. @@ -629,6 +753,11 @@ const SPALTEN = ` andere -- und beim naechsten Mal schriebe er offen, was er vertraulich meinte. */ e.nur_leitung, + /* Wann der Eintrag der Person geschickt wurde -- oder NULL. Die + Oberflaeche baut daraus entweder einen Knopf oder einen Satz; ohne + diese Angabe koennte sie nur einen Knopf zeigen, der beim zweiten + Druecken eine Absage bringt. */ + e.gesendet_am, ${externSql("pc.name", "e.creator_extern")} AS creator_name, pe.name AS erstellt_name, (SELECT name FROM content_saeulen s WHERE s.id = e.saeule_id) AS saeule_name, @@ -675,6 +804,88 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => { } }); +/* ===================================================================== + EINEN EINTRAG AN DIE PERSON SCHICKEN (11.09.2026) + + Der Entwicklungs-Bereich ist eine Aufzeichnung fuer DogFather und die + rechte Hand; die Person selbst sieht ihn nicht. Diese Route ist die + einzige Bruecke -- und sie geht nur in EINE Richtung, ausdruecklich + und einzeln. + + WARUM NICHT EINFACH ALLES SICHTBAR MACHEN: Eine halb sichtbare Akte + ist schlimmer als eine geschlossene. Niemand weiss mehr, was der + andere gerade liest, und man schreibt anders -- vorsichtiger, + weniger, oder gar nicht mehr. + + WARUM ES DIESE BRUECKE TROTZDEM BRAUCHT: Die Forschung zu + freiwilligen Moderatoren ist an dieser Stelle eindeutig. Was Menschen + haelt, ist Anerkennung -- Dank und Rueckmeldung erhoehen die + Verweildauer messbar. Ein "Das laeuft gut", das nur in einer Akte + steht, hat niemandem geholfen. + + DER WEG IST DER CHAT und keine neue Benachrichtigungsart: Die Person + findet die Nachricht dort, wo sie alles andere auch findet, kann + antworten, und sie steht in ihrem Verlauf. Ein eigener Kanal dafuer + waere ein zweiter Ort mit einer roten Zahl. + + EINMAL IST EINMAL. `gesendet_am` haelt fest, wann es geschehen ist -- + die Route schickt danach nicht noch einmal. Zweimal dasselbe Lob ist + kein doppeltes Lob, sondern ein Versehen, das jeder bemerkt. + ===================================================================== */ +bereicheRouter.post("/workspace/api/bereich/entwicklung/:id/senden", + gleicheHerkunft, (req, res) => { + try { + if (!fuehrtTeamDogi(req.person)) return res.status(404).json({ fehler: "nicht_gefunden" }); + const id = Number(req.params.id); + if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" }); + + const e = db().prepare( + `SELECT id, bereich, art, titel, text, einsatz, creator_id, gesendet_am + FROM eintraege WHERE id = ?`).get(id); + if (!e || e.bereich !== "entwicklung") { + return res.status(404).json({ fehler: "nicht_gefunden" }); + } + if (!e.creator_id) { + return res.status(400).json({ + fehler: "Dieser Eintrag gehört zu niemandem – trag zuerst ein, um wen es geht.", + }); + } + if (e.gesendet_am) { + return res.status(409).json({ fehler: "Das ist schon geschickt.", am: e.gesendet_am }); + } + + /* DER TEXT WIRD GEBAUT UND NICHT ROH GESCHICKT. Im Bereich steht + eine Notiz ("Ruhig geblieben, als es kippte"); als Nachricht + ohne Zusammenhang liest sich das wie ein Fetzen. Die Art davor + sagt, was gemeint ist, und der Einsatz dahinter, was daraus + folgen soll -- beides steht ohnehin schon da. */ + const art = BEREICHE.entwicklung.arten[e.art] || ""; + const stuecke = [ + art ? `${art}: ${e.titel}` : e.titel, + e.text || "", + e.einsatz ? `Was daraus folgen soll: ${e.einsatz}` : "", + ].filter(Boolean); + + const geschickt = nachrichtSchicken(req.person, e.creator_id, stuecke.join("\n\n")); + if (!geschickt) { + return res.status(400).json({ + fehler: "Die Nachricht ging nicht raus – ist die Person noch aktiv?", + }); + } + + const am = jetzt(); + db().prepare("UPDATE eintraege SET gesendet_am = ? WHERE id = ?").run(am, id); + protokolliere("entwicklung_geschickt", { + personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), + detail: `#${id} an #${e.creator_id}`, + }); + res.json({ ok: true, am, raum_id: geschickt.raum_id }); + } catch (fehler) { + console.error("[workspace] Entwicklung schicken:", fehler?.message); + res.status(503).json({ fehler: "nicht_verfuegbar" }); + } + }); + /* ---------- Prüfen ------------------------------------------------------- */ function pruefe(bereich, körper, { neu }) { @@ -875,9 +1086,32 @@ bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, re aus.creator_id = erster; } } + /* WEM GEHOERT DER EINTRAG? DAS HAENGT AM BEREICH (11.09.2026). + + Ueberall sonst zeigt `creator_id` auf einen Creator -- die + Bereichsseiten sind Betreuungsakten. Im Entwicklungs-Bereich + zeigt sie auf ein Mitglied von Team Dogi: Dort steht, was ueber + die Zeit an einem Menschen aus dem Team auffaellt. + + Die Spalte heisst weiterhin creator_id. Das ist Geschichte, kein + Inhalt -- sie haelt eine Personennummer, und dieselbe + Ueberlegung steht schon in workspace-checkliste.js. + + BEIM ERSTEN ANLAUF FEHLTE DAS, und die Meldung war irrefuehrend + ("Zugeordneter Creator existiert nicht" -- die Person existiert + sehr wohl, sie ist nur kein Creator). Gefunden von der Pruefung, + nicht vom Auge. */ + const zielRollen = bereich === "entwicklung" + ? "rolle IN ('hand','modi')" + : "rolle = 'creator'"; if (aus.creator_id - && !db().prepare("SELECT 1 FROM personen WHERE id = ? AND rolle = 'creator'").get(aus.creator_id)) { - return res.status(400).json({ fehler: "Zugeordneter Creator existiert nicht." }); + && !db().prepare(`SELECT 1 FROM personen WHERE id = ? AND ${zielRollen}`) + .get(aus.creator_id)) { + return res.status(400).json({ + fehler: bereich === "entwicklung" + ? "Diese Person gehört nicht zum Team." + : "Zugeordneter Creator existiert nicht.", + }); } /* Eine Saeule gehoert immer GENAU EINEM Creator. Ohne diese Pruefung diff --git a/server/workspace-chat.js b/server/workspace-chat.js index f922c532..a5d1c6e2 100644 --- a/server/workspace-chat.js +++ b/server/workspace-chat.js @@ -1542,6 +1542,51 @@ chatRouter.use((fehler, req, res, next) => { }); /* Für Prüfungen und andere Module: Sieht diese Person gerade zu? */ +/* ===================================================================== + EINE NACHRICHT AUS EINEM ANDEREN MODUL SCHICKEN (11.09.2026) + + Gebraucht vom Entwicklungs-Bereich: Dort steht, was DogFather und die + rechte Hand ueber die Zeit beobachten -- und die Person selbst sieht + diesen Bereich nicht. Damit Anerkennung trotzdem ankommt, laesst sich + ein Eintrag als Nachricht schicken. + + WARUM HIER UND NICHT DORT: Ein Zweier-Gespraech darf es nur EINMAL + geben (siehe direktRaum), die Leseraender muessen mitwandern, der + Live-Strom und die Benachrichtigung haengen daran. Wer das im + Nachbarmodul nachbaut, hat zwei Fassungen -- und die zweite ist die, + in der jemand eine Nachricht nicht bekommt. + + DIE REGEL "wer darf mit wem" WIRD MITGEPRUEFT, obwohl der einzige + Aufrufer heute ohnehin darf. Eine Funktion, die aus einem fremden + Modul heraus jeden erreichen kann, ist genau die Abkuerzung, die man + spaeter woanders benutzt. + ===================================================================== */ +export function nachrichtSchicken(person, anId, text) { + const ziel = Number(anId); + const satz = String(text ?? "").trim().slice(0, TEXT_MAX); + if (!person || !Number.isInteger(ziel) || ziel < 1 || !satz) return null; + if (!darfSchreibenMit(person, ziel)) return null; + if (!db().prepare("SELECT 1 FROM personen WHERE id = ? AND aktiv = 1").get(ziel)) return null; + + const raumId = direktRaum(person, ziel); + const d = db(); + const n = jetzt(); + d.prepare("INSERT INTO chat_nachrichten (raum_id, person_id, text, erstellt) VALUES (?,?,?,?)") + .run(raumId, person.id, satz, n); + const id = d.prepare("SELECT last_insert_rowid() AS id").get().id; + d.prepare("UPDATE chat_raeume SET letzte_am = ? WHERE id = ?").run(n, raumId); + d.prepare("UPDATE chat_teilnehmer SET gelesen_bis = ? WHERE raum_id = ? AND person_id = ?") + .run(id, raumId, person.id); + + const nachricht = { + id, raum_id: raumId, von_id: person.id, von: person.name, + rolle: person.rolle, text: satz, erstellt: n, + zurueckgenommen: false, antwort: null, anhang: null, + }; + chatEreignis(raumId, nachricht, [ziel], "neu"); + return { id, raum_id: raumId }; +} + export function siehtZu(personId) { return (zuschauer.get(Number(personId)) || []).size > 0; } diff --git a/server/workspace.js b/server/workspace.js index 6bf97833..e6e8a2b2 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -563,6 +563,40 @@ const EINGANG_KACHEL = { werden koennen nur Team-Rollen (siehe hausBedingung und darfAnlegen). Eine Kachel, die etwas anderes verspricht als die Antwort dahinter, waere schlimmer als keine. */ +/* ===================================================================== + ENTWICKLUNG UND TALENTE (11.09.2026) + + Filipe: "ich will dass ich genau so eine kategorie habe in der + dogfather und rechten hand rollen, wo wir aufgaben oder bewertungen + ueber modis eingeben koennen. auch fuer zuschauer die vielleicht + modis werden koennten waere auch geil." + + DIE GRUPPE HEISST NICHT "TEAM FUEHREN". Das waere der bequeme Name + und der falsche: Im Haus gilt, dass DogFather nicht ueber seinem Team + steht. "Entwicklung & Nachwuchs" sagt, WAS drinsteht, nicht, wer wem + uebergeordnet ist. + + ZWEI KACHELN UND NICHT DREI: Aufgaben an das Team gibt es laengst -- + sie stehen im Aufgabenbrett und haengen dort an einer Person, einer + Frist und einem Status. Eine zweite Stelle dafuer waere ein zweiter + Ort, an dem man nachsehen muesste, welche Aufgabe wirklich gilt. */ +const FUEHRUNG_GRUPPE = { + gruppe: "Entwicklung & Nachwuchs", + gruppeUnter: "Was ihr über die Zeit beobachtet – und wer dazukommen könnte", +}; + +const ENTWICKLUNG_KACHEL = { + ...FUEHRUNG_GRUPPE, + name: "Entwicklung", unter: "Was gut läuft, woran ihr arbeitet – je Person", + zeichen: "steckbrief", ton: 26, ziel: "bereich.html?b=entwicklung", szene: "studio", +}; + +const TALENTE_KACHEL = { + ...FUEHRUNG_GRUPPE, + name: "Talente", unter: "Zuschauer, die ins Team passen könnten", + zeichen: "scouting", ton: 27, ziel: "bereich.html?b=talente", szene: "portal", +}; + const PERSONEN_KACHEL = { gruppe: "Für dich", gruppeUnter: "Dein Platz im Team", name: "Personen & Zugänge", unter: "Wer dabei ist – und mit welchem Zugang", @@ -594,7 +628,8 @@ const PERSONEN_KACHEL = { Also bekommt sie die Seite -- lesend. Damit stimmt der Knopf wieder, und er steht in derselben Liste wie alles andere, was sie und DogFather auf dieser Adresse teilen. */ -const HAND_BEREICHE = [...MODI_BEREICHE, EINGANG_KACHEL, PERSONEN_KACHEL]; +const HAND_BEREICHE = [...MODI_BEREICHE, EINGANG_KACHEL, PERSONEN_KACHEL, + ENTWICKLUNG_KACHEL, TALENTE_KACHEL]; export function bereicheFuer(person) { if (person?.rolle === "modi") return MODI_BEREICHE; @@ -883,6 +918,11 @@ const ZUSATZ_BEREICHE = { name: "Rückmeldung", unter: "Was gut läuft, was hakt – in beide Richtungen", zeichen: "reports", ton: 25, ziel: "bereich.html?b=rueckmeldung", szene: "lounge", }, + /* Eine eigene Gruppe, direkt hinter Team Dogi: Was ueber Menschen + aufgeschrieben wird, steht nicht zwischen den taeglichen + Werkzeugen. */ + { ...ENTWICKLUNG_KACHEL, gruppeNach: TEAM_GRUPPE.gruppe }, + { ...TALENTE_KACHEL, gruppeNach: TEAM_GRUPPE.gruppe }, ], }; @@ -1475,6 +1515,24 @@ function umstellungen(d) { Sie kann selbst genauso schreiben -- auch ueber ihn. */ ["eintraege", "nur_leitung", "INTEGER"], + /* WANN WURDE ES DER PERSON GESAGT? (11.09.2026) + + Der Entwicklungs-Bereich ist eine Aufzeichnung fuer DogFather und + die rechte Hand -- die Person selbst sieht ihn nicht. Das ist + Absicht: Eine halb sichtbare Akte ist schlimmer als eine + geschlossene, weil niemand mehr weiss, was der andere gerade + liest. + + Damit Anerkennung trotzdem ANKOMMT (die Forschung dazu ist + eindeutig: Dank und Rueckmeldung sind der staerkste Grund, warum + Freiwillige bleiben), kann jeder Eintrag als Nachricht an die + Person geschickt werden. Hier steht, WANN das geschehen ist. + + Ohne diese Spalte gaebe es die Frage "habe ich ihr das eigentlich + schon gesagt?" -- und die beantwortet man nach zwei Wochen + falsch. */ + ["eintraege", "gesendet_am", "TEXT"], + /* ANGEHEFTETE NACHRICHTEN (10.09.2026, Kapitel 7.2). "Ankuendigungs-Funktion: Pin-Nachrichten von Owner/rechte Hand @@ -2057,6 +2115,14 @@ function umstellungen(d) { ["live", "content", "technik", "community", "schutz", "agentur", "ideen", "angebote", "rueckmeldung"], jetztStempel); + /* ENTWICKLUNG UND TALENTE (11.09.2026, Wunsch Filipe). */ + checkListeErweitern(d, "eintraege", "bereich", "entwicklung", + ["live", "content", "technik", "community", "schutz", "agentur", "ideen", + "angebote", "rueckmeldung", "entwicklung"], jetztStempel); + checkListeErweitern(d, "eintraege", "bereich", "talente", + ["live", "content", "technik", "community", "schutz", "agentur", "ideen", + "angebote", "rueckmeldung", "entwicklung", "talente"], jetztStempel); + checkListeErweitern(d, "eintraege", "status", "angenommen", ["offen", "erledigt", "angenommen", "abgelehnt"], jetztStempel); diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 01bcd2a1..7ca6afbe 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=202609110045", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609110109", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609110045", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609110109", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609110045", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609110109", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609110045", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609110109", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/bereich.css b/workspace/assets/css/bereich.css index 9e1bcc8e..5afa7f38 100644 --- a/workspace/assets/css/bereich.css +++ b/workspace/assets/css/bereich.css @@ -550,3 +550,41 @@ background: rgba(217, 174, 98, .16); color: #d9ae62; } + +/* ===================================================================== + ENTWICKLUNG: der Weg zur Person (11.09.2026) + + Der Knopf steht IM KOPF der Karte, neben der Art -- nicht unten bei + den anderen Handgriffen. Er ist keine Bearbeitung, sondern der eine + Schritt, der den Eintrag aus der Aufzeichnung heraus zur Person + bringt; unten zwischen „Ändern" und „Löschen" wäre er einer von + dreien statt der besondere. + ===================================================================== */ +.eintrag-schicken { + appearance: none; + padding: 2px 11px; border-radius: 999px; + border: 1px solid color-mix(in srgb, var(--akzent) 40%, transparent); + background: color-mix(in srgb, var(--akzent) 12%, transparent); + color: var(--text); + /* .72rem und nicht .68 -- unter 11,5 px gilt im Haus als zu klein, + und dieser Knopf traegt den einzigen Schritt, der einen Eintrag + zur Person bringt. Gefunden von pruef-css-klassen. */ + font: inherit; font-size: .72rem; font-weight: 700; + letter-spacing: .04em; text-transform: uppercase; + white-space: nowrap; cursor: pointer; + transition: background var(--tempo), border-color var(--tempo); +} +.eintrag-schicken:hover { + background: color-mix(in srgb, var(--akzent) 22%, transparent); + border-color: color-mix(in srgb, var(--akzent) 60%, transparent); +} +.eintrag-schicken:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; } +.eintrag-schicken[disabled] { opacity: .5; cursor: default; } + +/* Schon geschickt: kein Knopf mehr, sondern eine Auskunft. Leise -- + sie ist eine Fußnote, keine Meldung. */ +.marke-art--weg { + background: rgba(255, 255, 255, .05); + color: var(--text-still); + text-transform: none; letter-spacing: 0; font-weight: 600; +} diff --git a/workspace/assets/css/start.css b/workspace/assets/css/start.css index 9442020d..04dec67f 100644 --- a/workspace/assets/css/start.css +++ b/workspace/assets/css/start.css @@ -1010,6 +1010,26 @@ body.start > main { position: relative; z-index: 1; } nicht nach Beschwerde aussehen. */ [data-ton="25"] { --ton: #43c95e; } /* Rückmeldung */ +/* TON 26 UND 27 (11.09.2026) -- und hier wird die Rechnung ehrlich: + + Bei fuenfundzwanzig Toenen ist der Farbkreis voll. Die groessten + Luecken liegen jetzt bei 35 Grad, und beide waeren ein DRITTES Gruen + zwischen zwei vorhandenen. Wer den Farbwinkel weiter allein + entscheiden laesst, bekommt Nachbarn, die sich nur noch der Messung + nach unterscheiden. + + Deshalb trennen diese beiden ueber SAETTIGUNG und HELLIGKEIT, nicht + ueber den Winkel: ein blasses Minzgruen (viel heller und flauer als + das satte Gruen der Rueckmeldung und als das dunkle Teal der Reports) + und ein gedaempftes Lavendel (grauer und dunkler als das kraeftige + Violett der Dateien). Beide halten ueber 7:1 auf dem dunklen Grund. + + Wenn eine achtundzwanzigste Kachel kommt, ist das der Zeitpunkt, an + dem die Kacheln Gruppenfarben brauchen statt Einzelfarben -- nicht + der, an dem man den naechsten Ton dazwischenquetscht. */ +[data-ton="26"] { --ton: #7fd4a8; } /* Entwicklung */ +[data-ton="27"] { --ton: #8f7bd6; } /* Talente */ + /* Rückfallwert für alles, was einen Ton braucht, aber keine Nummer hat. Steht bewusst in :where() -- damit zählt die Zeile beim Wettstreit der Regeln als NICHT vorhanden und die Nummer oben gewinnt immer. Ohne diff --git a/workspace/assets/js/bereich.js b/workspace/assets/js/bereich.js index d32b0b69..cb5181ec 100644 --- a/workspace/assets/js/bereich.js +++ b/workspace/assets/js/bereich.js @@ -113,6 +113,45 @@ keine Sperre, sondern eine Auskunft: Ohne ihn saehe die eigene Zeile aus wie jede andere, und beim naechsten Mal schriebe man offen, was man vertraulich gemeint hat. */ + /* AN DIE PERSON SCHICKEN (11.09.2026). + + Nur im Entwicklungs-Bereich, und nur dort, wo ein Eintrag + überhaupt zu jemandem gehört. Er ist die einzige Brücke aus einer + Aufzeichnung, die die Person selbst nicht sieht -- und deshalb + ein ausdrücklicher Schritt und keine Automatik. + + WURDE SCHON GESCHICKT? Dann steht dort ein Satz statt eines + Knopfs. Ein Knopf, der beim zweiten Drücken eine Absage bringt, + ist eine Falle; und zweimal dasselbe Lob ist kein doppeltes Lob, + sondern ein Versehen, das jeder bemerkt. */ + if (bereich === 'entwicklung' && e.creator_id) { + if (e.gesendet_am) { + const hin = el('span', 'marke-art marke-art--weg', + `geschickt ${new Date(e.gesendet_am).toLocaleDateString('de-DE', + { day: 'numeric', month: 'short' })}`); + hin.title = 'Diese Person hat den Eintrag als Nachricht bekommen.'; + kopf.append(hin); + } else { + const b = el('button', 'eintrag-schicken', 'An die Person schicken'); + b.type = 'button'; + b.title = 'Schickt Art, Titel und Text als Nachricht in den Chat.'; + b.addEventListener('click', async () => { + if (!confirm(`„${e.titel}“ als Nachricht schicken?\n\n` + + 'Die Person bekommt Art, Titel und Text im Chat – einmalig.')) return; + b.disabled = true; + const a = await hole(`/workspace/api/bereich/entwicklung/${e.id}/senden`, + { method: 'POST', headers: { 'Content-Type': 'application/json' } }); + if (!a.ok) { + melde((await a.json().catch(() => ({}))).fehler || 'Ging nicht.'); + b.disabled = false; + return; + } + await laden(); + }); + kopf.append(b); + } + } + if (e.nur_leitung) { const v = el('span', 'marke-art marke-art--nur', 'nur DogFather'); v.title = 'Das liest ausser dir nur DogFather.'; diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 8115c5a6..3e57b703 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. --> - - - - - - + + + + + + - + @@ -252,10 +252,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index b66f3b7d..2614808e 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. --> - - - - - - - + + + + + + + - + @@ -147,10 +147,10 @@ - - - - - + + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 8ea34379..21ae53b4 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 4d6767fb..159a20df 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 18aeabc2..f52266fc 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. --> - - - - + + + + - - - + + + - + @@ -289,10 +289,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index 3f70bcd1..51a1b76f 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 058bb46b..fe51f41a 100644 --- a/workspace/crew-index.html +++ b/workspace/crew-index.html @@ -30,15 +30,15 @@ - - - + + + - + @@ -252,6 +252,6 @@ - + diff --git a/workspace/crew.webmanifest b/workspace/crew.webmanifest index 1b83e4d4..21e9108c 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=202609110045", + "src": "/assets/img/app-symbole/crew-192.png?v=202609110109", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609110045", + "src": "/assets/img/app-symbole/crew-512.png?v=202609110109", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609110045", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609110109", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609110045", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609110109", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index 53d45de4..5595ae0d 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/index.html b/workspace/index.html index c09b0eb4..723580bd 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 500e635b..0c939357 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 1cfe3a54..e47ea56b 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. --> - - - - - - - + + + + + + + - + @@ -224,10 +224,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index e6f3be90..80347b10 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. --> - - - - - - - + + + + + + + - + @@ -396,10 +396,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 3e9e11e8..2f9d35e0 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/report.html b/workspace/report.html index b409a1db..888478d6 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 8f168e2b..18b24233 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. --> - - - - - - - + + + + + + + - + @@ -139,10 +139,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index c07af1bf..6d8f1696 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 646c4f87..c3a6bfb9 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 cc75f45d..3234d14f 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. --> - - - - - - - + + + + + + + - + @@ -126,10 +126,10 @@ - - - - - + + + + + diff --git a/workspace/team.html b/workspace/team.html index c7194582..72f4e88c 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 69c3d500..3ab0ccd4 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index 9166ef79..18a18096 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 daa3436c..a66db8f4 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 @@ - - - - - + + + + +