Ein Zugang, den ausser DogFather niemand bemerkt
Aus dem Anforderungsdokument (Master-Blueprint V3.0) fehlte die Rolle
"Modi" ganz -- es gab nur spicy, admin, manager, scout, creator. Filipes
Bedingung dazu: "dass die keine neue eingangs kachel bekommen wie spicy
dogfather und so sondern einfach einen code. damit die von der workspace
auch nicht mal sehen dass die modis von mir einen eigenen zugang haben."
DER EINGANG. Es gibt keine sechste Kachel und es laesst sich auch keine
erzwingen: Wer von aussen `rolle: "modi"` schickt, bekommt wortgleich
dieselbe Antwort wie bei einer erfundenen Rolle. Ein Modi tippt auf
irgendeine vorhandene Kachel -- welche, ist gleichgueltig -- und gibt
seinen Code ein. Der Code allein entscheidet.
Moeglich macht das eine neue Spalte `code_kennung`: ein HMAC ueber den
Code, in Mikrosekunden nachgeschlagen. Zwei naheliegende Wege wurden
verworfen, weil man sie finden kann: ein Merkmal im Code ("M-...") waere
ein sichtbares Kennzeichen auf dem Zettel des Modis, eine eigene Adresse
(/modi.html) eine Seite, die man aufrufen kann. Der Suchschluessel steht
VOR dem gewohnten Weg, nicht dahinter -- ein Rueckfall nach einem
Fehlversuch haette genau die Fehlversuche verlaengert, und daran waere
es zu erkennen gewesen.
Unbedenklich, weil nachgemessen: Ein Code hat 16 Zeichen aus einem
32er-Alphabet, also 80 Bit Zufall. Der Suchschluessel sagt ausserdem nur,
WEN man pruefen soll -- ob der Code stimmt, sagt weiterhin scrypt.
DIE UNSICHTBARKEIT sitzt in verborgeneIds(), also an derselben einen
Stelle wie die Regel fuer den zweiten Admin-Zugang, und nicht in den
rund 170 Abfragen, die Personen lesen. Sie haengt dabei an der ROLLE und
nicht an einer Nummer -- die Schwachstelle, die im Kommentar der alten
Regel offen dasteht (wird Zugang 1 geloescht, rueckt der naechste nach),
kann einer Rolle nicht passieren.
Nach Filipes Entscheidungen: Die Modis sehen sich untereinander (Kapitel
7.2 des Dokuments), VanVan sieht sie mit (Kapitel 3, sie traegt dieselbe
Rolle), Codes gibt Filipe selbst weiter -- kein Einladelink, der in einem
Verlauf landen kann.
ZWEIMAL WAERE DAS WORT "MODI" BEINAHE IN EINER DATEI GELANDET, DIE JEDER
BEKOMMT: in den Rollenlisten von start.js und personen.js. Ein Blick in
den Quelltext haette genuegt. Der Anzeigename kommt jetzt aus
/workspace/api/ich (beschreibt immer nur den Angemeldeten selbst), die
Rollenauswahl aus der Antwort des Servers und nur an die DogFather-Rolle.
GEPRUEFT mit pruef-modi-verborgen.mjs (45 Pruefungen): fuenf Kacheln
fuehren mit dem Modi-Code hinein, ein Manager-Code auf fremder Kachel
weiterhin nicht (sonst waere nebenbei die Rollenpruefung abgeschafft),
und ueber fuenf Schnittstellen sieht ausser DogFather, VanVan und den
Modis niemand etwas -- auch nicht die ZAHL daneben.
Die Gegenprobe steht bewusst ganz unten, weil sie eine Person absichtlich
aus der Regel aushaengt: Weiter oben haette sie jeden Abschnitt danach
verfaelscht. Beim ersten Anlauf stand sie in der Mitte, und prompt tauchte
die Person in einer spaeteren Managerliste auf.
Abschnitt 5 prueft den Weg durch die Anwendung selbst (DogFather legt an,
der Modi meldet sich an). Die Abschnitte davor tragen die Personen von
Hand ein und rechnen den Suchschluessel selbst aus -- damit waere NICHT
bewiesen, dass personAnlegen() ihn im Betrieb schreibt. Ohne ihn kaeme
kein einziger echter Modi herein, und oben waere trotzdem alles gruen.
Die Umstellung der Rollenliste in der Datenbank steht ab jetzt einmal in
rollenRegelUmstellen() statt zum dritten Mal abgeschrieben. Jede Abschrift
waere eine Gelegenheit, eine der vier Absicherungen zu vergessen: Sicherung
vorher, Zaehlung innerhalb der Transaktion, Spaltenliste aus der Tabelle,
Pruefung auf verwaiste Verweise danach.
pruef-personen-formular erwartet jetzt sechs Rollen statt fuenf und prueft
die Liste statt nur die Anzahl -- sechs Karten koennten auch fuenf richtige
und eine doppelte sein. Dass die sechste dort auftaucht, ist gleichzeitig
der Nachweis, dass der Weg ueber den Server funktioniert.
Unveraendert bestanden: pruef-spicy (60), pruef-rollen (97),
pruef-verborgen, pruef-personen-liste, pruef-css-klassen.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
/* =====================================================================
|
||||
DER VERBORGENE MODI-ZUGANG
|
||||
|
||||
Zwei Fragen, und sie sind nicht dieselbe:
|
||||
|
||||
1. KOMMT EIN MODI HEREIN, ohne dass es dafuer eine Kachel gibt?
|
||||
2. MERKT SONST JEMAND ETWAS DAVON?
|
||||
|
||||
Die zweite ist die schwierigere. Eine Liste zu filtern ist leicht;
|
||||
uebersehen wird die ZAHL daneben, die weiter alles mitzaehlt -- und
|
||||
eine Personenzahl, die nicht zur Liste passt, verraet genauso viel
|
||||
wie ein Name.
|
||||
|
||||
MIT GEGENPROBE. Ganz unten wird die Regel absichtlich ausgehebelt
|
||||
(ein Modi bekommt in der Datenbank eine andere Rolle), und dann MUSS
|
||||
der Manager ihn sehen. Ohne diesen Teil waere nicht bewiesen, dass
|
||||
die Pruefung ueberhaupt anschlagen kann -- eine Pruefung, die immer
|
||||
bestaetigt, bestaetigt nichts.
|
||||
===================================================================== */
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-modi-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
process.env.PORT = "4359";
|
||||
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");
|
||||
/* NACH index.js: Der Server haengt dort zwei Auffangnetze ein, die
|
||||
einen Absturz nur protokollieren. Fuer den Betrieb richtig, fuer eine
|
||||
Pruefung fatal -- ohne diese Zeile bliebe der Prozess ewig stehen. */
|
||||
notbremse(180_000, "pruef-modi-verborgen");
|
||||
await new Promise((r) => setTimeout(r, 700));
|
||||
const BASIS = "http://127.0.0.1:4359";
|
||||
|
||||
let fehler = 0;
|
||||
let geprueft = 0;
|
||||
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
/* Der dritte Ausgang: Konnte nicht nachgesehen werden. Kein Befund,
|
||||
aber auch keine Bestaetigung -- und es wird gesagt, damit niemand
|
||||
glaubt, hier sei etwas geprueft worden. */
|
||||
let offen = 0;
|
||||
const unklar = (t) => { offen++; console.log(" -- " + t); };
|
||||
|
||||
/* ---------- Personen anlegen ---------------------------------------- */
|
||||
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();
|
||||
|
||||
/* Der Suchschluessel muss VOR dem Anlegen dastehen -- die Personen
|
||||
werden hier von Hand eingetragen, nicht ueber personAnlegen(). */
|
||||
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);
|
||||
const kennung = (code) => createHmac("sha256", schluessel).update(code).digest("hex");
|
||||
|
||||
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, kennung(code), jetzt);
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
|
||||
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||||
const idVan = anlegen("VanVan", "admin", "CODE-VAN-0001");
|
||||
const idSpicy = anlegen("Cigdem", "spicy", "CODE-SPIC-0001");
|
||||
const idMan = anlegen("Schulle", "manager", "CODE-MAN-0001");
|
||||
const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001");
|
||||
const idCreat = anlegen("Tili", "creator", "CODE-ANNA-0001");
|
||||
const idModi1 = anlegen("Marina", "modi", "CODE-MODI-0001");
|
||||
const idModi2 = anlegen("Kim", "modi", "CODE-MODI-0002");
|
||||
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)")
|
||||
.run(idCreat, idScout, jetzt);
|
||||
d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)")
|
||||
.run(idScout, idMan, jetzt);
|
||||
d.close();
|
||||
|
||||
const MODIS = [idModi1, idModi2];
|
||||
void idSpicy;
|
||||
|
||||
/* ---------- Anmelden ueber die Schnittstelle ------------------------- */
|
||||
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 daten = await a.json().catch(() => ({}));
|
||||
const keks = (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
||||
return { status: a.status, daten, keks };
|
||||
}
|
||||
|
||||
console.log("\n=== 1. Der Eingang: eine Kachel gibt es nicht ===\n");
|
||||
|
||||
for (const kachel of ["creator", "scout", "manager", "spicy", "admin"]) {
|
||||
const a = await anmelden(kachel, "CODE-MODI-0001");
|
||||
ok(a.status === 200 && a.daten?.rolle === "modi",
|
||||
`Kachel "${kachel}" + Modi-Code -> angemeldet als ${a.daten?.rolle || a.status}`);
|
||||
}
|
||||
|
||||
/* Der Umkehrschluss, und er ist der wichtigere: Der stille Weg darf
|
||||
NICHT dazu fuehren, dass auch alle anderen die Kachel ignorieren
|
||||
duerfen. Ein Manager mit der Kachel "creator" muss weiterhin
|
||||
scheitern -- sonst haette ich nebenbei die Rollenpruefung
|
||||
abgeschafft, ohne es zu merken. */
|
||||
{
|
||||
const a = await anmelden("creator", "CODE-MAN-0001");
|
||||
ok(a.status === 401, `Manager-Code auf der Creator-Kachel bleibt abgewiesen (${a.status})`);
|
||||
}
|
||||
|
||||
/* Wortgleichheit: "modi" von aussen anzugeben darf nicht anders klingen
|
||||
als eine erfundene Rolle. */
|
||||
{
|
||||
const m = await anmelden("modi", "FALSCH-FALSCH-1");
|
||||
const x = await anmelden("gibtesnicht", "FALSCH-FALSCH-1");
|
||||
ok(m.status === 401 && x.status === 401
|
||||
&& JSON.stringify(m.daten) === JSON.stringify(x.daten),
|
||||
"Rolle \"modi\" von aussen klingt wie eine erfundene "
|
||||
+ `(${m.status} ${JSON.stringify(m.daten)} vs. ${x.status} ${JSON.stringify(x.daten)})`);
|
||||
}
|
||||
|
||||
{
|
||||
const a = await anmelden("creator", "CODE-MODI-FALSCH");
|
||||
ok(a.status === 401, `ein falscher Modi-Code wird abgewiesen (${a.status})`);
|
||||
}
|
||||
|
||||
/* ---------- 2. Die Unsichtbarkeit ------------------------------------ */
|
||||
console.log("\n=== 2. Wer sieht die Modis? ===\n");
|
||||
|
||||
/* Jede Schnittstelle, die Personen ausgibt. Dieselbe Landkarte wie in
|
||||
pruef-verborgen.mjs -- wer eine neue baut, traegt sie in BEIDE ein. */
|
||||
const WEGE = [
|
||||
["Chat-Partner", "/workspace/api/chat/partner", (x) => x.personen],
|
||||
["Personenwahl", "/workspace/api/personen", (x) => x.personen],
|
||||
["Personenverwaltung", "/workspace/api/verwaltung/personen", (x) => x.personen],
|
||||
["Betreuer-Auswahl", "/workspace/api/verwaltung/personen", (x) => x.betreuer],
|
||||
/* NACHGESEHEN, NICHT ANGENOMMEN: Die Segmente der Zentrale tragen
|
||||
KEINE Nummer -- nur `{ name, rolle, stufe, heute, alt }`, und der
|
||||
Name ist auf den Vornamen gekuerzt. Wer hier nur nach `id` sucht,
|
||||
bekommt einen gruenen Haken, ohne etwas angesehen zu haben. Genau
|
||||
deshalb vergleicht der Treffer unten BEIDES. */
|
||||
["Zentrale (Team)", "/workspace/api/zentrale", (x) => x.segmente],
|
||||
];
|
||||
|
||||
/* Die Namen der Modis -- fuer die Wege, die keine Nummern liefern. */
|
||||
const MODI_NAMEN = ["Marina", "Kim"];
|
||||
|
||||
const LEUTE = [
|
||||
["DogFather", "admin", "CODE-DOGI-0001", true],
|
||||
["VanVan", "admin", "CODE-VAN-0001", true],
|
||||
["Spicy Media", "spicy", "CODE-SPIC-0001", false],
|
||||
["Manager", "manager", "CODE-MAN-0001", false],
|
||||
["Scout", "scout", "CODE-SCOU-0001", false],
|
||||
["Creator", "creator", "CODE-ANNA-0001", false],
|
||||
["Modi Marina", "creator", "CODE-MODI-0001", true],
|
||||
];
|
||||
|
||||
async function hole(keks, pfad) {
|
||||
const a = await fetch(BASIS + pfad, { headers: { cookie: keks } });
|
||||
if (!a.ok) return { status: a.status, daten: null };
|
||||
return { status: 200, daten: await a.json().catch(() => null) };
|
||||
}
|
||||
|
||||
for (const [name, kachel, code, darf] of LEUTE) {
|
||||
const an = await anmelden(kachel, code);
|
||||
if (an.status !== 200) { unklar(`${name}: Anmeldung misslang (${an.status})`); continue; }
|
||||
for (const [was, pfad, ziehen] of WEGE) {
|
||||
const { status, daten } = await hole(an.keks, pfad);
|
||||
if (status !== 200) {
|
||||
unklar(`${name.padEnd(12)} ${was.padEnd(20)} kein Zugriff (${status})`);
|
||||
continue;
|
||||
}
|
||||
let liste = [];
|
||||
try { liste = ziehen(daten) || []; } catch { liste = []; }
|
||||
const drin = liste.filter((z) => z
|
||||
&& (MODIS.includes(z.id ?? z.person_id)
|
||||
|| MODI_NAMEN.includes(String(z.name || "").trim().split(/\s+/)[0])));
|
||||
if (darf) {
|
||||
ok(true, `${name.padEnd(12)} ${was.padEnd(20)} sieht ${drin.length} Modi(s)`);
|
||||
} else {
|
||||
ok(drin.length === 0,
|
||||
`${name.padEnd(12)} ${was.padEnd(20)} `
|
||||
+ (drin.length ? `SIEHT ${drin.length} MODI -- FEHLER` : "sieht keinen Modi"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 3. Die Zahl daneben -------------------------------------- */
|
||||
console.log("\n=== 3. Auch die ZAHL darf sie nicht mitzaehlen ===\n");
|
||||
{
|
||||
const an = await anmelden("manager", "CODE-MAN-0001");
|
||||
const { status, daten } = await hole(an.keks, "/workspace/api/personen");
|
||||
if (status !== 200 || !Array.isArray(daten?.personen)) {
|
||||
unklar(`Personenwahl fuer den Manager nicht lesbar (${status})`);
|
||||
} else {
|
||||
const namen = daten.personen.map((z) => z.name);
|
||||
/* Die Zahl der geprueften Dinge gehoert in die BEDINGUNG, nicht nur
|
||||
in den Meldetext: Eine leere Liste wuerde sonst als "keine Modis
|
||||
drin" durchgehen, und die Pruefung waere wertlos. */
|
||||
ok(namen.length > 0 && !namen.includes("Marina") && !namen.includes("Kim"),
|
||||
`der Manager sieht ${namen.length} Personen, keine davon ein Modi `
|
||||
+ `(${namen.join(", ")})`);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 4. Sehen die Modis einander? ----------------------------- */
|
||||
console.log("\n=== 4. Die Modis sind untereinander ein Team ===\n");
|
||||
{
|
||||
const an = await anmelden("creator", "CODE-MODI-0001");
|
||||
const { status, daten } = await hole(an.keks, "/workspace/api/personen");
|
||||
if (status !== 200 || !Array.isArray(daten?.personen)) {
|
||||
unklar(`Personenwahl fuer den Modi nicht lesbar (${status})`);
|
||||
} else {
|
||||
const ids = daten.personen.map((z) => z.id);
|
||||
ok(ids.includes(idModi2), "Marina sieht Kim");
|
||||
ok(ids.includes(idDogi), "Marina sieht DogFather");
|
||||
/* VanVan bleibt fuer alle ausser DogFather verborgen -- das ist die
|
||||
Regel vom 09.09.2026, und sie gilt auch gegenueber Modis. */
|
||||
ok(!ids.includes(idVan), "Marina sieht VanVan NICHT (verborgener Zugang)");
|
||||
ok(!ids.includes(idMan) && !ids.includes(idScout) && !ids.includes(idCreat),
|
||||
"Marina sieht weder Manager noch Scout noch Creator");
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 5. Der echte Weg durch die Anwendung --------------------- */
|
||||
console.log("\n=== 5. Anlegen wie im Betrieb: DogFather legt einen Modi an ===\n");
|
||||
{
|
||||
const an = await anmelden("admin", "CODE-DOGI-0001");
|
||||
const kopf = { "Content-Type": "application/json", cookie: an.keks };
|
||||
|
||||
/* a) Steht die Rolle ueberhaupt zur Auswahl -- und kommt sie vom
|
||||
Server, nicht aus einer Datei im Browser? */
|
||||
const { status, daten } = await hole(an.keks, "/workspace/api/verwaltung/personen");
|
||||
if (status !== 200) {
|
||||
unklar(`Verwaltung fuer DogFather nicht lesbar (${status})`);
|
||||
} else {
|
||||
const zusatz = daten?.zusatzrollen || [];
|
||||
ok(zusatz.some((z) => z.wert === "modi"),
|
||||
"DogFather bekommt die Rolle zur Auswahl angeboten "
|
||||
+ `(${zusatz.map((z) => z.wert).join(", ") || "keine"})`);
|
||||
}
|
||||
|
||||
/* b) Anlegen -- ueber genau den Weg, den die Oberflaeche benutzt. */
|
||||
const a = await fetch(BASIS + "/workspace/api/verwaltung/personen", {
|
||||
method: "POST", headers: kopf,
|
||||
body: JSON.stringify({ name: "Testmodi Neu", rolle: "modi" }),
|
||||
});
|
||||
const neuDaten = await a.json().catch(() => ({}));
|
||||
ok(a.status === 201 && neuDaten?.rolle === "modi" && typeof neuDaten?.code === "string",
|
||||
`angelegt (${a.status}, Rolle ${neuDaten?.rolle}, `
|
||||
+ `Code ${neuDaten?.code ? "erhalten" : "FEHLT"})`);
|
||||
|
||||
/* c) DER EIGENTLICHE PUNKT: Kommt der so angelegte Modi auch herein?
|
||||
|
||||
Die Abschnitte oben haben die Personen VON HAND eingetragen und
|
||||
den Suchschluessel selbst ausgerechnet. Damit ist nicht bewiesen,
|
||||
dass personAnlegen() ihn im Betrieb auch wirklich schreibt -- und
|
||||
ohne ihn findet der stille Zugang niemanden. Waere die Zeile dort
|
||||
vergessen worden, waeren oben trotzdem alle Haken gruen und kein
|
||||
einziger echter Modi kaeme herein. */
|
||||
if (neuDaten?.code) {
|
||||
const b = await anmelden("creator", neuDaten.code);
|
||||
ok(b.status === 200 && b.daten?.rolle === "modi",
|
||||
"und meldet sich mit seinem Code an, ueber eine fremde Kachel "
|
||||
+ `(${b.status}, ${b.daten?.rolle})`);
|
||||
} else {
|
||||
unklar("ohne Code laesst sich die Anmeldung nicht pruefen");
|
||||
}
|
||||
|
||||
/* d) Und ist auch der Neue fuer den Manager unsichtbar? */
|
||||
const m = await anmelden("manager", "CODE-MAN-0001");
|
||||
const sicht = await hole(m.keks, "/workspace/api/personen");
|
||||
if (sicht.status !== 200 || !Array.isArray(sicht.daten?.personen)) {
|
||||
unklar(`Personenwahl fuer den Manager nicht lesbar (${sicht.status})`);
|
||||
} else {
|
||||
const namen = sicht.daten.personen.map((z) => z.name);
|
||||
ok(namen.length > 0 && !namen.includes("Testmodi Neu"),
|
||||
`der Manager sieht ihn nicht (${namen.length} Personen: ${namen.join(", ")})`);
|
||||
}
|
||||
}
|
||||
/* ---------- 6. Die Gegenprobe ----------------------------------------
|
||||
Sie steht ABSICHTLICH GANZ UNTEN: Sie haengt eine Person
|
||||
absichtlich aus der Regel aus. Lief sie weiter oben, tauchte
|
||||
diese Person in jeder Liste DANACH auf -- und jeder spaetere
|
||||
Abschnitt maesse einen Zustand, den es im Betrieb nicht gibt.
|
||||
-------------------------------------------------------------------- */
|
||||
console.log("\n=== 6. Gegenprobe: kann diese Pruefung ueberhaupt anschlagen? ===\n");
|
||||
{
|
||||
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
d2.prepare("UPDATE personen SET rolle = 'creator' WHERE id = ?").run(idModi1);
|
||||
/* Betreuung dazu, sonst waere der Creator fuer den Manager auch ohne
|
||||
die Verbergen-Regel unsichtbar -- und die Gegenprobe pruefte nichts. */
|
||||
d2.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)")
|
||||
.run(idModi1, idScout, jetzt);
|
||||
d2.close();
|
||||
|
||||
const an = await anmelden("manager", "CODE-MAN-0001");
|
||||
const { status, daten } = await hole(an.keks, "/workspace/api/personen");
|
||||
if (status !== 200 || !Array.isArray(daten?.personen)) {
|
||||
unklar(`Gegenprobe nicht durchfuehrbar (${status})`);
|
||||
} else {
|
||||
const sichtbar = daten.personen.some((z) => z.id === idModi1);
|
||||
ok(sichtbar, sichtbar
|
||||
? "ohne die Rolle 'modi' sieht der Manager dieselbe Person sehr wohl "
|
||||
+ "-- die Pruefung oben kann also anschlagen"
|
||||
: "GEGENPROBE FEHLGESCHLAGEN: auch ohne die Regel bleibt die Person "
|
||||
+ "unsichtbar. Die Pruefung oben beweist damit nichts.");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -108,17 +108,33 @@ const mass = await dogi.evaluate(() => {
|
||||
wahlOben: Math.round(w.top),
|
||||
listeOben: dn ? Math.round(dn.top) : null,
|
||||
karten: karten.length,
|
||||
kartenRollen: karten.map((k) => k.dataset.rolle),
|
||||
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),
|
||||
};
|
||||
});
|
||||
|
||||
/* FÜNF, seit es Spicy Media gibt. Die Zahl steht hier absichtlich fest
|
||||
und wird nicht aus der Oberfläche abgeleitet: Sie ist die Erwartung,
|
||||
gegen die gemessen wird. Fiele eine Rolle aus der Wahl, müsste das
|
||||
auffallen -- eine Zahl, die sich selbst nachzählt, kann das nicht. */
|
||||
ok(mass.karten === 5, `alle fünf Rollen stehen zur Wahl (${mass.karten})`);
|
||||
/* SECHS, seit es die verborgene Modi-Rolle gibt (09.09.2026) -- vorher
|
||||
fünf, seit es Spicy Media gibt. Die Erwartung steht hier absichtlich
|
||||
fest und wird nicht aus der Oberfläche abgeleitet: Fiele eine Rolle
|
||||
aus der Wahl, müsste das auffallen -- eine Zahl, die sich selbst
|
||||
nachzählt, kann das nicht.
|
||||
|
||||
DIE SECHSTE IST NICHT WIE DIE ANDEREN. Die fünf stehen in
|
||||
assets/js/personen.js, also in einer Datei, die jeder bekommt, der
|
||||
die Seite öffnet. "Modi" darf dort nicht stehen -- ein Blick in den
|
||||
Quelltext würde sonst genügen, um den verborgenen Zugang zu
|
||||
entdecken. Sie kommt deshalb aus der Antwort des Servers und nur an
|
||||
die DogFather-Rolle. Dass sie hier auftaucht, ist gleichzeitig der
|
||||
Nachweis, dass dieser Weg funktioniert.
|
||||
|
||||
Geprüft wird ab jetzt die LISTE und nicht nur die Anzahl: Sechs
|
||||
Karten könnten auch fünf richtige und eine doppelte sein. */
|
||||
ok(mass.karten === 6, `alle sechs Rollen stehen zur Wahl (${mass.karten})`);
|
||||
ok(JSON.stringify([...mass.kartenRollen].sort())
|
||||
=== JSON.stringify(["admin", "creator", "manager", "modi", "scout", "spicy"]),
|
||||
`und es sind die richtigen (${mass.kartenRollen.join(", ")})`);
|
||||
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");
|
||||
|
||||
@@ -211,6 +211,33 @@ personenRouter.get("/workspace/api/verwaltung/personen", (req, res) => {
|
||||
`SELECT id, name, rolle FROM personen
|
||||
WHERE rolle IN ('admin', 'manager', 'scout') AND aktiv = 1
|
||||
ORDER BY ${ROLLEN_SORTIERUNG}, name`).all().filter((z) => !weg.has(z.id)),
|
||||
|
||||
/* =============================================================
|
||||
ROLLEN, DIE NICHT IM BROWSER STEHEN DUERFEN (09.09.2026)
|
||||
|
||||
Die Rollenauswahl im Formular kommt sonst aus einer Liste in
|
||||
assets/js/personen.js. Diese Datei bekommt JEDER ausgeliefert,
|
||||
der die Seite oeffnet -- auch ein Manager, ein Scout, ein
|
||||
Creator. Steht dort `modi: 'Modi'`, genuegt ein Blick in den
|
||||
Quelltext, und der ganze verborgene Zugang ist verraten.
|
||||
|
||||
Deshalb kommt dieser eine Eintrag vom Server und nur an die
|
||||
DogFather-Rolle. Wer die Rolle nicht hat, bekommt eine leere
|
||||
Liste -- nicht eine gefilterte Liste, sondern gar keine
|
||||
Angabe. Ein Feld, das mal Inhalt hat und mal nicht, verraet
|
||||
nichts; ein Feld mit einem "darfst du nicht" darin schon.
|
||||
|
||||
Text und Zeichen stehen deshalb ebenfalls hier und nicht im
|
||||
Browser. `schutz` ist das vorhandene Schild-Zeichen -- es passt
|
||||
zur Moderation und spart ein neues, das in der Zeichenliste
|
||||
wieder fuer alle sichtbar waere. */
|
||||
zusatzrollen: istDogFather(req.person)
|
||||
? [{
|
||||
wert: "modi", name: "Modi", symbol: "schutz",
|
||||
text: "Moderation und Team-Aufgaben. Sichtbar nur fuer die "
|
||||
+ "DogFather-Rolle und fuer die Modis untereinander.",
|
||||
}]
|
||||
: [],
|
||||
});
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Personen lesen:", fehler?.message);
|
||||
@@ -502,7 +529,23 @@ personenRouter.post("/workspace/api/verwaltung/personen", gleicheHerkunft, (req,
|
||||
const rolle = String(req.body?.rolle ?? "");
|
||||
if (name.length < 2) return res.status(400).json({ fehler: "Name fehlt." });
|
||||
if (name.length > NAME_MAX) return res.status(400).json({ fehler: "Name ist zu lang." });
|
||||
if (!ROLLEN.includes(rolle)) return res.status(400).json({ fehler: "Unbekannte Rolle." });
|
||||
/* 'MODI' IST FUER ALLE AUSSER DOGFATHER KEINE BEKANNTE ROLLE
|
||||
(09.09.2026).
|
||||
|
||||
EHRLICHERWEISE: Diese Zeile schliesst heute keine Luecke. Der
|
||||
ganze Zweig /workspace/api/verwaltung haengt an nurAdmin, und
|
||||
nurAdmin laesst nur die DogFather-Rolle durch -- weiter kommt
|
||||
ohnehin niemand. Sie steht hier fuer den Tag, an dem jemand
|
||||
nurAdmin lockert, etwa damit Manager Leute verwalten duerfen.
|
||||
Dann waere sonst genau hier der stille Weg zu einem Modi-Zugang.
|
||||
|
||||
WARUM DIESELBE ANTWORT WIE BEI EINER ERFUNDENEN ROLLE: Ein
|
||||
"das darfst du nicht" waere schlimmer als das Recht selbst -- es
|
||||
verriete, dass es die Rolle gibt. Deshalb wortgleich, mit
|
||||
demselben Statuscode. */
|
||||
const unbekannt = !ROLLEN.includes(rolle)
|
||||
|| (rolle === "modi" && !istDogFather(req.person));
|
||||
if (unbekannt) return res.status(400).json({ fehler: "Unbekannte Rolle." });
|
||||
|
||||
/* ERSTER VORBEHALT: Eine Leitung anlegen darf nur DogFather.
|
||||
Duerfte ein Manager das, koennte er sich einen zweiten Zugang mit
|
||||
|
||||
+383
-103
@@ -21,7 +21,7 @@
|
||||
|
||||
import express from "express";
|
||||
import {
|
||||
randomBytes, scryptSync, timingSafeEqual, createHash,
|
||||
randomBytes, scryptSync, timingSafeEqual, createHash, createHmac,
|
||||
} from "node:crypto";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
@@ -57,19 +57,34 @@ const COOKIE = "dfw_sitzung";
|
||||
const SITZUNG_STUNDEN = 12;
|
||||
const VERSUCHE_MAX = 8; // pro IP
|
||||
const VERSUCHE_FENSTER_MIN = 10;
|
||||
const ROLLEN = new Set(["spicy", "admin", "manager", "scout", "creator"]);
|
||||
const ROLLEN = new Set(["spicy", "admin", "manager", "scout", "creator", "modi"]);
|
||||
|
||||
/* Die Reihenfolge, in der Rollen ueberall erscheinen: Spicy Media
|
||||
zuerst, dann DogFather, Manager, Scout, Creator. Steht hier einmal,
|
||||
damit keine Liste eine eigene Reihenfolge erfindet. */
|
||||
export const ROLLEN_REIHE = ["spicy", "admin", "manager", "scout", "creator"];
|
||||
export const ROLLEN_REIHE = ["spicy", "admin", "manager", "scout", "creator", "modi"];
|
||||
|
||||
/* DIE ROLLEN MIT EINER KACHEL AUF DER ANMELDESEITE.
|
||||
|
||||
'modi' steht hier NICHT drin, und das ist der Kern des verborgenen
|
||||
Zugangs: Es gibt keine sechste Kachel, und es laesst sich auch keine
|
||||
erzwingen. Wer von aussen `rolle: "modi"` schickt, bekommt genau
|
||||
dieselbe Antwort wie bei einer erfundenen Rolle -- er erfaehrt also
|
||||
nicht einmal, dass es sie gibt.
|
||||
|
||||
Zwei Listen statt einer, weil es zwei verschiedene Fragen sind:
|
||||
ROLLEN sagt, welche Rollen es GIBT (die Datenbank laesst nur diese
|
||||
zu). Diese hier sagt, mit welchen man sich ANMELDEN kann, indem man
|
||||
sie anklickt. Waere es eine Liste, haette 'modi' entweder eine Kachel
|
||||
-- oder es gaebe die Rolle gar nicht. */
|
||||
const ROLLEN_KACHEL = new Set(["spicy", "admin", "manager", "scout", "creator"]);
|
||||
|
||||
/* Als SQL-Ausdruck fuer ORDER BY. "ORDER BY rolle" waere alphabetisch
|
||||
(admin, creator, manager, scout, spicy) -- also fast genau falsch
|
||||
herum. */
|
||||
export const ROLLEN_SORTIERUNG =
|
||||
"CASE rolle WHEN 'spicy' THEN 0 WHEN 'admin' THEN 1 WHEN 'manager' THEN 2"
|
||||
+ " WHEN 'scout' THEN 3 ELSE 4 END";
|
||||
+ " WHEN 'scout' THEN 3 WHEN 'creator' THEN 4 ELSE 5 END";
|
||||
|
||||
/* LEITUNG = DogFather und Manager. Ein Manager darf alles, was
|
||||
DogFather darf -- mit genau zwei Ausnahmen, die in
|
||||
@@ -168,6 +183,7 @@ export const ROLLEN_NAME = {
|
||||
manager: "Manager",
|
||||
scout: "Scout",
|
||||
creator: "Creator",
|
||||
modi: "Modi",
|
||||
};
|
||||
|
||||
/* scrypt-Parameter. N=2^15 braucht auf diesem Server rund 150 ms — spürbar
|
||||
@@ -194,6 +210,122 @@ let _dbFehler = null;
|
||||
SQLite selbst und in sich konsistent, anders als ein Dateikopie
|
||||
waehrend laufender Schreibvorgaenge).
|
||||
===================================================================== */
|
||||
/* =====================================================================
|
||||
DIE ROLLENLISTE DER DATENBANK ERWEITERN (09.09.2026)
|
||||
|
||||
Welche Rollen es geben darf, steht als CHECK-Regel in der Tabelle
|
||||
`personen`. SQLite kann eine CHECK-Regel nicht aendern -- der einzige
|
||||
saubere Weg ist: neue Tabelle, Daten hinueber, alte weg, neue
|
||||
umbenennen. Das ist der Moment, in dem eine Datenbank kaputtgehen
|
||||
kann. Deshalb steht der Ablauf ab jetzt EINMAL hier.
|
||||
|
||||
WARUM ALS FUNKTION: Er stand vorher zweimal da -- einmal fuer
|
||||
'manager', einmal fuer 'spicy'. Fuer 'modi' waere es die dritte
|
||||
Abschrift geworden, und jede Abschrift ist eine Gelegenheit, eine der
|
||||
vier Absicherungen zu vergessen: die Sicherung VORHER, die Zaehlung
|
||||
INNERHALB der Transaktion, die Spaltenliste aus der Tabelle statt aus
|
||||
dem Gedaechtnis, die Pruefung auf verwaiste Verweise DANACH. Der
|
||||
manager-Block darueber bleibt unangetastet: Er baut die Tabelle mit
|
||||
einer fest hingeschriebenen Spaltenliste auf und ist damit ein
|
||||
anderer Fall -- ihn mit einzufangen waere ein zweiter Umbau an einer
|
||||
Stelle, an der ein Fehler Daten kostet.
|
||||
|
||||
GEPRUEFT WIRD DIE REGEL, NICHT DER TEXT (07.09.2026, gefunden von
|
||||
pruef-spicy): SQLite hebt den CREATE-Text woertlich auf, mitsamt
|
||||
Kommentaren. Ein erklaerender Satz mit dem Wort 'spicy' genuegte, und
|
||||
die Umstellung hielt die Tabelle fuer schon umgestellt, obwohl die
|
||||
CHECK-Regel noch die alte war. Deshalb wird die Regel herausgeschnitten
|
||||
und NUR darin gesucht.
|
||||
|
||||
@param marker Die Rolle, an der erkannt wird, ob schon umgestellt ist.
|
||||
@param rollen Die vollstaendige neue Liste erlaubter Rollen.
|
||||
===================================================================== */
|
||||
function rollenRegelUmstellen(d, marker, rollen, jetztStempel) {
|
||||
const rollenPlan = d.prepare(
|
||||
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'personen'").get()?.sql || "";
|
||||
const regel = rollenPlan.match(/rolle\s+IN\s*\(([^)]*)\)/i)?.[1] || "";
|
||||
/* Keine Regel gefunden heisst: Es gibt nichts umzustellen. Nicht
|
||||
etwa "dann bauen wir sie neu" -- eine Tabelle ohne CHECK ist ein
|
||||
Zustand, den jemand ansehen sollte, kein Fall fuer Automatik. */
|
||||
if (!rollenPlan || !regel || regel.includes(`'${marker}'`)) return;
|
||||
|
||||
const sicherung = `${DB_PFAD}.vor-${marker}-${jetztStempel}`;
|
||||
try {
|
||||
d.exec(`VACUUM INTO '${sicherung.replace(/'/g, "''")}'`);
|
||||
console.log("[workspace] Sicherung vor der Umstellung:", sicherung);
|
||||
} catch (fehler) {
|
||||
/* Ohne Sicherung wird NICHT umgestellt. Lieber laeuft die neue Rolle
|
||||
noch nicht, als dass Daten ohne Netz angefasst werden. */
|
||||
console.error("[workspace] Sicherung fehlgeschlagen, Umstellung abgebrochen:", fehler?.message);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Nur die Rollenliste ersetzen -- und pruefen, dass es geklappt hat.
|
||||
|
||||
`IF NOT EXISTS` MUSS MIT INS MUSTER (07.09.2026, gefunden von
|
||||
pruef-spicy). Die Tabelle wurde mit `CREATE TABLE IF NOT EXISTS
|
||||
personen` angelegt, also steht das auch in sqlite_master. Ohne diese
|
||||
drei Woerter im Muster griff die Ersetzung nicht, der Bauplan blieb
|
||||
unveraendert, die Schutzabfrage darunter schlug an -- und die
|
||||
Umstellung waere auf dem echten Server NIE gelaufen. Sie haette es
|
||||
gesagt (das ist der Wert der Abfrage), aber sie waere nie gelaufen. */
|
||||
const neuerPlan = rollenPlan
|
||||
.replace(/CREATE TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?personen["'`]?/i,
|
||||
"CREATE TABLE personen_neu")
|
||||
.replace(/rolle\s+IN\s*\([^)]*\)/i,
|
||||
`rolle IN (${rollen.map((r) => `'${r}'`).join(",")})`);
|
||||
if (!neuerPlan.includes(`'${marker}'`) || !neuerPlan.includes("personen_neu")) {
|
||||
console.error("[workspace] Umstellung abgebrochen: Der Bauplan liess sich nicht "
|
||||
+ "umschreiben. Steht die CHECK-Regel noch so da wie erwartet?");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Die Spaltenliste kommt aus der Tabelle, nicht aus dem Gedaechtnis. */
|
||||
const spalten = d.prepare("PRAGMA table_info(personen)").all().map((z) => z.name);
|
||||
if (!spalten.length) {
|
||||
console.error("[workspace] Umstellung abgebrochen: keine Spalten gefunden.");
|
||||
return;
|
||||
}
|
||||
const liste = spalten.map((n) => `"${n}"`).join(", ");
|
||||
|
||||
/* Fremdschluessel muessen aus sein, weil andere Tabellen auf
|
||||
personen(id) zeigen -- und das laesst sich nicht innerhalb einer
|
||||
Transaktion umschalten. */
|
||||
d.exec("PRAGMA foreign_keys = OFF");
|
||||
try {
|
||||
const vorher = d.prepare("SELECT COUNT(*) AS n FROM personen").get().n;
|
||||
d.exec("BEGIN");
|
||||
d.exec(neuerPlan);
|
||||
d.exec(`INSERT INTO personen_neu (${liste}) SELECT ${liste} FROM personen;`);
|
||||
const nachher = d.prepare("SELECT COUNT(*) AS n FROM personen_neu").get().n;
|
||||
/* Die Zaehlung steht INNERHALB der Transaktion -- stimmt sie nicht,
|
||||
wird zurueckgerollt und die alte Tabelle bleibt unberuehrt. */
|
||||
if (nachher !== vorher) {
|
||||
d.exec("ROLLBACK");
|
||||
console.error(`[workspace] Umstellung abgebrochen: ${vorher} Personen vorher, `
|
||||
+ `${nachher} nachher. Sicherung: ${sicherung}`);
|
||||
} else {
|
||||
d.exec("DROP TABLE personen;");
|
||||
d.exec("ALTER TABLE personen_neu RENAME TO personen;");
|
||||
d.exec("COMMIT");
|
||||
const kaputt = d.prepare("PRAGMA foreign_key_check").all();
|
||||
if (kaputt.length) {
|
||||
console.error("[workspace] ACHTUNG: nach der Umstellung", kaputt.length,
|
||||
"verwaiste Verweise. Sicherung liegt unter", sicherung);
|
||||
} else {
|
||||
console.log(`[workspace] Rolle '${marker}' freigeschaltet, ${nachher} Personen, `
|
||||
+ `${spalten.length} Spalten uebernommen, Verweise geprueft.`);
|
||||
}
|
||||
}
|
||||
} catch (fehler) {
|
||||
try { d.exec("ROLLBACK"); } catch { /* schon zurueckgerollt */ }
|
||||
console.error("[workspace] Umstellung fehlgeschlagen:", fehler?.message,
|
||||
"-- Sicherung:", sicherung);
|
||||
} finally {
|
||||
d.exec("PRAGMA foreign_keys = ON");
|
||||
}
|
||||
}
|
||||
|
||||
function umstellungen(d) {
|
||||
/* MIT SEKUNDEN, nicht nur mit Minuten (07.09.2026).
|
||||
|
||||
@@ -458,6 +590,30 @@ function umstellungen(d) {
|
||||
naheliegende Abkuerzung gewesen -- und faellt in dem Moment um,
|
||||
in dem jemand den Titel einer uebernommenen Aufgabe aendert. */
|
||||
["aufgaben", "vorlage", "TEXT"],
|
||||
|
||||
/* DER SUCHSCHLUESSEL ZUM CODE (09.09.2026).
|
||||
|
||||
Bisher gab es nur den scrypt-Hash, und der ist mit Absicht
|
||||
langsam (rund 150 ms). Wer eine Person AM CODE finden will, muss
|
||||
deshalb alle Kandidaten der Reihe nach durchrechnen -- die
|
||||
Anmeldung tut genau das, und im Code steht seit langem der
|
||||
Hinweis, dass das ab etwa 50 Codes je Rolle spuerbar wird.
|
||||
|
||||
Diese Spalte ist die schnelle Antwort auf "zu welcher Person
|
||||
gehoert dieser Code?": ein HMAC ueber den Code, in Mikrosekunden
|
||||
berechnet und mit einem Index hinterlegt.
|
||||
|
||||
WARUM DAS UNBEDENKLICH IST -- nachgemessen, nicht angenommen: Ein
|
||||
Code besteht aus 16 Zeichen eines 32er-Alphabets, das sind 80 Bit
|
||||
Zufall. Selbst wenn jemand die ganze Datenbank haette, waere
|
||||
Durchprobieren aussichtslos, egal wie schnell die Rechnung ist.
|
||||
Der Schluessel steckt zusaetzlich nicht im Code, sondern in den
|
||||
Einstellungen (siehe kennungSchluessel).
|
||||
|
||||
DER SCRYPT-HASH BLEIBT DIE ENTSCHEIDUNG. Der Suchschluessel sagt
|
||||
nur, WEN man pruefen soll; ob der Code stimmt, sagt weiterhin
|
||||
allein scrypt. Ein Treffer hier allein laesst niemanden herein. */
|
||||
["personen", "code_kennung", "TEXT"],
|
||||
]) {
|
||||
try {
|
||||
const vorhanden = d.prepare(`PRAGMA table_info(${tabelle})`).all().map((s) => s.name);
|
||||
@@ -475,6 +631,16 @@ function umstellungen(d) {
|
||||
die ganze Termintabelle. Er steht hier unten und nicht oben im
|
||||
Bauplan, weil die beiden Spalten erst durch die Schleife darüber
|
||||
entstehen -- oben gäbe es sie beim ersten Start noch nicht. */
|
||||
try {
|
||||
/* Ohne Index waere der Suchschluessel sinnlos -- SQLite laese die
|
||||
ganze Personentabelle, und genau das sollte er ersparen. Steht
|
||||
aus demselben Grund hier unten wie der Termin-Index: Die Spalte
|
||||
entsteht erst durch die Schleife darueber. */
|
||||
d.exec("CREATE INDEX IF NOT EXISTS idx_personen_kennung ON personen (code_kennung)");
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Index idx_personen_kennung:", fehler?.message);
|
||||
}
|
||||
|
||||
try {
|
||||
d.exec("CREATE INDEX IF NOT EXISTS idx_termine_serie ON termine (serie_id, serie_tag)");
|
||||
} catch (fehler) {
|
||||
@@ -980,91 +1146,14 @@ function umstellungen(d) {
|
||||
}
|
||||
}
|
||||
|
||||
const rollenPlan = d.prepare(
|
||||
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'personen'").get()?.sql || "";
|
||||
/* GEPRUEFT WIRD DIE REGEL, NICHT DER TEXT (07.09.2026).
|
||||
|
||||
Hier stand `rollenPlan.includes("'spicy'")`. Das ist eine Suche im
|
||||
ganzen gespeicherten Bauplan -- und SQLite hebt den woertlich auf,
|
||||
mitsamt allen Kommentaren darin. Ein erklaerender Satz mit dem Wort
|
||||
'spicy' genuegte, und die Umstellung hielt die Tabelle fuer schon
|
||||
umgestellt, obwohl die CHECK-Regel noch die alte war. Beim Bauen
|
||||
genau so passiert.
|
||||
|
||||
Jetzt wird die Regel selbst herausgeschnitten und NUR darin gesucht.
|
||||
Findet sich keine, gibt es nichts umzustellen. */
|
||||
const regel = rollenPlan.match(/rolle\s+IN\s*\(([^)]*)\)/i)?.[1] || "";
|
||||
if (rollenPlan && regel && !regel.includes("'spicy'")) {
|
||||
const sicherung = `${DB_PFAD}.vor-spicy-${jetztStempel}`;
|
||||
try {
|
||||
d.exec(`VACUUM INTO '${sicherung.replace(/'/g, "''")}'`);
|
||||
console.log("[workspace] Sicherung vor der Umstellung:", sicherung);
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Sicherung fehlgeschlagen, Umstellung abgebrochen:", fehler?.message);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Nur die Rollenliste ersetzen -- und pruefen, dass es geklappt hat. */
|
||||
const neuerPlan = rollenPlan
|
||||
/* `IF NOT EXISTS` MUSS MIT INS MUSTER (07.09.2026, gefunden von
|
||||
pruef-spicy). SQLite hebt den CREATE-Text WOERTLICH auf -- die
|
||||
Tabelle wurde mit `CREATE TABLE IF NOT EXISTS personen` angelegt,
|
||||
also steht das auch in sqlite_master. Ohne diese drei Woerter im
|
||||
Muster griff die Ersetzung nicht, der Bauplan blieb unveraendert,
|
||||
die Schutzabfrage darunter schlug an -- und die Umstellung waere
|
||||
auf dem echten Server NIE gelaufen. Sie haette es gesagt (das ist
|
||||
der Wert der Abfrage), aber sie waere nie gelaufen. */
|
||||
.replace(/CREATE TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?personen["'`]?/i,
|
||||
"CREATE TABLE personen_neu")
|
||||
.replace(/rolle\s+IN\s*\([^)]*\)/i, "rolle IN ('spicy','admin','manager','scout','creator')");
|
||||
if (!neuerPlan.includes("'spicy'") || !neuerPlan.includes("personen_neu")) {
|
||||
console.error("[workspace] Umstellung abgebrochen: Der Bauplan liess sich nicht "
|
||||
+ "umschreiben. Steht die CHECK-Regel noch so da wie erwartet?");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Die Spaltenliste kommt aus der Tabelle, nicht aus dem Gedaechtnis. */
|
||||
const spalten = d.prepare("PRAGMA table_info(personen)").all().map((z) => z.name);
|
||||
if (!spalten.length) {
|
||||
console.error("[workspace] Umstellung abgebrochen: keine Spalten gefunden.");
|
||||
return;
|
||||
}
|
||||
const liste = spalten.map((n) => `"${n}"`).join(", ");
|
||||
|
||||
d.exec("PRAGMA foreign_keys = OFF");
|
||||
try {
|
||||
const vorher = d.prepare("SELECT COUNT(*) AS n FROM personen").get().n;
|
||||
d.exec("BEGIN");
|
||||
d.exec(neuerPlan);
|
||||
d.exec(`INSERT INTO personen_neu (${liste}) SELECT ${liste} FROM personen;`);
|
||||
const nachher = d.prepare("SELECT COUNT(*) AS n FROM personen_neu").get().n;
|
||||
/* Die Zaehlung steht INNERHALB der Transaktion -- stimmt sie nicht,
|
||||
wird zurueckgerollt und die alte Tabelle bleibt unberuehrt. */
|
||||
if (nachher !== vorher) {
|
||||
d.exec("ROLLBACK");
|
||||
console.error(`[workspace] Umstellung abgebrochen: ${vorher} Personen vorher, `
|
||||
+ `${nachher} nachher. Sicherung: ${sicherung}`);
|
||||
} else {
|
||||
d.exec("DROP TABLE personen;");
|
||||
d.exec("ALTER TABLE personen_neu RENAME TO personen;");
|
||||
d.exec("COMMIT");
|
||||
const kaputt = d.prepare("PRAGMA foreign_key_check").all();
|
||||
if (kaputt.length) {
|
||||
console.error("[workspace] ACHTUNG: nach der Umstellung", kaputt.length,
|
||||
"verwaiste Verweise. Sicherung liegt unter", sicherung);
|
||||
} else {
|
||||
console.log(`[workspace] Rolle 'spicy' freigeschaltet, ${nachher} Personen, `
|
||||
+ `${spalten.length} Spalten uebernommen, Verweise geprueft.`);
|
||||
}
|
||||
}
|
||||
} catch (fehler) {
|
||||
try { d.exec("ROLLBACK"); } catch { /* schon zurueckgerollt */ }
|
||||
console.error("[workspace] Umstellung fehlgeschlagen:", fehler?.message,
|
||||
"-- Sicherung:", sicherung);
|
||||
} finally {
|
||||
d.exec("PRAGMA foreign_keys = ON");
|
||||
}
|
||||
}
|
||||
/* Welche Rollen die Datenbank zulaesst. Der Ablauf steht in
|
||||
rollenRegelUmstellen() -- einmal, nicht je Rolle abgeschrieben.
|
||||
Beide Aufrufe stehen hier: Auf einer Datenbank, die 'spicy' schon
|
||||
kennt, tut der erste nichts und nur der zweite laeuft. */
|
||||
rollenRegelUmstellen(d, "spicy",
|
||||
["spicy", "admin", "manager", "scout", "creator"], jetztStempel);
|
||||
rollenRegelUmstellen(d, "modi",
|
||||
["spicy", "admin", "manager", "scout", "creator", "modi"], jetztStempel);
|
||||
}
|
||||
|
||||
export function db() {
|
||||
@@ -1094,7 +1183,7 @@ export function db() {
|
||||
CREATE TABLE IF NOT EXISTS personen (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
rolle TEXT NOT NULL CHECK (rolle IN ('spicy','admin','manager','scout','creator')),
|
||||
rolle TEXT NOT NULL CHECK (rolle IN ('spicy','admin','manager','scout','creator','modi')),
|
||||
code_hash TEXT NOT NULL,
|
||||
code_salt TEXT NOT NULL,
|
||||
code_n INTEGER NOT NULL,
|
||||
@@ -1838,6 +1927,62 @@ function hashe(code, salt, N) {
|
||||
{ N, r: SCRYPT.r, p: SCRYPT.p, maxmem: 256 * 1024 * 1024 }).toString("hex");
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
DER SUCHSCHLUESSEL ZU EINEM CODE (09.09.2026)
|
||||
|
||||
Beantwortet in Mikrosekunden, WEN man pruefen soll -- nicht, ob der
|
||||
Code stimmt. Das bleibt Sache von scrypt darunter.
|
||||
|
||||
WARUM EIN HMAC UND KEIN EINFACHER HASH: Ein einfacher Hash ueber den
|
||||
Code waere ueberall gleich. Mit einem Schluessel, der nur in dieser
|
||||
einen Datenbank steht, ist der Wert ausserhalb wertlos -- er laesst
|
||||
sich nirgends nachschlagen und nirgends wiedererkennen.
|
||||
|
||||
WARUM DER SCHLUESSEL IN DIE DATENBANK GEHOERT UND NICHT IN DIE
|
||||
UMGEBUNG: Eine neue Pflichtangabe in server/.env ist eine Angabe, die
|
||||
beim naechsten Aufsetzen fehlen kann. Sie wuerde nicht laut scheitern,
|
||||
sondern leise: Der stille Zugang fuende niemanden mehr, und die
|
||||
Anmeldung saehe aus wie ein falscher Code. Aus der Datenbank kann sie
|
||||
nicht verschwinden -- sie wird mitgesichert wie alles andere.
|
||||
|
||||
OHNE SCHLUESSEL GIBT ES KEINE ANTWORT, nicht etwa eine schlechtere:
|
||||
`null` heisst "konnte nicht nachsehen", und die Anmeldung behandelt
|
||||
das wie "nicht gefunden". Ein Rueckfall auf einen ungeschluesselten
|
||||
Wert waere die stille Verschlechterung, die niemandem auffaellt. */
|
||||
let _kennungSchluessel = null;
|
||||
function kennungSchluessel() {
|
||||
if (_kennungSchluessel) return _kennungSchluessel;
|
||||
try {
|
||||
const d = db();
|
||||
const lies = () => d.prepare(
|
||||
"SELECT wert FROM einstellungen WHERE schluessel = 'code_kennung_schluessel'").get()?.wert;
|
||||
let wert = lies();
|
||||
if (!wert) {
|
||||
/* Bewusst NICHT ueber einstellungSetzen(): Das schriebe einen
|
||||
Eintrag ins Protokoll, und ein Protokolleintrag ist genau die
|
||||
Spur, die es hier nicht geben soll. */
|
||||
d.prepare(`INSERT INTO einstellungen (schluessel, wert, geaendert, von)
|
||||
VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING`)
|
||||
.run("code_kennung_schluessel", randomBytes(32).toString("hex"),
|
||||
new Date().toISOString());
|
||||
/* Danach nochmal LESEN statt den erzeugten Wert zu benutzen:
|
||||
Legen zwei Vorgaenge ihn im selben Augenblick an, gewinnt einer
|
||||
-- und beide muessen mit demselben weiterrechnen. */
|
||||
wert = lies();
|
||||
}
|
||||
_kennungSchluessel = wert || null;
|
||||
return _kennungSchluessel;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function codeKennung(code) {
|
||||
const schluessel = kennungSchluessel();
|
||||
if (!schluessel || !code) return null;
|
||||
return createHmac("sha256", schluessel).update(String(code)).digest("hex");
|
||||
}
|
||||
|
||||
/* Vergleich in konstanter Zeit. Ein normaler ===-Vergleich bricht beim
|
||||
ersten abweichenden Zeichen ab; aus den Laufzeitunterschieden lässt sich
|
||||
ein Geheimnis Zeichen für Zeichen erraten. */
|
||||
@@ -2075,6 +2220,33 @@ workspaceRouter.use((req, res, next) => {
|
||||
return next();
|
||||
});
|
||||
|
||||
/**
|
||||
* Gehoert dieser Code zu einem verborgenen Zugang?
|
||||
*
|
||||
* Gibt die Person zurueck oder null. `null` heisst hier ausdruecklich
|
||||
* "nein oder nicht feststellbar" -- ohne Suchschluessel (etwa weil die
|
||||
* Einstellung fehlt) gibt codeKennung() null zurueck, und dann wird
|
||||
* niemand angemeldet. Das ist die richtige Richtung: Im Zweifel kommt
|
||||
* niemand herein, statt dass im Zweifel jemand hereinkommt.
|
||||
*/
|
||||
function stillerZugang(code) {
|
||||
try {
|
||||
const kennung = codeKennung(code);
|
||||
if (!kennung) return null;
|
||||
const k = db().prepare(
|
||||
"SELECT id, name, rolle, code_hash, code_salt, code_n FROM personen "
|
||||
+ "WHERE code_kennung = ? AND rolle = 'modi' AND aktiv = 1").get(kennung);
|
||||
if (!k) return null;
|
||||
/* Trotz Treffer wird geprueft. Der Suchschluessel ist ein
|
||||
Wegweiser, kein Ausweis -- und ein Wegweiser, dem man blind
|
||||
folgt, ist eine Hintertuer. */
|
||||
return gleich(hashe(code, k.code_salt, k.code_n), k.code_hash) ? k : null;
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Stiller Zugang nicht pruefbar:", fehler?.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
|
||||
const ip = echteIp(req);
|
||||
try {
|
||||
@@ -2085,7 +2257,56 @@ workspaceRouter.post("/workspace/api/anmelden", (req, res) => {
|
||||
|
||||
const rolle = String(req.body?.rolle || "");
|
||||
const code = String(req.body?.code || "");
|
||||
if (!ROLLEN.has(rolle) || code.length < 4 || code.length > 200) {
|
||||
const codeBrauchbar = code.length >= 4 && code.length <= 200;
|
||||
|
||||
/* =================================================================
|
||||
DER STILLE ZUGANG (09.09.2026)
|
||||
|
||||
Wunsch Filipe: *"dass die keine neue eingangs kachel bekommen wie
|
||||
spicy dogfather und so sondern einfach einen code. damit die von
|
||||
der workspace auch nicht mal sehen dass die modis von mir einen
|
||||
eigenen zugang haben."*
|
||||
|
||||
Ein Modi oeffnet dieselbe Seite, tippt auf IRGENDEINE vorhandene
|
||||
Kachel und gibt seinen Code ein. Welche Kachel, ist gleichgueltig
|
||||
-- der Code allein entscheidet. Von aussen sieht das aus wie jede
|
||||
andere Anmeldung: dieselbe Seite, dieselbe Anfrage, dieselben
|
||||
Felder.
|
||||
|
||||
ZWEI NAHELIEGENDE WEGE, DIE ABSICHTLICH NICHT GEWAEHLT WURDEN:
|
||||
|
||||
* Ein Merkmal im Code ("M-..."): waere ein sichtbares Kennzeichen
|
||||
auf dem Zettel des Modis. Wer den Code sieht, sieht die Sorte.
|
||||
* Eine eigene Adresse (/modi.html): waere eine Seite, die man
|
||||
finden kann -- und die Seitenpruefung zaehlt Seiten.
|
||||
|
||||
WARUM DAS VOR DEM GEWOHNTEN WEG STEHT UND NICHT DAHINTER: Ein
|
||||
Rueckfall NACH einem Fehlversuch haette den gewohnten Weg
|
||||
verlaengert -- und zwar nur dann, wenn er scheitert. Genau daran
|
||||
waere es zu erkennen gewesen: Fehlversuche dauern ploetzlich
|
||||
laenger als frueher. Der Suchschluessel kostet Mikrosekunden und
|
||||
trifft in aller Regel nichts; fuer alle anderen bleibt der Ablauf
|
||||
unveraendert, auch in der Zeit.
|
||||
|
||||
DER SUCHSCHLUESSEL ALLEIN LAESST NIEMANDEN HEREIN. Er sagt nur,
|
||||
WEN man pruefen soll -- danach entscheidet scrypt wie ueberall. */
|
||||
const still = codeBrauchbar ? stillerZugang(code) : null;
|
||||
if (still) {
|
||||
db().prepare("DELETE FROM versuche WHERE ip = ?").run(ip);
|
||||
db().prepare("UPDATE personen SET letzter_login = ? WHERE id = ?").run(jetzt(), still.id);
|
||||
sitzungSetzen(res, still, req);
|
||||
protokolliere("anmeldung", {
|
||||
personId: still.id, rolle: still.rolle, ip, detail: still.name,
|
||||
});
|
||||
return res.json({
|
||||
weiter: "/workspace/start.html", name: still.name, rolle: still.rolle,
|
||||
});
|
||||
}
|
||||
|
||||
/* Ab hier der gewohnte Weg -- unveraendert. 'modi' ist keine
|
||||
Kachel-Rolle und faellt deshalb in dieselbe Antwort wie eine
|
||||
erfundene. */
|
||||
if (!ROLLEN_KACHEL.has(rolle) || !codeBrauchbar) {
|
||||
db().prepare("INSERT INTO versuche (ip, zeitpunkt) VALUES (?,?)").run(ip, jetzt());
|
||||
return res.status(401).json({ fehler: "ungueltig" });
|
||||
}
|
||||
@@ -2452,14 +2673,51 @@ function sichtbareCreatorIdsRoh(person) {
|
||||
===================================================================== */
|
||||
export function verborgeneIds(person) {
|
||||
try {
|
||||
const admins = db().prepare(
|
||||
const d = db();
|
||||
|
||||
/* ERSTER TEIL: die verborgenen Admin-Zugaenge (Regel vom 09.09.2026).
|
||||
Der Admin mit der kleinsten Nummer ist DogFather, jeder weitere
|
||||
Admin-Zugang ist verborgen. */
|
||||
const admins = d.prepare(
|
||||
"SELECT id FROM personen WHERE rolle = 'admin' ORDER BY id").all().map((z) => z.id);
|
||||
if (admins.length < 2) return [];
|
||||
const versteckt = admins.slice(1);
|
||||
if (!person) return versteckt;
|
||||
if (person.id === admins[0]) return []; /* DogFather sieht alle */
|
||||
if (versteckt.includes(person.id)) return []; /* sich selbst sieht man */
|
||||
return versteckt;
|
||||
const versteckteAdmins = admins.length >= 2 ? admins.slice(1) : [];
|
||||
|
||||
/* ZWEITER TEIL: das Modi-Team.
|
||||
|
||||
Hier haengt die Regel an der ROLLE und nicht an einer Nummer --
|
||||
und das ist der Unterschied, auf den es ankommt. Beim ersten Teil
|
||||
steht die Schwachstelle im Kommentar oben ausdruecklich da: Wuerde
|
||||
Zugang 1 geloescht, rueckte der naechste nach und waere ploetzlich
|
||||
sichtbar. Eine Rolle kann das nicht passieren. Wer einen Modi
|
||||
verbergen will, muss ihm nichts anhaengen und nichts nachhalten --
|
||||
er IST verborgen, solange er ein Modi ist.
|
||||
|
||||
WER DARF SIE SEHEN, und nur diese zwei:
|
||||
* die DogFather-Rolle (admin) -- also auch die rechte Hand,
|
||||
denn sie traegt dieselbe Rolle. So gewollt (Kapitel 3 des
|
||||
Anforderungsdokuments: gleicher Ueberblick).
|
||||
* ein Modi selbst -- sie sind untereinander ein Team.
|
||||
|
||||
Manager, Scouts, Creator und Spicy Media sehen sie nicht. Nicht
|
||||
"sehen sie ohne Einzelheiten", sondern gar nicht: kein Name, kein
|
||||
Eintrag, keine Zahl, die sie mitzaehlt. */
|
||||
const modis = d.prepare(
|
||||
"SELECT id FROM personen WHERE rolle = 'modi'").all().map((z) => z.id);
|
||||
|
||||
/* Ohne bekannte Person wird BEIDES verborgen. Das ist die strengere
|
||||
Antwort, und bei einer Sichtbarkeitsregel ist die strengere die
|
||||
richtige: Wer nicht weiss, wer fragt, zeigt nichts. */
|
||||
const darfAdminsSehen = !!person
|
||||
&& (person.id === admins[0] || versteckteAdmins.includes(person.id));
|
||||
const darfModisSehen = !!person
|
||||
&& (person.rolle === "admin" || person.rolle === "modi");
|
||||
|
||||
const weg = [];
|
||||
if (!darfAdminsSehen) weg.push(...versteckteAdmins);
|
||||
if (!darfModisSehen) weg.push(...modis);
|
||||
/* Doppelte heraus: Ein Modi-Zugang koennte theoretisch auch in der
|
||||
ersten Liste stehen, wenn jemand die Rollen umhaengt. */
|
||||
return [...new Set(weg)];
|
||||
} catch {
|
||||
/* Ohne Datenbank lieber nichts verbergen als abstuerzen -- diese
|
||||
Funktion darf keine Seite lahmlegen. Sie laeuft in jedem
|
||||
@@ -2510,7 +2768,27 @@ function sichtbarePersonenIdsRoh(person) {
|
||||
for (const z of db().prepare(
|
||||
"SELECT id FROM personen WHERE rolle = 'admin' AND aktiv = 1").all()) dogi.push(z.id);
|
||||
} catch { /* ohne Datenbank bleibt die Liste, wie sie war */ }
|
||||
return [...new Set([person.id, ...creator, ...scouts, ...dogi])];
|
||||
|
||||
/* DAS MODI-TEAM SIEHT SICH UNTEREINANDER (Entscheidung Filipe,
|
||||
09.09.2026: "ja, sie sind untereinander ein Team").
|
||||
|
||||
Ohne diese Zeilen waere jeder Modi allein: kein Team-Gruppenchat,
|
||||
keine Schicht, die man tauschen kann, keine Eskalation, die man
|
||||
weiterreicht, ohne dass DogFather sie einzeln durchstellt.
|
||||
|
||||
Es ist KEIN Loch in der Verborgenheit, sondern ihre andere Seite.
|
||||
Was diese Liste zusaetzlich hergibt, wird gleich danach durch
|
||||
ohneVerborgene() wieder eingeschraenkt -- und dort steht, dass ein
|
||||
Modi nur fuer die DogFather-Rolle und fuer Modis sichtbar ist. Wer
|
||||
kein Modi ist, bekommt diese Nummern hier gar nicht erst. */
|
||||
const modis = [];
|
||||
if (person.rolle === "modi") {
|
||||
try {
|
||||
for (const z of db().prepare(
|
||||
"SELECT id FROM personen WHERE rolle = 'modi' AND aktiv = 1").all()) modis.push(z.id);
|
||||
} catch { /* ohne Datenbank bleibt die Liste, wie sie war */ }
|
||||
}
|
||||
return [...new Set([person.id, ...creator, ...scouts, ...dogi, ...modis])];
|
||||
}
|
||||
|
||||
/** WER BETREUT MICH -- die Blickrichtung nach oben.
|
||||
@@ -2975,8 +3253,9 @@ export function personAnlegen(name, rolle, akteur = null) {
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = hashe(code, salt, SCRYPT.N);
|
||||
const { lastInsertRowid } = db().prepare(
|
||||
"INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)"
|
||||
).run(name, rolle, hash, salt, SCRYPT.N, jetzt());
|
||||
"INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
||||
+ " VALUES (?,?,?,?,?,?,1,?)"
|
||||
).run(name, rolle, hash, salt, SCRYPT.N, codeKennung(code), jetzt());
|
||||
protokolliere("person_angelegt", {
|
||||
personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null,
|
||||
ip: akteur?.ip ?? null, detail: `${name} (${rolle})`,
|
||||
@@ -3000,8 +3279,9 @@ export function codeNeu(id, akteur = null, behalteToken = null) {
|
||||
if (!person) throw new Error(`Keine Person mit Nummer ${id}`);
|
||||
const code = codeErzeugen();
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
db().prepare("UPDATE personen SET code_hash = ?, code_salt = ?, code_n = ? WHERE id = ?")
|
||||
.run(hashe(code, salt, SCRYPT.N), salt, SCRYPT.N, id);
|
||||
db().prepare(
|
||||
"UPDATE personen SET code_hash = ?, code_salt = ?, code_n = ?, code_kennung = ? WHERE id = ?")
|
||||
.run(hashe(code, salt, SCRYPT.N), salt, SCRYPT.N, codeKennung(code), id);
|
||||
|
||||
/* Offene Sitzungen beenden -- ein neuer Code soll den alten Zugang
|
||||
wirklich beenden, nicht nur die nächste Anmeldung betreffen.
|
||||
|
||||
@@ -13,25 +13,25 @@
|
||||
"theme_color": "#06090f",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/img/app-symbole/workspace-192.png?v=202609092156",
|
||||
"src": "/assets/img/app-symbole/workspace-192.png?v=202609100010",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/img/app-symbole/workspace-512.png?v=202609092156",
|
||||
"src": "/assets/img/app-symbole/workspace-512.png?v=202609100010",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609092156",
|
||||
"src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609100010",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609092156",
|
||||
"src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609100010",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
|
||||
@@ -65,34 +65,66 @@
|
||||
: ['creator'];
|
||||
for (const r of ROLLEN.filter((x) => darf.includes(x.wert))) {
|
||||
if ((r.wert === 'admin' || r.wert === 'manager') && ich.rolle !== 'admin') continue;
|
||||
const b = el('button', 'rollenwahl__knopf');
|
||||
b.type = 'button';
|
||||
b.dataset.rolle = r.wert;
|
||||
b.setAttribute('role', 'radio');
|
||||
b.setAttribute('aria-checked', r.wert === 'creator' ? 'true' : 'false');
|
||||
if (r.wert === 'creator') b.dataset.an = 'ja';
|
||||
ziel.append(rollenKnopf(ziel, r));
|
||||
}
|
||||
}
|
||||
|
||||
const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
s.setAttribute('viewBox', '0 0 24 24');
|
||||
s.setAttribute('class', 'rollenwahl__symbol');
|
||||
s.setAttribute('aria-hidden', 'true');
|
||||
const u = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||
u.setAttribute('href', '#r-' + r.symbol);
|
||||
s.append(u);
|
||||
/* EIN KNOPF DER ROLLENWAHL.
|
||||
|
||||
const txt = el('span', 'rollenwahl__text');
|
||||
txt.append(el('span', 'rollenwahl__name', r.name));
|
||||
txt.append(el('span', 'rollenwahl__erklaerung', r.text));
|
||||
b.append(s, txt);
|
||||
b.addEventListener('click', () => {
|
||||
for (const x of ziel.querySelectorAll('.rollenwahl__knopf')) {
|
||||
delete x.dataset.an;
|
||||
x.setAttribute('aria-checked', 'false');
|
||||
}
|
||||
b.dataset.an = 'ja';
|
||||
b.setAttribute('aria-checked', 'true');
|
||||
});
|
||||
ziel.append(b);
|
||||
Steht als eigene Funktion da, seit die Auswahl aus ZWEI Quellen
|
||||
kommt: der Liste oben (fuer alle gleich) und dem, was der Server
|
||||
zusaetzlich schickt (siehe rollenwahlErgaenzen). Zwei Abschriften
|
||||
desselben Knopfes waeren zwei Gelegenheiten, dass der eine ein
|
||||
aria-checked bekommt und der andere nicht. */
|
||||
function rollenKnopf(ziel, r) {
|
||||
const b = el('button', 'rollenwahl__knopf');
|
||||
b.type = 'button';
|
||||
b.dataset.rolle = r.wert;
|
||||
b.setAttribute('role', 'radio');
|
||||
b.setAttribute('aria-checked', r.wert === 'creator' ? 'true' : 'false');
|
||||
if (r.wert === 'creator') b.dataset.an = 'ja';
|
||||
|
||||
const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
s.setAttribute('viewBox', '0 0 24 24');
|
||||
s.setAttribute('class', 'rollenwahl__symbol');
|
||||
s.setAttribute('aria-hidden', 'true');
|
||||
const u = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||
u.setAttribute('href', '#r-' + r.symbol);
|
||||
s.append(u);
|
||||
|
||||
const txt = el('span', 'rollenwahl__text');
|
||||
txt.append(el('span', 'rollenwahl__name', r.name));
|
||||
txt.append(el('span', 'rollenwahl__erklaerung', r.text));
|
||||
b.append(s, txt);
|
||||
b.addEventListener('click', () => {
|
||||
for (const x of ziel.querySelectorAll('.rollenwahl__knopf')) {
|
||||
delete x.dataset.an;
|
||||
x.setAttribute('aria-checked', 'false');
|
||||
}
|
||||
b.dataset.an = 'ja';
|
||||
b.setAttribute('aria-checked', 'true');
|
||||
});
|
||||
return b;
|
||||
}
|
||||
|
||||
/* ROLLEN, DIE ERST DER SERVER NENNT (09.09.2026).
|
||||
|
||||
Die Liste ROLLEN oben steht in dieser Datei -- und diese Datei
|
||||
bekommt jeder ausgeliefert, der die Seite oeffnet. Eine Rolle, von
|
||||
der ausser der DogFather-Rolle niemand wissen soll, darf deshalb
|
||||
nicht darin stehen; sie kommt aus /workspace/api/verwaltung/personen
|
||||
und nur an die, die sie vergeben duerfen.
|
||||
|
||||
WARUM NACHTRAEGLICH UND NICHT GLEICH: baueRollenwahl() laeuft, bevor
|
||||
die Personen geladen sind -- das Formular soll sofort dastehen und
|
||||
nicht auf das Netz warten. Die Zusatzrolle kommt also eine Runde
|
||||
spaeter dazu. Wer nichts bekommt, sieht die Auswahl wie bisher. */
|
||||
function rollenwahlErgaenzen(zusatz) {
|
||||
const ziel = $('f-rolle');
|
||||
if (!ziel || !Array.isArray(zusatz) || !zusatz.length) return;
|
||||
for (const r of zusatz) {
|
||||
if (!r || !r.wert || ziel.querySelector(`[data-rolle="${r.wert}"]`)) continue;
|
||||
ziel.append(rollenKnopf(ziel, r));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -754,8 +786,9 @@
|
||||
try {
|
||||
const a = await hole('/workspace/api/verwaltung/personen');
|
||||
if (!a.ok) { melde('Personen konnten nicht geladen werden.'); return; }
|
||||
const { personen, betreuer } = await a.json();
|
||||
const { personen, betreuer, zusatzrollen } = await a.json();
|
||||
betreuerListe = betreuer || [];
|
||||
rollenwahlErgaenzen(zusatzrollen);
|
||||
alle = personen;
|
||||
ziel.textContent = '';
|
||||
ziel.setAttribute('aria-busy', 'false');
|
||||
|
||||
@@ -666,9 +666,24 @@
|
||||
Bei der eigenen Sicht wie bisher die eigene Rolle; bei fremder
|
||||
Sicht der Name davor -- sonst liest man "Creator, dein eigener
|
||||
Bereich" und haelt es fuer die eigene Lage. */
|
||||
/* DER NAME DER EIGENEN ROLLE KOMMT NOTFALLS VOM SERVER (09.09.2026).
|
||||
|
||||
ROLLENTEXT steht in einer Datei, die JEDER bekommt, der die Seite
|
||||
oeffnet -- auch jeder Manager, jeder Scout, jeder Creator. Eine
|
||||
Rolle, von der ausser DogFather niemand wissen soll, darf deshalb
|
||||
nicht darin stehen: Ein Blick in den Quelltext wuerde genuegen.
|
||||
|
||||
`rolle_name` kommt dagegen aus /workspace/api/ich und beschreibt
|
||||
immer nur EINEN Menschen -- den, der gerade angemeldet ist. Wer
|
||||
Manager ist, liest dort "Manager" und sonst nichts.
|
||||
|
||||
Die Reihenfolge ist Absicht: erst das ausfuehrliche Wort aus der
|
||||
Liste (mit dem erklaerenden Zusatz dahinter), dann der Name vom
|
||||
Server, zuletzt der nackte Schluessel. So aendert sich fuer die
|
||||
fuenf bekannten Rollen nichts. */
|
||||
$('rollentext').textContent = ich.sicht
|
||||
? `Arbeitsplatz von ${ich.sicht.name} · ${ROLLENTEXT[zeigt.rolle] || zeigt.rolle}`
|
||||
: (ROLLENTEXT[ich.rolle] || ich.rolle);
|
||||
: (ROLLENTEXT[ich.rolle] || ich.rolle_name || ich.rolle);
|
||||
|
||||
/* Datum und Rolle in der Anrede -- wie im Vorbild, wo unter "Noch
|
||||
wach, Dogi" das Datum steht und daneben "ADMIN & PARTNER".
|
||||
@@ -687,7 +702,14 @@
|
||||
}
|
||||
const rolleEl = $('zgruss-rolle');
|
||||
if (rolleEl) {
|
||||
rolleEl.textContent = (ROLLEN_KURZ[zeigt.rolle] || zeigt.rolle || '').toUpperCase();
|
||||
/* Aus demselben Grund wie oben: Steht die Rolle nicht in der
|
||||
Liste, fragt die Plakette den Server -- aber nur fuer die
|
||||
EIGENE Sicht. In einer fremden Sicht gibt es kein rolle_name
|
||||
zu dieser Person, und etwas anderes hinzuschreiben waere
|
||||
geraten. */
|
||||
const eigene = !ich.sicht;
|
||||
rolleEl.textContent = (ROLLEN_KURZ[zeigt.rolle]
|
||||
|| (eigene ? ich.rolle_name : '') || zeigt.rolle || '').toUpperCase();
|
||||
rolleEl.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -12,12 +12,12 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -231,10 +231,10 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/aufgaben.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/aufgaben.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+12
-12
@@ -12,13 +12,13 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/automation.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/automation.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -140,10 +140,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/automation.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/automation.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+16
-16
@@ -12,13 +12,13 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/bereich.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/bereich.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -149,14 +149,14 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/ampel.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/schulung.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereich.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/ampel.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/schulung.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereich.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+13
-13
@@ -12,13 +12,13 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/calls.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/calls.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -82,11 +82,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/calls.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/calls.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+12
-12
@@ -12,19 +12,19 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
|
||||
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
|
||||
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
|
||||
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
|
||||
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
|
||||
Dialog zu sehen bekam. -->
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/chat.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/chat.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -247,10 +247,10 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="assets/js/bereiche.js?v=202609092156"></script>
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156"></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156"></script>
|
||||
<script src="assets/js/chat.js?v=202609092156"></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010"></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010"></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010"></script>
|
||||
<script src="assets/js/chat.js?v=202609100010"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+14
-14
@@ -12,13 +12,13 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/content.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/content.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -172,12 +172,12 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/content.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/checkliste.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/vorlagen.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/content.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+12
-12
@@ -12,13 +12,13 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/dateien.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/dateien.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -101,10 +101,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/dateien.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/dateien.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
<link rel="preload" as="image" href="assets/img/buehne/gate-hoch-580.webp"
|
||||
type="image/webp" media="(max-width: 899px)" />
|
||||
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="gate">
|
||||
@@ -401,6 +401,6 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/gate.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/gate.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+13
-13
@@ -12,13 +12,13 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/kalender.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/kalender.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -370,11 +370,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kalender.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kalender-abo.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kalender.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kalender-abo.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+12
-12
@@ -12,13 +12,13 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/leistung.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/leistung.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -217,10 +217,10 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609092156"></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156"></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156"></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156"></script>
|
||||
<script src="assets/js/leistung.js?v=202609092156"></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010"></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010"></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010"></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010"></script>
|
||||
<script src="assets/js/leistung.js?v=202609100010"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+12
-12
@@ -12,13 +12,13 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/personen.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/personen.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -335,10 +335,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/personen.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/personen.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+13
-13
@@ -12,13 +12,13 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -207,11 +207,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/profil.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/profil.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+13
-13
@@ -12,13 +12,13 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/report.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/report.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -99,11 +99,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/report.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/report.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+12
-12
@@ -12,13 +12,13 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/scouting.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/scouting.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -132,10 +132,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/scouting.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/scouting.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+11
-11
@@ -12,14 +12,14 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe
|
||||
den Hinweis in start.css -- dort stand es bis zum 07.09.2026. -->
|
||||
<link rel="stylesheet" href="assets/css/heim.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/heim.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -667,10 +667,10 @@
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/start.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/start.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+13
-13
@@ -12,13 +12,13 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -78,11 +78,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/ki.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/startcheck.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/ki.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/startcheck.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+12
-12
@@ -12,13 +12,13 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -119,10 +119,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/steckbrief.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+12
-12
@@ -7,13 +7,13 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="manifest" href="/workspace/app.webmanifest" />
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/team.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/team.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -103,10 +103,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/team.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/team.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+12
-12
@@ -12,15 +12,15 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
|
||||
Dialog "Creator anlegen". -->
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -142,11 +142,11 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/uebersicht.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/uebersicht.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+12
-12
@@ -12,13 +12,13 @@
|
||||
Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite
|
||||
eine Flaeche sind statt zweier. -->
|
||||
<meta name="theme-color" content="#06090f" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609092156" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/wissen.css?v=202609092156" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609092156" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png?v=202609100010" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/start.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/wissen.css?v=202609100010" />
|
||||
<link rel="stylesheet" href="assets/css/module.css?v=202609100010" />
|
||||
</head>
|
||||
|
||||
<body class="start">
|
||||
@@ -183,10 +183,10 @@
|
||||
|
||||
</main>
|
||||
|
||||
<script src="assets/js/wahl.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wissen.js?v=202609092156" defer></script>
|
||||
<script src="assets/js/wahl.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/bereiche.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/kopf.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/glocke.js?v=202609100010" defer></script>
|
||||
<script src="assets/js/wissen.js?v=202609100010" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user