diff --git a/server/pruef-kanaele.mjs b/server/pruef-kanaele.mjs new file mode 100644 index 00000000..1e6e36f1 --- /dev/null +++ b/server/pruef-kanaele.mjs @@ -0,0 +1,279 @@ +/* ===================================================================== + DREI ACCOUNTS, EINE AUFGABENLISTE (17.09.2026) + + Filipe: "ich hab ja auch 2 neben account, hasidog und dogfather + clips." Bis heute wusste der Arbeitsplatz davon nichts. + + WARUM DAS EIN ECHTER MANGEL WAR: "Schneide drei Ausschnitte" ist bei + drei Accounts keine Aufgabe, sondern eine Frage. Wer sie bekommt, + muss nachfragen -- oder raet. Raet er falsch, ist die Arbeit nicht + halb getan, sondern am falschen Ort, und das faellt erst auf, wenn + jemand hinsieht. + + --------------------------------------------------------------------- + VIER FRAGEN, UND DIE DRITTE WIRD GERN VERGESSEN + + 1. Kann jemand aus dem Haus einen Kanal setzen und wiederfinden? + 2. Faellt ein erfundener Kanal auf? + 3. Erfaehrt jemand, der ihn NICHT benutzen darf, dass es ihn gibt? + 4. Heisst "leer" wirklich "gilt fuer alles" -- und laesst sich ein + einmal gesetzter Kanal wieder entfernen? + + Die dritte ist dieselbe Regel wie bei den Kategorien: Eine Absage + ("Unbekannter Kanal") waere die Auskunft, dass es das Feld gibt -- + also muss der Wert bei ihm stillschweigend fallen. Wer ihn benutzen + darf, bekommt bei einem Tippfehler dagegen sehr wohl eine Absage. + + Die vierte klingt nach einer Kleinigkeit und ist der Grund, warum + viele Felder sich nicht mehr leeren lassen: Wer `""` als + "unveraendert" behandelt, macht aus einem Loeschversuch ein + Nichts-Tun -- und niemand merkt es, weil auch keine Fehlermeldung + kommt. + + Aufrufen mit: node server/pruef-kanaele.mjs + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { portMussFreiSein } from "./helfer-port.mjs"; + +const PORT = await portMussFreiSein(4477, "pruef-kanaele"); +const ordner = mkdtempSync(join(tmpdir(), "ws-kanal-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = String(PORT); +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-kanaele"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = `http://127.0.0.1:${PORT}`; + +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 { 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 idHand = anlegen("VanVan", "hand", "CODE-HAND-0001"); +const idModi = anlegen("Marina", "modi", "CODE-MODI-0001"); +const idCrea = anlegen("Tili", "creator", "CODE-CREA-0001"); +void idDogi; void idHand; + +/* DIE SPALTE MUSS ES GEBEN -- und zwar durch die Umstellung, nicht + durch den Bauplan allein. Steht sie nicht da, sind alle Pruefungen + darunter Aussagen ueber ein Feld, das es nicht gibt. */ +const spalten = d.prepare("PRAGMA table_info(aufgaben)").all().map((s) => s.name); +d.close(); + +async function anmelden(kachel, code) { + const a = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: kachel, code }), + }); + return (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; "); +} +async function ruf(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 dogi = await anmelden("admin", "CODE-DOGI-0001"); +const hand = await anmelden("creator", "CODE-HAND-0001"); +const modi = await anmelden("creator", "CODE-MODI-0001"); +const crea = await anmelden("creator", "CODE-CREA-0001"); + +/* ======================================================================= + 0. DIE SPALTE + ======================================================================= */ +melde(""); +melde("=== 0. Der Platz dafuer ==="); +ok(spalten.includes("kanal"), `die Aufgabe hat eine Spalte 'kanal' (${spalten.length} Spalten)`); + +/* ======================================================================= + 1. WER DIE KANAELE BEKOMMT + ======================================================================= */ +melde(""); +melde("=== 1. Wer sie ueberhaupt sieht ==="); +{ + const a = await ruf(dogi, "/workspace/api/aufgaben"); + const namen = (a.daten?.kanaele || []).map((k) => k.name); + ok(namen.length === 3, `DogFather bekommt drei Kanaele (${namen.join(", ")})`); + ok(namen.includes("HasiDog") && namen.includes("DogFather Clips"), + "darunter die beiden Nebenaccounts"); + /* JEDER TRAEGT EINE ERKLAERUNG. Ein Auswahlfeld mit drei Namen und + ohne ein Wort dazu ist eine Ratefrage fuer jemanden, der neu ist. */ + ok((a.daten?.kanaele || []).every((k) => (k.text || "").length > 30), + "und jeder erklaert sich in einem Satz"); + + const m = await ruf(modi, "/workspace/api/aufgaben"); + ok((m.daten?.kanaele || []).length === 3, "ein Modi auch"); + const h = await ruf(hand, "/workspace/api/aufgaben"); + ok((h.daten?.kanaele || []).length === 3, "die rechte Hand auch"); + + /* DIE VERBORGENHEIT: Wer sie nicht benutzt, bekommt die Liste gar + nicht erst -- statt sie zu bekommen und ausgeblendet zu sehen. */ + const c = await ruf(crea, "/workspace/api/aufgaben"); + ok((c.daten?.kanaele || []).length === 0, + `ein Creator bekommt keine (${(c.daten?.kanaele || []).length})`); +} + +/* ======================================================================= + 2. SETZEN UND WIEDERFINDEN + ======================================================================= */ +melde(""); +melde("=== 2. Setzen ==="); +let idClip = 0; +{ + const a = await ruf(dogi, "/workspace/api/aufgaben", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ titel: "Drei Ausschnitte schneiden", + verantwortlich_id: idModi, kanal: "clips", kategorie: "clipping" }), + }); + ok(a.status === 201, `eine Aufgabe fuer Clips anlegen (${a.status})`); + idClip = a.daten?.id || 0; + + const liste = await ruf(dogi, "/workspace/api/aufgaben"); + const gefunden = (liste.daten?.aufgaben || []).find((x) => x.id === idClip); + ok(gefunden?.kanal === "clips", `und sie kommt mit ihrem Kanal zurueck (${gefunden?.kanal})`); + + /* DER MODI SIEHT IHN AUCH -- es ist seine Aufgabe. */ + const beimModi = await ruf(modi, "/workspace/api/aufgaben"); + const seine = (beimModi.daten?.aufgaben || []).find((x) => x.id === idClip); + ok(seine?.kanal === "clips", `der Modi sieht ihn ebenfalls (${seine?.kanal})`); +} + +/* ======================================================================= + 3. EIN ERFUNDENER KANAL + ======================================================================= */ +melde(""); +melde("=== 3. Was nicht durchgeht ==="); +{ + const a = await ruf(dogi, "/workspace/api/aufgaben", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ titel: "Irgendwas", kanal: "tiktok-zweitkanal" }), + }); + ok(a.status === 400 && /Kanal/i.test(a.daten?.fehler || ""), + `ein erfundener Kanal wird abgelehnt (${a.status} ${a.daten?.fehler || ""})`); + + /* UND DIE GEGENPROBE ZUR ABLEHNUNG: Ohne sie hiesse "400" nur, dass + das Anlegen ueberhaupt scheitert -- etwa am fehlenden Titel. */ + const b = await ruf(dogi, "/workspace/api/aufgaben", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ titel: "Irgendwas", kanal: "hasidog" }), + }); + ok(b.status === 201, `derselbe Aufruf mit echtem Kanal geht durch (${b.status})`); +} + +/* ======================================================================= + 4. DIE VERBORGENHEIT + ======================================================================= */ +melde(""); +melde("=== 4. Ein Creator schickt einen Kanal ==="); +{ + const a = await ruf(crea, "/workspace/api/aufgaben", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ titel: "Meine eigene Sache", kanal: "clips" }), + }); + /* KEINE ABSAGE. Eine Fehlermeldung waere die Auskunft, dass es das + Feld gibt -- und der Creator soll von den Kanaelen nichts wissen. */ + ok(a.status === 201, `es wird angelegt, nicht abgelehnt (${a.status})`); + + const liste = await ruf(crea, "/workspace/api/aufgaben"); + const seine = (liste.daten?.aufgaben || []).find((x) => x.id === a.daten?.id); + ok(seine && !seine.kanal, + `aber der Kanal faellt stillschweigend (${seine?.kanal ?? "leer"})`); + /* Und ein erfundener Kanal von ihm ebenso -- gleiche Antwort, gleiche + Auskunft, kein Unterschied, an dem sich etwas ablesen liesse. */ + const b = await ruf(crea, "/workspace/api/aufgaben", { + method: "POST", headers: jsonKopf, + body: JSON.stringify({ titel: "Noch eine", kanal: "gibt-es-nicht" }), + }); + ok(b.status === a.status, + `ein erfundener klingt bei ihm genauso (${b.status} wie ${a.status})`); +} + +/* ======================================================================= + 5. LEER HEISST "FUER ALLES" -- UND LAESST SICH SETZEN + ======================================================================= */ +melde(""); +melde("=== 5. Wieder entfernen ==="); +{ + const a = await ruf(dogi, `/workspace/api/aufgaben/${idClip}`, { + method: "PATCH", headers: jsonKopf, body: JSON.stringify({ kanal: "" }), + }); + ok(a.status === 200, `leeren geht (${a.status})`); + const liste = await ruf(dogi, "/workspace/api/aufgaben"); + const jetztOhne = (liste.daten?.aufgaben || []).find((x) => x.id === idClip); + ok(jetztOhne && !jetztOhne.kanal, + `und der Kanal ist wirklich weg (${jetztOhne?.kanal ?? "leer"})`); + + /* HIN UND ZURUECK: Ein Feld, das sich nur einmal setzen laesst, faellt + erst beim zweiten Mal auf. */ + const b = await ruf(dogi, `/workspace/api/aufgaben/${idClip}`, { + method: "PATCH", headers: jsonKopf, body: JSON.stringify({ kanal: "hasidog" }), + }); + ok(b.status === 200, `und wieder setzen auch (${b.status})`); + const liste2 = await ruf(dogi, "/workspace/api/aufgaben"); + const wieder = (liste2.daten?.aufgaben || []).find((x) => x.id === idClip); + ok(wieder?.kanal === "hasidog", `jetzt steht HasiDog darin (${wieder?.kanal})`); +} + +/* ======================================================================= + 6. STEHT ES AUCH WIRKLICH IN DER DATENBANK? + ======================================================================= */ +melde(""); +melde("=== 6. Nicht nur nachgeplappert ==="); +{ + /* OHNE DIESEN ABSCHNITT koennte alles oben gruen sein, obwohl der Wert + nur durch die Auskunft zurueckgereicht wird -- zum Beispiel aus + einem Zwischenspeicher. Gefragt wird deshalb die Datei selbst. */ + const e = new DatabaseSync(process.env.WORKSPACE_DB); + const zeile = e.prepare("SELECT kanal FROM aufgaben WHERE id = ?").get(idClip); + const verteilung = e.prepare( + "SELECT kanal, COUNT(*) AS n FROM aufgaben GROUP BY kanal ORDER BY n DESC").all(); + e.close(); + ok(zeile?.kanal === "hasidog", `in der Datenbank steht "${zeile?.kanal}"`); + ok(verteilung.length >= 2, + `und es gibt mehr als einen Zustand (${verteilung + .map((v) => `${v.kanal ?? "leer"}:${v.n}`).join(" ")})`); +} + +/* ------------------------------------------------------------------- */ +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-aufgaben.js b/server/workspace-aufgaben.js index 0609ec9b..d2205e7c 100644 --- a/server/workspace-aufgaben.js +++ b/server/workspace-aufgaben.js @@ -13,6 +13,7 @@ import { db, protokolliere, echteIp, sitzungLesen, betreutWo, istLeitung, ohneDogFather, heuteLokal, ROLLEN_SORTIERUNG, ROLLEN_GRUPPE, betreuteIds, externPruefen, externSql, sichtbarePersonenIds, sichtbareCreatorIds, MODI_KATEGORIEN, kategorienFuer, kategoriePersonen, + KANAELE, kanaeleFuer, ohneTeamDogi, ohneAgentur, siehtModis, TEAM_DOGI_ROLLEN, } from "./workspace.js"; import { eskalationSetzen } from "./workspace-eskalation.js"; @@ -229,6 +230,7 @@ const SPALTEN = ` die es schon gibt, und damit eine zweite Gelegenheit, sie falsch zu ziehen. */ a.kategorie, + a.kanal, pa.name AS abbruch_von_name`; const VERBUND = ` @@ -295,6 +297,11 @@ aufgabenRouter.get("/workspace/api/aufgaben", (req, res) => { eine Auswahl aus fremden Kategorien waere ein Angebot, das der Server danach ablehnt. */ kategorie_fuer: kategoriePersonen(req.person), + /* DIE KANAELE (17.09.2026). Genau wie die Kategorien: Wer sie + nicht benutzen darf, bekommt eine leere Liste, und die + Oberflaeche zeigt dann gar kein Feld -- statt eines Feldes, + das da ist und nichts tut. */ + kanaele: kanaeleFuer(req.person), }); } catch (fehler) { console.error("[workspace] Aufgaben lesen:", fehler?.message); @@ -427,6 +434,7 @@ aufgabenRouter.get("/workspace/api/personen", (req, res) => { /* ---------- Anlegen und Ändern ------------------------------------------ */ const KATEGORIEN = new Set(MODI_KATEGORIEN.map((k) => k.wert)); +const KANAL_WERTE = new Set(KANAELE.map((k) => k.wert)); function pruefeFelder(körper, { neu, person }) { const fehler = []; @@ -494,6 +502,24 @@ function pruefeFelder(körper, { neu, person }) { else aus.kategorie = k; } + /* DER KANAL -- dieselbe Regel wie bei der Kategorie, Wort fuer Wort + (17.09.2026). Wer keine Kanaele hat, schickt auch keine; ein Wert + von ihm faellt stillschweigend, damit die Absage nicht verraet, + dass es das Feld gibt. Wer sie hat, bekommt bei einem falschen + Wert sehr wohl eine Absage -- fuer ihn ist ein Tippfehler ein + Fehler und kein Geheimnis. + + LEER HEISST `null` UND DAS HEISST "gilt fuer alles". Vieles ist + nicht kanalgebunden: eine Absprache im Team, ein Zugang, eine + Auswertung. Ein Pflichtfeld haette dafuer einen falschen Kanal + erzwungen. */ + if (körper.kanal !== undefined && kanaeleFuer(person).length) { + const k = String(körper.kanal ?? "").trim(); + if (!k) aus.kanal = null; + else if (!KANAL_WERTE.has(k)) fehler.push("Unbekannter Kanal."); + else aus.kanal = k; + } + /* Frei eingetragene Namen statt einer Person -- eine Agentur, eine Marke, jemand ohne Konto. Sie schlagen die Auswahl: Wer tippt, meint das Getippte, und die Verknuepfung faellt weg. */ @@ -697,12 +723,14 @@ aufgabenRouter.post("/workspace/api/aufgaben", gleicheHerkunft, (req, res) => { const { lastInsertRowid } = db().prepare(` INSERT INTO aufgaben (titel, beschreibung, status, prioritaet, creator_id, verantwortlich_id, - creator_extern, verantwortlich_extern, frist, kategorie, erstellt, erstellt_von) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`).run( + creator_extern, verantwortlich_extern, frist, kategorie, kanal, + erstellt, erstellt_von) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run( aus.titel, aus.beschreibung ?? null, aus.status ?? "offen", aus.prioritaet ?? "mittel", aus.creator_id ?? null, aus.verantwortlich_id ?? null, aus.creator_extern ?? null, aus.verantwortlich_extern ?? null, - aus.frist ?? null, aus.kategorie ?? null, jetzt(), req.person.id); + aus.frist ?? null, aus.kategorie ?? null, aus.kanal ?? null, + jetzt(), req.person.id); protokolliere("aufgabe_angelegt", { personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), diff --git a/server/workspace.js b/server/workspace.js index 4a54e0fb..66e3d94b 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -1231,6 +1231,56 @@ export const MODI_STUFEN = [ * niemand" heisst hier: Die Liste kommt gar nicht erst mit, statt * mitzukommen und ausgeblendet zu werden. */ +/* ===================================================================== + DIE KANAELE (17.09.2026) + + Filipe: "ich hab ja auch 2 neben account, hasidog und dogfather + clips." Bis heute wusste der Arbeitsplatz davon nichts -- es gab + genau eine Welt, und die hiess nirgends. + + WARUM DAS EIN ECHTER MANGEL WAR, nicht nur eine fehlende Spalte: + "Schneide drei Ausschnitte" ist bei drei Accounts keine Aufgabe, + sondern eine Frage. Wer sie bekommt, muss nachfragen -- oder raet. + Raet er falsch, ist die Arbeit nicht halb getan, sondern am + falschen Ort gelandet, und das faellt erst auf, wenn jemand es + sieht. Genau diese Sorte Mehrdeutigkeit kostet doppelt. + + WARUM EINE LISTE IM QUELLTEXT UND KEINE TABELLE. + Sonst gilt hier: Eine abgeschriebene Liste altert. Diese hier ist + keine Abschrift -- sie laesst sich aus nichts ableiten, weil sie + eine Tatsache ueber Filipes Betrieb ist und nirgends sonst steht. + Sie darf deshalb hier stehen, und einen Kanal dazuzunehmen ist eine + Zeile. Sobald sich das oefter aendert als ein paarmal im Jahr, + gehoert sie in die Verwaltung -- vorher waere das eine Oberflaeche + fuer drei Zeilen. + + KEIN KANAL IST EIN GUELTIGER ZUSTAND. Vieles gilt fuer alles (eine + Absprache im Team, ein Zugang, eine Auswertung). Ein Pflichtfeld + haette dafuer einen falschen Kanal erzwungen -- und ein falscher + Eintrag ist schlechter als ein leerer. + ===================================================================== */ +export const KANAELE = [ + { wert: "dogfather", name: "DogFather", + text: "Der Hauptkanal. Die Livestreams und alles, was unmittelbar " + + "daran hängt." }, + { wert: "hasidog", name: "HasiDog", + text: "Der zweite Account — eigene Zuschauer, eigener Ton. Was hier " + + "läuft, ist nicht einfach dasselbe noch einmal." }, + { wert: "clips", name: "DogFather Clips", + text: "Nur Ausschnitte. Hier wird geschnitten, beschriftet und " + + "hochgeladen, nicht gesendet." }, +]; + +/** Wer darf mit Kanaelen arbeiten? Dieselbe Runde wie bei den + * Kategorien -- das Team und DogFather. Fuer alle anderen kommt die + * Liste gar nicht erst mit, statt mitzukommen und ausgeblendet zu + * werden. */ +export function kanaeleFuer(person) { + if (!person) return []; + return TEAM_DOGI_ROLLEN.has(person.rolle) || person.rolle === "admin" + ? KANAELE : []; +} + export function kategorienFuer(person) { if (!person) return []; return TEAM_DOGI_ROLLEN.has(person.rolle) || person.rolle === "admin" @@ -1678,6 +1728,22 @@ function umstellungen(d) { falschen Moment. */ const jetztStempel = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, ""); + /* ---- Der Kanal an der Aufgabe (17.09.2026) ---- + Drei Accounts, eine Aufgabenliste: Ohne diese Spalte ist + "schneide drei Ausschnitte" bei HasiDog und Clips mehrdeutig. + ADD COLUMN und kein Tabellenneubau -- es aendert sich keine + CHECK-Regel, alte Zeilen bleiben leer, und leer heisst hier + ausdruecklich "gilt fuer alles" und nicht "fehlt". */ + try { + const spalten = d.prepare("PRAGMA table_info(aufgaben)").all().map((s) => s.name); + if (spalten.length && !spalten.includes("kanal")) { + d.exec("ALTER TABLE aufgaben ADD COLUMN kanal TEXT"); + console.log("[workspace] Spalte 'kanal' an den Aufgaben ergaenzt."); + } + } catch (fehler) { + console.error("[workspace] Spalte 'kanal':", fehler?.message); + } + /* ---- Zeitpunkt des Hochladens in der Bibliothek ---- Eine neue Spalte laesst SQLite anstandslos anhaengen -- anders als eine CHECK-Regel. Alte Eintraege bleiben leer und fallen auf das diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 2038374c..42183382 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=202609170241", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609170300", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609170241", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609170300", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609170241", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609170300", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609170241", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609170300", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/aufgaben.css b/workspace/assets/css/aufgaben.css index 239b2be7..630f9b58 100644 --- a/workspace/assets/css/aufgaben.css +++ b/workspace/assets/css/aufgaben.css @@ -1095,3 +1095,32 @@ color: var(--text-still); border-style: dashed; } + +/* ===================================================================== + DER KANAL AN DER AUFGABE (17.09.2026) + + Filipe: „ich hab ja auch 2 neben account, hasidog und dogfather + clips." Drei Accounts, eine Aufgabenliste — ohne ein Zeichen dafür + ist „schneide drei Ausschnitte" keine Aufgabe, sondern eine Frage. + + EIN ZEICHEN, KEIN ZWEITER SATZ. Die Zeile unter dem Titel trägt + schon Frist, Kategorie, Person und Bereich. Ein weiteres „· Wort" + wäre darin untergegangen; ein eigener kleiner Rahmen wird gelesen, + ohne lauter zu sein. + + GEDECKTE TÖNE, KEIN NEON: Die Farbe soll die drei unterscheidbar + machen, nicht um Aufmerksamkeit kämpfen — auf diesem Brett stehen + bis zu vierzig Karten untereinander. + ------------------------------------------------------------------- */ +.aufgabe__kanal { + --k-ton: #6f89a8; + display: inline-flex; align-items: center; + padding: 1px 8px; border-radius: 999px; + font-size: .72rem; font-weight: 600; letter-spacing: .02em; + color: color-mix(in srgb, var(--k-ton) 78%, #ffffff 22%); + border: 1px solid color-mix(in srgb, var(--k-ton) 34%, transparent); + background: color-mix(in srgb, var(--k-ton) 12%, transparent); +} +.aufgabe__kanal[data-kanal="dogfather"] { --k-ton: #c2544f; } +.aufgabe__kanal[data-kanal="hasidog"] { --k-ton: #7d6bbd; } +.aufgabe__kanal[data-kanal="clips"] { --k-ton: #3f9c8a; } diff --git a/workspace/assets/js/aufgaben.js b/workspace/assets/js/aufgaben.js index 3fb82646..c17fbbc7 100644 --- a/workspace/assets/js/aufgaben.js +++ b/workspace/assets/js/aufgaben.js @@ -238,6 +238,17 @@ viel Aufwand und sieben Gelegenheiten fuer einen Unterschied, fuer eine Zeile Text. Dieselbe schlichte Form wie "· Bereich X" daneben tut es. */ + /* DER KANAL STEHT VOR DER KATEGORIE. Er beantwortet die groebere + Frage -- WO ueberhaupt -- und die muss man zuerst wissen, um die + feinere zu verstehen. */ + if (a.kanal) { + const kName = kanaele.find((k) => k.wert === a.kanal)?.name; + if (kName) { + const z = el('span', 'aufgabe__kanal', kName); + z.dataset.kanal = a.kanal; + zeile.append(z); + } + } if (a.kategorie) { const name = kategorien.find((k) => k.wert === a.kategorie)?.name; if (name) zeile.append(el('span', null, '· ' + name)); @@ -739,6 +750,7 @@ die Frage auf, fuer wen sie da ist. Genau die soll niemand stellen. ===================================================================== */ let kategorien = []; + let kanaele = []; /* FUER WESSEN AUFGABEN die Kategorie gilt. Kommt fertig vom Server: null -> fuer alle eigenen (das Feld steht immer da) @@ -772,6 +784,47 @@ ausschliesslich an eigenen Aufgaben, und dort gilt sie unabhaengig davon, wen sie eintraegt. Sonst entscheidet die gewaehlte Person. Wer eine leere Liste bekommt, sieht das Feld nie. */ + /* ---- DIE KANAELE (17.09.2026) -------------------------------------- + Filipe: "ich hab ja auch 2 neben account, hasidog und dogfather + clips." Ohne diesen Wert ist "schneide drei Ausschnitte" bei drei + Accounts keine Aufgabe, sondern eine Frage. + + EINFACHER ALS BEI DEN KATEGORIEN, und das ist Absicht: Die + Kategorie haengt daran, FUER WEN die Aufgabe ist (`kategorie_fuer`); + ein Kanal gilt fuer jede Aufgabe, die jemand aus dem Haus + anfasst. Zwei verschiedene Sichtbarkeitsregeln fuer zwei Felder, + die nebeneinander stehen, waeren eine Gelegenheit, die falsche zu + ziehen. Hier gibt es genau eine Frage: Darf diese Person + ueberhaupt Kanaele benutzen? */ + function kanaeleEinsetzen() { + if (!kanaele.length) return; + for (const ziel of ['f-kanal', 'b-kanal']) { + const feld = $(ziel); + if (!feld) continue; + const vorher = feld.value; + /* "Für alle" ist kein Platzhalter, sondern eine Antwort -- vieles + ist nicht kanalgebunden (eine Absprache, ein Zugang, eine + Auswertung). Deshalb steht dort ein Wort und kein Strich. */ + feld.textContent = ''; + feld.append(el('option', null, 'Für alle')); + feld.firstChild.value = ''; + for (const k of kanaele) { + const o = el('option', null, k.name); + o.value = k.wert; + if (k.text) o.title = k.text; + feld.append(o); + } + feld.value = vorher; + } + } + + function kanalFeldZeigen(praefix) { + const huelle = $(praefix === 'f' ? 'feld-kanal' : 'b-feld-kanal'); + if (!huelle) return; + huelle.hidden = !kanaele.length; + if (!kanaele.length && $(`${praefix}-kanal`)) $(`${praefix}-kanal`).value = ''; + } + function kategorieNoetig(creatorId, verantId) { if (kategorieFuer === null) return true; if (!Array.isArray(kategorieFuer) || !kategorieFuer.length) return false; @@ -806,12 +859,16 @@ alleAufgaben = daten.aufgaben || []; aufwandUebernehmen(daten); kategorien = daten.kategorien || []; + kanaele = daten.kanaele || []; /* `null` muss `null` BLEIBEN -- ein `|| []` daraus zu machen waere der stille Fehler: Aus "gilt immer" wuerde "gilt nie", und das Feld verschwaende fuer genau die, die es brauchen. */ kategorieFuer = daten.kategorie_fuer === undefined ? [] : daten.kategorie_fuer; kategorienEinsetzen(); kategorieFeldZeigen('f'); + kanaeleEinsetzen(); + kanalFeldZeigen('f'); + kanalFeldZeigen('b'); lageZeigen(alleAufgaben); neuZeichnen(); /* Erst NACH dem Zeichnen -- vorher gibt es die Karten nicht, und @@ -853,6 +910,7 @@ daten.verantwortlich_extern = $('f-verant').dataset.extern || null; } if (!$('feld-kategorie')?.hidden) daten.kategorie = $('f-kategorie').value || ''; + if (!$('feld-kanal')?.hidden) daten.kanal = $('f-kanal').value || ''; knopfEl.disabled = true; try { @@ -891,6 +949,7 @@ } kategorieFeldZeigen('b'); if ($('b-kategorie')) $('b-kategorie').value = a.kategorie || ''; + if ($('b-kanal')) $('b-kanal').value = a.kanal || ''; if ($('b-aufwand')) $('b-aufwand').value = a.aufwand || ''; window.anhaengeLaden?.(a.id); dialog.showModal(); @@ -920,6 +979,7 @@ daten.verantwortlich_extern = $('b-verant').dataset.extern || null; } if (!$('b-feld-kategorie')?.hidden) daten.kategorie = $('b-kategorie').value || ''; + if (!$('b-feld-kanal')?.hidden) daten.kanal = $('b-kanal').value || ''; $('b-speichern').disabled = true; try { const a = await hole(`/workspace/api/aufgaben/${bearbeitet.id}`, { diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index c2803481..1f8b2555 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. --> - - - - - - + + + + + + - +
@@ -110,6 +110,11 @@ -- die Auswahl wird deshalb erst im Skript gefuellt. Stuenden die Namen hier, stuenden sie in einer Datei, die jeder bekommt, der die Seite oeffnet. --> +Drei Accounts, eine Liste. Leer heißt: gilt für alles.
+