Manager sehen nur noch ihre zugeteilten Scouts -- und deren Creator

Wunsch vom 01.09.2026: "er soll nur die Scouts sehen, die ihm zugeteilt
sind." Dazu entschieden: Ein Manager sieht dann AUCH die Creator dieser
Scouts, und zuteilen darf NUR DogFather.

WAS VORHER WAR. Ein Manager sah die gesamte Scout-Pipeline -- alle Leads
aller Scouts, dazu dieselben in der Suche und in "Was ist dran". Das war
die letzte Stelle, an der "nur DogFather sieht alles" noch nicht galt.

EINE EIGENE TABELLE, KEIN ZWEITER EINTRAG IN `betreuung`.
Dort heisst die Spalte creator_id, und der ganze uebrige Code liest sie
als "das ist ein Creator". Ein Scout darin waere technisch moeglich und
fachlich eine Luege gewesen: betreuteIds() gaebe Scout-Nummern zurueck,
die anderswo als Creator behandelt wuerden. Solche Abkuerzungen raechen
sich genau dann, wenn niemand mehr weiss, dass sie getroffen wurden.

DIE KETTE steht an EINER Stelle (betreuteIds in workspace.js):
Manager -> seine Scouts -> deren Creator. Ohne sie muesste jeder Creator
einem Manager einzeln zugewiesen werden, und beim ersten vergessenen
faende er ein Loch in seiner Uebersicht, ohne zu merken, dass es eines
ist. Dieselbe Regel gilt fuer Leads in Pipeline, Suche und Hinweisen --
aus einer Quelle (pipelineIds), nicht dreimal abgeschrieben.

ZUTEILEN DARF NUR DOGFATHER, und das ist keine Foermlichkeit: Diese
Zuteilung ERWEITERT die Sicht eines Managers. Duerfte er sie selbst
setzen, koennte er sich seine eigene Sichtbarkeit vergeben -- eine
Grenze, die der Begrenzte selbst verschieben kann, ist keine. Deshalb
istDogFather und ausdruecklich NICHT istLeitung.

In der Personenliste steht bei jedem Scout "Gehoert zu", sichtbar nur
fuer DogFather. Der Leerwert heisst "— direkt bei DogFather —" und nicht
"— niemand —": Ein Scout ohne Manager ist nicht unbetreut, er haengt an
DogFather. Das ist ein Zustand, kein Mangel.

NEBENBEI KORRIGIERT: In der Zustaendigkeits-Route stand als Kommentar
noch "ein Eintrag auf DogFather oder Manager aendert an den Rechten
nichts, die Leitung sieht ohnehin jeden Creator". Das gilt seit heute
nicht mehr -- fuer einen Manager entscheidet dieser Eintrag jetzt sehr
wohl ueber die Sichtbarkeit. Ein falscher Kommentar ist schlimmer als
keiner: Er wird geglaubt.

DIE PRUEFUNG STELLT DEN MISSBRAUCH AN DEN ANFANG. Diese Aenderung
erweitert Sichtbarkeit -- alles andere heute hat sie eingeschraenkt. Ein
Fehler dort zeigt jemandem zu wenig und faellt auf; ein Fehler hier
zeigt zu viel und faellt niemandem auf. Geprueft wird deshalb: Manager
und Scout duerfen nicht zuteilen (404, und es wird auch wirklich nichts
geschrieben), Scout an Scout und Scout an DogFather werden abgewiesen,
ein Creator laesst sich nicht zuteilen. Danach die Kette in beide
Richtungen, das Zuruecknehmen, und dass ein Scout zu genau einem Manager
gehoert.

Ein lehrreicher Fehlschlag in der Pruefung selbst: Sie meldete, die
Auswahl fehle in der Oberflaeche. Sie fehlte nicht -- die Personenliste
ist nach Rollen ZUGEKLAPPT, und die Pruefung hatte nicht aufgeklappt.
Sah aus wie ein Produktfehler, war einer der Pruefung. Sie klappt jetzt
auf und stellt vorher fest, dass wirklich alle sieben Zeilen dastehen.

Gesamtlauf: 30 von 30 Dateien, 1210 von 1210 Einzelpunkten.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-01 22:44:30 +02:00
co-authored by Claude Opus 5
parent 15cfee1078
commit 9e523d3ea2
44 changed files with 739 additions and 154 deletions
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 984 KiB

After

Width:  |  Height:  |  Size: 984 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 921 KiB

After

Width:  |  Height:  |  Size: 921 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 757 KiB

After

Width:  |  Height:  |  Size: 761 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 890 KiB

After

Width:  |  Height:  |  Size: 890 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 858 KiB

After

Width:  |  Height:  |  Size: 859 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1013 KiB

After

Width:  |  Height:  |  Size: 1014 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 835 KiB

After

Width:  |  Height:  |  Size: 816 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 924 KiB

After

Width:  |  Height:  |  Size: 924 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 324 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

