diff --git a/server/pruef-modi-livecheck.mjs b/server/pruef-modi-livecheck.mjs new file mode 100644 index 00000000..75580f1f --- /dev/null +++ b/server/pruef-modi-livecheck.mjs @@ -0,0 +1,240 @@ +/* ===================================================================== + DIE LIVE-CHECKLISTE AM TERMIN (Kapitel 5.7 und 11) + + "Wird pro Live-Termin als Checkliste erzeugt und den eingeteilten + Modis zugewiesen." + + Filipes Entscheidung dazu (10.09.2026): ein KNOPF statt Automatik. + Vierzehn Aufgaben, die bei jedem Termin von selbst erscheinen, + ueberrumpeln -- und was ungefragt Dinge anlegt, ist schwer wieder + loszuwerden. + + DREI STELLEN, AN DENEN SO ETWAS ERFAHRUNGSGEMAESS SCHIEFGEHT: + + 1. ZWEIMAL DRUECKEN. Der zweite Druck darf nichts verdoppeln -- + vierzehn Punkte doppelt auf einem Live-Tag sind unbrauchbar. + 2. DAS ZWEITE LIVE. Die Kennung der Vorlage entscheidet, was als + "schon uebernommen" gilt. Traegt sie die Terminnummer nicht, + steht beim zweiten Live alles als erledigt da -- und niemand + bekommt seine Liste. + 3. DIE FRIST. Eine Vorbereitung, die nach dem Live faellig wird, + ist keine. + ===================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-live-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = "4367"; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +import { notbremse } from "./helfer-notbremse.mjs"; +await import("./index.js"); +notbremse(180_000, "pruef-modi-livecheck"); +await new Promise((r) => setTimeout(r, 800)); +const BASIS = "http://127.0.0.1:4367"; + +let fehler = 0; +let geprueft = 0; +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; +let offen = 0; +const unklar = (t) => { offen++; console.log(" -- " + t); }; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const schluessel = randomBytes(32).toString("hex"); +d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") + .run("code_kennung_schluessel", schluessel, jetzt); +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, code_kennung, aktiv, erstellt)" + + " VALUES (?,?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, + createHmac("sha256", schluessel).update(code).digest("hex"), jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} +const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); +const idMan = anlegen("Schulle", "manager", "CODE-MAN-0001"); +const idModi = anlegen("Marina", "modi", "CODE-MODI-0001"); +d.close(); +void idMan; + +/* ZWEI TERMINE, damit Punkt 2 oben ueberhaupt pruefbar ist. Der Tag + liegt in der Zukunft und wird RELATIV gerechnet -- ein festes Datum + holt der Kalender irgendwann ein, und dann ist die Pruefung rot, + ohne dass etwas kaputt waere. */ +const inTagen = (n) => new Date(Date.now() + n * 86400_000).toISOString().slice(0, 10); +const TAG1 = inTagen(3); +const TAG2 = inTagen(10); + +async function anmelden(rolle, code) { + const a = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle, code }), + }); + const keks = (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; "); + return { status: a.status, keks }; +} +async function hole(keks, pfad, aufruf = {}) { + const a = await fetch(BASIS + pfad, { + ...aufruf, headers: { ...(aufruf.headers || {}), cookie: keks }, + }); + return { status: a.status, daten: await a.json().catch(() => null) }; +} +const jsonKopf = { "Content-Type": "application/json" }; + +const dogiAn = await anmelden("admin", "CODE-DOGI-0001"); +const modiAn = await anmelden("creator", "CODE-MODI-0001"); +const manAn = await anmelden("manager", "CODE-MAN-0001"); + +/* ---------- Zwei Live-Termine mit dem Modi als Teilnehmer ----------- */ +console.log("\n=== 1. Zwei Live-Termine ===\n"); +const termine = []; +for (const [tag, titel] of [[TAG1, "Freitags-Live"], [TAG2, "Turnier-Abend"]]) { + const a = await hole(dogiAn.keks, "/workspace/api/termine", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ + titel, art: "bigmatch", beginn: `${tag}T20:00`, dauer_min: 120, + teilnehmer: [idModi], + }), + }); + if (a.status === 201 || a.status === 200) termine.push(a.daten?.id); + else unklar(`Termin "${titel}" ging nicht (${a.status}: ${JSON.stringify(a.daten)})`); +} +ok(termine.length === 2 && termine.every(Boolean), + `zwei Termine angelegt (${termine.join(", ")})`); + +/* ---------- 2. Der Knopf ist angeboten ------------------------------ */ +console.log("\n=== 2. Die Auskunft am Termin ===\n"); +{ + const a = await hole(modiAn.keks, `/workspace/api/termine?von=${TAG1}&tage=30`); + ok(a.daten?.checkliste_moeglich === true, "dem Modi wird der Knopf angeboten"); + const b = await hole(manAn.keks, `/workspace/api/termine?von=${TAG1}&tage=30`); + /* Und dem Manager nicht -- sonst stuende bei ihm ein Knopf, der eine + Absage bringt, und die Absage waere die Auskunft, dass es das gibt. */ + ok(b.daten?.checkliste_moeglich === false, "dem Manager nicht"); +} + +/* ---------- 3. Anlegen ---------------------------------------------- */ +console.log("\n=== 3. Die Checkliste zum ersten Live ===\n"); +{ + const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog_termin", termin_id: termine[0] }), + }); + /* VIERZEHN: fuenf vorher, fuenf waehrend, vier nachher -- so steht es + in Kapitel 11. Die Zahl fest hingeschrieben: Faellt ein Punkt aus + dem Katalog, muss das auffallen. */ + ok(a.status === 200 && a.daten?.angelegt === 14, + `vierzehn Punkte angelegt (${a.daten?.angelegt})`); + + const b = await hole(modiAn.keks, "/workspace/api/aufgaben"); + const meine = (b.daten?.aufgaben || []).filter((x) => (x.vorlage || "").endsWith(`#${termine[0]}`)); + ok(meine.length === 14, `sie liegen auf seinem Brett (${meine.length})`); + ok(meine.every((x) => x.frist === TAG1), + `alle mit dem Termintag als Frist (${[...new Set(meine.map((x) => x.frist))].join(", ")})`); + ok(meine.every((x) => x.verantwortlich_id === idModi), + "und der eingeteilte Modi ist verantwortlich"); + ok(meine.every((x) => x.kategorie), "jede traegt ihre Kategorie"); +} + +/* ---------- 4. Zweimal druecken -------------------------------------- */ +console.log("\n=== 4. Und wenn man zweimal drückt? ===\n"); +{ + const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog_termin", termin_id: termine[0] }), + }); + ok(a.status === 200 && a.daten?.angelegt === 0, + `der zweite Druck legt nichts an (${a.daten?.angelegt})`); + const b = await hole(modiAn.keks, "/workspace/api/aufgaben"); + const meine = (b.daten?.aufgaben || []).filter((x) => (x.vorlage || "").endsWith(`#${termine[0]}`)); + ok(meine.length === 14, `es sind immer noch vierzehn (${meine.length})`); +} + +/* ---------- 5. Das ZWEITE Live --------------------------------------- */ +console.log("\n=== 5. Das zweite Live bekommt seine eigene Liste ===\n"); +{ + /* HIER WAERE DER FEHLER TEUER: Traegt die Kennung der Vorlage die + Terminnummer nicht, gilt beim zweiten Live alles als "schon + uebernommen" -- und niemand bekaeme seine Liste. */ + const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog_termin", termin_id: termine[1] }), + }); + ok(a.status === 200 && a.daten?.angelegt === 14, + `auch das zweite Live bekommt vierzehn (${a.daten?.angelegt})`); + const b = await hole(modiAn.keks, "/workspace/api/aufgaben"); + const zwei = (b.daten?.aufgaben || []).filter((x) => (x.vorlage || "").endsWith(`#${termine[1]}`)); + ok(zwei.length === 14 && zwei.every((x) => x.frist === TAG2), + `mit dem eigenen Termintag (${[...new Set(zwei.map((x) => x.frist))].join(", ")})`); +} + +/* ---------- 6. Die Zahl am Termin ------------------------------------ */ +console.log("\n=== 6. Der Knopf weiß, was schon da ist ===\n"); +{ + const a = await hole(modiAn.keks, `/workspace/api/termine?von=${TAG1}&tage=30`); + const t1 = (a.daten?.termine || []).find((t) => t.id === termine[0]); + ok(t1?.checkliste_aufgaben === 14, + `am ersten Termin stehen ${t1?.checkliste_aufgaben} Punkte`); +} + +/* ---------- 7. Wer es nicht darf ------------------------------------- */ +console.log("\n=== 7. Ein Manager versucht es ===\n"); +{ + const a = await hole(manAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog_termin", termin_id: termine[0] }), + }); + const b = await hole(manAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "gibtesnicht" }), + }); + ok(a.status === b.status && JSON.stringify(a.daten) === JSON.stringify(b.daten), + `wortgleich mit einer erfundenen Art (${a.status} ${JSON.stringify(a.daten)})`); +} + +/* ---------- 8. Gegenprobe -------------------------------------------- */ +console.log("\n=== 8. Gegenprobe ===\n"); +{ + /* Ein Termin, den es nicht gibt. Kaeme dafuer ein 200, wuerden die + Haken oben nichts beweisen -- dann sagt der Server zu allem ja. */ + const a = await hole(modiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog_termin", termin_id: 999999 }), + }); + ok(a.status === 404, `ein erfundener Termin wird abgelehnt (${a.status})`); + /* Und ein Termin OHNE eingeteilten Modi: Vierzehn herrenlose + Aufgaben waeren schlimmer als keine. */ + const t = await hole(dogiAn.keks, "/workspace/api/termine", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ titel: "Ohne Team", art: "termin", beginn: `${TAG1}T10:00` }), + }); + if (!t.daten?.id) { + unklar("Termin ohne Teilnehmer liess sich nicht anlegen"); + } else { + const b = await hole(dogiAn.keks, "/workspace/api/vorlagen/uebernehmen", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ art: "katalog_termin", termin_id: t.daten.id }), + }); + ok(b.status === 400 && /eingeteilt/i.test(b.daten?.fehler || ""), + `ohne eingeteilten Modi wird gefragt (${b.status} ${b.daten?.fehler || ""})`); + } +} + +console.log(`\n${fehler ? "FEHLGESCHLAGEN" : "BESTANDEN"} -- ` + + `${geprueft} geprueft, ${fehler} Fehler, ${offen} nicht nachsehbar`); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +process.exit(fehler ? 1 : 0); diff --git a/server/workspace-kalender.js b/server/workspace-kalender.js index 98434fb1..e0d24661 100644 --- a/server/workspace-kalender.js +++ b/server/workspace-kalender.js @@ -14,6 +14,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, istLeitung, istDogFather, siehtAlles, betreutWo, termineSichtbar, heuteLokal, + kategorienFuer, externPruefen, externSql, betreuteIds, einladbareIds, verborgeneIds, ROLLEN_SORTIERUNG, } from "./workspace.js"; import { @@ -338,7 +339,43 @@ kalenderRouter.get("/workspace/api/termine", (req, res) => { AND a.frist >= ? AND a.frist <= ? ORDER BY a.frist`).all(...aufgabenWo.werte, von, bis); - res.json({ termine, fristen, von, bis }); + /* ================================================================= + DIE LIVE-CHECKLISTE AM TERMIN (10.09.2026, Kapitel 5.7/11) + + Das Dokument sagt "wird pro Live-Termin automatisch als + Checkliste erzeugt". Filipes Entscheidung: ein KNOPF statt + Automatik -- vierzehn Aufgaben, die bei jedem Termin von selbst + erscheinen, ueberrumpeln, und was ungefragt Dinge anlegt, ist + schwer wieder loszuwerden. + + Hier kommt nur die AUSKUNFT mit, ob der Knopf hingehoert und ob + es die Liste schon gibt. Als Zahl und als Ja/Nein -- KEIN + Rollenname: Der Browser bekommt diese Datei ausgeliefert, und aus + einer Zahl laesst sich nichts schliessen. + + IN EINER ABFRAGE FUER ALLE TERMINE, nicht je Zeile eine. Bei + dreissig Terminen im Blick waeren das dreissig Abfragen -- den + Fehler hat das Haus bei den Teilnehmern schon einmal gemacht und + eine Zeile darueber ausdruecklich vermerkt. */ + const moeglich = kategorienFuer(req.person).length > 0; + if (moeglich && termine.length) { + const nummern = termine.map((t) => t.id).filter(Boolean); + if (nummern.length) { + const platz = nummern.map(() => "?").join(","); + const gezaehlt = new Map(); + for (const z of db().prepare( + `SELECT CAST(substr(vorlage, instr(vorlage, '#') + 1) AS INTEGER) AS termin_id, + COUNT(*) AS n + FROM aufgaben + WHERE vorlage LIKE 'mk-%#%' + AND CAST(substr(vorlage, instr(vorlage, '#') + 1) AS INTEGER) IN (${platz}) + GROUP BY termin_id`).all(...nummern)) { + gezaehlt.set(z.termin_id, z.n); + } + for (const t of termine) t.checkliste_aufgaben = gezaehlt.get(t.id) || 0; + } + } + res.json({ termine, fristen, von, bis, checkliste_moeglich: moeglich }); } catch (fehler) { console.error("[workspace] Termine lesen:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); diff --git a/server/workspace-vorlagen.js b/server/workspace-vorlagen.js index 32b8f584..9f1d921f 100644 --- a/server/workspace-vorlagen.js +++ b/server/workspace-vorlagen.js @@ -37,6 +37,7 @@ import express from "express"; import { db, sitzungLesen, istLeitung, darfCreator, protokolliere, echteIp, kategorienFuer, + termineSichtbar, } from "./workspace.js"; export const vorlagenRouter = express.Router(); @@ -1572,6 +1573,79 @@ vorlagenRouter.post("/workspace/api/vorlagen/uebernehmen", gleicheHerkunft, angelegt.push(v.titel); } + } else if (art === "katalog_termin") { + /* ============================================================= + DIE LIVE-CHECKLISTE ZU EINEM TERMIN (10.09.2026, Kap. 5.7/11) + + "Wird pro Live-Termin als Checkliste erzeugt und den + eingeteilten Modis zugewiesen." Genau das: die vierzehn + Punkte aus Vor / Waehrend / Nach, mit Frist auf den Tag des + Termins. + + DIE KENNUNG TRAEGT DIE TERMINNUMMER (`mk-vor-technik#42`). + Ohne sie stuende beim ZWEITEN Live alles als "schon + uebernommen" da -- die Kennung wird naemlich benutzt, um + genau das anzuzeigen. Mit ihr hat jedes Live seine eigene + Liste, und der Knopf kann sagen, ob es sie schon gibt. + + DIE FRIST IST DER TERMINTAG, nicht "in n Tagen". Eine + Vorbereitung, die nach dem Live faellig wird, ist keine. */ + if (!kategorienFuer(req.person).length) { + return res.status(400).json({ fehler: "Unbekannte Art." }); + } + const terminId = Number(req.body.termin_id); + if (!Number.isInteger(terminId) || terminId < 1) { + return res.status(400).json({ fehler: "Kein Termin angegeben." }); + } + /* Ueber dieselbe Sichtbarkeitsregel wie der Kalender -- wer den + Termin nicht sehen darf, soll auch nicht erfahren, dass es + ihn gibt. */ + const sicht = termineSichtbar(req.person, "t"); + const termin = db().prepare( + `SELECT t.id, t.beginn FROM termine t WHERE ${sicht.wo} AND t.id = ?`) + .get(...sicht.werte, terminId); + if (!termin) return res.status(404).json({ fehler: "nicht_gefunden" }); + + /* WEM sie gehoert: den Modis, die an diesem Termin eingeteilt + sind. Ist keiner eingetragen, bekommt sie der, der drueckt -- + sonst entstuenden vierzehn herrenlose Aufgaben. */ + let leute = db().prepare( + `SELECT p.id FROM termin_teilnehmer tt JOIN personen p ON p.id = tt.person_id + WHERE tt.termin_id = ? AND p.rolle = 'modi' AND p.aktiv = 1`).all(terminId) + .map((z) => z.id); + if (!leute.length && req.person.rolle === "modi") leute = [req.person.id]; + if (!leute.length) { + return res.status(400).json({ + fehler: "Für dieses Live ist noch niemand eingeteilt.", + }); + } + + const tag = String(termin.beginn).slice(0, 10); + const punkte = MODI_KATALOG.filter((v) => + v.gruppe === "vor" || v.gruppe === "waehrend" || v.gruppe === "nach"); + /* Was es schon gibt, wird nicht doppelt angelegt. Ein zweiter + Druck soll nichts kaputtmachen -- und "nochmal" waere hier + anders als beim uebrigen Katalog eine Falle: Vierzehn Punkte + doppelt auf einem Live-Tag sind nicht zu gebrauchen. */ + const schon = new Set(db().prepare( + "SELECT vorlage FROM aufgaben WHERE vorlage LIKE ?") + .all(`mk-%#${terminId}`).map((z) => z.vorlage)); + for (const v of punkte) { + for (const wer of leute) { + const kennung = `${v.schluessel}#${terminId}`; + if (schon.has(kennung) && leute.length === 1) continue; + db().prepare(`INSERT INTO aufgaben + (titel, beschreibung, status, prioritaet, verantwortlich_id, frist, + kategorie, vorlage, erstellt, erstellt_von) + VALUES (?,?,'offen','hoch',?,?,?,?,?,?)`) + .run(v.titel, v.text, wer, tag, v.kategorie, kennung, jetzt(), req.person.id); + angelegt.push(v.titel); + } + } + if (!angelegt.length) { + return res.json({ angelegt: 0, titel: [], hinweis: "schon vorhanden" }); + } + } else { return res.status(400).json({ fehler: "Unbekannte Art." }); } diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index aaed8da4..d7b0d009 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=202609100219", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609100240", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609100219", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609100240", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609100219", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609100240", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609100219", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609100240", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/js/kalender.js b/workspace/assets/js/kalender.js index 0aa01b49..afc1d8da 100644 --- a/workspace/assets/js/kalender.js +++ b/workspace/assets/js/kalender.js @@ -462,6 +462,10 @@ return `mit ${namen[0]}, ${namen[1]} +${namen.length - 2}`; } + /* Darf diese Person zu einem Termin eine Checkliste anlegen? Kommt + vom Server (siehe laden()). */ + let checklisteMoeglich = false; + function vereinheitlichen(roh) { const raus = []; for (const t of roh.termine || []) { @@ -482,6 +486,10 @@ creator_name: t.creator_name, teilnehmer_name: t.teilnehmer_name, erstellt_von: t.erstellt_von, dauer_min: t.dauer_min, serie_id: t.serie_id || null, + /* Wie viele Punkte der Live-Checkliste zu diesem Termin schon + angelegt sind. Kommt vom Server; fehlt die Angabe, gibt es + den Knopf gar nicht. */ + checkliste: t.checkliste_aufgaben ?? null, }); } for (const f of roh.fristen || []) { @@ -1025,6 +1033,40 @@ if (!a.ok) { melde('Ändern ging nicht.'); return; } await laden(); })); + /* ================================================================= + DIE LIVE-CHECKLISTE (10.09.2026, Kapitel 5.7/11) + + Das Dokument will sie "automatisch pro Live-Termin". Filipes + Entscheidung war ein Knopf: Vierzehn Aufgaben, die bei jedem + Termin von selbst erscheinen, ueberrumpeln -- und was ungefragt + Dinge anlegt, ist schwer wieder loszuwerden. + + DER KNOPF SAGT, WAS SCHON DA IST. "Checkliste (14)" statt + "Checkliste" -- sonst drueckt man ihn zur Sicherheit noch + einmal und weiss hinterher nicht, ob doppelt angelegt wurde. + (Der Server legt nichts doppelt an; der Knopf soll trotzdem + nicht dazu einladen, es zu versuchen.) */ + if (checklisteMoeglich && e.id && e.art !== 'frist') { + const wieViele = e.checkliste || 0; + rechts.append(tuKnopf(wieViele ? `Checkliste (${wieViele})` : 'Checkliste anlegen', + async () => { + const a = await hole('/workspace/api/vorlagen/uebernehmen', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ art: 'katalog_termin', termin_id: e.id }), + }); + if (!a.ok) { + const f = await a.json().catch(() => ({})); + melde(f.fehler || 'Das hat nicht geklappt.'); + return; + } + const d = await a.json().catch(() => ({})); + melde(d.angelegt + ? `${d.angelegt} Punkte auf dem Aufgabenbrett.` + : 'Die Checkliste steht schon.'); + await laden(); + })); + } if (darfAendern(e)) { /* BEARBEITEN STAND NIE DA (07.09.2026). Filipe: "ich hab die gemacht und kann sie nicht bearbeiten." Der Server liess das @@ -1234,6 +1276,11 @@ const a = await hole(`/workspace/api/termine?von=${von}&tage=${tage}`); if (!a.ok) { melde('Kalender konnte nicht geladen werden.'); return; } const roh = await a.json(); + /* OB der Knopf hingehoert, sagt der Server -- nicht ein + Rollenvergleich hier. Diese Datei bekommt jeder ausgeliefert, + der die Seite oeffnet; ein Rollenname darin waere mit einem + Blick in den Quelltext gefunden. */ + checklisteMoeglich = !!roh.checkliste_moeglich; daten = { eintraege: vereinheitlichen(roh), heute: heuteText() }; /* Die Wiederholungen NACH den Terminen holen: Der Serveraufruf oben legt fehlende Ausprägungen an, und erst danach stimmt der diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 993ee9d5..96af7809 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,12 +12,12 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + +
@@ -245,10 +245,10 @@ - - - - - + + + + +