diff --git a/server/pruef-code.png b/server/pruef-code.png index 4aeb8e2e..d5f9fe2e 100644 Binary files a/server/pruef-code.png and b/server/pruef-code.png differ diff --git a/server/pruef-personen-formular-handy.png b/server/pruef-personen-formular-handy.png new file mode 100644 index 00000000..941b3d79 Binary files /dev/null and b/server/pruef-personen-formular-handy.png differ diff --git a/server/pruef-personen-formular.mjs b/server/pruef-personen-formular.mjs new file mode 100644 index 00000000..e41e0e8d --- /dev/null +++ b/server/pruef-personen-formular.mjs @@ -0,0 +1,205 @@ +/* ===================================================================== + Prüfung: das Formular "Neue Person" und die Rechte auf den beiden + Verwaltungsbereichen (02.09.2026). + + Zwei Meldungen: + + "das sieht ja mal richtig scheisse aus. mach das besser bitte." + -- Die Rollenauswahl lief unten aus dem Formular heraus und legte + sich über die Personenliste. Ursache war keine schlampige + Gestaltung, sondern eine Raster-Regel für EINZEILIGE Felder + (feste Zeilenhöhe 44 px), die auch auf den Block aus vier hohen + Karten angewandt wurde. + + "die manager sollen diese kategorien garnicht sehen" + -- "Personen & Zugänge" und "Automationen" gehören allein DogFather. + + Der zweite Punkt wird auf DREI Ebenen geprüft, nicht nur an der + Kachel: Wegnehmen, was man sieht, ist kein Rechteentzug. Es zählt, + was der Server antwortet. + ===================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-pform-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = "4198"; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; +process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-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 }); }; +await import("./index.js"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = "http://127.0.0.1:4198"; +setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.(); + +let fehler = 0; +let geprueft = 0; +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +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; +} +anlegen("Dogfather", "admin", "CODE-DOGI-0001"); +anlegen("Cigdem", "manager", "CODE-CIGDEM-01"); +anlegen("Tili", "creator", "CODE-TILI-0001"); +d.close(); + +const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await chromium.launch(); + +async function anmelden(rolle, code, breite = 1440) { + const kontext = await browser.newContext({ viewport: { width: breite, height: 1000 } }); + const seite = await kontext.newPage(); + await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); + await seite.click(`.rolle[data-rolle="${rolle}"]`); + await seite.fill("#code", code); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 15000 }); + return seite; +} + +/* =================================================================== + TEIL 1 — Das Formular läuft nicht mehr über + =================================================================== */ + +console.log("\n— Das Formular „Neue Person“ —"); + +const dogi = await anmelden("admin", "CODE-DOGI-0001"); +await dogi.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" }); +await dogi.waitForTimeout(700); +await dogi.click("#neu-oeffnen").catch(() => {}); +await dogi.waitForTimeout(500); + +const mass = await dogi.evaluate(() => { + const form = document.getElementById("neu"); + const wahl = document.getElementById("f-rolle"); + const karten = [...document.querySelectorAll(".rollenwahl__knopf")]; + const f = form.getBoundingClientRect(); + const w = wahl.getBoundingClientRect(); + /* Das erste Element NACH dem Formular -- über das die Karten vorher + hinweggelaufen sind. */ + const danach = document.querySelector("#liste"); + const dn = danach ? danach.getBoundingClientRect() : null; + return { + formUnten: Math.round(f.bottom), + wahlUnten: Math.round(w.bottom), + wahlOben: Math.round(w.top), + listeOben: dn ? Math.round(dn.top) : null, + karten: karten.length, + kartenBreiten: karten.map((k) => Math.round(k.getBoundingClientRect().width)), + /* Liegt eine Karte weiter rechts als das Formular breit ist? */ + ueberRand: karten.some((k) => k.getBoundingClientRect().right > f.right + 1), + }; +}); + +ok(mass.karten === 4, `alle vier Rollen stehen zur Wahl (${mass.karten})`); +ok(mass.wahlUnten <= mass.formUnten, + `die Rollenauswahl endet INNERHALB des Formulars (${mass.wahlUnten} ≤ ${mass.formUnten})`); +ok(!mass.ueberRand, "keine Karte ragt seitlich aus dem Formular"); +ok(mass.listeOben === null || mass.wahlUnten <= mass.listeOben, + `die Karten liegen nicht über der Personenliste (${mass.wahlUnten} ≤ ${mass.listeOben})`); + +/* Alle Karten gleich breit -- ungleiche Kacheln sind das, was "sieht + scheisse aus" in Zahlen bedeutet. */ +const breiteste = Math.max(...mass.kartenBreiten); +const schmalste = Math.min(...mass.kartenBreiten); +ok(breiteste - schmalste <= 2, + `alle Karten sind gleich breit (${schmalste}–${breiteste} px)`); + +/* Die gewählte Rolle muss man SEHEN, nicht erraten. */ +const vorher = await dogi.evaluate(() => + getComputedStyle(document.querySelector('.rollenwahl__knopf[data-an="ja"]'), "::after").content); +ok(vorher.includes("✓"), `die gewählte Rolle trägt ein Häkchen (${vorher})`); + +await dogi.screenshot({ path: "pruef-personen-formular.png", fullPage: false }); + +/* Auf dem Handy darf nichts herausragen. */ +const handy = await anmelden("admin", "CODE-DOGI-0001", 390); +await handy.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" }); +await handy.waitForTimeout(700); +await handy.click("#neu-oeffnen").catch(() => {}); +await handy.waitForTimeout(500); +const engUeber = await handy.evaluate(() => + document.documentElement.scrollWidth - document.documentElement.clientWidth); +ok(engUeber <= 0, `auf dem Handy ragt nichts heraus (${engUeber}px)`); +await handy.screenshot({ path: "pruef-personen-formular-handy.png", fullPage: false }); + +/* =================================================================== + TEIL 2 — Die beiden Bereiche gehören DogFather allein + =================================================================== */ + +console.log("\n— Personen & Automationen: nur DogFather —"); + +const cigdem = await anmelden("manager", "CODE-CIGDEM-01"); +await cigdem.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" }); +await cigdem.waitForTimeout(800); + +const kacheln = await cigdem.evaluate(() => + [...document.querySelectorAll(".kachel")].map((k) => k.innerText.split("\n")[0].trim())); +ok(!kacheln.some((t) => /Personen/.test(t)), + "1. die Kachel „Personen & Zugänge“ fehlt bei der Managerin"); +ok(!kacheln.some((t) => /Automationen/.test(t)), + "1. die Kachel „Automationen“ fehlt ebenfalls"); +ok(kacheln.length > 8, + ` – aber der Rest steht noch da (${kacheln.length} Kacheln), es ist nicht alles weg`); + +/* 2. Die SEITE, falls jemand die Adresse kennt. */ +for (const pfad of ["personen.html", "automation.html"]) { + const a = await cigdem.goto(BASIS + "/workspace/" + pfad, { waitUntil: "networkidle" }); + const gelandet = cigdem.url(); + ok(gelandet.endsWith("/start.html"), + `2. ${pfad} direkt aufgerufen führt zurück zur Startseite (${gelandet.split("/").pop()})`); +} + +/* 3. Die SCHNITTSTELLE, falls jemand ganz an der Seite vorbeigeht. + Das ist die einzige Ebene, die wirklich zählt -- die beiden darüber + sind Bequemlichkeit. */ +const api = (seite, pfad, optionen) => seite.evaluate(async ([p, o]) => { + const a = await fetch(p, { credentials: "same-origin", ...(o || {}) }); + return a.status; +}, [pfad, optionen]); + +await cigdem.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" }); +ok(await api(cigdem, "/workspace/api/verwaltung/personen") === 404, + "3. die Personenverwaltung antwortet ihr mit 404"); +ok(await api(cigdem, "/workspace/api/zustand") === 404, + "3. der Systemzustand ebenfalls"); +ok(await api(cigdem, "/workspace/api/ki/schalter", { method: "PUT" }) !== 200, + "3. und den KI-Schalter darf sie nicht umlegen"); + +/* GEGENPROBE: Bei DogFather geht das alles weiterhin. Ohne sie wäre + jede Zeile oben auch dann grün, wenn die Bereiche für ALLE kaputt + wären -- und das würde erst auffallen, wenn er sie braucht. */ +ok(await api(dogi, "/workspace/api/verwaltung/personen") === 200, + "GEGENPROBE: DogFather kommt weiterhin an die Personenverwaltung"); +ok(await api(dogi, "/workspace/api/zustand") === 200, + "GEGENPROBE: und an den Systemzustand"); + +/* Und die Managerin behält, was sie behalten soll. */ +ok(await api(cigdem, "/workspace/api/aufgaben") === 200, + "die Managerin arbeitet ansonsten unverändert weiter (Aufgaben)"); +ok(await api(cigdem, "/workspace/api/calls") === 200, " und die Gespräche auch"); + +const ERWARTET = 17; +console.log(""); +ok(geprueft >= ERWARTET, `es sind ${geprueft} Prüfungen gelaufen (erwartet mindestens ${ERWARTET})`); + +await browser.close(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +console.log(`\n${fehler ? "FEHLER: " + fehler : "Alles in Ordnung"} — ${geprueft} Prüfungen`); +process.exit(fehler ? 1 : 0); diff --git a/server/pruef-personen-formular.png b/server/pruef-personen-formular.png new file mode 100644 index 00000000..9e3a3a63 Binary files /dev/null and b/server/pruef-personen-formular.png differ diff --git a/server/pruef-personen.png b/server/pruef-personen.png index eed12a12..78c9e6f1 100644 Binary files a/server/pruef-personen.png and b/server/pruef-personen.png differ diff --git a/server/pruef-rollen-admin.png b/server/pruef-rollen-admin.png index 087c8756..1a6d4a4b 100644 Binary files a/server/pruef-rollen-admin.png and b/server/pruef-rollen-admin.png differ diff --git a/server/pruef-rollen-manager.png b/server/pruef-rollen-manager.png index 9594d97c..6238a464 100644 Binary files a/server/pruef-rollen-manager.png and b/server/pruef-rollen-manager.png differ diff --git a/server/pruef-team.png b/server/pruef-team.png new file mode 100644 index 00000000..2b4556d6 Binary files /dev/null and b/server/pruef-team.png differ diff --git a/server/workspace-ki.js b/server/workspace-ki.js index cdc565b0..bc84440d 100644 --- a/server/workspace-ki.js +++ b/server/workspace-ki.js @@ -28,7 +28,7 @@ import express from "express"; import { - sitzungLesen, protokolliere, echteIp, einstellung, einstellungSetzen, istLeitung, + sitzungLesen, protokolliere, echteIp, einstellung, einstellungSetzen, istLeitung, istDogFather, } from "./workspace.js"; export const kiRouter = express.Router(); @@ -179,11 +179,15 @@ kiRouter.get("/workspace/api/ki/status", async (req, res) => { } }); -/* Ein- und ausschalten. Nur die Leitung -- es ist eine Entscheidung - ueber Rechenzeit, die der Website gehoert. */ +/* Ein- und ausschalten. NUR DogFather (02.09.2026). + + Vorher stand hier istLeitung. Der Schalter sitzt aber auf der Seite + "Automationen", und die sieht seit heute nur noch DogFather -- ein + Weg, der offen bleibt, waehrend die Seite dazu verschwindet, ist + genau die Art Rest, die man spaeter nicht mehr findet. */ kiRouter.put("/workspace/api/ki/schalter", gleicheHerkunft, express.json(), (req, res) => { try { - if (!istLeitung(req.person)) return res.status(403).json({ fehler: "nicht_erlaubt" }); + if (!istDogFather(req.person)) return res.status(403).json({ fehler: "nicht_erlaubt" }); const an = req.body?.an === true || req.body?.an === "1"; einstellungSetzen("ki_an", an ? "1" : "0", { ...req.person, ip: echteIp(req) }); res.json({ ok: true, angeschaltet: an }); diff --git a/server/workspace-personen.js b/server/workspace-personen.js index 8e9259dd..470cf7a0 100644 --- a/server/workspace-personen.js +++ b/server/workspace-personen.js @@ -28,10 +28,27 @@ const NAME_MAX = 60; /* Nur Management. Alles andere bekommt 404 statt 403 -- wer nicht berechtigt ist, muss nicht erfahren, dass es diesen Bereich gibt. */ +/* NUR DogFather -- seit dem 02.09.2026 auch wirklich. + + Hier stand istLeitung, und damit kam auch jede Managerin an die + Personenverwaltung: Codes erneuern, sperren, Zuteilungen ändern. Die + Rollenauswahl im selben Programm versprach dabei die ganze Zeit das + Gegenteil -- "Manager: dieselben Rechte wie DogFather, AUSSER der + Personenverwaltung". + + Eine Beschreibung, die etwas anderes sagt als die Software, ist + schlimmer als beides einzeln: Man weiss danach nicht mehr, welcher + von beiden man glauben soll. Jetzt sagen beide dasselbe. + + Wunsch vom 02.09.2026: "die manager sollen diese kategorien garnicht + sehen." + + Weiterhin 404 statt 403: Wer nicht hierher gehört, muss nicht + erfahren, dass es diesen Bereich gibt. */ function nurAdmin(req, res, next) { const person = sitzungLesen(req); if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" }); - if (!istLeitung(person)) return res.status(404).json({ fehler: "nicht_gefunden" }); + if (!istDogFather(person)) return res.status(404).json({ fehler: "nicht_gefunden" }); req.person = person; next(); } diff --git a/server/workspace-sicherung.js b/server/workspace-sicherung.js index 0c1a15c8..5573ae8d 100644 --- a/server/workspace-sicherung.js +++ b/server/workspace-sicherung.js @@ -316,8 +316,12 @@ function angemeldet(req, res, next) { next(); } +/* NUR DogFather (02.09.2026). Diese Wege speisen die Seite + "Automationen" -- Systemzustand, Sicherungen, Protokoll. Sie steht + seit heute nur noch DogFather offen; der Weg dorthin folgt ihr. + Der Name bleibt, damit die Aufrufer unveraendert bleiben. */ function nurLeitung(req, res, next) { - if (!istLeitung(req.person)) return res.status(404).json({ fehler: "nicht gefunden" }); + if (!istDogFather(req.person)) return res.status(404).json({ fehler: "nicht gefunden" }); next(); } diff --git a/server/workspace.js b/server/workspace.js index 3fd97508..93041277 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -880,7 +880,11 @@ const GESCHUETZT = { "/workspace/uebersicht.html": null, "/workspace/content.html": null, "/workspace/aufgaben.html": null, - "/workspace/personen.html": ["admin", "manager"], + /* NUR DogFather (02.09.2026). Die Rollenauswahl verspricht das seit + jeher ("Manager -- dieselben Rechte, ausser der + Personenverwaltung"), die Schranke hielt sich nur nicht daran. + Wunsch: "die manager sollen diese kategorien garnicht sehen". */ + "/workspace/personen.html": ["admin"], "/workspace/profil.html": ["admin", "manager", "scout", "creator"], /* Der eigene Steckbrief -- jede Rolle hat einen. Ein Creator wird dort nicht hingeschickt (er sieht seinen auf profil.html), darf die @@ -894,7 +898,9 @@ const GESCHUETZT = { "/workspace/calls.html": null, "/workspace/startcheck.html": null, "/workspace/wissen.html": null, - "/workspace/automation.html": ["admin", "manager"], + /* Ebenfalls nur DogFather: Was von allein passiert, bestimmt, was + allen anderen zugeschoben wird. */ + "/workspace/automation.html": ["admin"], }; workspaceRouter.use((req, res, next) => { diff --git a/workspace/assets/css/personen.css b/workspace/assets/css/personen.css index ad7d59ce..891fb23c 100644 --- a/workspace/assets/css/personen.css +++ b/workspace/assets/css/personen.css @@ -108,10 +108,57 @@ zwischen Raten und Wissen. =================================================================== */ +/* ---------- Das Formular "Neue Person" (neu gebaut 02.09.2026) ---------- + + Vorher lief die Rollenauswahl unten aus dem Formular heraus und legte + sich über die Personenliste darunter. Der Grund war keine schlampige + Gestaltung, sondern eine Regel, die für etwas anderes gemacht ist: + + `.neu__raster > *` (aufgaben.css) gibt JEDEM direkten Kind ein Raster + mit fester Zeilenhöhe von 44 px -- damit Beschriftung und Eingabefeld + in allen Spalten auf derselben Linie sitzen. Für ein einzeiliges Feld + ist das genau richtig. Die Rollenauswahl ist aber kein Feld, sondern + ein Block aus vier hohen Karten. Die passten nicht in 44 px, und was + nicht hineinpasst, steht eben daneben -- sichtbar über allem, was + darunter liegt. + + Deshalb wird das Formular hier bewusst aus dem Spaltenraster gelöst + und von oben nach unten gelesen: + + NAME (schmal -- ein Name braucht keine 1200 px) + ROLLE (volle Breite, Karten nebeneinander) + Knöpfe + + Das ist nicht nur reparierter Überlauf: Es ist auch die Reihenfolge, + in der man die Sache tatsächlich entscheidet. */ +#neu .neu__raster { + grid-template-columns: 1fr; + /* 26 px, nicht 14: Zwischen "Name" und "Rolle" liegt ein Gedanken- + sprung -- erst WER, dann WAS DARF ER. Stehen die beiden Blöcke dicht + beieinander, liest man sie als einen. */ + gap: 26px; + margin-bottom: 4px; +} +/* Die Feldregel aus aufgaben.css hier ausdrücklich zurücknehmen. */ +#neu .neu__raster > * { + display: block; + grid-column: 1 / -1; + height: auto; +} +#neu .neu__raster > *:first-child { max-width: 420px; } +#neu .neu__raster > * > .feld-schild { margin-bottom: 7px; } +/* Ohne feste Höhe wird das Namensfeld hier gut 60 px hoch: Die Regel + `height: 100%` aus aufgaben.css bezieht sich auf eine Rasterzeile, + die es hier nicht mehr gibt. Ein Eingabefeld, das höher ist als die + Knöpfe daneben, sieht nach Versehen aus -- weil es eins wäre. */ +#neu .neu__raster > * > input { height: 44px; } + .rollenwahl-block { grid-column: 1 / -1; } .rollenwahl { - display: grid; gap: 8px; margin-top: 2px; - grid-template-columns: repeat(auto-fit, minmax(min(232px, 100%), 1fr)); + display: grid; gap: 10px; margin-top: 8px; + /* Vier Karten passen ab etwa 1100 px nebeneinander, darunter bricht + es von selbst auf zwei und dann auf eine Spalte um. */ + grid-template-columns: repeat(auto-fit, minmax(min(250px, 100%), 1fr)); } .rollenwahl__knopf { @@ -221,3 +268,36 @@ @media (prefers-reduced-motion: reduce) { .gruppe__kopf--klappbar { transition: none; } } + +/* ---------- Die gewählte Rolle: ein Häkchen, nicht nur ein Farbton ------ + Bisher unterschied die gewählte Karte nur ein etwas hellerer Rahmen + und ein Hauch Farbe. Das ist genau die Art Unterschied, die im + Kontrastmodus verschwindet, bei hellem Umgebungslicht untergeht und + für farbunsichere Augen gar nicht erst existiert. Ein Häkchen sagt + dasselbe, ohne dass man Farben vergleichen muss. */ +.rollenwahl__knopf { position: relative; padding-right: 44px; } +.rollenwahl__knopf::after { + content: ""; + position: absolute; top: 13px; right: 14px; + width: 19px; height: 19px; border-radius: 50%; + border: 1.5px solid color-mix(in srgb, var(--r) 40%, var(--rand)); + transition: background .16s ease, border-color .16s ease; +} +.rollenwahl__knopf[data-an="ja"]::after { + content: "✓"; + display: grid; place-items: center; + border-color: var(--r); + background: var(--r); + color: #0b1017; + font-size: .8rem; font-weight: 800; line-height: 1; +} + +@media (prefers-reduced-motion: reduce) { + .rollenwahl__knopf, .rollenwahl__knopf::after { transition: none; } +} +@media (forced-colors: active) { + /* Ohne Farben trägt allein das Häkchen die Aussage. */ + .rollenwahl__knopf[data-an="ja"] { border: 2px solid Highlight; } + .rollenwahl__knopf::after { border-color: CanvasText; } + .rollenwahl__knopf[data-an="ja"]::after { background: Highlight; color: HighlightText; } +} diff --git a/workspace/assets/js/bereiche.js b/workspace/assets/js/bereiche.js index 6d410783..6e1254bd 100644 --- a/workspace/assets/js/bereiche.js +++ b/workspace/assets/js/bereiche.js @@ -149,14 +149,27 @@ nameCreator: 'Wissen', unterCreator: 'Nachschlagen, wenn du etwas brauchst', bereiche: [ + /* NUR DogFather (02.09.2026, Wunsch: "die manager sollen diese + kategorien garnicht sehen"). + + Es war ohnehin ein Widerspruch: In der Rollenauswahl steht seit + jeher "Manager -- dieselben Rechte wie DogFather, AUSSER der + Personenverwaltung". Die Kachel stand trotzdem bei ihm, und die + Seite liess ihn auch hinein. Eine Beschreibung, die etwas + anderes sagt als die Software, ist schlimmer als beides + einzeln: Man weiss danach nicht mehr, welcher von beiden man + glauben soll. */ { name: 'Personen & Zugänge', unter: 'Rollen und Codes', zeichen: 'personen', ton: 10, - rollen: ['admin', 'manager'], ziel: 'personen.html', szene: 'halle' }, + rollen: ['admin'], ziel: 'personen.html', szene: 'halle' }, { name: 'Scout-Pipeline', unter: 'Entdeckt bis übergeben', zeichen: 'scouting', ton: 17, rollen: ['admin', 'manager', 'scout'], ziel: 'scouting.html', szene: 'wald' }, { name: 'Wissens-Bibliothek', unter: 'Anleitungen und PDFs', zeichen: 'wissen', ton: 4, rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'wissen.html', szene: 'arena' }, + /* Ebenfalls nur DogFather (02.09.2026). Was von allein passiert, + legt fest, was ALLEN anderen automatisch zugeschoben wird -- + das gehört in dieselbe Hand wie die Rollenvergabe. */ { name: 'Automationen', unter: 'Was von allein passiert', zeichen: 'automation', ton: 5, - rollen: ['admin', 'manager'], ziel: 'automation.html', szene: 'garage' }, + rollen: ['admin'], ziel: 'automation.html', szene: 'garage' }, ], }, ]; diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 76fe0ff2..f0d1e6fe 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -7,9 +7,9 @@ - - - + + +
@@ -152,9 +152,9 @@ - - - - + + + +