+335
View File
@@ -0,0 +1,335 @@
/* Prueft die Zuteilung Manager -> Scouts.
Wunsch vom 01.09.2026: "er soll nur die Scouts sehen, die ihm
zugeteilt sind." Dazu entschieden: Ein Manager sieht dann AUCH die
Creator dieser Scouts ("ja, alles seiner Scouts"), und zuteilen darf
NUR DogFather.
WARUM DAS DIE HEIKELSTE AENDERUNG DES TAGES IST.
Diese Zuteilung ERWEITERT Sichtbarkeit. Alles andere heute hat sie
eingeschraenkt -- ein Fehler dort zeigt jemandem zu wenig, und das
faellt auf. Ein Fehler HIER zeigt jemandem zu viel, und das faellt
niemandem auf. Deshalb steht der Missbrauch am Anfang:
Darf ein Manager sich selbst Scouts zuteilen? -> muss 404 sein
Darf ein Scout das? -> muss 404 sein
Kann man einen Scout an einen Scout haengen? -> muss 400 sein
Kann man einen Creator zuteilen? -> muss 404 sein
Und danach die Kette in beide Richtungen: Was der Manager sehen SOLL,
sieht er -- und was er nicht sehen soll, sieht er nicht. */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ordner = mkdtempSync(join(tmpdir(), "ws-zut-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = "4217";
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 }); };
await import("./index.js");
await new Promise((r) => setTimeout(r, 700));
const BASIS = "http://127.0.0.1:4217";
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.();
let fehler = 0;
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
for (let i = 0; i < 60; i++) {
try {
const p = new DatabaseSync(process.env.WORKSPACE_DB);
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
} catch { await new Promise((r) => setTimeout(r, 250)); }
}
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
const heute = jetzt.slice(0, 10);
function anlegen(name, rolle, code) {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768, jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
const idMara = anlegen("Mara", "manager", "CODE-MANA-0001");
const idNina = anlegen("Nina", "manager", "CODE-MANB-0001");
const idPat = anlegen("Patrick", "scout", "CODE-SCOU-0001");
const idBen = anlegen("Ben", "scout", "CODE-SCOB-0001");
const idTili = anlegen("Tili", "creator", "CODE-ANNA-0001");
const idLuna = anlegen("Luna", "creator", "CODE-LUNA-0001");
/* Patrick betreut Tili, Ben betreut Luna. Noch KEINE Scout-Zuteilung --
die wird gleich ueber die Schnittstelle gesetzt, damit auch der Weg
dorthin geprueft ist und nicht nur die Datenbank. */
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idTili, idPat, jetzt);
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idBen, jetzt);
const lead = (name, scout) =>
d.prepare(`INSERT INTO leads (name, plattform, handle, status, prioritaet, scout_id, erstellt)
VALUES (?,?,?,?,?,?,?)`)
.run(name, "tiktok", "@" + name.toLowerCase(), "neu", "mittel", scout, jetzt);
lead("PATRICK-LEAD", idPat);
lead("BEN-LEAD", idBen);
const aufgabe = (titel, creator, verant) =>
d.prepare(`INSERT INTO aufgaben (titel, beschreibung, status, prioritaet,
creator_id, verantwortlich_id, erstellt, erstellt_von) VALUES (?,?,?,?,?,?,?,?)`)
.run(titel, "", "offen", "mittel", creator, verant, jetzt, idDogi);
aufgabe("TILI-AUFGABE", idTili, idPat);
aufgabe("LUNA-AUFGABE", idLuna, idBen);
d.close();
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 }),
});
return (a.headers.getSetCookie?.() || []).map((x) => x.split(";")[0]).join("; ");
}
const setzen = (scoutId, managerId, keks) =>
fetch(`${BASIS}/workspace/api/verwaltung/scout-zuteilung/${scoutId}`, {
method: "PUT",
headers: { cookie: keks, "Content-Type": "application/json", origin: BASIS },
body: JSON.stringify({ manager_id: managerId }),
});
const holen = async (pfad, keks) => {
const a = await fetch(BASIS + pfad, { headers: { cookie: keks } });
return { status: a.status, daten: a.ok ? await a.json() : null };
};
const kDogi = await anmelden("admin", "CODE-DOGI-0001");
const kMara = await anmelden("manager", "CODE-MANA-0001");
const kNina = await anmelden("manager", "CODE-MANB-0001");
const kPat = await anmelden("scout", "CODE-SCOU-0001");
/* ================================================================
1. ZUERST DER MISSBRAUCH
================================================================ */
console.log("\n=== Nur DogFather darf zuteilen");
{
const a = await setzen(idPat, idMara, kMara);
ok(a.status === 404,
`ein Manager kann sich NICHT selbst einen Scout zuteilen (${a.status}, 404 erwartet)`);
/* 404 und nicht 403: Wer es nicht darf, soll nicht einmal erfahren,
dass es diesen Weg gibt. */
const b = await setzen(idPat, idMara, kPat);
ok(b.status === 404, `ein Scout auch nicht (${b.status})`);
/* Und wirklich nichts passiert? Eine abgewiesene Anfrage, die trotzdem
schreibt, waere der schlimmste Fall -- deshalb wird nachgesehen. */
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
const n = d2.prepare("SELECT COUNT(*) AS n FROM scout_zuteilung").get().n;
d2.close();
ok(n === 0, `und es wurde auch nichts geschrieben (${n} Zuteilungen)`);
}
console.log("\n=== Unsinnige Zuteilungen");
{
const a = await setzen(idPat, idBen, kDogi);
ok(a.status === 400, `Scout an Scout geht nicht (${a.status})`);
const b = await setzen(idPat, idDogi, kDogi);
ok(b.status === 400,
`Scout an DogFather geht nicht (${b.status}) – er sieht ohnehin alles, `
+ "und ein Eintrag ohne Wirkung wird spaeter fuer eine Wirkung gehalten");
const c = await setzen(idTili, idMara, kDogi);
ok(c.status === 404, `einen Creator kann man nicht zuteilen (${c.status})`);
const e = await setzen(999999, idMara, kDogi);
ok(e.status === 404, `eine erfundene Nummer ebenfalls nicht (${e.status})`);
}
/* ================================================================
2. DIE KETTE: Manager -> Scout -> dessen Creator
================================================================ */
console.log("\n=== Vorher: Mara sieht nichts von Patrick");
{
const l = await holen("/workspace/api/leads", kMara);
ok((l.daten?.leads || []).length === 0,
`ohne Zuteilung keine fremden Leads (${(l.daten?.leads || []).length})`);
const a = await holen("/workspace/api/aufgaben", kMara);
ok((a.daten?.aufgaben || []).length === 0,
`und keine fremden Aufgaben (${(a.daten?.aufgaben || []).length})`);
}
console.log("\n=== DogFather teilt Patrick an Mara zu");
{
const a = await setzen(idPat, idMara, kDogi);
ok(a.status === 200, `die Zuteilung geht durch (${a.status})`);
const l = await holen("/workspace/api/leads", kMara);
const namen = (l.daten?.leads || []).map((x) => x.name).sort();
ok(namen.length === 1 && namen[0] === "PATRICK-LEAD",
`Mara sieht jetzt Patricks Lead (${namen.join(", ")})`);
ok(!namen.includes("BEN-LEAD"),
" und NICHT den des anderen Scouts");
/* DIE KETTE -- der eigentliche Punkt der Entscheidung: Sie sieht auch
den Creator, den Patrick betreut, obwohl er ihr nie direkt zugeteilt
wurde. */
const auf = await holen("/workspace/api/aufgaben", kMara);
const titel = (auf.daten?.aufgaben || []).map((x) => x.titel).sort();
ok(titel.length === 1 && titel[0] === "TILI-AUFGABE",
`und die Aufgaben von Patricks Creator (${titel.join(", ")})`);
ok(!titel.includes("LUNA-AUFGABE"),
" aber nicht die des Creators vom anderen Scout");
}
console.log("\n=== Die zweite Managerin sieht davon nichts");
{
const l = await holen("/workspace/api/leads", kNina);
const a = await holen("/workspace/api/aufgaben", kNina);
ok((l.daten?.leads || []).length === 0 && (a.daten?.aufgaben || []).length === 0,
`Nina sieht weiterhin nichts (${(l.daten?.leads || []).length} Leads, `
+ `${(a.daten?.aufgaben || []).length} Aufgaben) – eine Zuteilung gilt nur fuer den, `
+ "dem zugeteilt wurde");
}
console.log("\n=== Auch Suche und Hinweise ziehen mit");
{
/* Ein Hinweis oder Suchtreffer, der mehr verraet als die Seite
dahinter, waere ein Leck -- deshalb dieselbe Regel, aus derselben
Quelle. */
const s = await holen("/workspace/api/suche?q=LEAD", kMara);
const treffer = (s.daten?.gruppen || []).flatMap((g) => g.treffer.map((t) => t.titel || t.name));
ok(!treffer.some((t) => String(t).includes("BEN")),
`die Suche zeigt Mara keinen fremden Lead (${treffer.join(", ") || "nichts"})`);
const sN = await holen("/workspace/api/suche?q=LEAD", kNina);
const trefferN = (sN.daten?.gruppen || []).flatMap((g) => g.treffer.map((t) => t.titel || t.name));
ok(trefferN.length === 0,
`und Nina gar keinen (${trefferN.join(", ") || "nichts"})`);
}
/* ================================================================
3. ZURUECKNEHMEN -- und die Sicht geht wirklich wieder weg
================================================================ */
console.log("\n=== Zuruecknehmen");
{
const a = await setzen(idPat, null, kDogi);
ok(a.status === 200, `die Zuteilung laesst sich loesen (${a.status})`);
const l = await holen("/workspace/api/leads", kMara);
const auf = await holen("/workspace/api/aufgaben", kMara);
ok((l.daten?.leads || []).length === 0 && (auf.daten?.aufgaben || []).length === 0,
`und Mara sieht danach wieder nichts (${(l.daten?.leads || []).length} Leads, `
+ `${(auf.daten?.aufgaben || []).length} Aufgaben)`);
}
/* Ein Scout gehoert zu HOECHSTENS einem Manager -- eine zweite Zuteilung
ersetzt die erste, sie kommt nicht dazu. Sonst waere "wer ist
zustaendig?" eine Frage ohne Antwort. */
console.log("\n=== Ein Scout gehoert zu genau einem Manager");
{
await setzen(idPat, idMara, kDogi);
await setzen(idPat, idNina, kDogi);
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
const z = d2.prepare("SELECT COUNT(*) AS n FROM scout_zuteilung WHERE scout_id = ?").get(idPat);
const wer = d2.prepare("SELECT manager_id FROM scout_zuteilung WHERE scout_id = ?").get(idPat);
d2.close();
ok(z.n === 1, `nach zwei Zuteilungen genau ein Eintrag (${z.n})`);
ok(wer.manager_id === idNina, " und zwar der zuletzt gesetzte");
const lMara = await holen("/workspace/api/leads", kMara);
ok((lMara.daten?.leads || []).length === 0,
`die erste Managerin sieht Patricks Lead nicht mehr (${(lMara.daten?.leads || []).length})`);
}
/* ================================================================
4. IM BROWSER: die Auswahl steht da -- und nur bei DogFather
================================================================ */
console.log("\n=== Die Auswahl in der Personenliste");
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
async function seiteAls(rolle, code) {
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1100 } });
const konsole = [];
const seite = await kontext.newPage();
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
await seite.click(`.rolle[data-rolle="${rolle}"]`);
await seite.fill("#code", code);
await seite.click("button[type=submit]");
await seite.waitForURL("**/start.html", { timeout: 15000 });
await seite.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" });
await seite.waitForTimeout(1200);
/* DIE LISTE IST NACH ROLLEN ZUGEKLAPPT -- offen ist nur die eigene
Gruppe. Der erste Anlauf dieser Pruefung mass deshalb genau eine
Zeile ("Filipe (du)") und meldete, die Auswahl fehle. Sie fehlte
nicht, sie war nur zugeklappt.
Ein lehrreicher Fehlschlag: Er sah aus wie ein Produktfehler und war
einer der Pruefung. Wer ihn ungeprueft geglaubt haette, haette am
falschen Ende repariert. */
await seite.click("#rest-schalter").catch(() => {});
await seite.waitForTimeout(600);
return { kontext, seite, konsole };
}
const bDogi = await seiteAls("admin", "CODE-DOGI-0001");
const r = await bDogi.seite.evaluate(() => ({
personen: document.querySelectorAll(".person").length,
/* Die Auswahl "Gehört zu" -- eine je Scout. */
wahlen: [...document.querySelectorAll('select[id^="manager-"]')].map((s) => ({
id: s.id,
wert: s.value,
erste: s.querySelector("option")?.textContent,
anzahl: s.querySelectorAll("option").length,
})),
/* Die Zeilen sollen es auch in Worten sagen. */
texte: [...document.querySelectorAll(".person__zeile")].map((z) => z.textContent),
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
}));
ok(r.personen === 7,
`aufgeklappt stehen alle sieben Personen da (${r.personen}) – `
+ "sonst misst die Pruefung eine zugeklappte Liste");
ok(r.wahlen.length === 2, `zwei Scouts, zwei Auswahlfelder (${r.wahlen.length})`);
ok(/direkt bei DogFather/.test(r.wahlen[0]?.erste || ""),
`der Leer-Wert heisst, was er ist ("${r.wahlen[0]?.erste}") – `
+ '"niemand" waere falsch, ein Scout ohne Manager haengt an dir');
ok(r.wahlen.some((w) => w.wert === String(idNina)),
"die bestehende Zuteilung ist vorausgewaehlt");
ok(r.texte.some((t) => /gehört zu Nina/.test(t)),
"und steht auch in Worten in der Zeile");
ok(r.texte.some((t) => /1 Scout zugeteilt/.test(t)),
"beim Manager steht die Zahl seiner Scouts");
ok(r.ueber <= 0, `nichts steht ueber (${r.ueber}px)`);
ok(bDogi.konsole.length === 0,
`keine Fehler in der Konsole${bDogi.konsole.length ? " -> " + bDogi.konsole.join(" | ") : ""}`);
await bDogi.seite.screenshot({ path: "pruef-scout-zuteilung.png", fullPage: false });
await bDogi.kontext.close();
/* GEGENPROBE: Ein Manager darf die Personenliste sehen -- aber diese
Auswahl nicht. */
const bMara = await seiteAls("manager", "CODE-MANA-0001");
const rM = await bMara.seite.evaluate(() => ({
wahlen: document.querySelectorAll('select[id^="manager-"]').length,
personen: document.querySelectorAll(".person").length,
}));
ok(rM.personen >= 7,
`ein Manager sieht die ganze Personenliste (${rM.personen} Zeilen) – `
+ "die Personenverwaltung ist ihm nicht genommen worden");
ok(rM.wahlen === 0,
`Gegenprobe: aber KEINE Zuteilungs-Auswahl (${rM.wahlen}) – er koennte sich sonst `
+ "seine eigene Sichtbarkeit vergeben");
await bMara.kontext.close();
await browser.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
console.log(fehler ? `\n${fehler} Prüfung(en) fehlgeschlagen.` : "\nAlles in Ordnung.");
process.exit(fehler ? 1 : 0);
+11 -4
View File
@@ -24,7 +24,7 @@
import express from "express";
import {
db, sitzungLesen, betreuteIds, istLeitung,
db, sitzungLesen, betreuteIds, istLeitung, istDogFather, pipelineIds,
} from "./workspace.js";
import { sichtbar as sichtbarAufgaben } from "./workspace-aufgaben.js";
import { sichtbar as sichtbarTermine } from "./workspace-kalender.js";
@@ -204,9 +204,16 @@ hinweisRouter.get("/workspace/api/hinweise", (req, res) => {
}
/* ---------- Scout-Pipeline ---------- */
if (istLeitung(person) || person.rolle === "scout") {
const nur = istLeitung(person) ? "1=1" : "l.scout_id = ?";
const werte = istLeitung(person) ? [] : [person.id];
/* NUR DogFather sieht alle Leads (01.09.2026). Ein Scout seine
eigenen, ein Manager seine eigenen und die seiner zugeteilten
Scouts -- dieselbe Regel wie in der Pipeline selbst, aus
derselben Quelle (pipelineIds in workspace.js). Ein Hinweis, der
mehr verraet als die Seite dahinter, waere ein Leck. */
if (istDogFather(person) || person.rolle === "scout" || person.rolle === "manager") {
const ids = istDogFather(person) ? null : pipelineIds(person);
const nur = ids === null ? "1=1"
: ids.length ? `l.scout_id IN (${ids.map(() => "?").join(",")})` : "0=1";
const werte = ids === null ? [] : ids;
dazu("followup_faellig", "warnung",
"Follow-up fällig", "scouting.html",
zaehle(`SELECT COUNT(*) n FROM leads l
+79 -7
View File
@@ -15,7 +15,7 @@
import express from "express";
import {
db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, personSperren, betreuungSetzen, istLeitung, istDogFather, ROLLEN_SORTIERUNG, ROLLEN_REIHE,
db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, ROLLEN_SORTIERUNG, ROLLEN_REIHE,
} from "./workspace.js";
import { sicherungJetzt } from "./workspace-sicherung.js";
@@ -98,9 +98,18 @@ personenRouter.get("/workspace/api/verwaltung/personen", (req, res) => {
AND a.status <> 'erledigt') AS offene_aufgaben,
b.betreuer_id,
(SELECT name FROM personen x WHERE x.id = b.betreuer_id) AS betreuer_name,
(SELECT COUNT(*) FROM betreuung y WHERE y.betreuer_id = p.id) AS betreut_anzahl
(SELECT COUNT(*) FROM betreuung y WHERE y.betreuer_id = p.id) AS betreut_anzahl,
/* Manager -> Scouts (01.09.2026). Bei einem Scout steht
hier, wem er zugeteilt ist; bei einem Manager, wie viele
Scouts an ihm haengen. Beides aus derselben Abfrage --
eine zweite waere eine zweite Gelegenheit, dass die
Liste etwas anderes sagt als die Rechte. */
sz.manager_id,
(SELECT name FROM personen m WHERE m.id = sz.manager_id) AS manager_name,
(SELECT COUNT(*) FROM scout_zuteilung z WHERE z.manager_id = p.id) AS scouts_anzahl
FROM personen p
LEFT JOIN betreuung b ON b.creator_id = p.id
LEFT JOIN scout_zuteilung sz ON sz.scout_id = p.id
ORDER BY ${ROLLEN_SORTIERUNG.replace("rolle", "p.rolle")}, p.aktiv DESC, p.name`).all(),
/* Wer ueberhaupt als zustaendig eingetragen werden kann.
@@ -151,11 +160,17 @@ personenRouter.put("/workspace/api/verwaltung/betreuung/:id", gleicheHerkunft, (
/* DogFather, Manager und Scouts. Ein Creator kann nicht fuer einen
anderen zustaendig sein -- das waere eine Rolle, die es nicht gibt.
WICHTIG: Ein Eintrag auf DogFather oder Manager aendert an den
RECHTEN nichts. Die Leitung sieht ohnehin jeden Creator; der
Eintrag sagt nur, WER sich kuemmert. Nur beim Scout entscheidet er
zusaetzlich ueber die Sichtbarkeit -- deshalb steht die Regel
dafuer unveraendert in betreuteIds(). */
ACHTUNG, SEIT DEM 01.09.2026 ANDERS: Hier stand frueher "ein
Eintrag auf DogFather oder Manager aendert an den Rechten nichts,
die Leitung sieht ohnehin jeden Creator". Das gilt nicht mehr --
nur noch DogFather sieht alles. Fuer einen MANAGER entscheidet
dieser Eintrag jetzt genauso ueber die Sichtbarkeit wie fuer einen
Scout: Wer ihm nicht zugeteilt ist (weder direkt noch ueber einen
seiner Scouts), taucht bei ihm nicht auf.
Nur bei DogFather sagt der Eintrag weiterhin bloss, WER sich
kuemmert. Die Regel dazu steht an genau einer Stelle:
betreuteIds() in workspace.js. */
if (!betreuer || !["admin", "manager", "scout"].includes(betreuer.rolle)) {
return res.status(400).json({
fehler: "Zuständig können nur aktive DogFather, Manager oder Scouts sein.",
@@ -170,6 +185,63 @@ personenRouter.put("/workspace/api/verwaltung/betreuung/:id", gleicheHerkunft, (
}
});
/* ---------- Welcher Scout gehoert zu welchem Manager --------------------
"er soll nur die Scouts sehen, die ihm zugeteilt sind." (01.09.2026)
NUR DOGFATHER darf das setzen -- ausdruecklich so entschieden, und
der Grund ist nicht Misstrauen, sondern Bauart: Diese Zuteilung
ERWEITERT die Sicht eines Managers (auf die Leads seiner Scouts und
auf deren Creator). Duerfte er sie selbst setzen, koennte er sich
seine eigene Sichtbarkeit vergeben -- und eine Grenze, die der
Begrenzte selbst verschieben kann, ist keine.
Deshalb istDogFather und NICHT istLeitung. Der Unterschied ist hier
der ganze Punkt. */
personenRouter.put("/workspace/api/verwaltung/scout-zuteilung/:id", gleicheHerkunft, (req, res) => {
try {
if (!istDogFather(req.person)) {
/* 404 und nicht 403: Wer es nicht darf, soll nicht einmal
erfahren, dass es diesen Weg gibt. */
return res.status(404).json({ fehler: "nicht_gefunden" });
}
const id = Number(req.params.id);
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
const scout = db().prepare("SELECT id, rolle FROM personen WHERE id = ?").get(id);
if (!scout || scout.rolle !== "scout") {
return res.status(404).json({ fehler: "nicht_gefunden" });
}
const w = req.body?.manager_id;
if (w === null || w === "" || w === undefined) {
scoutZuteilungSetzen(id, null, req.person.id);
protokolliere("scout_zuteilung", req.person, echteIp(req), `${id}: geloest`);
return res.json({ ok: true, manager_id: null });
}
const z = Number(w);
if (!Number.isInteger(z) || z < 1) return res.status(400).json({ fehler: "Ungültige Auswahl." });
const manager = db().prepare(
"SELECT id, name, rolle FROM personen WHERE id = ? AND aktiv = 1").get(z);
/* NUR ein Manager. Ein Scout unter einem Scout waere eine Ordnung,
die es nicht gibt; ein Scout unter DogFather ist ueberfluessig, da
er ohnehin alles sieht -- und ein Eintrag, der nichts bewirkt,
wird beim naechsten Mal fuer eine Wirkung gehalten. */
if (!manager || manager.rolle !== "manager") {
return res.status(400).json({ fehler: "Zuteilen kann man nur an einen aktiven Manager." });
}
scoutZuteilungSetzen(id, z, req.person.id);
protokolliere("scout_zuteilung", req.person, echteIp(req), `${id} -> ${z}`);
res.json({ ok: true, manager_id: z, manager_name: manager.name });
} catch (fehler) {
console.error("[workspace] Scout-Zuteilung:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/* ---------- Anlegen ----------------------------------------------------- */
personenRouter.post("/workspace/api/verwaltung/personen", gleicheHerkunft, (req, res) => {
+17 -3
View File
@@ -17,7 +17,7 @@
import express from "express";
import {
db, protokolliere, echteIp, sitzungLesen, personAnlegen, betreuungSetzen, istLeitung,
db, protokolliere, echteIp, sitzungLesen, personAnlegen, betreuungSetzen, istLeitung, istDogFather, pipelineIds,
} from "./workspace.js";
export const scoutRouter = express.Router();
@@ -57,9 +57,23 @@ function gleicheHerkunft(req, res, next) {
scoutRouter.use("/workspace/api/leads", angemeldet);
/* Wessen Pipeline sieht diese Person?
NUR DogFather sieht alle (01.09.2026). Ein Scout sieht seine
eigenen Leads, ein Manager seine eigenen UND die der Scouts, die
ihm zugeteilt sind: "er soll nur die Scouts sehen, die ihm
zugeteilt sind."
Vorher stand hier istLeitung() -- damit sah jeder Manager jeden
Lead jedes Scouts. pipelineIds() steht in workspace.js und wird von
drei Stellen benutzt (Liste, Suche, Hinweise); dieselbe Regel
dreimal abgeschrieben waere dreimal Gelegenheit, sie einmal falsch
zu aendern. */
function sichtbar(person) {
if (istLeitung(person)) return { wo: "1=1", werte: [] };
return { wo: "l.scout_id = ?", werte: [person.id] };
if (istDogFather(person)) return { wo: "1=1", werte: [] };
const ids = pipelineIds(person);
if (!ids.length) return { wo: "0=1", werte: [] };
return { wo: `l.scout_id IN (${ids.map(() => "?").join(",")})`, werte: ids };
}
const SPALTEN = `
+11 -4
View File
@@ -24,7 +24,7 @@
import express from "express";
import {
db, sitzungLesen, betreuteIds, istLeitung,
db, sitzungLesen, betreuteIds, istLeitung, istDogFather, pipelineIds,
} from "./workspace.js";
import { sichtbar as sichtbarAufgaben } from "./workspace-aufgaben.js";
import { sichtbar as sichtbarTermine } from "./workspace-kalender.js";
@@ -186,9 +186,16 @@ sucheRouter.get("/workspace/api/suche", (req, res) => {
}
/* ---------- Scout-Pipeline ---------- */
if (istLeitung(person) || person.rolle === "scout") {
const nur = istLeitung(person) ? "1=1" : "l.scout_id = ?";
const werte = istLeitung(person) ? [] : [person.id];
/* NUR DogFather sieht alle Leads (01.09.2026). Ein Scout seine
eigenen, ein Manager seine eigenen und die seiner zugeteilten
Scouts -- dieselbe Regel wie in der Pipeline selbst, aus
derselben Quelle (pipelineIds in workspace.js). Ein Hinweis, der
mehr verraet als die Seite dahinter, waere ein Leck. */
if (istDogFather(person) || person.rolle === "scout" || person.rolle === "manager") {
const ids = istDogFather(person) ? null : pipelineIds(person);
const nur = ids === null ? "1=1"
: ids.length ? `l.scout_id IN (${ids.map(() => "?").join(",")})` : "0=1";
const werte = ids === null ? [] : ids;
quelle("Scout-Pipeline", `
SELECT l.id, l.name, l.plattform, l.handle, l.status, l.notizen, l.potenzial, l.aktivitaet
FROM leads l
+91 -1
View File
@@ -553,6 +553,34 @@ export function db() {
);
CREATE INDEX IF NOT EXISTS idx_betreuung_betreuer ON betreuung (betreuer_id);
/* Welcher Scout gehoert zu welchem Manager (01.09.2026).
"er soll nur die Scouts sehen, die ihm zugeteilt sind."
WARUM EINE EIGENE TABELLE und nicht ein zweiter Eintrag in
betreuung: Dort heisst die Spalte creator_id, und der ganze
uebrige Code liest sie als "das ist ein Creator". Ein Scout in
dieser Spalte waere technisch moeglich und fachlich eine Luege --
betreuteIds() gaebe dann Scout-Nummern zurueck, die anderswo als
Creator behandelt wuerden. Solche Abkuerzungen raechen sich
genau dann, wenn niemand mehr weiss, dass sie getroffen wurden.
Ein Scout gehoert zu hoechstens EINEM Manager -- deshalb ist
scout_id der Schluessel. Eine Zuteilung an mehrere waere die
Frage "wer ist zustaendig?" ohne Antwort.
ON DELETE CASCADE auf beiden Seiten: Verschwindet der Scout oder
der Manager, ist die Zuteilung gegenstandslos. Sie stehenzulassen
hiesse, dass eine geloeschte Person weiter Sichtbarkeit steuert. */
CREATE TABLE IF NOT EXISTS scout_zuteilung (
scout_id INTEGER PRIMARY KEY REFERENCES personen(id) ON DELETE CASCADE,
manager_id INTEGER NOT NULL REFERENCES personen(id) ON DELETE CASCADE,
seit TEXT NOT NULL,
gesetzt_von INTEGER REFERENCES personen(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_scout_zuteilung_manager
ON scout_zuteilung (manager_id);
/* Meeting-Protokoll zu einem Termin (Konzept, Seite 13).
Bewusst eine eigene Tabelle statt neuer Spalten in termine: Es
gibt hier keine Migrationen, und ein Protokoll gehoert ohnehin
@@ -909,13 +937,75 @@ export function codeErzeugen(gruppen = 4, laenge = 4) {
export function betreuteIds(person) {
if (!person || (person.rolle !== "scout" && person.rolle !== "manager")) return [];
try {
return db().prepare("SELECT creator_id FROM betreuung WHERE betreuer_id = ?")
const eigene = db().prepare("SELECT creator_id FROM betreuung WHERE betreuer_id = ?")
.all(person.id).map((z) => z.creator_id);
if (person.rolle !== "manager") return eigene;
/* DIE KETTE: Manager -> seine Scouts -> deren Creator (01.09.2026).
Entschieden auf die Frage "sieht er dann auch die Creator dieser
Scouts?" -- "ja, alles seiner Scouts". Das ist die uebliche
Ordnung: Wer einen Scout fuehrt, muss sehen, woran der arbeitet.
Ohne das muesste jeder Creator einem Manager EINZELN zugewiesen
werden, und beim ersten vergessenen faende er ein Loch in seiner
Uebersicht, ohne zu merken, dass es eines ist.
Ein Set, weil ein Creator auf beiden Wegen kommen kann: direkt
zugeteilt UND ueber seinen Scout. Doppelte Nummern wuerden in den
IN-Listen zu doppelten Fragezeichen -- fachlich harmlos, aber
jede Abfrage unnoetig laenger. */
const scouts = scoutsVon(person.id);
if (!scouts.length) return eigene;
const ueberScouts = db().prepare(
`SELECT creator_id FROM betreuung
WHERE betreuer_id IN (${scouts.map(() => "?").join(",")})`)
.all(...scouts).map((z) => z.creator_id);
return [...new Set([...eigene, ...ueberScouts])];
} catch {
return []; // im Zweifel nichts sehen, nie mehr
}
}
/** Die Scouts, die diesem Manager zugeteilt sind. Fuer alle anderen
* leer -- ein Scout fuehrt keine Scouts, ein Creator schon gar nicht.
* DogFather braucht sie nicht: Er sieht ohnehin alles. */
export function scoutsVon(managerId) {
if (!managerId) return [];
try {
return db().prepare("SELECT scout_id FROM scout_zuteilung WHERE manager_id = ?")
.all(managerId).map((z) => z.scout_id);
} catch {
return [];
}
}
/** Wessen Leads darf diese Person sehen? Gibt die Personennummern
* zurueck, deren Pipeline sichtbar ist -- der eigene immer dabei.
* Ein Scout sieht nur sich, ein Manager sich und seine Scouts. */
export function pipelineIds(person) {
if (!person) return [];
if (person.rolle !== "manager") return [person.id];
return [...new Set([person.id, ...scoutsVon(person.id)])];
}
/** Zuteilung setzen oder loesen (null loest sie).
* Wer das DARF, entscheidet die Route -- hier steht nur, WIE. */
export function scoutZuteilungSetzen(scoutId, managerId, akteur = null) {
const d = db();
if (managerId === null) {
d.prepare("DELETE FROM scout_zuteilung WHERE scout_id = ?").run(scoutId);
return;
}
d.prepare(`
INSERT INTO scout_zuteilung (scout_id, manager_id, seit, gesetzt_von)
VALUES (?,?,?,?)
ON CONFLICT(scout_id) DO UPDATE SET
manager_id = excluded.manager_id,
seit = excluded.seit,
gesetzt_von = excluded.gesetzt_von`)
.run(scoutId, managerId, new Date().toISOString(), akteur);
}
/* SQL-Baustein "diese Spalte gehoert zu einem meiner Creator".
Gibt null zurueck, wenn es nichts zu ergaenzen gibt -- eine leere
IN-Liste waere ungueltiges SQL. */
+60
View File
@@ -137,6 +137,21 @@
if (p.betreut_anzahl) {
z.append(el('span', null, `betreut ${p.betreut_anzahl} Creator`));
}
/* Bei einem Manager: wie viele Scouts an ihm haengen. Ohne diese Zahl
muesste man die ganze Liste durchgehen, um zu sehen, ob eine
Zuteilung ueberhaupt angekommen ist.
Steht in DERSELBEN Zeile wie "betreut N Creator" -- ein eigenes
Element haette eine eigene Klasse gebraucht, die es im Stil nicht
gibt, und waere als nackter Text erschienen. */
if (p.rolle === 'manager' && p.scouts_anzahl) {
z.append(el('span', null,
`${p.scouts_anzahl} ${p.scouts_anzahl === 1 ? 'Scout' : 'Scouts'} zugeteilt`));
}
/* Beim Scout umgekehrt: zu wem er gehoert. */
if (p.rolle === 'scout' && p.manager_name) {
z.append(el('span', null, `gehört zu ${p.manager_name}`));
}
links.append(z);
/* Zuständigkeit. Steht nur bei Creator und nur dann, wenn es
@@ -194,6 +209,51 @@
links.append(b);
}
/* ---- Welchem Manager gehoert dieser Scout? (01.09.2026) ----
"er soll nur die Scouts sehen, die ihm zugeteilt sind."
NUR fuer DogFather sichtbar -- und das ist kein Schoenheitsgrund:
Diese Zuteilung ERWEITERT die Sicht eines Managers (auf die Leads
seiner Scouts und auf deren Creator). Duerfte er sie selbst
setzen, koennte er sich seine eigene Sichtbarkeit vergeben. Der
Server weist es ohnehin ab; hier geht es nur darum, niemandem
einen Knopf hinzustellen, der nur eine Absage bringt.
Und nur, wenn es ueberhaupt einen Manager gibt -- eine leere
Auswahl sieht aus wie ein Fehler. */
const managerListe = betreuerListe.filter((x) => x.rolle === 'manager');
if (p.rolle === 'scout' && ich.rolle === 'admin' && managerListe.length) {
const b = el('div', 'betreuung');
const s2 = el('label', 'betreuung__schild', 'Gehört zu');
s2.htmlFor = 'manager-' + p.id;
const w2 = el('select', 'betreuung__wahl');
w2.id = 'manager-' + p.id;
const leer2 = document.createElement('option');
leer2.value = '';
/* Nicht "— niemand —": Ein Scout ohne Manager ist nicht
unbetreut, er haengt direkt an dir. Das ist ein Zustand, kein
Mangel. */
leer2.textContent = '— direkt bei DogFather —';
w2.append(leer2);
for (const m of managerListe) w2.append(new Option(m.name, String(m.id)));
w2.value = p.manager_id ? String(p.manager_id) : '';
w2.addEventListener('change', async () => {
w2.disabled = true; melde('');
try {
const a = await hole(`/workspace/api/verwaltung/scout-zuteilung/${p.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ manager_id: w2.value || null }),
});
if (!a.ok) { melde((await a.json().catch(() => ({}))).fehler || 'Ging nicht.'); return; }
await laden();
} catch { /* umgeleitet */ } finally { w2.disabled = false; }
});
b.append(s2, w2);
links.append(b);
}
const knoepfe = el('div', 'person__knoepfe');
knoepfe.append(tuKnopf('Neuer Code', async () => {
const eigen = p.id === ich.id;
+7 -7
View File
@@ -7,9 +7,9 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
</head>
<body class="start">
@@ -152,9 +152,9 @@
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609012128" defer></script>
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
<script src="assets/js/kopf.js?v=202609012128" defer></script>
<script src="assets/js/aufgaben.js?v=202609012128" defer></script>
<script src="assets/js/wahl.js?v=202609012233" defer></script>
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
<script src="assets/js/kopf.js?v=202609012233" defer></script>
<script src="assets/js/aufgaben.js?v=202609012233" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609012233" />
</head>
<body class="start">
@@ -132,9 +132,9 @@
</main>
<script src="assets/js/wahl.js?v=202609012128" defer></script>
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
<script src="assets/js/kopf.js?v=202609012128" defer></script>
<script src="assets/js/automation.js?v=202609012128" defer></script>
<script src="assets/js/wahl.js?v=202609012233" defer></script>
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
<script src="assets/js/kopf.js?v=202609012233" defer></script>
<script src="assets/js/automation.js?v=202609012233" defer></script>
</body>
</html>
+12 -12
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609012233" />
</head>
<body class="start">
@@ -115,13 +115,13 @@
</main>
<script src="assets/js/wahl.js?v=202609012128" defer></script>
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
<script src="assets/js/kopf.js?v=202609012128" defer></script>
<script src="assets/js/checkliste.js?v=202609012128" defer></script>
<script src="assets/js/ampel.js?v=202609012128" defer></script>
<script src="assets/js/schulung.js?v=202609012128" defer></script>
<script src="assets/js/vorlagen.js?v=202609012128" defer></script>
<script src="assets/js/bereich.js?v=202609012128" defer></script>
<script src="assets/js/wahl.js?v=202609012233" defer></script>
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
<script src="assets/js/kopf.js?v=202609012233" defer></script>
<script src="assets/js/checkliste.js?v=202609012233" defer></script>
<script src="assets/js/ampel.js?v=202609012233" defer></script>
<script src="assets/js/schulung.js?v=202609012233" defer></script>
<script src="assets/js/vorlagen.js?v=202609012233" defer></script>
<script src="assets/js/bereich.js?v=202609012233" defer></script>
</body>
</html>
+9 -9
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609012233" />
</head>
<body class="start">
@@ -54,10 +54,10 @@
</main>
<script src="assets/js/ki.js?v=202609012128" defer></script>
<script src="assets/js/wahl.js?v=202609012128" defer></script>
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
<script src="assets/js/kopf.js?v=202609012128" defer></script>
<script src="assets/js/calls.js?v=202609012128" defer></script>
<script src="assets/js/ki.js?v=202609012233" defer></script>
<script src="assets/js/wahl.js?v=202609012233" defer></script>
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
<script src="assets/js/kopf.js?v=202609012233" defer></script>
<script src="assets/js/calls.js?v=202609012233" defer></script>
</body>
</html>
+10 -10
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/content.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/content.css?v=202609012233" />
</head>
<body class="start">
@@ -164,11 +164,11 @@
</main>
<script src="assets/js/wahl.js?v=202609012128" defer></script>
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
<script src="assets/js/kopf.js?v=202609012128" defer></script>
<script src="assets/js/checkliste.js?v=202609012128" defer></script>
<script src="assets/js/vorlagen.js?v=202609012128" defer></script>
<script src="assets/js/content.js?v=202609012128" defer></script>
<script src="assets/js/wahl.js?v=202609012233" defer></script>
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
<script src="assets/js/kopf.js?v=202609012233" defer></script>
<script src="assets/js/checkliste.js?v=202609012233" defer></script>
<script src="assets/js/vorlagen.js?v=202609012233" defer></script>
<script src="assets/js/content.js?v=202609012233" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609012233" />
</head>
<body class="start">
@@ -93,9 +93,9 @@
</main>
<script src="assets/js/wahl.js?v=202609012128" defer></script>
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
<script src="assets/js/kopf.js?v=202609012128" defer></script>
<script src="assets/js/dateien.js?v=202609012128" defer></script>
<script src="assets/js/wahl.js?v=202609012233" defer></script>
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
<script src="assets/js/kopf.js?v=202609012233" defer></script>
<script src="assets/js/dateien.js?v=202609012233" defer></script>
</body>
</html>
+2 -2
View File
@@ -25,7 +25,7 @@
type="image/webp" media="(max-width: 899px)" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
</head>
<body class="gate">
@@ -143,6 +143,6 @@
</p>
</main>
<script src="assets/js/gate.js?v=202609012128" defer></script>
<script src="assets/js/gate.js?v=202609012233" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609012233" />
</head>
<body class="start">
@@ -147,9 +147,9 @@
</main>
<script src="assets/js/wahl.js?v=202609012128" defer></script>
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
<script src="assets/js/kopf.js?v=202609012128" defer></script>
<script src="assets/js/kalender.js?v=202609012128" defer></script>
<script src="assets/js/wahl.js?v=202609012233" defer></script>
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
<script src="assets/js/kopf.js?v=202609012233" defer></script>
<script src="assets/js/kalender.js?v=202609012233" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609012233" />
</head>
<body class="start">
@@ -121,9 +121,9 @@
</main>
<script src="assets/js/wahl.js?v=202609012128" defer></script>
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
<script src="assets/js/kopf.js?v=202609012128" defer></script>
<script src="assets/js/personen.js?v=202609012128" defer></script>
<script src="assets/js/wahl.js?v=202609012233" defer></script>
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
<script src="assets/js/kopf.js?v=202609012233" defer></script>
<script src="assets/js/personen.js?v=202609012233" defer></script>
</body>
</html>
+9 -9
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609012233" />
</head>
<body class="start">
@@ -193,10 +193,10 @@
</main>
<script src="assets/js/wahl.js?v=202609012128" defer></script>
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
<script src="assets/js/kopf.js?v=202609012128" defer></script>
<script src="assets/js/steckbrief.js?v=202609012128" defer></script>
<script src="assets/js/profil.js?v=202609012128" defer></script>
<script src="assets/js/wahl.js?v=202609012233" defer></script>
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
<script src="assets/js/kopf.js?v=202609012233" defer></script>
<script src="assets/js/steckbrief.js?v=202609012233" defer></script>
<script src="assets/js/profil.js?v=202609012233" defer></script>
</body>
</html>
+9 -9
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/report.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/report.css?v=202609012233" />
</head>
<body class="start">
@@ -91,10 +91,10 @@
</main>
<script src="assets/js/ki.js?v=202609012128" defer></script>
<script src="assets/js/wahl.js?v=202609012128" defer></script>
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
<script src="assets/js/kopf.js?v=202609012128" defer></script>
<script src="assets/js/report.js?v=202609012128" defer></script>
<script src="assets/js/ki.js?v=202609012233" defer></script>
<script src="assets/js/wahl.js?v=202609012233" defer></script>
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
<script src="assets/js/kopf.js?v=202609012233" defer></script>
<script src="assets/js/report.js?v=202609012233" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609012233" />
</head>
<body class="start">
@@ -124,9 +124,9 @@
</div>
</div>
<script src="assets/js/wahl.js?v=202609012128" defer></script>
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
<script src="assets/js/kopf.js?v=202609012128" defer></script>
<script src="assets/js/scouting.js?v=202609012128" defer></script>
<script src="assets/js/wahl.js?v=202609012233" defer></script>
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
<script src="assets/js/kopf.js?v=202609012233" defer></script>
<script src="assets/js/scouting.js?v=202609012233" defer></script>
</body>
</html>
+6 -6
View File
@@ -7,8 +7,8 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
</head>
<body class="start">
@@ -99,9 +99,9 @@
</p>
</main>
<script src="assets/js/wahl.js?v=202609012128" defer></script>
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
<script src="assets/js/kopf.js?v=202609012128" defer></script>
<script src="assets/js/start.js?v=202609012128" defer></script>
<script src="assets/js/wahl.js?v=202609012233" defer></script>
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
<script src="assets/js/kopf.js?v=202609012233" defer></script>
<script src="assets/js/start.js?v=202609012233" defer></script>
</body>
</html>
+9 -9
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609012233" />
</head>
<body class="start">
@@ -70,10 +70,10 @@
</main>
<script src="assets/js/ki.js?v=202609012128" defer></script>
<script src="assets/js/wahl.js?v=202609012128" defer></script>
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
<script src="assets/js/kopf.js?v=202609012128" defer></script>
<script src="assets/js/startcheck.js?v=202609012128" defer></script>
<script src="assets/js/ki.js?v=202609012233" defer></script>
<script src="assets/js/wahl.js?v=202609012233" defer></script>
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
<script src="assets/js/kopf.js?v=202609012233" defer></script>
<script src="assets/js/startcheck.js?v=202609012233" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609012233" />
</head>
<body class="start">
@@ -111,9 +111,9 @@
</main>
<script src="assets/js/wahl.js?v=202609012128" defer></script>
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
<script src="assets/js/kopf.js?v=202609012128" defer></script>
<script src="assets/js/steckbrief.js?v=202609012128" defer></script>
<script src="assets/js/wahl.js?v=202609012233" defer></script>
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
<script src="assets/js/kopf.js?v=202609012233" defer></script>
<script src="assets/js/steckbrief.js?v=202609012233" defer></script>
</body>
</html>
+6 -6
View File
@@ -7,9 +7,9 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609012233" />
</head>
<body class="start">
@@ -57,9 +57,9 @@
</main>
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
<script src="assets/js/kopf.js?v=202609012128" defer></script>
<script src="assets/js/uebersicht.js?v=202609012128" defer></script>
<script src="assets/js/kopf.js?v=202609012233" defer></script>
<script src="assets/js/uebersicht.js?v=202609012233" defer></script>
</body>
</html>
+8 -8
View File
@@ -7,10 +7,10 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<link rel="icon" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609012128" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/start.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609012233" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609012233" />
</head>
<body class="start">
@@ -175,9 +175,9 @@
</main>
<script src="assets/js/wahl.js?v=202609012128" defer></script>
<script src="assets/js/bereiche.js?v=202609012128" defer></script>
<script src="assets/js/kopf.js?v=202609012128" defer></script>
<script src="assets/js/wissen.js?v=202609012128" defer></script>
<script src="assets/js/wahl.js?v=202609012233" defer></script>
<script src="assets/js/bereiche.js?v=202609012233" defer></script>
<script src="assets/js/kopf.js?v=202609012233" defer></script>
<script src="assets/js/wissen.js?v=202609012233" defer></script>
</body>
</html>