Rund um das Team -- die Arbeitslage von Managern und Scouts
Filipe: "so eine kategorie wie ueber die creator will ich dass nur fuer
die spicy und dogfather rolle auch ueber manager und scouts gibt ...
ich will dass es so ultra krass gut ist dass die spicy und dogfather
rolle einen kompletten teil haben mit daten ueber die arbeit von den
manager und scout. keine geheimen sachen also termine, chats und
geheime dateien soll auch so bleiben dass keiner."
--- ZUERST DIE KOPFLEISTE ---
Filipe meldete, die Kopfleiste sei bei DogFather "nicht gemacht".
Nachgemessen auf ALLEN 18 Seiten, in allen fuenf Rollen, bei drei
Breiten: einzeilig, Spanne 4 px. Und die neuen Dateien liegen
nachweislich auf dem Server (a70bc4f, `abmelden__zeichen` in der
ausgelieferten kopf.js). Das Bild war vor dem Ausliefern entstanden.
Die Messung hat aber zwei echte Sachen gefunden, die vorher niemand
gesehen hatte -- beide bei 320 px auf UNTERseiten, wo links der
Zurueck-Knopf und rechts zusaetzlich die Glocke steht: 305 px
gebraucht, 294 verfuegbar. Eine Stufe kleiner (34 px je Knopf, 4 px
Abstand) macht 283 und passt; 34 px bleiben weit ueber den 24 px
Mindestmass fuer ein Beruehrziel.
Ausserdem die Auslieferung geordnet: HTML wird immer nachgefragt,
Dateien mit Versionsstempel duerfen ein Jahr liegenbleiben (vorher
bekam ALLES `no-cache`, also auch jede Stilvorlage bei jedem Aufruf).
sw.js ausgenommen -- er wird ohne Stempel geladen, ein Fehler darin
bliebe sonst ein Jahr stehen.
--- DIE NEUE SEITE ---
workspace/team.html, nur fuer `spicy` und `admin`. Aufbau:
DIE LUECKEN ZUERST. Creator ohne Betreuung, Scouts ohne Manager,
Leute ohne einen einzigen Creator -- mit NAMEN, nicht nur als Zahl.
Eine Kennzahl sagt, wie es laeuft; eine Luecke sagt, wo etwas fehlt,
und nur das Zweite kann man heute abstellen.
DANN DIE LAGE in fuenf Zahlen, dann JEDE PERSON EINZELN: betreute
Creator, laufende und ueberfaellige Aufgaben, in 30 Tagen erledigte,
Durchlaufzeit, Startcheck-Fortschritt der betreuten Creator,
LIVE-Tage und Diamanten. Bei Scouts zusaetzlich die Pipeline mit
Uebernahmequote und Zeit bis zur Uebergabe.
EIN MANAGER TRAEGT DIE CREATOR SEINER SCOUTS MIT. Ohne das saehe
einer mit fuenf Scouts aus wie jemand ohne Arbeit.
--- DREI ENTSCHEIDUNGEN, DIE ALLES TRAGEN ---
1. TERMINE, CHATS UND DATEIEN KOMMEN NICHT VOR -- weder Inhalte noch
Zaehlungen. Ausdruecklicher Wunsch, und der richtige: Ein Kalender
verraet, wann jemand nicht da war; ein Chatzaehler, mit wem jemand
oft spricht.
Das ist keine Zusicherung im Kommentar. pruef-team liest den
Quelltext von workspace-team.js und schlaegt an, wenn eine dieser
Tabellen darin auftaucht -- mit Gegenprobe, dass die Suche `aufgaben`
und `leads` auch wirklich findet. Der Weg ueber die Antwort allein
waere schwaecher: Ein leerer Testbestand kann ein Feld verstecken.
2. SEGMENTIEREN, NICHT MITTELN. Aus der Recherche zu
Arbeitslast-Dashboards: Ein Durchschnitt versteckt genau die Person,
bei der es klemmt. Markiert wird gegen den MEDIAN der eigenen Rolle
-- ein Manager traegt naturgemaess mehr als ein Scout, und ihn daran
zu messen waere unfair und nutzlos.
3. ES IST EINE ARBEITSLAGE, KEINE UEBERWACHUNG. Das steht so auf der
Seite, im Kopf, in einem eigenen Kasten. Wer das nicht dazuschreibt,
baut ein Kontrollwerkzeug, auch wenn er es nicht wollte. Deshalb
zeigen die Kennzahlen auf ZUSTAENDE (unbetreute Creator,
liegengebliebene Kontakte) und nicht auf Anwesenheit oder Fleiss.
--- WAS DIE MESSUNG UNTERWEGS GEFUNDEN HAT ---
* Die Lead-Status hiessen anders, als ich angenommen hatte: "kontakt"
gibt es nicht. Die CHECK-Bedingung der Datenbank hat es sofort
abgelehnt -- ohne sie waere "offen" still zu klein gewesen.
* Die Pruefung fand ihr eigenes Hinweisschild: Die Antwort traegt ein
Feld `ausgenommen: ["Termine","Chats","Dateien"]`, aus dem die Seite
den Satz baut. Es wird jetzt herausgenommen UND eigens geprueft --
ignorieren waere bequem gewesen und haette kuenftig jedes Feld unter
diesem Namen durchgelassen.
* Die Lektion vom Vorlagenbrett gleich mitgenommen: alle Karten haben
einen DECKENDEN Grund. Eine Karte mit sieben Prozent Farbe auf
durchsichtigem Grund laesst das Buehnenfoto durch -- dort waren es
3,61:1. Gemessen jetzt: 6,61 bis 14,80:1. Eine der Regeln hatte den
deckenden Grund selbst wieder aufgehoben (zwei Regeln, die spaetere
gewinnt) -- gefunden, bevor es jemand sehen musste.
* "1 Scouts" statt "1 Scout". Eine Kleinigkeit, und das Erste, was
auffaellt: Eine Seite, die ihre eigene Sprache nicht beherrscht, wird
auch bei den Zahlen nicht geglaubt.
--- Pruefung ---
server/pruef-team.mjs, neu, 51 Pruefungen, alle gruen. Darunter: alle
fuenf Rollen an Schnittstelle UND Seite (Manager, Scout und Creator
bekommen 404 bzw. eine Umleitung), Spicy sieht VanVan nicht (mit
Gegenprobe an der Personenliste), die Zahlen an einem gebauten
Bestand, acht Kontrastmessungen an der wirklichen Flaeche, Handy.
server/pruef-zwischenspeicher.mjs, neu, 15 Pruefungen: was liegenbleiben
darf und was nicht -- an echten Kopfzeilen gemessen, nicht am
Quelltext. Sie meldete zuerst drei Fehler, und das war sie selbst: Sie
fragte unangemeldet und bekam Umleitungen. Eine Pruefung braucht ihre
Voraussetzung, bevor sie misst.
Ausserdem gruen: pruef-handy (das Handy fand die 320-px-Sache),
pruef-workspace-seiten, pruef-alle-wege, pruef-sicht, pruef-css-klassen.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+67
-1
@@ -35,6 +35,10 @@ import { contentRouter } from "./workspace-content.js";
|
|||||||
import { sicherungRouter, sicherungStarten } from "./workspace-sicherung.js";
|
import { sicherungRouter, sicherungStarten } from "./workspace-sicherung.js";
|
||||||
import { steckbriefRouter } from "./workspace-steckbrief.js";
|
import { steckbriefRouter } from "./workspace-steckbrief.js";
|
||||||
import { vorlagenRouter } from "./workspace-vorlagen.js";
|
import { vorlagenRouter } from "./workspace-vorlagen.js";
|
||||||
|
/* Die Arbeitslage von Managern und Scouts -- nur fuer Spicy Media und
|
||||||
|
DogFather. Sie ruehrt Termine, Chats und Dateien nicht an; die
|
||||||
|
Begruendung steht im Kopf der Datei. */
|
||||||
|
import { teamRouter } from "./workspace-team.js";
|
||||||
import { schulungRouter } from "./workspace-schulung.js";
|
import { schulungRouter } from "./workspace-schulung.js";
|
||||||
import { ampelRouter } from "./workspace-ampel.js";
|
import { ampelRouter } from "./workspace-ampel.js";
|
||||||
import { checklisteRouter } from "./workspace-checkliste.js";
|
import { checklisteRouter } from "./workspace-checkliste.js";
|
||||||
@@ -203,6 +207,7 @@ app.use(fruehwarnungRouter);
|
|||||||
deshalb der Zugang -- 32 Byte Zufall, jederzeit widerrufbar. */
|
deshalb der Zugang -- 32 Byte Zufall, jederzeit widerrufbar. */
|
||||||
app.use(icsRouter);
|
app.use(icsRouter);
|
||||||
app.use(vorlagenRouter);
|
app.use(vorlagenRouter);
|
||||||
|
app.use(teamRouter);
|
||||||
app.use(schulungRouter);
|
app.use(schulungRouter);
|
||||||
app.use(ampelRouter);
|
app.use(ampelRouter);
|
||||||
app.use(checklisteRouter);
|
app.use(checklisteRouter);
|
||||||
@@ -297,7 +302,68 @@ app.use((req, res, next) => {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(express.static(SITE_DIR, { index: "index.html" }));
|
/* =====================================================================
|
||||||
|
HTML WIRD IMMER NACHGEFRAGT, ALLES ANDERE DARF LIEGENBLEIBEN
|
||||||
|
(09.09.2026)
|
||||||
|
|
||||||
|
DER FALL, DER ES GEZEIGT HAT: Filipe meldete am Nachmittag, die
|
||||||
|
Kopfleiste auf dem Handy sei "nicht gemacht" -- gemessen war sie auf
|
||||||
|
allen 18 Seiten, in allen fuenf Rollen und bei drei Breiten
|
||||||
|
einzeilig, und die neuen Dateien lagen nachweislich auf dem Server.
|
||||||
|
Er sah trotzdem den alten Stand.
|
||||||
|
|
||||||
|
Die Ursache ist eine Luecke, die man nur bemerkt, wenn man sie sucht:
|
||||||
|
Die HTML-Seiten gingen OHNE JEDE Cache-Angabe hinaus. Ohne
|
||||||
|
`Cache-Control` und ohne `Expires` darf ein Browser heuristisch
|
||||||
|
zwischenspeichern -- ueblicherweise ein Zehntel des Alters seit der
|
||||||
|
letzten Aenderung. Bei einer Datei, die vor zwei Wochen zuletzt
|
||||||
|
angefasst wurde, sind das anderthalb Tage.
|
||||||
|
|
||||||
|
Und das ist schlimmer als es klingt: In der HTML stehen die
|
||||||
|
VERSIONSSTEMPEL der Stilvorlagen (`start.css?v=...`). Eine alte HTML
|
||||||
|
verweist auf alte Stempel -- der Browser holt sich dann brav die
|
||||||
|
ALTEN CSS- und JS-Dateien, obwohl die neuen bereitliegen. Der ganze
|
||||||
|
Stempelmechanismus haengt an einer Datei, die selbst
|
||||||
|
zwischengespeichert werden durfte.
|
||||||
|
|
||||||
|
Deshalb: HTML immer nachfragen (`no-cache` heisst NICHT "nicht
|
||||||
|
speichern", sondern "vor jeder Benutzung rueckfragen" -- die Antwort
|
||||||
|
ist meistens ein 304 ohne Rumpf und kostet fast nichts). Alles mit
|
||||||
|
Stempel darf dagegen ein Jahr liegenbleiben; sein Name aendert sich
|
||||||
|
ja, sobald sich der Inhalt aendert.
|
||||||
|
|
||||||
|
Bilder und Schriften ohne Stempel bekommen einen Tag -- lang genug,
|
||||||
|
um jeden Seitenaufruf zu tragen, kurz genug, um nach einem Austausch
|
||||||
|
nicht ewig alt zu bleiben. */
|
||||||
|
app.use(express.static(SITE_DIR, {
|
||||||
|
index: "index.html",
|
||||||
|
setHeaders(res, pfad) {
|
||||||
|
if (pfad.endsWith(".html")) {
|
||||||
|
res.setHeader("Cache-Control", "no-cache");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* Mit Versionsstempel in der Adresse: Der Name ist der Beweis.
|
||||||
|
`originalUrl` steht hier nicht zur Verfuegung -- gepruefft wird
|
||||||
|
deshalb die Dateiart, und die Stempel setzt ohnehin nur das
|
||||||
|
Haus selbst. */
|
||||||
|
/* AUSNAHME: sw.js. Der Service Worker wird OHNE Stempel geladen
|
||||||
|
(glocke.js: register('/workspace/sw.js')). Ein Jahr unveraenderlich
|
||||||
|
hiesse: Ein Fehler darin bliebe ein Jahr stehen. Browser
|
||||||
|
begrenzen das zwar selbst auf 24 Stunden -- sich darauf zu
|
||||||
|
verlassen waere aber genau die Sorte Annahme, die still bricht. */
|
||||||
|
if (pfad.endsWith("sw.js")) {
|
||||||
|
res.setHeader("Cache-Control", "no-cache");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (/\.(css|js|mjs)$/.test(pfad)) {
|
||||||
|
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (/\.(webp|png|jpe?g|svg|gif|ico|woff2?)$/.test(pfad)) {
|
||||||
|
res.setHeader("Cache-Control", "public, max-age=86400");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
res.status(404).sendFile(join(SITE_DIR, "404.html"), (err) => {
|
res.status(404).sendFile(join(SITE_DIR, "404.html"), (err) => {
|
||||||
|
|||||||
+401
-290
@@ -1,72 +1,72 @@
|
|||||||
/* Prueft zwei Wuensche vom 01.09.2026:
|
/* DIE ARBEITSLAGE VON MANAGERN UND SCOUTS (09.09.2026)
|
||||||
|
|
||||||
SCREEN 2: "ich will, dass wir Manager, Scouts, DogFather auch die
|
Filipe: "so eine kategorie wie über die creator will ich dass nur für
|
||||||
Fotos, Namen und so alles sehen, alles was die in ihrem Profil
|
die spicy und dogfather rolle auch über manager und scouts gibt ...
|
||||||
eintragen."
|
keine geheimen sachen also termine, chats und geheime dateien soll
|
||||||
|
auch so bleiben dass keiner."
|
||||||
|
|
||||||
SCREEN 5: "wieso seh ich mein Bild da nicht, obwohl ich schon ein Foto
|
---------------------------------------------------------------------
|
||||||
drin hab?"
|
DER WICHTIGSTE ABSCHNITT IST DER ERSTE, NICHT DER LETZTE
|
||||||
|
|
||||||
WAS AN DIESER PRUEFUNG WICHTIG IST.
|
Diese Seite zeigt Zahlen über Kollegen. Zwei Dinge daran können
|
||||||
|
schiefgehen, und beide wären schwer wieder gutzumachen:
|
||||||
|
|
||||||
Ein Profilbild zu pruefen, ohne ein echtes Bild zu haben, beweist
|
1. Sie zeigt sie den Falschen. Ein Manager, der die Auslastung
|
||||||
nichts. Beim .wer__bild-Fehler im August benutzte die Pruefung ein
|
seiner Kollegen sieht, hat eine Auskunft, die ihm nicht gehört.
|
||||||
1x1-Pixel-PNG -- damit kann kein Stil-Fehler sichtbar werden, weil
|
2. Sie zeigt etwas, das nicht hineingehört -- Termine, Chats,
|
||||||
ein Punkt nichts ueberdecken kann. Hier wird deshalb ein RICHTIGES
|
Dateien.
|
||||||
Bild hochgeladen (600x600) und danach gemessen, ob es auch die
|
|
||||||
Flaeche fuellt, die es fuellen soll -- und ob es rund beschnitten ist
|
|
||||||
statt quadratisch ueber den Rahmen zu ragen.
|
|
||||||
|
|
||||||
Und zu jeder Aussage die Gegenprobe: Wer KEIN Bild hat, muss den
|
Punkt 2 wird hier auf ZWEI Wegen geprüft: an der Antwort (keine
|
||||||
Buchstaben sehen. Sonst koennte die Pruefung gruen sein, weil
|
Felder mit diesen Namen) UND am Quelltext (die Datei nennt diese
|
||||||
ueberall Bilder erscheinen -- auch da, wo keine sind. */
|
Tabellen nicht). Der zweite Weg ist der stärkere: Er hält auch dann,
|
||||||
|
wenn jemand morgen ein Feld hinzufügt, das im Testbestand zufällig
|
||||||
|
leer bleibt.
|
||||||
|
|
||||||
import { mkdtempSync, rmSync } from "node:fs";
|
DREI AUSGÄNGE: 0 in Ordnung · 1 Befunde · 2 konnte nicht arbeiten. */
|
||||||
|
|
||||||
|
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join, dirname } from "node:path";
|
||||||
import { deflateSync } from "node:zlib";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
const ordner = mkdtempSync(join(tmpdir(), "ws-team-"));
|
const ordner = mkdtempSync(join(tmpdir(), "ws-team-"));
|
||||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||||
/* DATEN_ORDNER wird aus dem Datenbankpfad abgeleitet (workspace.js) --
|
|
||||||
die Profilbilder landen also von selbst im Testordner. */
|
import { notbremse } from "./helfer-notbremse.mjs";
|
||||||
process.env.PORT = "4213";
|
const { portMussFreiSein } = await import("./helfer-port.mjs");
|
||||||
|
await portMussFreiSein(4345, "die Team-Pruefung");
|
||||||
|
|
||||||
|
process.env.PORT = "4345";
|
||||||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
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";
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
||||||
|
|
||||||
const express = (await import("express")).default;
|
const express = (await import("express")).default;
|
||||||
const ec = express.response.cookie;
|
const ec = express.response.cookie;
|
||||||
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
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");
|
await import("./index.js");
|
||||||
/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort
|
notbremse(300_000, "pruef-team");
|
||||||
zwei Auffangnetze ein, die einen Absturz nur protokollieren --
|
await new Promise((r) => setTimeout(r, 900));
|
||||||
richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese
|
const BASIS = "http://127.0.0.1:4345";
|
||||||
Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der
|
|
||||||
Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */
|
|
||||||
notbremse(480_000, "pruef-team");
|
|
||||||
await new Promise((r) => setTimeout(r, 700));
|
|
||||||
const BASIS = "http://127.0.0.1:4213";
|
|
||||||
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.();
|
|
||||||
|
|
||||||
let fehler = 0;
|
let fehler = 0, geprueft = 0;
|
||||||
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||||
|
|
||||||
|
/* ---- Bestand ------------------------------------------------------------ */
|
||||||
|
const { DatabaseSync } = await import("node:sqlite");
|
||||||
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
||||||
await fetch(BASIS + "/workspace/api/anmelden", {
|
await fetch(BASIS + "/workspace/api/anmelden", {
|
||||||
method: "POST", headers: { "Content-Type": "application/json" },
|
method: "POST", headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
||||||
|
|
||||||
const { DatabaseSync } = await import("node:sqlite");
|
|
||||||
const { scryptSync, randomBytes, createHash } = await import("node:crypto");
|
|
||||||
for (let i = 0; i < 60; i++) {
|
for (let i = 0; i < 60; i++) {
|
||||||
try {
|
try { const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||||
const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
||||||
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
|
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
|
||||||
} catch { await new Promise((r) => setTimeout(r, 250)); }
|
} catch { await new Promise((r) => setTimeout(r, 250)); }
|
||||||
}
|
}
|
||||||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||||
const jetzt = new Date().toISOString();
|
const jetzt = new Date().toISOString();
|
||||||
|
const tagT = (v) => { const x = new Date(Date.now() + v * 86400000);
|
||||||
|
return `${x.getFullYear()}-${String(x.getMonth() + 1).padStart(2, "0")}`
|
||||||
|
+ `-${String(x.getDate()).padStart(2, "0")}`; };
|
||||||
function anlegen(name, rolle, code) {
|
function anlegen(name, rolle, code) {
|
||||||
const salt = randomBytes(16).toString("hex");
|
const salt = randomBytes(16).toString("hex");
|
||||||
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||||||
@@ -75,267 +75,378 @@ function anlegen(name, rolle, code) {
|
|||||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||||
}
|
}
|
||||||
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||||||
const idMgr = anlegen("Mara", "manager", "CODE-MANA-0001");
|
const idSpicy = anlegen("Cigdem", "spicy", "CODE-SPIC-0001");
|
||||||
const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001");
|
const idVan = anlegen("VanVan", "admin", "CODE-VANV-0001"); /* der zweite Admin */
|
||||||
|
const idMara = anlegen("Mara", "manager", "CODE-MANA-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 idTili = anlegen("Tili", "creator", "CODE-ANNA-0001");
|
||||||
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)")
|
const idLuna = anlegen("Luna", "creator", "CODE-LUNA-0001");
|
||||||
.run(idTili, idScout, jetzt);
|
const idRoy = anlegen("Roy", "creator", "CODE-ROYY-0001");
|
||||||
|
const idFrei = anlegen("Nio", "creator", "CODE-NIOO-0001"); /* ohne Betreuung */
|
||||||
|
|
||||||
/* Jeder traegt etwas in seinen Steckbrief ein -- sonst koennte die
|
/* Patrick betreut Tili und Luna, Ben betreut Roy, Mara fuehrt Patrick.
|
||||||
Pruefung nicht unterscheiden, ob "alles was die eintragen" wirklich
|
Nio hat NIEMANDEN -- das ist die Luecke, die die Seite zeigen soll. */
|
||||||
ankommt oder ob nur Namen angezeigt werden. */
|
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idTili, idPat, jetzt);
|
||||||
d.prepare("UPDATE personen SET ueber_mich = ?, tiktok = ? WHERE id = ?")
|
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idPat, jetzt);
|
||||||
.run("Manager & CEO dieser Seite", "dogfather", idDogi);
|
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idRoy, idBen, jetzt);
|
||||||
d.prepare("UPDATE personen SET ueber_mich = ?, instagram = ? WHERE id = ?")
|
d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)").run(idPat, idMara, jetzt);
|
||||||
.run("Ich kuemmere mich um die Vertraege.", "mara.mgmt", idMgr);
|
|
||||||
d.prepare("UPDATE personen SET ueber_mich = ? WHERE id = ?")
|
const auf = (titel, creator, status, frist) =>
|
||||||
.run("Ich finde neue Leute.", idScout);
|
d.prepare(`INSERT INTO aufgaben (titel, beschreibung, status, prioritaet, frist,
|
||||||
/* Tili bekommt BEWUSST nichts -- die Gegenprobe zum Platzhaltersatz. */
|
creator_id, verantwortlich_id, erstellt, erstellt_von, erledigt_am)
|
||||||
|
VALUES (?,?,?,'mittel',?,?,?,?,?,?)`)
|
||||||
|
.run(titel, "", status, frist, creator, creator, jetzt, idDogi,
|
||||||
|
status === "erledigt" ? jetzt : null);
|
||||||
|
auf("T1", idTili, "offen", tagT(-3)); /* ueberfaellig */
|
||||||
|
auf("T2", idTili, "arbeit", tagT(5));
|
||||||
|
auf("T3", idLuna, "erledigt", tagT(-1));
|
||||||
|
auf("T4", idRoy, "offen", tagT(2));
|
||||||
|
|
||||||
|
d.prepare(`INSERT INTO leads (name, plattform, handle, status, scout_id, erstellt, erstellt_von,
|
||||||
|
naechster_followup, uebergeben_am) VALUES (?,?,?,?,?,?,?,?,?)`)
|
||||||
|
.run("Kandidat A", "tiktok", "@a", "angesprochen", idPat, jetzt, idPat, tagT(-2), null);
|
||||||
|
d.prepare(`INSERT INTO leads (name, plattform, handle, status, scout_id, erstellt, erstellt_von,
|
||||||
|
naechster_followup, uebergeben_am) VALUES (?,?,?,?,?,?,?,?,?)`)
|
||||||
|
.run("Kandidat B", "tiktok", "@b", "uebergeben", idPat, jetzt, idPat, null, jetzt);
|
||||||
d.close();
|
d.close();
|
||||||
|
|
||||||
/* ---- Ein echtes PNG, 600x600, einfarbig. Kein 1x1-Punkt: Der kann
|
const keks = async (rolle, code) => {
|
||||||
nichts ueberdecken und wuerde jeden Stil-Fehler verstecken. ---- */
|
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
||||||
function pngBauen(breite, hoehe) {
|
method: "POST", headers: { "Content-Type": "application/json" },
|
||||||
const roh = Buffer.alloc((breite * 3 + 1) * hoehe);
|
body: JSON.stringify({ rolle, code }) });
|
||||||
for (let y = 0; y < hoehe; y++) {
|
return (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; ");
|
||||||
const z = y * (breite * 3 + 1);
|
};
|
||||||
roh[z] = 0;
|
const kDogi = await keks("admin", "CODE-DOGI-0001");
|
||||||
for (let x = 0; x < breite; x++) {
|
const kSpicy = await keks("spicy", "CODE-SPIC-0001");
|
||||||
roh[z + 1 + x * 3] = 200; roh[z + 2 + x * 3] = 60; roh[z + 3 + x * 3] = 120;
|
const kMara = await keks("manager", "CODE-MANA-0001");
|
||||||
}
|
const kPat = await keks("scout", "CODE-SCOU-0001");
|
||||||
|
const kTili = await keks("creator", "CODE-ANNA-0001");
|
||||||
|
const ruf = (pfad, k) => fetch(BASIS + pfad, { headers: { cookie: k }, redirect: "manual" });
|
||||||
|
|
||||||
|
/* =======================================================================
|
||||||
|
1. WER DARF HINEIN -- und wer nicht
|
||||||
|
======================================================================= */
|
||||||
|
console.log("\n=== Wer darf hinein");
|
||||||
|
{
|
||||||
|
for (const [name, k, soll] of [
|
||||||
|
["DogFather", kDogi, 200], ["Spicy Media", kSpicy, 200],
|
||||||
|
["Manager", kMara, 404], ["Scout", kPat, 404], ["Creator", kTili, 404],
|
||||||
|
]) {
|
||||||
|
const a = await ruf("/workspace/api/team", k);
|
||||||
|
ok(a.status === soll, `${name}: HTTP ${a.status} (erwartet ${soll})`);
|
||||||
}
|
}
|
||||||
const stueck = (art, daten) => {
|
/* Ohne Anmeldung ueberhaupt nichts. */
|
||||||
const l = Buffer.alloc(4); l.writeUInt32BE(daten.length);
|
const ohne = await fetch(BASIS + "/workspace/api/team", { redirect: "manual" });
|
||||||
const k = Buffer.concat([Buffer.from(art), daten]);
|
ok(ohne.status === 401, `ohne Anmeldung: HTTP ${ohne.status}`);
|
||||||
const c = Buffer.alloc(4);
|
|
||||||
c.writeUInt32BE(crc32(k) >>> 0);
|
/* Und die SEITE ebenso -- eine offene Seite mit gesperrter
|
||||||
return Buffer.concat([l, k, c]);
|
Schnittstelle waere eine leere Seite und eine offene Frage. */
|
||||||
};
|
for (const [name, k, offen] of [
|
||||||
const ihdr = Buffer.alloc(13);
|
["DogFather", kDogi, true], ["Spicy Media", kSpicy, true],
|
||||||
ihdr.writeUInt32BE(breite, 0); ihdr.writeUInt32BE(hoehe, 4);
|
["Manager", kMara, false], ["Scout", kPat, false], ["Creator", kTili, false],
|
||||||
ihdr[8] = 8; ihdr[9] = 2; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
|
]) {
|
||||||
return Buffer.concat([
|
const a = await ruf("/workspace/team.html", k);
|
||||||
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
const drin = a.status === 200;
|
||||||
stueck("IHDR", ihdr), stueck("IDAT", deflateSync(roh)), stueck("IEND", Buffer.alloc(0)),
|
ok(drin === offen, `team.html für ${name}: HTTP ${a.status}`);
|
||||||
]);
|
|
||||||
}
|
|
||||||
let tabelle = null;
|
|
||||||
function crc32(buf) {
|
|
||||||
if (!tabelle) {
|
|
||||||
tabelle = new Int32Array(256);
|
|
||||||
for (let n = 0; n < 256; n++) {
|
|
||||||
let c = n;
|
|
||||||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
||||||
tabelle[n] = c;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let c = -1;
|
|
||||||
for (const b of buf) c = tabelle[(c ^ b) & 0xff] ^ (c >>> 8);
|
|
||||||
return c ^ -1;
|
|
||||||
}
|
}
|
||||||
const BILD = pngBauen(600, 600);
|
|
||||||
|
|
||||||
/* ---------------------------------------------------------------- */
|
/* =======================================================================
|
||||||
|
2. WAS NICHT HINEINGEHÖRT -- Termine, Chats, Dateien
|
||||||
|
|
||||||
const { chromium } = await import(
|
Auf zwei Wegen geprüft. Der zweite ist der stärkere.
|
||||||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
======================================================================= */
|
||||||
const browser = await chromium.launch();
|
console.log("\n=== Termine, Chats und Dateien bleiben draußen");
|
||||||
|
{
|
||||||
|
const daten = await (await ruf("/workspace/api/team", kDogi)).json();
|
||||||
|
|
||||||
async function anmelden(rolle, code) {
|
/* DAS HINWEISSCHILD WIRD AUSGENOMMEN -- und zwar ausdrücklich.
|
||||||
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
|
|
||||||
|
Die Antwort trägt ein Feld `ausgenommen: ["Termine","Chats",
|
||||||
|
"Dateien"]`; daraus schreibt die Seite den Satz, der dem Betrachter
|
||||||
|
sagt, was hier NICHT steht. Beim ersten Lauf hat die Wortsuche
|
||||||
|
genau dieses Schild gefunden und Alarm geschlagen.
|
||||||
|
|
||||||
|
Es einfach zu ignorieren wäre die bequeme Lösung und eine schlechte:
|
||||||
|
Dann könnte künftig jedes beliebige Feld unter diesem Namen
|
||||||
|
durchrutschen. Stattdessen wird es HERAUSGENOMMEN und danach
|
||||||
|
eigens geprüft -- was es enthalten darf, steht dann fest. */
|
||||||
|
const { ausgenommen, ...ohneSchild } = daten;
|
||||||
|
const text = JSON.stringify(ohneSchild).toLowerCase();
|
||||||
|
const verboten = ["termin", "chat", "datei", "nachricht", "anhang"];
|
||||||
|
const gefunden = verboten.filter((w) => text.includes(w));
|
||||||
|
ok(gefunden.length === 0,
|
||||||
|
gefunden.length ? `in der Antwort steht: ${gefunden.join(", ")}`
|
||||||
|
: `kein einziges dieser Wörter in der Antwort (${verboten.join(", ")})`);
|
||||||
|
ok(Array.isArray(ausgenommen) && ausgenommen.join(",") === "Termine,Chats,Dateien",
|
||||||
|
`und das Schild nennt genau die drei (${(ausgenommen || []).join(", ")})`);
|
||||||
|
|
||||||
|
/* GEGENPROBE ZUR WORTSUCHE: Sie muss ein verbotenes Wort auch finden.
|
||||||
|
Ohne sie hiesse "nichts gefunden" nur, dass nicht gesucht wurde. */
|
||||||
|
ok(verboten.some((w) => JSON.stringify({ x: "Termine am Montag" }).toLowerCase().includes(w)),
|
||||||
|
"Gegenprobe: die Wortsuche schlägt bei einem verbotenen Wort an");
|
||||||
|
|
||||||
|
/* DER STÄRKERE WEG: der Quelltext selbst. Ein leerer Testbestand
|
||||||
|
kann ein Feld verstecken; eine Abfrage, die es gar nicht gibt,
|
||||||
|
kann nichts liefern. */
|
||||||
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const quelle = readFileSync(join(HIER, "workspace-team.js"), "utf8");
|
||||||
|
/* Nur der CODE, nicht die Kommentare -- dort steht ausdrücklich, was
|
||||||
|
ausgenommen ist, und das soll dort auch stehen bleiben. */
|
||||||
|
const nurCode = quelle.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
||||||
|
const tabellen = ["termine", "termin_teilnehmer", "chat_nachrichten", "chat_raeume",
|
||||||
|
"chat_teilnehmer", "dateien", "datei_personen"];
|
||||||
|
const benutzt = tabellen.filter((t) => new RegExp(`\\b${t}\\b`).test(nurCode));
|
||||||
|
ok(benutzt.length === 0,
|
||||||
|
benutzt.length ? `der Quelltext fragt ab: ${benutzt.join(", ")}`
|
||||||
|
: `keine dieser ${tabellen.length} Tabellen wird abgefragt`);
|
||||||
|
|
||||||
|
/* GEGENPROBE: Die Suche findet eine Tabelle, die WIRKLICH benutzt
|
||||||
|
wird. Ohne sie hiesse "nichts gefunden" nur, dass die Suche nicht
|
||||||
|
sucht. */
|
||||||
|
ok(/\baufgaben\b/.test(nurCode) && /\bleads\b/.test(nurCode),
|
||||||
|
"Gegenprobe: `aufgaben` und `leads` werden gefunden — die Suche sucht wirklich");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =======================================================================
|
||||||
|
3. STIMMEN DIE ZAHLEN?
|
||||||
|
======================================================================= */
|
||||||
|
console.log("\n=== Die Zahlen");
|
||||||
|
{
|
||||||
|
const daten = await (await ruf("/workspace/api/team", kDogi)).json();
|
||||||
|
const p = (name) => daten.leute.find((x) => x.name === name);
|
||||||
|
|
||||||
|
ok(daten.leute.length === 3,
|
||||||
|
`drei Leute: Manager und zwei Scouts (${daten.leute.map((x) => x.name).join(", ")})`);
|
||||||
|
ok(!daten.leute.some((x) => x.rolle === "creator" || x.rolle === "admin"),
|
||||||
|
"keine Creator und keine Admins in der Liste — die Seite ist für Manager und Scouts");
|
||||||
|
|
||||||
|
const pat = p("Patrick");
|
||||||
|
ok(pat?.creator.length === 2, `Patrick betreut zwei Creator (${pat?.creator.length})`);
|
||||||
|
ok(pat?.aufgaben.ueberfaellig === 1,
|
||||||
|
`eine überfällige Aufgabe bei seinen Creators (${pat?.aufgaben.ueberfaellig})`);
|
||||||
|
ok(pat?.aufgaben.laufend === 2, `zwei laufende (${pat?.aufgaben.laufend})`);
|
||||||
|
|
||||||
|
/* DER MANAGER TRÄGT DIE CREATOR SEINER SCOUTS MIT.
|
||||||
|
Ohne das sähe ein Manager mit fünf Scouts aus wie jemand ohne
|
||||||
|
Arbeit -- und genau das wäre die falsche Auskunft. */
|
||||||
|
const mara = p("Mara");
|
||||||
|
ok(mara?.creator.length === 0 && mara?.creator_mittelbar === 2,
|
||||||
|
`Mara betreut niemanden direkt, aber zwei über Patrick `
|
||||||
|
+ `(${mara?.creator.length} / ${mara?.creator_mittelbar})`);
|
||||||
|
ok(mara?.aufgaben.ueberfaellig === 1,
|
||||||
|
`und sieht deren überfällige Aufgabe (${mara?.aufgaben.ueberfaellig})`);
|
||||||
|
ok(mara?.scouts.includes("Patrick"), `mit Namen des Scouts (${mara?.scouts.join(", ")})`);
|
||||||
|
|
||||||
|
/* Die Pipeline gibt es nur bei Scouts. */
|
||||||
|
ok(pat?.pipeline && pat.pipeline.gesamt === 2 && pat.pipeline.uebergeben === 1,
|
||||||
|
`Patricks Pipeline: ${pat?.pipeline?.gesamt} Kontakte, ${pat?.pipeline?.uebergeben} übergeben`);
|
||||||
|
ok(pat?.pipeline?.faellig === 1,
|
||||||
|
`davon einer überfällig im Nachfassen (${pat?.pipeline?.faellig})`);
|
||||||
|
ok(mara?.pipeline === null, "ein Manager hat keine Pipeline — und bekommt auch keine leere");
|
||||||
|
|
||||||
|
/* ---- DIE LÜCKEN ---- */
|
||||||
|
ok(daten.luecken.creator_ohne_betreuung.some((c) => c.name === "Nio"),
|
||||||
|
`Nio steht als Creator ohne Betreuung da `
|
||||||
|
+ `(${daten.luecken.creator_ohne_betreuung.map((c) => c.name).join(", ")})`);
|
||||||
|
ok(daten.luecken.scouts_ohne_manager.some((s) => s.name === "Ben"),
|
||||||
|
`Ben steht als Scout ohne Manager da `
|
||||||
|
+ `(${daten.luecken.scouts_ohne_manager.map((s) => s.name).join(", ")})`);
|
||||||
|
/* GEGENPROBE: Wer versorgt ist, taucht NICHT auf. */
|
||||||
|
ok(!daten.luecken.creator_ohne_betreuung.some((c) => c.name === "Tili")
|
||||||
|
&& !daten.luecken.scouts_ohne_manager.some((s) => s.name === "Patrick"),
|
||||||
|
"und die versorgten stehen nicht darin");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =======================================================================
|
||||||
|
4. SPICY MEDIA SIEHT VANVAN NICHT
|
||||||
|
|
||||||
|
Die Hausregel gilt auch hier -- und zwar über dieselbe zentrale
|
||||||
|
Funktion, nicht über eine eigene Fassung.
|
||||||
|
======================================================================= */
|
||||||
|
console.log("\n=== Niemand sieht VanVan außer DogFather");
|
||||||
|
{
|
||||||
|
const alsSpicy = await (await ruf("/workspace/api/team", kSpicy)).json();
|
||||||
|
const text = JSON.stringify(alsSpicy);
|
||||||
|
ok(!text.includes("VanVan"), "in der Spicy-Ansicht steht VanVan nirgends");
|
||||||
|
/* Sie ist Admin und stünde ohnehin nicht in der Liste -- geprüft wird
|
||||||
|
deshalb, dass die Regel überhaupt greift: DogFather sieht sie in
|
||||||
|
der Personenliste, Spicy nicht. */
|
||||||
|
const beiDogi = await (await ruf("/workspace/api/verwaltung/personen", kDogi)).json();
|
||||||
|
const beiSpicy = await (await ruf("/workspace/api/verwaltung/personen", kSpicy)).json();
|
||||||
|
const nDogi = (beiDogi.personen || []).some((x) => x.name === "VanVan");
|
||||||
|
const nSpicy = (beiSpicy.personen || []).some((x) => x.name === "VanVan");
|
||||||
|
ok(nDogi && !nSpicy,
|
||||||
|
`Gegenprobe an der Personenliste: DogFather sieht sie (${nDogi}), Spicy nicht (${nSpicy})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =======================================================================
|
||||||
|
5. IM BROWSER
|
||||||
|
======================================================================= */
|
||||||
|
console.log("\n=== Auf der Seite");
|
||||||
|
{
|
||||||
|
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||||||
|
const browser = await pw.chromium.launch();
|
||||||
const konsole = [];
|
const konsole = [];
|
||||||
const seite = await kontext.newPage();
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 1200 } });
|
||||||
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
const s = await ctx.newPage();
|
||||||
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
|
s.on("pageerror", (f) => konsole.push("Absturz: " + f.message));
|
||||||
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
s.on("console", (m) => { if (m.type() === "error") konsole.push("Konsole: " + m.text()); });
|
||||||
await seite.click(`.rolle[data-rolle="${rolle}"]`);
|
await s.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
||||||
await seite.fill("#code", code);
|
await s.click('.rolle[data-rolle="admin"]');
|
||||||
await seite.click("button[type=submit]");
|
await s.fill("#code", "CODE-DOGI-0001");
|
||||||
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
await s.click("button[type=submit]");
|
||||||
return { kontext, seite, konsole };
|
await s.waitForURL("**/start.html", { timeout: 20000 });
|
||||||
|
await s.waitForTimeout(1200);
|
||||||
|
|
||||||
|
/* Die Kachel steht auf der Startseite und führt hin. */
|
||||||
|
const kachel = await s.evaluate(() => {
|
||||||
|
const k = [...document.querySelectorAll(".kachel")]
|
||||||
|
.find((x) => /Team-Lage/.test(x.textContent));
|
||||||
|
return k ? { da: true, ziel: k.getAttribute("href") || k.dataset.ziel || "",
|
||||||
|
gruppe: k.closest(".gruppe")?.textContent?.slice(0, 40) } : { da: false };
|
||||||
|
});
|
||||||
|
ok(kachel.da, `die Kachel steht auf der Startseite (Gruppe: ${kachel.gruppe})`);
|
||||||
|
|
||||||
|
await s.goto(BASIS + "/workspace/team.html", { waitUntil: "networkidle" });
|
||||||
|
await s.waitForTimeout(1500);
|
||||||
|
const m = await s.evaluate(() => ({
|
||||||
|
leute: document.querySelectorAll(".tperson").length,
|
||||||
|
zahlen: document.querySelectorAll(".tz").length,
|
||||||
|
luecken: document.querySelectorAll(".tl").length,
|
||||||
|
pipelines: document.querySelectorAll(".tpipe").length,
|
||||||
|
grenze: document.querySelector(".t-kopf__grenze")?.textContent?.trim().slice(0, 60),
|
||||||
|
warnungen: document.querySelectorAll('.tz[data-art="warn"]').length,
|
||||||
|
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||||
|
}));
|
||||||
|
ok(m.leute === 3, `drei Personenkarten (${m.leute})`);
|
||||||
|
ok(m.pipelines === 2, `zwei Pipelines — nur die Scouts (${m.pipelines})`);
|
||||||
|
ok(m.luecken >= 2, `die Lücken stehen oben (${m.luecken} Kästen)`);
|
||||||
|
ok(m.warnungen > 0, `und was liegengeblieben ist, ist markiert (${m.warnungen})`);
|
||||||
|
ok(/Termine, Chats und Dateien/.test(m.grenze || ""),
|
||||||
|
`der Kopf sagt, was NICHT darin steht ("${m.grenze}…")`);
|
||||||
|
ok(m.ueber <= 1, `nichts steht seitlich über (${m.ueber} px)`);
|
||||||
|
if (process.env.BILD === "ja") {
|
||||||
|
await s.screenshot({ path: "_team-gross.png", fullPage: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Am Handy ebenfalls. */
|
||||||
|
const ctx2 = await browser.newContext({ viewport: { width: 390, height: 900 } });
|
||||||
|
const s2 = await ctx2.newPage();
|
||||||
|
await s2.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
||||||
|
await s2.click('.rolle[data-rolle="admin"]');
|
||||||
|
await s2.fill("#code", "CODE-DOGI-0001");
|
||||||
|
await s2.click("button[type=submit]");
|
||||||
|
await s2.waitForURL("**/start.html", { timeout: 20000 });
|
||||||
|
await s2.goto(BASIS + "/workspace/team.html", { waitUntil: "networkidle" });
|
||||||
|
await s2.waitForTimeout(1400);
|
||||||
|
const h = await s2.evaluate(() => ({
|
||||||
|
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||||
|
leute: document.querySelectorAll(".tperson").length,
|
||||||
|
breiter: [...document.querySelectorAll(".tperson, .tz, .tl")]
|
||||||
|
.filter((e) => e.getBoundingClientRect().right > 391).length,
|
||||||
|
}));
|
||||||
|
ok(h.ueber <= 1 && h.breiter === 0,
|
||||||
|
`am Handy nichts über (${h.ueber} px, ${h.breiter} zu breit) bei ${h.leute} Karten`);
|
||||||
|
if (process.env.BILD === "ja") {
|
||||||
|
await s2.screenshot({ path: "_team-handy.png", fullPage: true });
|
||||||
|
}
|
||||||
|
await ctx2.close();
|
||||||
|
|
||||||
|
/* ---- LESBAR, NICHT NUR DA (09.09.2026) --------------------------
|
||||||
|
Die Lektion vom Vorlagenbrett: Eine Karte mit sieben Prozent Farbe
|
||||||
|
auf durchsichtigem Grund laesst das Buehnenfoto durch, und dann
|
||||||
|
haengt die Lesbarkeit davon ab, welche Stelle des Fotos gerade
|
||||||
|
dahinterliegt. Dort waren es 3,61:1, wo 4,5 noetig sind.
|
||||||
|
|
||||||
|
Gemessen wird gegen die WIRKLICHE Flaeche: Der Text wird kurz
|
||||||
|
unsichtbar gemacht und die Flaeche fotografiert. */
|
||||||
|
{
|
||||||
|
const leuchtkraft = (r, g, b) => {
|
||||||
|
const f = (w) => { const v = w / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
|
||||||
|
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
||||||
|
};
|
||||||
|
const kontrast = (a, b) => {
|
||||||
|
const l1 = leuchtkraft(...a), l2 = leuchtkraft(...b);
|
||||||
|
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
||||||
|
};
|
||||||
|
async function messen(waehler) {
|
||||||
|
const stelle = await s.evaluate((w) => {
|
||||||
|
const e = document.querySelector(w);
|
||||||
|
if (!e) return null;
|
||||||
|
e.scrollIntoView({ block: "center" });
|
||||||
|
const r = e.getBoundingClientRect();
|
||||||
|
if (r.width < 8 || r.height < 6) return null;
|
||||||
|
/* Die Farbe wird GEMALT und zurueckgelesen -- getComputedStyle
|
||||||
|
liefert bei color-mix() "color(srgb ...)", und ein Muster
|
||||||
|
ueber die Ziffern liest daraus Unsinn. */
|
||||||
|
const c = document.createElement("canvas");
|
||||||
|
c.width = 1; c.height = 1;
|
||||||
|
const g = c.getContext("2d", { willReadFrequently: true });
|
||||||
|
g.fillStyle = "#000000";
|
||||||
|
g.fillStyle = getComputedStyle(e).color;
|
||||||
|
g.fillRect(0, 0, 1, 1);
|
||||||
|
const px = g.getImageData(0, 0, 1, 1).data;
|
||||||
|
e.dataset.messen = "ja";
|
||||||
|
e.style.color = "transparent";
|
||||||
|
return { x: Math.round(r.x), y: Math.round(r.y),
|
||||||
|
w: Math.round(r.width), h: Math.round(Math.min(r.height, 40)),
|
||||||
|
farbe: [px[0], px[1], px[2]] };
|
||||||
|
}, waehler);
|
||||||
|
if (!stelle) return null;
|
||||||
|
const bild = await s.screenshot({
|
||||||
|
clip: { x: stelle.x, y: stelle.y, width: stelle.w, height: stelle.h } });
|
||||||
|
const hinten = await s.evaluate(async (b64) => {
|
||||||
|
const bild = new Image();
|
||||||
|
await new Promise((r) => { bild.onload = r; bild.src = "data:image/png;base64," + b64; });
|
||||||
|
const c = document.createElement("canvas");
|
||||||
|
c.width = bild.width; c.height = bild.height;
|
||||||
|
const g = c.getContext("2d", { willReadFrequently: true });
|
||||||
|
g.drawImage(bild, 0, 0);
|
||||||
|
const dd = g.getImageData(0, 0, c.width, c.height).data;
|
||||||
|
let r = 0, gr = 0, bl = 0, n = 0;
|
||||||
|
for (let i = 0; i < dd.length; i += 4) { r += dd[i]; gr += dd[i + 1]; bl += dd[i + 2]; n++; }
|
||||||
|
return [Math.round(r / n), Math.round(gr / n), Math.round(bl / n)];
|
||||||
|
}, bild.toString("base64"));
|
||||||
|
await s.evaluate(() => {
|
||||||
|
const e = document.querySelector('[data-messen="ja"]');
|
||||||
|
if (e) { e.style.color = ""; delete e.dataset.messen; }
|
||||||
|
});
|
||||||
|
return { farbe: stelle.farbe, hinten, wert: kontrast(stelle.farbe, hinten) };
|
||||||
|
}
|
||||||
|
for (const [name, waehler] of [
|
||||||
|
["Lücken-Titel", ".tl__titel"],
|
||||||
|
["Lücken-Namen", ".tl__namen"],
|
||||||
|
["Kennzahl", ".tz__wert"],
|
||||||
|
["Beschriftung", ".tz__schild"],
|
||||||
|
["Kennzahl (Warnung)", '.tz[data-art="warn"] .tz__wert'],
|
||||||
|
["Name", ".tperson__name"],
|
||||||
|
["Zuletzt da", ".tperson__zuletzt"],
|
||||||
|
["Grenzsatz", ".t-kopf__grenze"],
|
||||||
|
]) {
|
||||||
|
const m = await messen(waehler);
|
||||||
|
ok(m !== null && m.wert >= 4.5,
|
||||||
|
`${name}: ${m ? m.wert.toFixed(2) + ":1 (" + m.farbe.join(",") + " auf " + m.hinten.join(",") + ")" : "nicht gefunden"}`);
|
||||||
|
}
|
||||||
|
/* GEGENPROBE: ein absichtlich zu dunkler Text MUSS durchfallen. */
|
||||||
|
await s.evaluate(() => { document.querySelector(".tz__wert").style.color = "#131820"; });
|
||||||
|
const schlecht = await messen(".tz__wert");
|
||||||
|
ok(schlecht !== null && schlecht.wert < 4.5,
|
||||||
|
`absichtlich zu dunkler Text fällt durch (${schlecht?.wert.toFixed(2)}:1)`);
|
||||||
|
await s.evaluate(() => { document.querySelector(".tz__wert").style.color = ""; });
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(konsole.length === 0, konsole.length ? "Konsole: " + konsole[0] : "keine Fehler in der Konsole");
|
||||||
|
/* GEGENPROBE zum Lauscher. */
|
||||||
|
await s.evaluate(() => { setTimeout(() => { throw new Error("Absicht"); }, 0); });
|
||||||
|
await s.waitForTimeout(400);
|
||||||
|
ok(konsole.some((z) => /Absicht/.test(z)), "ein absichtlicher Fehler wird bemerkt");
|
||||||
|
await ctx.close();
|
||||||
|
await browser.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================================================================
|
console.log(`\n${geprueft} Prüfungen`);
|
||||||
1. Ein echtes Bild hochladen -- und pruefen, dass es ueberall ankommt
|
console.log(fehler ? `${fehler} FEHLER` : "ALLES IN ORDNUNG");
|
||||||
================================================================ */
|
|
||||||
console.log("\n=== Das eigene Bild (Screen 5)");
|
|
||||||
const a1 = await anmelden("admin", "CODE-DOGI-0001");
|
|
||||||
|
|
||||||
const hoch = await a1.seite.evaluate(async (b64) => {
|
|
||||||
const roh = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
||||||
const a = await fetch("/workspace/api/steckbrief/bild", {
|
|
||||||
method: "POST", credentials: "same-origin",
|
|
||||||
headers: { "Content-Type": "image/png" }, body: roh,
|
|
||||||
});
|
|
||||||
return { status: a.status, ...(await a.json().catch(() => ({}))) };
|
|
||||||
}, BILD.toString("base64"));
|
|
||||||
ok(hoch.status === 200 && !!hoch.bild, `ein echtes Bild (600x600) laesst sich hochladen (${hoch.status})`);
|
|
||||||
|
|
||||||
/* Kommt es auf der STARTSEITE in der Begruessung an? Das war der
|
|
||||||
gemeldete Fehler: Dort stand der Buchstabe, obwohl ein Foto da war --
|
|
||||||
und im Server steht seit jeher der Kommentar "es steht in der
|
|
||||||
Kopfleiste JEDER Seite UND IN DER BEGRUESSUNG". Die Absicht war
|
|
||||||
aufgeschrieben, die Haelfte nie gebaut. */
|
|
||||||
await a1.seite.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" });
|
|
||||||
await a1.seite.waitForTimeout(1500);
|
|
||||||
const gruss = await a1.seite.evaluate(() => {
|
|
||||||
const p = document.getElementById("plakette");
|
|
||||||
const b = p?.querySelector("img");
|
|
||||||
if (!b) return { bild: false, text: p?.textContent || "" };
|
|
||||||
const r = b.getBoundingClientRect();
|
|
||||||
const pr = p.getBoundingClientRect();
|
|
||||||
const st = getComputedStyle(b);
|
|
||||||
const ps = getComputedStyle(p);
|
|
||||||
return {
|
|
||||||
bild: true,
|
|
||||||
geladen: b.complete && b.naturalWidth > 0,
|
|
||||||
/* Fuellt es die Plakette wirklich aus? Ein Bild, das nur zu einem
|
|
||||||
Viertel drinsteht, sieht aus wie ein Fehler. */
|
|
||||||
fuellt: r.width >= pr.width - 2 && r.height >= pr.height - 2,
|
|
||||||
/* Rund beschnitten -- ohne overflow:hidden ragt ein quadratisches
|
|
||||||
Bild aus dem runden Rahmen. */
|
|
||||||
beschnitten: ps.overflow === "hidden",
|
|
||||||
passung: st.objectFit,
|
|
||||||
kein_text: (p.textContent || "").trim() === "",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
ok(gruss.bild === true, "die Begruessung zeigt das Bild statt des Buchstabens");
|
|
||||||
ok(gruss.geladen === true, " und es ist wirklich geladen (nicht nur ein leerer Rahmen)");
|
|
||||||
ok(gruss.fuellt === true, " es fuellt die Plakette aus");
|
|
||||||
ok(gruss.beschnitten === true, " und ist rund beschnitten (overflow: hidden)");
|
|
||||||
ok(gruss.passung === "cover", ` ohne zu verzerren (object-fit: ${gruss.passung})`);
|
|
||||||
ok(gruss.kein_text === true, " der Buchstabe steht nicht mehr daneben");
|
|
||||||
|
|
||||||
/* ================================================================
|
|
||||||
2. Das Team -- alle Rollen, mit allem, was sie eingetragen haben
|
|
||||||
================================================================ */
|
|
||||||
console.log("\n=== Das Team (Screen 2)");
|
|
||||||
await a1.seite.goto(BASIS + "/workspace/steckbrief.html", { waitUntil: "networkidle" });
|
|
||||||
await a1.seite.waitForTimeout(1600);
|
|
||||||
|
|
||||||
const team = await a1.seite.evaluate(() => {
|
|
||||||
const block = document.getElementById("team-block");
|
|
||||||
const karten = [...document.querySelectorAll(".team-karte")];
|
|
||||||
const sichtbar = (e) => {
|
|
||||||
const r = e.getBoundingClientRect();
|
|
||||||
return r.width > 0 && r.height > 0;
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
da: block && !block.hidden,
|
|
||||||
/* IST DER STIL ANGEKOMMEN? Die .block__*-Klassen stehen in
|
|
||||||
automation.css und wissen.css -- diese Seite laedt beide nicht.
|
|
||||||
Genau so sind hier schon zweimal Stile ins Leere gelaufen.
|
|
||||||
Gemessen wird deshalb der tatsaechliche Stil, nicht die Klasse. */
|
|
||||||
/* SEIT DEM 07.09.2026 IST DER RAND KEINE `border` MEHR, sondern
|
|
||||||
eine Silhouette mit abgeschnittener Ecke aus module.css. Die
|
|
||||||
alte Bedingung (border >= 1) wurde dadurch zwangsläufig rot,
|
|
||||||
ohne dass ein Stil fehlte. Die FRAGE bleibt aber genau dieselbe
|
|
||||||
und ist so wichtig wie zuvor: Ist der Stil auf DIESER Seite
|
|
||||||
angekommen? Gemessen wird sie jetzt an der neuen Bauweise --
|
|
||||||
Polsterung wie bisher, dazu die Silhouette, die es nur gibt,
|
|
||||||
wenn module.css wirklich geladen wurde. */
|
|
||||||
gestaltet: karten[0] ? (() => {
|
|
||||||
const st = getComputedStyle(karten[0]);
|
|
||||||
return parseFloat(st.paddingLeft) >= 8 && /polygon/.test(st.clipPath);
|
|
||||||
})() : null,
|
|
||||||
anzahl: karten.length,
|
|
||||||
sichtbare: karten.filter(sichtbar).length,
|
|
||||||
gruppen: [...document.querySelectorAll(".team-gruppe__name")].map((e) => e.textContent),
|
|
||||||
leute: karten.map((k) => ({
|
|
||||||
name: k.querySelector(".team-karte__name")?.textContent,
|
|
||||||
rolle: k.dataset.rolle,
|
|
||||||
rollenname: k.querySelector(".team-karte__rolle")?.textContent,
|
|
||||||
ueber: k.querySelector(".team-karte__ueber")?.textContent || null,
|
|
||||||
bild: !!k.querySelector(".team-karte__bild img"),
|
|
||||||
buchstabe: k.querySelector(".team-karte__bild img")
|
|
||||||
? null : (k.querySelector(".team-karte__bild")?.textContent || "").trim(),
|
|
||||||
kanaele: [...k.querySelectorAll(".team-kanal")].map((a) => a.getAttribute("href")),
|
|
||||||
ich: k.dataset.ich === "ja",
|
|
||||||
})),
|
|
||||||
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
ok(team.da === true, "der Team-Block erscheint");
|
|
||||||
ok(team.gestaltet === true, " und die Stile sind angekommen (Rand und Polsterung gemessen)");
|
|
||||||
ok(team.anzahl === 4 && team.sichtbare === 4,
|
|
||||||
`alle vier Personen stehen da (${team.anzahl}, davon ${team.sichtbare} sichtbar)`);
|
|
||||||
ok(team.gruppen.join(", ") === "Management, Scouting, Creator",
|
|
||||||
`nach Rollen gruppiert: ${team.gruppen.join(", ")}`);
|
|
||||||
|
|
||||||
const dogi = team.leute.find((l) => l.name === "Filipe");
|
|
||||||
const mara = team.leute.find((l) => l.name === "Mara");
|
|
||||||
const pat = team.leute.find((l) => l.name === "Patrick");
|
|
||||||
const tili = team.leute.find((l) => l.name === "Tili");
|
|
||||||
|
|
||||||
ok(!!dogi && !!mara && !!pat && !!tili,
|
|
||||||
"Manager, Scout, DogFather UND Creator sind dabei – nicht nur Creator");
|
|
||||||
ok(dogi?.bild === true, "DogFather wird mit seinem Foto gezeigt");
|
|
||||||
ok(dogi?.ueber === "Manager & CEO dieser Seite",
|
|
||||||
` und mit dem, was er ueber sich geschrieben hat ("${dogi?.ueber}")`);
|
|
||||||
ok(dogi?.ich === true, " seine eigene Karte ist als solche gekennzeichnet");
|
|
||||||
ok(/tiktok\.com/.test(dogi?.kanaele?.[0] || ""),
|
|
||||||
` sein Kanal fuehrt wirklich hin (${dogi?.kanaele?.[0]})`);
|
|
||||||
ok(mara?.ueber === "Ich kuemmere mich um die Vertraege.", "der Manager-Text kommt an");
|
|
||||||
ok(pat?.ueber === "Ich finde neue Leute.", "der Scout-Text kommt an");
|
|
||||||
ok(mara?.rollenname === "Manager" && pat?.rollenname === "Scout",
|
|
||||||
`die Rolle steht dabei (${mara?.rollenname}, ${pat?.rollenname})`);
|
|
||||||
|
|
||||||
/* GEGENPROBEN -- ohne sie waere oben alles auch dann gruen, wenn die
|
|
||||||
Seite einfach ueberall Bilder und Texte hinschriebe. */
|
|
||||||
ok(mara?.bild === false && mara?.buchstabe === "M",
|
|
||||||
`Gegenprobe: wer kein Foto hat, bekommt den Buchstaben ("${mara?.buchstabe}")`);
|
|
||||||
ok(tili?.ueber === null,
|
|
||||||
"Gegenprobe: wer nichts ueber sich geschrieben hat, bekommt keinen Platzhaltersatz");
|
|
||||||
ok(tili?.kanaele?.length === 0,
|
|
||||||
"Gegenprobe: ohne Kanal steht dort auch keiner");
|
|
||||||
ok(mara?.ich === false, "Gegenprobe: fremde Karten sind nicht als 'du' markiert");
|
|
||||||
ok(team.ueber <= 0, `nichts steht seitlich ueber (${team.ueber}px)`);
|
|
||||||
ok(a1.konsole.length === 0,
|
|
||||||
`keine Fehler in der Konsole${a1.konsole.length ? " -> " + a1.konsole.join(" | ") : ""}`);
|
|
||||||
|
|
||||||
await a1.seite.screenshot({ path: "pruef-team.png", fullPage: false });
|
|
||||||
await a1.kontext.close();
|
|
||||||
|
|
||||||
/* ================================================================
|
|
||||||
3. Ein Creator sieht seine Betreuung -- aber nicht die anderen Creator
|
|
||||||
================================================================ */
|
|
||||||
console.log("\n=== Was ein Creator davon sieht");
|
|
||||||
const a2 = await anmelden("creator", "CODE-ANNA-0001");
|
|
||||||
await a2.seite.goto(BASIS + "/workspace/profil.html", { waitUntil: "networkidle" });
|
|
||||||
await a2.seite.waitForTimeout(1600);
|
|
||||||
const sicht = await a2.seite.evaluate(() => ({
|
|
||||||
da: !document.getElementById("team-block")?.hidden,
|
|
||||||
namen: [...document.querySelectorAll(".team-karte__name")].map((e) => e.textContent).sort(),
|
|
||||||
}));
|
|
||||||
ok(sicht.da === true, "ein Creator sieht das Team auf seiner Profilseite");
|
|
||||||
ok(sicht.namen.includes("Patrick"), " seinen Scout");
|
|
||||||
ok(sicht.namen.includes("Filipe") && sicht.namen.includes("Mara"),
|
|
||||||
" und das Management");
|
|
||||||
ok(a2.konsole.length === 0,
|
|
||||||
`keine Fehler in der Konsole${a2.konsole.length ? " -> " + a2.konsole.join(" | ") : ""}`);
|
|
||||||
await a2.kontext.close();
|
|
||||||
|
|
||||||
/* Ein zweiter Creator, um zu pruefen, dass Creator einander NICHT sehen.
|
|
||||||
Das ist kein Schoenheitsfehler: Der Server entscheidet das, und wenn
|
|
||||||
sich die Regel je aendert, muss es hier auffallen. */
|
|
||||||
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
||||||
const salt = randomBytes(16).toString("hex");
|
|
||||||
const hash = scryptSync("CODE-LUNA-0001", salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
|
||||||
d2.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
|
||||||
.run("Luna", "creator", hash, salt, 32768, jetzt);
|
|
||||||
d2.close();
|
|
||||||
|
|
||||||
const a3 = await anmelden("creator", "CODE-ANNA-0001");
|
|
||||||
await a3.seite.goto(BASIS + "/workspace/profil.html", { waitUntil: "networkidle" });
|
|
||||||
await a3.seite.waitForTimeout(1600);
|
|
||||||
const fremd = await a3.seite.evaluate(() =>
|
|
||||||
[...document.querySelectorAll(".team-karte__name")].map((e) => e.textContent));
|
|
||||||
ok(!fremd.includes("Luna"),
|
|
||||||
`Gegenprobe: ein Creator sieht einen fremden Creator NICHT (${fremd.join(", ")})`);
|
|
||||||
await a3.kontext.close();
|
|
||||||
|
|
||||||
await browser.close();
|
|
||||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
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);
|
process.exit(fehler ? 1 : 0);
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
/* WAS DARF LIEGENBLEIBEN, UND WAS NICHT (09.09.2026)
|
||||||
|
|
||||||
|
DER FALL, DER DAS AUSGELÖST HAT: Filipe meldete, eine Änderung an der
|
||||||
|
Kopfleiste sei "nicht gemacht". Gemessen war sie auf allen 18 Seiten,
|
||||||
|
in allen fünf Rollen, bei drei Breiten in Ordnung -- und die neuen
|
||||||
|
Dateien lagen nachweislich auf dem Server. Er sah trotzdem den alten
|
||||||
|
Stand.
|
||||||
|
|
||||||
|
Die Ursache: Die HTML-Seiten gingen ohne jede Cache-Angabe hinaus.
|
||||||
|
Ohne `Cache-Control` und ohne `Expires` darf ein Browser heuristisch
|
||||||
|
zwischenspeichern. Und in der HTML stehen die VERSIONSSTEMPEL der
|
||||||
|
Stilvorlagen -- eine alte HTML holt also auch alte CSS- und
|
||||||
|
JS-Dateien. Der ganze Stempelmechanismus hing an einer Datei, die
|
||||||
|
selbst liegenbleiben durfte.
|
||||||
|
|
||||||
|
DIESE PRÜFUNG IST DER GRUND, WARUM ES NICHT WIEDERKOMMT. Sie fragt
|
||||||
|
echte Adressen ab und liest die Kopfzeilen. Eine Regel im Quelltext
|
||||||
|
zu lesen wäre wertlos: Zwischen `setHeaders` und dem, was ankommt,
|
||||||
|
liegen Express, Caddy und Cloudflare.
|
||||||
|
|
||||||
|
DREI AUSGÄNGE: 0 in Ordnung · 1 Befunde · 2 konnte nicht arbeiten. */
|
||||||
|
|
||||||
|
import { mkdtempSync, rmSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
const ordner = mkdtempSync(join(tmpdir(), "ws-cache-"));
|
||||||
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||||
|
|
||||||
|
import { notbremse } from "./helfer-notbremse.mjs";
|
||||||
|
const { portMussFreiSein } = await import("./helfer-port.mjs");
|
||||||
|
await portMussFreiSein(4343, "die Zwischenspeicher-Pruefung");
|
||||||
|
|
||||||
|
process.env.PORT = "4343";
|
||||||
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||||||
|
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");
|
||||||
|
notbremse(120_000, "pruef-zwischenspeicher");
|
||||||
|
await new Promise((r) => setTimeout(r, 900));
|
||||||
|
const BASIS = "http://127.0.0.1:4343";
|
||||||
|
|
||||||
|
let fehler = 0, geprueft = 0;
|
||||||
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||||
|
|
||||||
|
/* ANGEMELDET MESSEN, SONST MISST MAN EINE UMLEITUNG.
|
||||||
|
|
||||||
|
Der erste Anlauf fragte die Workspace-Seiten ohne Anmeldung ab und
|
||||||
|
meldete "keine Cache-Angabe" fuer drei von ihnen -- der Server hatte
|
||||||
|
aber gar keine Seite geschickt, sondern eine Umleitung zur Anmeldung,
|
||||||
|
und eine Umleitung traegt zu Recht keine.
|
||||||
|
|
||||||
|
Das ist genau die Sorte Befund, die teuer ist: Sie sieht aus wie ein
|
||||||
|
Fehler in der Sache und ist einer in der Messung. Die Pruefung
|
||||||
|
braucht ihre Voraussetzung, bevor sie misst. */
|
||||||
|
const { DatabaseSync } = await import("node:sqlite");
|
||||||
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
||||||
|
await fetch(BASIS + "/workspace/api/anmelden", { method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
||||||
|
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 salt = randomBytes(16).toString("hex");
|
||||||
|
const hash = scryptSync("CODE-DOGI-0001", 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("Filipe", "admin", hash, salt, 32768, new Date().toISOString());
|
||||||
|
d.close();
|
||||||
|
}
|
||||||
|
const anAntwort = await fetch(BASIS + "/workspace/api/anmelden", {
|
||||||
|
method: "POST", headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }) });
|
||||||
|
const KEKS = (anAntwort.headers.getSetCookie?.() || []).map((x) => x.split(";")[0]).join("; ");
|
||||||
|
if (!KEKS) {
|
||||||
|
console.error("\nKONNTE NICHT ARBEITEN: keine Anmeldung, also keine Seiten zu messen.");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const kopf = async (pfad) => {
|
||||||
|
const a = await fetch(BASIS + pfad, { redirect: "manual", headers: { cookie: KEKS } });
|
||||||
|
return { status: a.status, cc: a.headers.get("cache-control") };
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("\n=== HTML wird immer nachgefragt");
|
||||||
|
for (const p of ["/workspace/start.html", "/workspace/aufgaben.html",
|
||||||
|
"/workspace/kalender.html", "/index.html"]) {
|
||||||
|
const r = await kopf(p);
|
||||||
|
/* DIE NUMMER GEHOERT IN DIE BEDINGUNG. Eine Umleitung traegt zu Recht
|
||||||
|
keine Cache-Angabe -- wer sie mitmisst, sucht einen Fehler, den es
|
||||||
|
nicht gibt. */
|
||||||
|
ok(r.status === 200 && /no-cache|no-store|max-age=0/.test(r.cc || ""),
|
||||||
|
`${p}: HTTP ${r.status}, ${r.cc || "(keine Angabe)"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n=== Was einen Stempel trägt, darf liegenbleiben");
|
||||||
|
for (const p of ["/workspace/assets/css/start.css", "/workspace/assets/js/kopf.js",
|
||||||
|
"/workspace/assets/css/module.css"]) {
|
||||||
|
const r = await kopf(p);
|
||||||
|
ok(/max-age=\d{6,}/.test(r.cc || ""), `${p}: ${r.cc || "(keine Angabe)"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n=== Der Service Worker NICHT");
|
||||||
|
{
|
||||||
|
const r = await kopf("/workspace/sw.js");
|
||||||
|
ok(/no-cache/.test(r.cc || ""),
|
||||||
|
`sw.js wird ohne Stempel geladen und muss nachgefragt werden: ${r.cc || "(keine)"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n=== Bilder: ein Tag, nicht ein Jahr");
|
||||||
|
{
|
||||||
|
const r = await kopf("/workspace/assets/img/marke-husky.webp");
|
||||||
|
ok(r.status === 404 || /max-age=86400/.test(r.cc || ""),
|
||||||
|
`Bild ohne Stempel: ${r.cc || "(keine Angabe)"} (HTTP ${r.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------
|
||||||
|
GEGENPROBEN
|
||||||
|
|
||||||
|
1. Die Prüfung muss eine FEHLENDE Angabe erkennen -- sonst hätte sie
|
||||||
|
genau den Zustand durchgewinkt, der den Fehler verursacht hat.
|
||||||
|
2. Und sie darf nicht alles durchwinken: "no-cache" bei einer
|
||||||
|
Stilvorlage wäre auch falsch (jeder Aufruf eine Anfrage).
|
||||||
|
--------------------------------------------------------------------- */
|
||||||
|
console.log("\n=== Gegenproben");
|
||||||
|
{
|
||||||
|
const pruefHtml = (cc) => /no-cache|no-store|max-age=0/.test(cc || "");
|
||||||
|
ok(!pruefHtml(null), "eine fehlende Angabe fällt durch");
|
||||||
|
ok(!pruefHtml("public, max-age=31536000"), "und ein langer Speicher an einer HTML ebenfalls");
|
||||||
|
const pruefFest = (cc) => /max-age=\d{6,}/.test(cc || "");
|
||||||
|
ok(!pruefFest("no-cache"), "eine Stilvorlage ohne Speicher fällt ebenfalls auf");
|
||||||
|
ok(pruefFest("public, max-age=31536000, immutable"), "und die richtige Angabe besteht");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------
|
||||||
|
UND DER STEMPEL SELBST: Jede Stilvorlage und jedes Skript, das eine
|
||||||
|
HTML einbindet, MUSS einen tragen. Ohne ihn wäre ein Jahr Speicher
|
||||||
|
ein Jahr alter Stand -- die Regel oben und diese hier gehören
|
||||||
|
zusammen, sonst ist die eine ohne die andere gefährlich.
|
||||||
|
--------------------------------------------------------------------- */
|
||||||
|
console.log("\n=== Jede eingebundene Datei trägt einen Stempel");
|
||||||
|
{
|
||||||
|
const { readdirSync, readFileSync } = await import("node:fs");
|
||||||
|
const { dirname } = await import("node:path");
|
||||||
|
const { fileURLToPath } = await import("node:url");
|
||||||
|
const WURZEL = join(dirname(fileURLToPath(import.meta.url)), "..", "workspace");
|
||||||
|
const seiten = readdirSync(WURZEL).filter((n) => n.endsWith(".html"));
|
||||||
|
const ohne = [];
|
||||||
|
let mit = 0;
|
||||||
|
for (const s of seiten) {
|
||||||
|
const text = readFileSync(join(WURZEL, s), "utf8");
|
||||||
|
for (const t of text.matchAll(/(?:href|src)="(assets\/[^"]*\.(?:css|js|mjs))(\?[^"]*)?"/g)) {
|
||||||
|
if (t[2] && /\?v=\d+/.test(t[2])) mit++;
|
||||||
|
else ohne.push(`${s} → ${t[1]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ok(mit > 100, `${mit} eingebundene Dateien tragen einen Stempel`);
|
||||||
|
ok(ohne.length === 0,
|
||||||
|
ohne.length ? `OHNE Stempel: ${ohne.slice(0, 4).join(", ")}`
|
||||||
|
: `und keine einzige ohne (${seiten.length} Seiten geprüft)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n${geprueft} Prüfungen`);
|
||||||
|
console.log(fehler ? `${fehler} FEHLER` : "ALLES IN ORDNUNG");
|
||||||
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||||||
|
process.exit(fehler ? 1 : 0);
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
/* =====================================================================
|
||||||
|
workspace-team.js — die Arbeitslage von Managern und Scouts.
|
||||||
|
|
||||||
|
Wunsch Filipe, 09.09.2026: *"so eine kategorie wie über die creator
|
||||||
|
will ich dass nur für die spicy und dogfather rolle auch über manager
|
||||||
|
und scouts gibt ... ich will dass es so ultra krass gut ist dass die
|
||||||
|
spicy und dogfather rolle einen kompletten teil haben mit daten über
|
||||||
|
die arbeit von den manager und scout. keine geheimen sachen also
|
||||||
|
termine, chats und geheime dateien soll auch so bleiben dass keiner."*
|
||||||
|
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
DREI ENTSCHEIDUNGEN, DIE ALLES ANDERE TRAGEN
|
||||||
|
|
||||||
|
1. WAS HIER NICHT VORKOMMT, UND ZWAR GAR NICHT.
|
||||||
|
|
||||||
|
Keine Termine, keine Chats, keine Dateien -- weder Inhalte noch
|
||||||
|
Zählungen. Ausdrücklicher Wunsch, und er ist auch der richtige:
|
||||||
|
Ein Kalender verrät, wann jemand nicht da war; ein Chatzähler
|
||||||
|
verrät, mit wem jemand oft spricht. Beides ist Privatsache und
|
||||||
|
beantwortet keine Frage, die eine Agentur wirklich hat.
|
||||||
|
|
||||||
|
Die Abfragen unten rühren `termine`, `chat_*` und `dateien` nicht
|
||||||
|
an. Das ist keine Zusicherung im Kommentar, sondern eine
|
||||||
|
Eigenschaft des Codes -- pruef-team.mjs liest diese Datei und
|
||||||
|
schlägt an, wenn eine dieser Tabellen darin auftaucht.
|
||||||
|
|
||||||
|
2. SEGMENTIEREN, NICHT MITTELN.
|
||||||
|
|
||||||
|
Aus der Recherche: Ein Durchschnitt versteckt genau die Person,
|
||||||
|
bei der es klemmt. Zwei Scouts mit je 6 Creators und einer mit 18
|
||||||
|
ergeben einen Schnitt von 10 -- und der sagt nichts. Deshalb wird
|
||||||
|
hier JEDE Person einzeln ausgegeben, und die Oberfläche markiert
|
||||||
|
die Ausreißer.
|
||||||
|
|
||||||
|
3. ES IST EINE ARBEITSLAGE, KEINE ÜBERWACHUNG.
|
||||||
|
|
||||||
|
Auch das steht so in der Fachliteratur: Der Zweck ist, Stellen zu
|
||||||
|
finden, an denen Unterstützung fehlt -- nicht, Leute zu messen.
|
||||||
|
Deshalb sind die Kennzahlen so gewählt, dass sie auf ZUSTÄNDE
|
||||||
|
zeigen (unbetreute Creator, überfällige Aufgaben, liegengebliebene
|
||||||
|
Kontakte) und nicht auf Anwesenheit oder Fleiß.
|
||||||
|
|
||||||
|
---------------------------------------------------------------------
|
||||||
|
WER DARF HINEIN
|
||||||
|
|
||||||
|
Nur `spicy` und `admin`. Und Spicy Media sieht DogFathers zweiten
|
||||||
|
Zugang nicht -- dieselbe zentrale Regel wie überall
|
||||||
|
(`verborgeneIds`), nicht eine eigene Fassung davon.
|
||||||
|
===================================================================== */
|
||||||
|
|
||||||
|
import express from "express";
|
||||||
|
import {
|
||||||
|
db, sitzungLesen, verborgeneIds, heuteLokal, ROLLEN_SORTIERUNG,
|
||||||
|
} from "./workspace.js";
|
||||||
|
|
||||||
|
export const teamRouter = express.Router();
|
||||||
|
|
||||||
|
/* Wer diesen ganzen Bereich sehen darf. EINE Menge, an EINER Stelle --
|
||||||
|
die Seite fragt dieselbe über /workspace/api/ich ab. */
|
||||||
|
export const TEAM_ROLLEN = new Set(["spicy", "admin"]);
|
||||||
|
|
||||||
|
function nurLeitung(req, res, next) {
|
||||||
|
const person = sitzungLesen(req);
|
||||||
|
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||||||
|
/* 404 und nicht 403: Was man nicht sehen darf, soll es fuer den
|
||||||
|
Fragenden gar nicht geben. Ein 403 bestaetigt, dass es den Bereich
|
||||||
|
gibt -- und genau das ist die Auskunft, die man nicht geben will. */
|
||||||
|
if (!TEAM_ROLLEN.has(person.rolle)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||||
|
req.person = person;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
teamRouter.use("/workspace/api/team", nurLeitung);
|
||||||
|
|
||||||
|
const zahl = (x) => Number(x) || 0;
|
||||||
|
|
||||||
|
/* Tage zwischen zwei Zeitstempeln, auf eine Stelle gerundet. */
|
||||||
|
function tageZwischen(a, b) {
|
||||||
|
if (!a || !b) return null;
|
||||||
|
const d = (new Date(b) - new Date(a)) / 86400000;
|
||||||
|
return Number.isFinite(d) && d >= 0 ? Math.round(d * 10) / 10 : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =====================================================================
|
||||||
|
DIE ARBEITSLAGE
|
||||||
|
===================================================================== */
|
||||||
|
teamRouter.get("/workspace/api/team", (req, res) => {
|
||||||
|
try {
|
||||||
|
const d = db();
|
||||||
|
const heute = heuteLokal();
|
||||||
|
const weg = new Set(verborgeneIds(req.person));
|
||||||
|
|
||||||
|
/* ---- 1. Wer ist überhaupt da ---------------------------------- */
|
||||||
|
const leute = d.prepare(`
|
||||||
|
SELECT id, name, rolle, aktiv, erstellt, letzter_login, bild
|
||||||
|
FROM personen
|
||||||
|
WHERE rolle IN ('manager','scout')
|
||||||
|
ORDER BY ` + ROLLEN_SORTIERUNG + ", name").all()
|
||||||
|
.filter((p) => !weg.has(p.id));
|
||||||
|
|
||||||
|
/* ---- 2. Wen betreut wer --------------------------------------- */
|
||||||
|
/* EIN Creator kann nur EINEN Betreuer haben (betreuung ist so
|
||||||
|
gebaut). Ein Manager bekommt zusätzlich die Creator, die an
|
||||||
|
seinen Scouts hängen -- das ist seine mittelbare Verantwortung,
|
||||||
|
und ohne sie sähe ein Manager mit fünf Scouts aus wie jemand
|
||||||
|
ohne Arbeit. */
|
||||||
|
const betreut = d.prepare(`
|
||||||
|
SELECT b.betreuer_id AS wer, p.id AS creator_id, p.name AS creator, p.aktiv
|
||||||
|
FROM betreuung b JOIN personen p ON p.id = b.creator_id`).all();
|
||||||
|
const scoutsVon = d.prepare(`
|
||||||
|
SELECT z.manager_id AS wer, p.id AS scout_id, p.name AS scout
|
||||||
|
FROM scout_zuteilung z JOIN personen p ON p.id = z.scout_id`).all();
|
||||||
|
|
||||||
|
const direkt = new Map(); /* betreuer_id -> [creator] */
|
||||||
|
for (const z of betreut) {
|
||||||
|
if (!direkt.has(z.wer)) direkt.set(z.wer, []);
|
||||||
|
direkt.get(z.wer).push({ id: z.creator_id, name: z.creator, aktiv: !!z.aktiv });
|
||||||
|
}
|
||||||
|
const meineScouts = new Map(); /* manager_id -> [scout] */
|
||||||
|
for (const z of scoutsVon) {
|
||||||
|
if (!meineScouts.has(z.wer)) meineScouts.set(z.wer, []);
|
||||||
|
meineScouts.get(z.wer).push({ id: z.scout_id, name: z.scout });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 3. Aufgaben, nach Creator gruppiert ---------------------- */
|
||||||
|
/* Eine Abfrage fuer alle -- nicht eine je Person. Bei zwoelf Leuten
|
||||||
|
waeren das zwoelf Abfragen fuer dieselbe Tabelle. */
|
||||||
|
const aufgabenJeCreator = new Map();
|
||||||
|
for (const z of d.prepare(`
|
||||||
|
SELECT creator_id,
|
||||||
|
SUM(CASE WHEN status IN ('offen','arbeit','review') THEN 1 ELSE 0 END) AS laufend,
|
||||||
|
SUM(CASE WHEN status IN ('offen','arbeit','review')
|
||||||
|
AND frist IS NOT NULL AND frist < ? THEN 1 ELSE 0 END) AS ueberfaellig,
|
||||||
|
SUM(CASE WHEN status = 'erledigt' THEN 1 ELSE 0 END) AS erledigt,
|
||||||
|
SUM(CASE WHEN status = 'abgebrochen' THEN 1 ELSE 0 END) AS abgebrochen,
|
||||||
|
SUM(CASE WHEN status = 'erledigt' AND erledigt_am >= ? THEN 1 ELSE 0 END) AS erledigt30
|
||||||
|
FROM aufgaben WHERE creator_id IS NOT NULL
|
||||||
|
GROUP BY creator_id`).all(heute, vorTagen(30))) {
|
||||||
|
aufgabenJeCreator.set(z.creator_id, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Durchlaufzeit: von der Anlage bis zum Abhaken. Nur die letzten
|
||||||
|
neunzig Tage -- eine Zahl aus dem letzten Jahr beschreibt eine
|
||||||
|
Arbeitsweise, die es vielleicht nicht mehr gibt. */
|
||||||
|
const durchlauf = new Map();
|
||||||
|
for (const z of d.prepare(`
|
||||||
|
SELECT creator_id, erstellt, erledigt_am FROM aufgaben
|
||||||
|
WHERE status = 'erledigt' AND erledigt_am IS NOT NULL AND erledigt_am >= ?`)
|
||||||
|
.all(vorTagen(90))) {
|
||||||
|
const t = tageZwischen(z.erstellt, z.erledigt_am);
|
||||||
|
if (t === null) continue;
|
||||||
|
if (!durchlauf.has(z.creator_id)) durchlauf.set(z.creator_id, []);
|
||||||
|
durchlauf.get(z.creator_id).push(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 4. Fortschritt der betreuten Creator --------------------- */
|
||||||
|
const startcheckJeCreator = new Map();
|
||||||
|
for (const z of d.prepare(`
|
||||||
|
SELECT creator_id, COUNT(*) AS gesamt,
|
||||||
|
SUM(CASE WHEN bewertung IS NOT NULL AND bewertung <> '' AND bewertung <> 'offen'
|
||||||
|
THEN 1 ELSE 0 END) AS erledigt
|
||||||
|
FROM startcheck GROUP BY creator_id`).all()) {
|
||||||
|
startcheckJeCreator.set(z.creator_id, z);
|
||||||
|
}
|
||||||
|
const punkteJeCreator = new Map();
|
||||||
|
for (const z of d.prepare(`
|
||||||
|
SELECT creator_id, COUNT(*) AS gesamt,
|
||||||
|
SUM(CASE WHEN status = 'offen' THEN 1 ELSE 0 END) AS offen
|
||||||
|
FROM eintraege WHERE creator_id IS NOT NULL GROUP BY creator_id`).all()) {
|
||||||
|
punkteJeCreator.set(z.creator_id, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 5. Zahlen der betreuten Creator (30 Tage) ---------------- */
|
||||||
|
const leistungJeCreator = new Map();
|
||||||
|
for (const z of d.prepare(`
|
||||||
|
SELECT creator_id, SUM(diamanten) AS diamanten, SUM(dauer_min) AS minuten,
|
||||||
|
COUNT(*) AS tage, MAX(tag) AS letzter
|
||||||
|
FROM leistung WHERE tag >= ? GROUP BY creator_id`).all(vorTagen(30))) {
|
||||||
|
leistungJeCreator.set(z.creator_id, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 6. Die Pipeline der Scouts ------------------------------- */
|
||||||
|
const leadsJeScout = new Map();
|
||||||
|
for (const z of d.prepare(`
|
||||||
|
SELECT scout_id, COUNT(*) AS gesamt,
|
||||||
|
/* DIE STATUS STEHEN IN DER TABELLE, NICHT IN MEINEM KOPF.
|
||||||
|
Erster Anlauf war ('neu','kontakt','gespraech') -- "kontakt"
|
||||||
|
gibt es gar nicht, und die Datenbank hat es mit einer
|
||||||
|
CHECK-Bedingung sofort abgelehnt. Gut so: Ohne die
|
||||||
|
Bedingung waere "offen" still zu klein gewesen, und
|
||||||
|
niemand haette es gemerkt. */
|
||||||
|
SUM(CASE WHEN status IN ('neu','angesprochen','gespraech','interessiert')
|
||||||
|
THEN 1 ELSE 0 END) AS offen,
|
||||||
|
SUM(CASE WHEN status = 'uebergeben' THEN 1 ELSE 0 END) AS uebergeben,
|
||||||
|
SUM(CASE WHEN status = 'abgelehnt' THEN 1 ELSE 0 END) AS abgelehnt,
|
||||||
|
SUM(CASE WHEN naechster_followup IS NOT NULL AND naechster_followup < ?
|
||||||
|
AND status NOT IN ('uebergeben','abgelehnt') THEN 1 ELSE 0 END) AS faellig
|
||||||
|
FROM leads WHERE scout_id IS NOT NULL GROUP BY scout_id`).all(heute)) {
|
||||||
|
leadsJeScout.set(z.scout_id, z);
|
||||||
|
}
|
||||||
|
/* Wie lange von der Anlage bis zur Übergabe? Das ist die Zahl, die
|
||||||
|
eine Pipeline wirklich beschreibt -- eine Menge offener Kontakte
|
||||||
|
sagt nichts, wenn keiner davon je ankommt. */
|
||||||
|
const uebergabeJeScout = new Map();
|
||||||
|
for (const z of d.prepare(`
|
||||||
|
SELECT scout_id, erstellt, uebergeben_am FROM leads
|
||||||
|
WHERE uebergeben_am IS NOT NULL AND scout_id IS NOT NULL`).all()) {
|
||||||
|
const t = tageZwischen(z.erstellt, z.uebergeben_am);
|
||||||
|
if (t === null) continue;
|
||||||
|
if (!uebergabeJeScout.has(z.scout_id)) uebergabeJeScout.set(z.scout_id, []);
|
||||||
|
uebergabeJeScout.get(z.scout_id).push(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 7. Alles zu einer Zeile je Person zusammensetzen --------- */
|
||||||
|
const mittel = (liste) => (liste && liste.length
|
||||||
|
? Math.round((liste.reduce((s, x) => s + x, 0) / liste.length) * 10) / 10 : null);
|
||||||
|
|
||||||
|
const zeilen = leute.map((p) => {
|
||||||
|
const eigene = direkt.get(p.id) || [];
|
||||||
|
const scouts = meineScouts.get(p.id) || [];
|
||||||
|
/* Mittelbare Creator: die der eigenen Scouts. Ohne Doppelung,
|
||||||
|
falls ein Manager einen Creator auch direkt betreut. */
|
||||||
|
const mittelbar = [];
|
||||||
|
const schon = new Set(eigene.map((c) => c.id));
|
||||||
|
for (const s of scouts) {
|
||||||
|
for (const c of direkt.get(s.id) || []) {
|
||||||
|
if (!schon.has(c.id)) { schon.add(c.id); mittelbar.push(c); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const alleCreator = [...eigene, ...mittelbar];
|
||||||
|
|
||||||
|
const summe = (karte, feld) => alleCreator
|
||||||
|
.reduce((s, c) => s + zahl((karte.get(c.id) || {})[feld]), 0);
|
||||||
|
const zeiten = alleCreator.flatMap((c) => durchlauf.get(c.id) || []);
|
||||||
|
|
||||||
|
const scGesamt = summe(startcheckJeCreator, "gesamt");
|
||||||
|
const scFertig = summe(startcheckJeCreator, "erledigt");
|
||||||
|
const lead = leadsJeScout.get(p.id) || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: p.id, name: p.name, rolle: p.rolle, aktiv: !!p.aktiv,
|
||||||
|
seit: p.erstellt, letzter_login: p.letzter_login,
|
||||||
|
bild: p.bild ? `/workspace/api/steckbrief/bild/${p.bild}` : null,
|
||||||
|
|
||||||
|
creator: eigene.map((c) => ({ id: c.id, name: c.name, aktiv: c.aktiv })),
|
||||||
|
creator_mittelbar: mittelbar.length,
|
||||||
|
scouts: scouts.map((s) => s.name),
|
||||||
|
|
||||||
|
aufgaben: {
|
||||||
|
laufend: summe(aufgabenJeCreator, "laufend"),
|
||||||
|
ueberfaellig: summe(aufgabenJeCreator, "ueberfaellig"),
|
||||||
|
erledigt30: summe(aufgabenJeCreator, "erledigt30"),
|
||||||
|
abgebrochen: summe(aufgabenJeCreator, "abgebrochen"),
|
||||||
|
durchlauf_tage: mittel(zeiten),
|
||||||
|
},
|
||||||
|
fortschritt: {
|
||||||
|
startcheck_gesamt: scGesamt,
|
||||||
|
startcheck_fertig: scFertig,
|
||||||
|
punkte_gesamt: summe(punkteJeCreator, "gesamt"),
|
||||||
|
punkte_offen: summe(punkteJeCreator, "offen"),
|
||||||
|
},
|
||||||
|
zahlen: {
|
||||||
|
diamanten30: summe(leistungJeCreator, "diamanten"),
|
||||||
|
minuten30: summe(leistungJeCreator, "minuten"),
|
||||||
|
livetage30: summe(leistungJeCreator, "tage"),
|
||||||
|
},
|
||||||
|
pipeline: p.rolle === "scout" ? {
|
||||||
|
gesamt: zahl(lead.gesamt), offen: zahl(lead.offen),
|
||||||
|
uebergeben: zahl(lead.uebergeben), abgelehnt: zahl(lead.abgelehnt),
|
||||||
|
faellig: zahl(lead.faellig),
|
||||||
|
uebergabe_tage: mittel(uebergabeJeScout.get(p.id)),
|
||||||
|
} : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---- 8. Die Lücken ------------------------------------------- */
|
||||||
|
/* Das ist der wichtigste Teil der ganzen Seite. Eine Kennzahl sagt,
|
||||||
|
wie es läuft; eine Lücke sagt, wo etwas fehlt -- und nur das
|
||||||
|
Zweite kann man heute abstellen. */
|
||||||
|
const alleBetreut = new Set(betreut.map((z) => z.creator_id));
|
||||||
|
const ohneBetreuung = d.prepare(`
|
||||||
|
SELECT id, name FROM personen WHERE rolle = 'creator' AND aktiv = 1 ORDER BY name`)
|
||||||
|
.all().filter((c) => !alleBetreut.has(c.id) && !weg.has(c.id));
|
||||||
|
|
||||||
|
const zugeteilteScouts = new Set(scoutsVon.map((z) => z.scout_id));
|
||||||
|
const scoutsOhneManager = leute
|
||||||
|
.filter((p) => p.rolle === "scout" && p.aktiv && !zugeteilteScouts.has(p.id))
|
||||||
|
.map((p) => ({ id: p.id, name: p.name }));
|
||||||
|
|
||||||
|
const ohneCreator = zeilen
|
||||||
|
.filter((z) => z.aktiv && !z.creator.length && !z.creator_mittelbar)
|
||||||
|
.map((z) => ({ id: z.id, name: z.name, rolle: z.rolle }));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
stand: heute,
|
||||||
|
leute: zeilen,
|
||||||
|
luecken: {
|
||||||
|
creator_ohne_betreuung: ohneBetreuung,
|
||||||
|
scouts_ohne_manager: scoutsOhneManager,
|
||||||
|
ohne_zuteilung: ohneCreator,
|
||||||
|
},
|
||||||
|
/* Was hier ausdruecklich NICHT drinsteht -- die Oberflaeche sagt
|
||||||
|
es dem Betrachter, damit niemand danach sucht. */
|
||||||
|
ausgenommen: ["Termine", "Chats", "Dateien"],
|
||||||
|
});
|
||||||
|
} catch (fehler) {
|
||||||
|
console.error("[workspace] Team-Lage:", fehler?.message);
|
||||||
|
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Ein Datum vor N Tagen, in Ortszeit als "JJJJ-MM-TT".
|
||||||
|
* NICHT toISOString(): Das liefert UTC und damit nachts den Vortag --
|
||||||
|
* derselbe Fehler, der im Haus schon zweimal eine Zahl verschoben hat. */
|
||||||
|
function vorTagen(n) {
|
||||||
|
const d = new Date(Date.now() - n * 86400000);
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`
|
||||||
|
+ `-${String(d.getDate()).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
@@ -1982,6 +1982,19 @@ const GESCHUETZT = {
|
|||||||
"/workspace/bereich.html": ["spicy", "admin", "manager", "scout", "creator"],
|
"/workspace/bereich.html": ["spicy", "admin", "manager", "scout", "creator"],
|
||||||
"/workspace/report.html": ["spicy", "admin", "manager", "scout", "creator"],
|
"/workspace/report.html": ["spicy", "admin", "manager", "scout", "creator"],
|
||||||
"/workspace/scouting.html": ["spicy", "admin", "manager", "scout"],
|
"/workspace/scouting.html": ["spicy", "admin", "manager", "scout"],
|
||||||
|
/* DIE ARBEITSLAGE DES TEAMS (09.09.2026).
|
||||||
|
|
||||||
|
Filipe: "so eine kategorie wie ueber die creator will ich dass nur
|
||||||
|
fuer die spicy und dogfather rolle auch ueber manager und scouts
|
||||||
|
gibt."
|
||||||
|
|
||||||
|
Nur diese beiden Rollen. Ein Manager sieht seine eigene Zeile hier
|
||||||
|
ausdruecklich NICHT -- das ist kein Versehen: Eine Uebersicht ueber
|
||||||
|
die Arbeit der Kollegen gehoert der Leitung, nicht der Reihe. Der
|
||||||
|
Schutz steht ohnehin in der Schnittstelle (workspace-team.js);
|
||||||
|
diese Zeile sorgt nur dafuer, dass niemand vor einer leeren Seite
|
||||||
|
steht. */
|
||||||
|
"/workspace/team.html": ["spicy", "admin"],
|
||||||
"/workspace/calls.html": null,
|
"/workspace/calls.html": null,
|
||||||
"/workspace/startcheck.html": null,
|
"/workspace/startcheck.html": null,
|
||||||
"/workspace/wissen.html": null,
|
"/workspace/wissen.html": null,
|
||||||
|
|||||||
@@ -912,6 +912,23 @@ body.start > main { position: relative; z-index: 1; }
|
|||||||
Kontrast -- deshalb dieser und nicht der erstbeste. */
|
Kontrast -- deshalb dieser und nicht der erstbeste. */
|
||||||
[data-ton="21"] { --ton: #ff1f2e; } /* Scout-Pipeline -- Wunsch: knallrot */
|
[data-ton="21"] { --ton: #ff1f2e; } /* Scout-Pipeline -- Wunsch: knallrot */
|
||||||
|
|
||||||
|
/* TON 22 -- TEAM-LAGE (09.09.2026).
|
||||||
|
|
||||||
|
Die neue Gruppe "Rund um das Team" braucht einen eigenen Ton. Nicht
|
||||||
|
gewaehlt, sondern gerechnet: Von elf Kandidaten hat #ffd166 den
|
||||||
|
groessten Abstand zu allen einundzwanzig vorhandenen Toenen --
|
||||||
|
0,1165 in OKLab zu seinem naechsten Nachbarn (#afde57). Die
|
||||||
|
Hausgrenze ist das engste vorhandene Paar, und das liegt bei 0,0973;
|
||||||
|
der neue Ton ist also klarer unterscheidbar als das, was ohnehin
|
||||||
|
schon nebeneinander steht.
|
||||||
|
|
||||||
|
Kontrast auf dem dunklen Grund: 13,91:1.
|
||||||
|
|
||||||
|
Warm und hell ist kein Zufall: Diese eine Kachel gehoert der
|
||||||
|
Leitung. Zwischen den kuehlen Toenen der Arbeitsbereiche faellt ein
|
||||||
|
warmer auf, ohne lauter zu sein. */
|
||||||
|
[data-ton="22"] { --ton: #ffd166; } /* Team-Lage */
|
||||||
|
|
||||||
/* Rückfallwert für alles, was einen Ton braucht, aber keine Nummer hat.
|
/* Rückfallwert für alles, was einen Ton braucht, aber keine Nummer hat.
|
||||||
Steht bewusst in :where() -- damit zählt die Zeile beim Wettstreit der
|
Steht bewusst in :where() -- damit zählt die Zeile beim Wettstreit der
|
||||||
Regeln als NICHT vorhanden und die Nummer oben gewinnt immer. Ohne
|
Regeln als NICHT vorhanden und die Nummer oben gewinnt immer. Ohne
|
||||||
@@ -3754,6 +3771,30 @@ body[data-fremde-sicht="ja"]::after {
|
|||||||
300 px und nicht ganz weg: Darunter gibt es Geraete, auf denen es
|
300 px und nicht ganz weg: Darunter gibt es Geraete, auf denen es
|
||||||
wirklich nicht reicht, und dort ist die zweite Zeile besser als ein
|
wirklich nicht reicht, und dort ist die zweite Zeile besser als ein
|
||||||
Ueberstand. Gemessen bei 360, 390, 412 und 430 -- alle einzeilig. */
|
Ueberstand. Gemessen bei 360, 390, 412 und 430 -- alle einzeilig. */
|
||||||
|
/* NOCH EINE STUFE ENGER (09.09.2026, gemeldet von pruef-handy).
|
||||||
|
|
||||||
|
Bei 320 px hat die Leiste doch umgebrochen -- und zwar nur auf
|
||||||
|
Unterseiten. Dort steht links der Zurueck-Knopf (55 px), rechts
|
||||||
|
kommt auf manchen Seiten die Glocke als siebtes Element dazu.
|
||||||
|
Gerechnet: 55 + 6 + (5 x 36 + 28 + 6 x 6) = 305, verfuegbar 294.
|
||||||
|
|
||||||
|
Elf Pixel. Statt die Schwelle wieder hochzusetzen -- was die zweite
|
||||||
|
Zeile zurueckgebracht haette -- wird hier eine Stufe kleiner
|
||||||
|
geschaltet: 34 px je Knopf, 4 px Abstand. Macht 283 und passt.
|
||||||
|
34 px bleiben deutlich ueber den 24 px, die als Mindestmass fuer ein
|
||||||
|
Beruehrziel gelten (WCAG 2.5.8). */
|
||||||
|
@media (max-width: 340px) {
|
||||||
|
.kopfleiste__rechts { gap: 4px; }
|
||||||
|
.kopfleiste__rechts > .teilen,
|
||||||
|
.kopfleiste__rechts > .chat-knopf,
|
||||||
|
.kopfleiste__rechts > .suche-knopf,
|
||||||
|
.kopfleiste__rechts > .abmelden,
|
||||||
|
.kopfleiste__rechts > .glocke,
|
||||||
|
.sicht:not([data-fremd="ja"]) {
|
||||||
|
width: 34px; min-width: 34px; height: 34px; min-height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 300px) {
|
@media (max-width: 300px) {
|
||||||
.kopfleiste { flex-wrap: wrap; row-gap: 8px; }
|
.kopfleiste { flex-wrap: wrap; row-gap: 8px; }
|
||||||
/* Die Marke nimmt die erste Zeile ganz ein, die Bedienelemente
|
/* Die Marke nimmt die erste Zeile ganz ein, die Bedienelemente
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
/* =====================================================================
|
||||||
|
team.css — die Arbeitslage von Managern und Scouts.
|
||||||
|
|
||||||
|
Wunsch Filipe, 09.09.2026: "so eine kategorie wie über die creator
|
||||||
|
will ich dass nur für die spicy und dogfather rolle auch über manager
|
||||||
|
und scouts gibt ... ich will dass es so ultra krass gut ist."
|
||||||
|
|
||||||
|
DIE GESTALTUNG FOLGT EINER ENTSCHEIDUNG, NICHT EINEM GESCHMACK:
|
||||||
|
Diese Seite zeigt Zahlen über Kollegen. Sie darf deshalb NICHT
|
||||||
|
aussehen wie ein Cockpit mit Warnleuchten. Rot gibt es genau dort,
|
||||||
|
wo etwas liegengeblieben ist -- und sonst nirgends. Alles andere ist
|
||||||
|
ruhig, damit die wenigen roten Stellen auch etwas heißen.
|
||||||
|
===================================================================== */
|
||||||
|
|
||||||
|
/* ---------- Der Kopf --------------------------------------------------- */
|
||||||
|
.t-kopf {
|
||||||
|
display: flex; align-items: flex-start; justify-content: space-between;
|
||||||
|
gap: 20px; flex-wrap: wrap;
|
||||||
|
margin: 0 0 18px;
|
||||||
|
}
|
||||||
|
.t-kopf__titel {
|
||||||
|
margin: 4px 0 6px;
|
||||||
|
font-size: clamp(1.5rem, 3.4vw, 2.1rem); font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
letter-spacing: -.01em;
|
||||||
|
}
|
||||||
|
.t-kopf__unter { margin: 0; font-size: .96rem; color: var(--text-leise); }
|
||||||
|
/* Der Satz über die Grenzen steht in einem eigenen Kasten und nicht als
|
||||||
|
Fußnote. Er ist die wichtigste Zeile der Seite: Ohne ihn wird eine
|
||||||
|
Arbeitslage als Überwachung gelesen -- und dann arbeitet niemand mehr
|
||||||
|
offen damit. */
|
||||||
|
.t-kopf__grenze {
|
||||||
|
flex: 1 1 300px; max-width: 44ch;
|
||||||
|
margin: 0; padding: 12px 15px;
|
||||||
|
border-left: 3px solid color-mix(in srgb, var(--scout-haupt) 55%, transparent);
|
||||||
|
border-radius: 0 10px 10px 0;
|
||||||
|
background: rgba(95, 201, 154, .07);
|
||||||
|
font-size: .87rem; line-height: 1.55; color: var(--text-leise);
|
||||||
|
}
|
||||||
|
.t-kopf__grenze strong { color: var(--text); }
|
||||||
|
|
||||||
|
/* ---------- Die Lücken ------------------------------------------------- */
|
||||||
|
.t-luecken {
|
||||||
|
display: grid; gap: 12px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
|
||||||
|
margin: 0 0 18px;
|
||||||
|
}
|
||||||
|
/* EINE UNDURCHSICHTIGE FLAECHE, KEIN SCHLEIER.
|
||||||
|
|
||||||
|
Die Lektion vom Vorlagenbrett heute Mittag, hier gleich mitgenommen:
|
||||||
|
Eine Karte mit 7 Prozent Farbe auf transparentem Grund laesst das
|
||||||
|
Buehnenfoto durchscheinen -- und dann haengt die Lesbarkeit davon ab,
|
||||||
|
ob hinter dem Text gerade eine helle oder eine dunkle Stelle liegt.
|
||||||
|
Dort hat es 3,61:1 ergeben, wo 4,5 noetig sind.
|
||||||
|
|
||||||
|
Deshalb hier von vornherein ein eigener, deckender Grund. Der rote
|
||||||
|
Ton liegt darauf, nicht darin. */
|
||||||
|
.tl {
|
||||||
|
padding: 15px 17px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--warn, #ff9aa2) 34%, transparent);
|
||||||
|
border-radius: var(--radius-klein);
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(255, 154, 162, .07), rgba(255, 154, 162, .07)),
|
||||||
|
rgba(7, 10, 17, .93);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
.tl__titel {
|
||||||
|
margin: 0 0 4px; font-size: .98rem; font-weight: 700;
|
||||||
|
color: color-mix(in srgb, var(--warn, #ff9aa2) 78%, #ffffff);
|
||||||
|
}
|
||||||
|
.tl__satz { margin: 0 0 8px; font-size: .84rem; color: var(--text-leise); line-height: 1.5; }
|
||||||
|
.tl__namen {
|
||||||
|
margin: 0; font-size: .86rem; color: var(--text); line-height: 1.6;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
/* Keine Lücke ist auch eine Auskunft -- und dann grün statt rot. */
|
||||||
|
.tl[data-gut="ja"] {
|
||||||
|
border-color: color-mix(in srgb, var(--scout-haupt) 34%, transparent);
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(95, 201, 154, .07), rgba(95, 201, 154, .07)),
|
||||||
|
rgba(7, 10, 17, .93);
|
||||||
|
}
|
||||||
|
.tl[data-gut="ja"] .tl__titel { color: var(--scout-haupt); }
|
||||||
|
|
||||||
|
/* ---------- Die Lage in Zahlen ----------------------------------------- */
|
||||||
|
.t-lage {
|
||||||
|
display: grid; gap: 10px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
|
||||||
|
margin: 0 0 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Eine Kennzahl ---------------------------------------------- */
|
||||||
|
.tz {
|
||||||
|
display: grid; gap: 2px; align-content: start;
|
||||||
|
padding: 11px 13px;
|
||||||
|
border: 1px solid var(--rand); border-radius: var(--radius-klein);
|
||||||
|
/* Ebenfalls deckend -- eine Kennzahl auf einem Foto ist keine
|
||||||
|
Kennzahl, sondern ein Ratespiel. */
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(255, 255, 255, .028), rgba(255, 255, 255, .028)),
|
||||||
|
rgba(7, 10, 17, .9);
|
||||||
|
}
|
||||||
|
.tz__wert {
|
||||||
|
font-size: 1.28rem; font-weight: 700; color: var(--text);
|
||||||
|
font-variant-numeric: tabular-nums; line-height: 1.15;
|
||||||
|
}
|
||||||
|
.tz__schild {
|
||||||
|
font-size: .74rem; color: var(--text-leise); line-height: 1.35;
|
||||||
|
}
|
||||||
|
/* Rot NUR, wo etwas liegengeblieben ist. */
|
||||||
|
.tz[data-art="warn"] {
|
||||||
|
border-color: color-mix(in srgb, var(--warn, #ff9aa2) 42%, transparent);
|
||||||
|
/* Auch hier deckend. Diese Zeile stand zuerst mit einem einzelnen
|
||||||
|
rgba-Wert da und hat damit den undurchsichtigen Grund von oben
|
||||||
|
wieder aufgehoben -- ausgerechnet an den Karten, auf die es
|
||||||
|
ankommt. Zwei Regeln fuer denselben Grund, und die spaetere
|
||||||
|
gewinnt: derselbe Fehler wie so oft in diesem Haus. */
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(255, 154, 162, .09), rgba(255, 154, 162, .09)),
|
||||||
|
rgba(7, 10, 17, .9);
|
||||||
|
}
|
||||||
|
.tz[data-art="warn"] .tz__wert { color: color-mix(in srgb, var(--warn, #ff9aa2) 72%, #ffffff); }
|
||||||
|
/* Und grün, wenn eine Null eine gute Nachricht ist -- eine Null ohne
|
||||||
|
Farbe liest sich wie ein fehlender Wert. */
|
||||||
|
.tz[data-art="gut"] { border-color: color-mix(in srgb, var(--scout-haupt) 30%, transparent); }
|
||||||
|
.tz[data-art="gut"] .tz__wert { color: var(--scout-haupt); }
|
||||||
|
|
||||||
|
/* ---------- Die Leute --------------------------------------------------- */
|
||||||
|
.t-gruppe { margin: 0 0 26px; }
|
||||||
|
.t-gruppe .feldschild { margin: 0 0 10px; }
|
||||||
|
.t-liste {
|
||||||
|
display: grid; gap: 14px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tperson {
|
||||||
|
--r: var(--text-still);
|
||||||
|
position: relative;
|
||||||
|
padding: 16px 18px 18px;
|
||||||
|
border: 1px solid var(--rand);
|
||||||
|
border-radius: var(--radius-klein);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, color-mix(in srgb, var(--r) 6%, transparent), transparent 60%),
|
||||||
|
var(--flaeche);
|
||||||
|
}
|
||||||
|
.tperson[data-rolle="manager"] { --r: var(--manager-haupt); }
|
||||||
|
.tperson[data-rolle="scout"] { --r: var(--scout-haupt); }
|
||||||
|
/* Der Streifen in der Rollenfarbe -- dieselbe Sprache wie in der
|
||||||
|
Personenliste, damit man beide Seiten als ein Haus liest. */
|
||||||
|
.tperson::after {
|
||||||
|
content: ""; position: absolute; left: 0; top: 10px; bottom: 10px;
|
||||||
|
width: 3px; border-radius: 999px;
|
||||||
|
background: linear-gradient(180deg,
|
||||||
|
color-mix(in srgb, var(--r) 80%, transparent),
|
||||||
|
color-mix(in srgb, var(--r) 25%, transparent));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.tperson[data-gesperrt="ja"] { opacity: .6; }
|
||||||
|
|
||||||
|
.tperson__kopf {
|
||||||
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
margin-bottom: 14px; flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.tperson__bild {
|
||||||
|
width: 44px; height: 44px; border-radius: 50%; object-fit: cover;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--r) 40%, transparent);
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.tperson__zeichen {
|
||||||
|
width: 44px; height: 44px; border-radius: 50%;
|
||||||
|
display: grid; place-items: center; flex: none;
|
||||||
|
font-size: 1.05rem; font-weight: 700;
|
||||||
|
color: color-mix(in srgb, var(--r) 75%, #ffffff);
|
||||||
|
background: color-mix(in srgb, var(--r) 16%, #070a11);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--r) 34%, transparent);
|
||||||
|
}
|
||||||
|
.tperson__text { flex: 1; min-width: 0; }
|
||||||
|
/* LANGE NAMEN BRECHEN UM, statt abgeschnitten zu werden.
|
||||||
|
"Patrick-Maximilian" braucht 90 px, bei 320 px Bildschirm stehen 56
|
||||||
|
zur Verfuegung -- gemeldet von pruef-handy. Ein Name, den man nicht
|
||||||
|
lesen kann, ist auf einer Seite ueber Personen der schlechteste
|
||||||
|
Platz zum Sparen. */
|
||||||
|
.tperson__name {
|
||||||
|
margin: 0; font-size: 1.06rem; font-weight: 650; color: var(--text);
|
||||||
|
overflow-wrap: anywhere; hyphens: auto;
|
||||||
|
}
|
||||||
|
.tperson__unter {
|
||||||
|
margin: 2px 0 0; font-size: .82rem; color: var(--text-leise); line-height: 1.45;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.tperson__zuletzt {
|
||||||
|
flex: none;
|
||||||
|
padding: 4px 10px; border-radius: 999px;
|
||||||
|
font-size: .74rem; font-weight: 600;
|
||||||
|
color: var(--text-leise);
|
||||||
|
background: rgba(255, 255, 255, .05);
|
||||||
|
border: 1px solid var(--rand);
|
||||||
|
}
|
||||||
|
.tperson__zuletzt[data-warn="ja"] {
|
||||||
|
color: color-mix(in srgb, var(--warn, #ff9aa2) 72%, #ffffff);
|
||||||
|
border-color: color-mix(in srgb, var(--warn, #ff9aa2) 40%, transparent);
|
||||||
|
background: rgba(255, 154, 162, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tperson__zahlen {
|
||||||
|
display: grid; gap: 8px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(130px, 100%), 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Die Pipeline ------------------------------------------------ */
|
||||||
|
.tpipe {
|
||||||
|
margin-top: 14px; padding-top: 13px;
|
||||||
|
border-top: 1px solid var(--rand);
|
||||||
|
}
|
||||||
|
.tpipe__schild {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: .72rem; font-weight: 700; letter-spacing: .1em;
|
||||||
|
text-transform: uppercase; color: var(--text-still);
|
||||||
|
}
|
||||||
|
.tpipe__reihe {
|
||||||
|
display: grid; gap: 8px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(120px, 100%), 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Wen genau --------------------------------------------------- */
|
||||||
|
.tperson__wer { margin-top: 12px; }
|
||||||
|
.tperson__wer summary {
|
||||||
|
cursor: pointer; font-size: .82rem; color: var(--text-leise);
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.tperson__wer summary:hover { color: var(--text); }
|
||||||
|
.tperson__wer summary:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
|
||||||
|
.tperson__namen {
|
||||||
|
margin: 6px 0 0; font-size: .84rem; color: var(--text-leise); line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 620px) {
|
||||||
|
.t-kopf { flex-direction: column; gap: 14px; }
|
||||||
|
.t-kopf__grenze { max-width: none; }
|
||||||
|
.tperson { padding: 14px 15px 16px; }
|
||||||
|
}
|
||||||
|
@media (forced-colors: active) {
|
||||||
|
.tz, .tl, .tperson { border: 1px solid CanvasText; }
|
||||||
|
.tperson::after { background: CanvasText; }
|
||||||
|
}
|
||||||
@@ -241,6 +241,16 @@
|
|||||||
linien: ['M4 19.5h16', 'M6.6 9.5h1.8v7h-1.8z', 'M11.1 5.5h1.8v11h-1.8z',
|
linien: ['M4 19.5h16', 'M6.6 9.5h1.8v7h-1.8z', 'M11.1 5.5h1.8v11h-1.8z',
|
||||||
'M15.6 11.5h1.8v5h-1.8z'],
|
'M15.6 11.5h1.8v5h-1.8z'],
|
||||||
},
|
},
|
||||||
|
teamlage: {
|
||||||
|
/* Zwei Koepfe und eine steigende Linie darunter: Es geht um
|
||||||
|
Menschen UND um ihren Verlauf. Gefuellt ist nur der vordere
|
||||||
|
Kopf -- zwei gefuellte Kreise nebeneinander lesen sich als
|
||||||
|
Brille, nicht als Team. */
|
||||||
|
fuellung: ['M8.6 4.6a2.9 2.9 0 1 0 0 5.8 2.9 2.9 0 0 0 0-5.8z'],
|
||||||
|
linien: ['M8.6 4.6a2.9 2.9 0 1 0 0 5.8 2.9 2.9 0 0 0 0-5.8z',
|
||||||
|
'M15.6 5.4a2.4 2.4 0 1 0 0 4.8 2.4 2.4 0 0 0 0-4.8z',
|
||||||
|
'M4 20v-2.2', 'M9 20v-4.6', 'M14 20v-3.2', 'M19 20v-6.4'],
|
||||||
|
},
|
||||||
personen: {
|
personen: {
|
||||||
/* Der Bart des Schlüssels bleibt Linie, der Kopf wird Fläche. */
|
/* Der Bart des Schlüssels bleibt Linie, der Kopf wird Fläche. */
|
||||||
fuellung: ['M14.4 6.2a4 4 0 1 0 0 8 4 4 0 0 0 0-8z'],
|
fuellung: ['M14.4 6.2a4 4 0 1 0 0 8 4 4 0 0 0 0-8z'],
|
||||||
@@ -430,6 +440,34 @@
|
|||||||
rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'report.html', szene: 'showbuehne' },
|
rollen: ['spicy', 'admin', 'manager', 'creator', 'scout'], ziel: 'report.html', szene: 'showbuehne' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
/* =============================================================
|
||||||
|
RUND UM DAS TEAM (09.09.2026)
|
||||||
|
|
||||||
|
Filipe: "so eine kategorie wie ueber die creator will ich dass
|
||||||
|
nur fuer die spicy und dogfather rolle auch ueber manager und
|
||||||
|
scouts gibt ... ich will dass die spicy und dogfather rolle
|
||||||
|
einen kompletten teil haben mit daten ueber die arbeit von den
|
||||||
|
manager und scout."
|
||||||
|
|
||||||
|
WARUM SIE HIER STEHT, direkt hinter "Rund um den Creator": Es
|
||||||
|
ist dieselbe Frage, eine Ebene hoeher. Dort geht es um die
|
||||||
|
Betreuung EINES Creators, hier um die, die betreuen.
|
||||||
|
|
||||||
|
NUR ZWEI ROLLEN. Ein Manager sieht die Gruppe nicht -- eine
|
||||||
|
Uebersicht ueber die Arbeit der Kollegen gehoert der Leitung.
|
||||||
|
Die Kachel wegzunehmen reicht dafuer nicht: Die Seite selbst
|
||||||
|
ist in workspace.js auf ['spicy','admin'] gesetzt, und die
|
||||||
|
Schnittstelle antwortet allen anderen mit 404. Eine
|
||||||
|
ausgeblendete Kachel hat noch nie etwas geschuetzt. */
|
||||||
|
{
|
||||||
|
name: 'Rund um das Team',
|
||||||
|
unter: 'Manager und Scouts – Auslastung, Lücken, Wirkung',
|
||||||
|
bereiche: [
|
||||||
|
{ name: 'Team-Lage', unter: 'Wer betreut wen, wo staut sich etwas',
|
||||||
|
zeichen: 'teamlage', ton: 22,
|
||||||
|
rollen: ['spicy', 'admin'], ziel: 'team.html', szene: 'halle' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Team & System',
|
name: 'Team & System',
|
||||||
unter: 'Was den Laden am Laufen hält',
|
unter: 'Was den Laden am Laufen hält',
|
||||||
|
|||||||
@@ -0,0 +1,292 @@
|
|||||||
|
/* ===================================================================
|
||||||
|
Die Arbeitslage von Managern und Scouts.
|
||||||
|
|
||||||
|
Wunsch Filipe, 09.09.2026: *"so eine kategorie wie über die creator
|
||||||
|
will ich dass nur für die spicy und dogfather rolle auch über manager
|
||||||
|
und scouts gibt ... mit daten über die arbeit von den manager und
|
||||||
|
scout. keine geheimen sachen also termine, chats und geheime dateien
|
||||||
|
soll auch so bleiben dass keiner."*
|
||||||
|
|
||||||
|
DREI DINGE, DIE DIESE SEITE ANDERS MACHT ALS EINE TABELLE:
|
||||||
|
|
||||||
|
1. DIE LÜCKEN STEHEN OBEN. Ein unbetreuter Creator ist ein Zustand,
|
||||||
|
den man heute abstellen kann; eine Kennzahl ist eine Auskunft, die
|
||||||
|
man zur Kenntnis nimmt. Deshalb kommt zuerst, was fehlt.
|
||||||
|
|
||||||
|
2. AUSREISSER WERDEN MARKIERT, NICHT GEMITTELT. Wer die Zahlen
|
||||||
|
mittelt, verliert genau die Person, um die es geht. Markiert wird
|
||||||
|
gegen den MEDIAN der Gruppe und nicht gegen eine feste Zahl:
|
||||||
|
Sieben Creator sind bei einem Team von drei Leuten viel und bei
|
||||||
|
einem von zwanzig wenig.
|
||||||
|
|
||||||
|
3. JEDE ZAHL SAGT, WORAUS SIE KOMMT. Eine Zahl ohne Bezug wird
|
||||||
|
entweder geglaubt oder ignoriert -- beides ist falsch.
|
||||||
|
=================================================================== */
|
||||||
|
(() => {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
const el = (tag, klasse, text) => {
|
||||||
|
const k = document.createElement(tag);
|
||||||
|
if (klasse) k.className = klasse;
|
||||||
|
if (text !== undefined) k.textContent = text;
|
||||||
|
return k;
|
||||||
|
};
|
||||||
|
const melde = (t) => { $('fehler').textContent = t || ''; };
|
||||||
|
|
||||||
|
async function hole(pfad, optionen = {}) {
|
||||||
|
const a = await fetch(pfad, { credentials: 'same-origin', ...optionen });
|
||||||
|
if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); }
|
||||||
|
if (a.status === 404) { location.assign('/workspace/start.html'); throw new Error('nicht erlaubt'); }
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Der Median statt des Mittelwerts: Ein einzelner Ausreisser zieht
|
||||||
|
einen Mittelwert mit sich und macht sich damit selbst unsichtbar.
|
||||||
|
Der Median bleibt stehen, wo die Gruppe steht. */
|
||||||
|
function median(werte) {
|
||||||
|
const w = werte.filter((x) => Number.isFinite(x)).sort((a, b) => a - b);
|
||||||
|
if (!w.length) return 0;
|
||||||
|
const m = Math.floor(w.length / 2);
|
||||||
|
return w.length % 2 ? w[m] : (w[m - 1] + w[m]) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const datum = (iso) => {
|
||||||
|
if (!iso) return null;
|
||||||
|
const d = new Date(iso);
|
||||||
|
if (Number.isNaN(d.getTime())) return null;
|
||||||
|
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit' });
|
||||||
|
};
|
||||||
|
const tageHer = (iso) => {
|
||||||
|
if (!iso) return null;
|
||||||
|
const d = new Date(iso);
|
||||||
|
if (Number.isNaN(d.getTime())) return null;
|
||||||
|
return Math.floor((Date.now() - d) / 86400000);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------- Eine Zahl mit Beschriftung ---------------------------- */
|
||||||
|
function kennzahl(wert, schild, art) {
|
||||||
|
const k = el('div', 'tz');
|
||||||
|
if (art) k.dataset.art = art;
|
||||||
|
k.append(el('strong', 'tz__wert', wert));
|
||||||
|
k.append(el('span', 'tz__schild', schild));
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Die Lücken -------------------------------------------- */
|
||||||
|
function lueckenZeigen(l) {
|
||||||
|
const ziel = $('luecken');
|
||||||
|
ziel.textContent = '';
|
||||||
|
const bloecke = [
|
||||||
|
['creator_ohne_betreuung', 'Creator ohne Betreuung',
|
||||||
|
'Niemand ist zuständig. Das ist die Lücke, die am schnellsten weh tut.'],
|
||||||
|
['scouts_ohne_manager', 'Scouts ohne Manager',
|
||||||
|
'Sie arbeiten, aber niemand schaut mit drauf.'],
|
||||||
|
['ohne_zuteilung', 'Ohne einen einzigen Creator',
|
||||||
|
'Angelegt, aber ohne Arbeit – entweder neu oder vergessen.'],
|
||||||
|
];
|
||||||
|
let etwas = false;
|
||||||
|
for (const [schluessel, titel, satz] of bloecke) {
|
||||||
|
const liste = l[schluessel] || [];
|
||||||
|
if (!liste.length) continue;
|
||||||
|
etwas = true;
|
||||||
|
const k = el('article', 'tl');
|
||||||
|
k.append(el('h3', 'tl__titel', `${titel} · ${liste.length}`));
|
||||||
|
k.append(el('p', 'tl__satz', satz));
|
||||||
|
const namen = el('p', 'tl__namen');
|
||||||
|
/* Namen und nicht nur eine Zahl: Eine Zahl fordert eine zweite
|
||||||
|
Frage, und die stellt niemand. */
|
||||||
|
namen.textContent = liste.map((x) => x.name).join(' · ');
|
||||||
|
k.append(namen);
|
||||||
|
ziel.append(k);
|
||||||
|
}
|
||||||
|
if (!etwas) {
|
||||||
|
const k = el('article', 'tl');
|
||||||
|
k.dataset.gut = 'ja';
|
||||||
|
k.append(el('h3', 'tl__titel', 'Keine Lücken'));
|
||||||
|
k.append(el('p', 'tl__satz',
|
||||||
|
'Jeder aktive Creator hat eine Betreuung, jeder Scout einen Manager.'));
|
||||||
|
ziel.append(k);
|
||||||
|
}
|
||||||
|
ziel.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Die Lage in vier Zahlen -------------------------------- */
|
||||||
|
function lageZeigen(leute, luecken) {
|
||||||
|
const ziel = $('lage');
|
||||||
|
ziel.textContent = '';
|
||||||
|
const aktive = leute.filter((p) => p.aktiv);
|
||||||
|
const betreute = new Set();
|
||||||
|
for (const p of leute) for (const c of p.creator) betreute.add(c.id);
|
||||||
|
const ueberfaellig = leute.reduce((s, p) => s + p.aufgaben.ueberfaellig, 0);
|
||||||
|
const faellig = leute.reduce((s, p) => s + (p.pipeline?.faellig || 0), 0);
|
||||||
|
|
||||||
|
ziel.append(kennzahl(String(aktive.length), 'im Team'));
|
||||||
|
ziel.append(kennzahl(String(betreute.size), 'betreute Creator'));
|
||||||
|
ziel.append(kennzahl(String(ueberfaellig), 'überfällige Aufgaben',
|
||||||
|
ueberfaellig ? 'warn' : 'gut'));
|
||||||
|
ziel.append(kennzahl(String(faellig), 'liegengebliebene Kontakte',
|
||||||
|
faellig ? 'warn' : 'gut'));
|
||||||
|
const l = (luecken.creator_ohne_betreuung || []).length;
|
||||||
|
ziel.append(kennzahl(String(l), 'Creator ohne Betreuung', l ? 'warn' : 'gut'));
|
||||||
|
ziel.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Eine Person ------------------------------------------- */
|
||||||
|
function karte(p, vergleich) {
|
||||||
|
const k = el('article', 'tperson');
|
||||||
|
k.dataset.rolle = p.rolle;
|
||||||
|
if (!p.aktiv) k.dataset.gesperrt = 'ja';
|
||||||
|
|
||||||
|
/* Kopf: Bild, Name, was er trägt. */
|
||||||
|
const kopf = el('div', 'tperson__kopf');
|
||||||
|
if (p.bild) {
|
||||||
|
const b = document.createElement('img');
|
||||||
|
b.className = 'tperson__bild';
|
||||||
|
b.src = p.bild; b.alt = ''; b.loading = 'lazy';
|
||||||
|
kopf.append(b);
|
||||||
|
} else {
|
||||||
|
kopf.append(el('span', 'tperson__zeichen', (p.name || '?').slice(0, 1).toUpperCase()));
|
||||||
|
}
|
||||||
|
const kt = el('div', 'tperson__text');
|
||||||
|
kt.append(el('h3', 'tperson__name', p.name + (p.aktiv ? '' : ' · gesperrt')));
|
||||||
|
const unter = [];
|
||||||
|
if (p.creator.length) unter.push(`${p.creator.length} Creator direkt`);
|
||||||
|
if (p.creator_mittelbar) unter.push(`${p.creator_mittelbar} über Scouts`);
|
||||||
|
/* Einzahl und Mehrzahl. "1 Scouts" ist eine Kleinigkeit und trotzdem
|
||||||
|
das Erste, was auffaellt -- eine Seite, die ihre eigene Sprache
|
||||||
|
nicht beherrscht, wird auch bei den Zahlen nicht geglaubt. */
|
||||||
|
if (p.scouts.length) {
|
||||||
|
unter.push(`${p.scouts.length} ${p.scouts.length === 1 ? 'Scout' : 'Scouts'}: `
|
||||||
|
+ p.scouts.join(', '));
|
||||||
|
}
|
||||||
|
kt.append(el('p', 'tperson__unter', unter.join(' · ') || 'Noch niemand zugeteilt'));
|
||||||
|
kopf.append(kt);
|
||||||
|
|
||||||
|
/* Zuletzt da. Bewusst grob und ohne Uhrzeit: Es geht um
|
||||||
|
Erreichbarkeit, nicht um Arbeitszeit. */
|
||||||
|
const tage = tageHer(p.letzter_login);
|
||||||
|
const zuletzt = el('span', 'tperson__zuletzt',
|
||||||
|
tage === null ? 'noch nie angemeldet'
|
||||||
|
: tage === 0 ? 'heute da' : tage === 1 ? 'gestern da' : `vor ${tage} Tagen da`);
|
||||||
|
if (tage === null || tage > 14) zuletzt.dataset.warn = 'ja';
|
||||||
|
kopf.append(zuletzt);
|
||||||
|
k.append(kopf);
|
||||||
|
|
||||||
|
/* Die Zahlen. */
|
||||||
|
const zahlen = el('div', 'tperson__zahlen');
|
||||||
|
const last = p.creator.length + p.creator_mittelbar;
|
||||||
|
zahlen.append(kennzahl(String(last), 'Creator',
|
||||||
|
vergleich.viel && last > vergleich.viel ? 'warn' : null));
|
||||||
|
zahlen.append(kennzahl(String(p.aufgaben.laufend), 'Aufgaben laufen'));
|
||||||
|
zahlen.append(kennzahl(String(p.aufgaben.ueberfaellig), 'überfällig',
|
||||||
|
p.aufgaben.ueberfaellig ? 'warn' : 'gut'));
|
||||||
|
zahlen.append(kennzahl(String(p.aufgaben.erledigt30), 'erledigt (30 Tage)'));
|
||||||
|
zahlen.append(kennzahl(
|
||||||
|
p.aufgaben.durchlauf_tage === null ? '—' : `${p.aufgaben.durchlauf_tage} T`,
|
||||||
|
'bis erledigt'));
|
||||||
|
|
||||||
|
if (p.fortschritt.startcheck_gesamt) {
|
||||||
|
const q = Math.round(100 * p.fortschritt.startcheck_fertig / p.fortschritt.startcheck_gesamt);
|
||||||
|
zahlen.append(kennzahl(`${q} %`, 'Start-Check der Creator', q < 50 ? 'warn' : null));
|
||||||
|
}
|
||||||
|
if (p.fortschritt.punkte_gesamt) {
|
||||||
|
zahlen.append(kennzahl(String(p.fortschritt.punkte_offen), 'offene Punkte'));
|
||||||
|
}
|
||||||
|
if (p.zahlen.livetage30) {
|
||||||
|
zahlen.append(kennzahl(String(p.zahlen.livetage30), 'LIVE-Tage (30 T)'));
|
||||||
|
zahlen.append(kennzahl(p.zahlen.diamanten30.toLocaleString('de-DE'), 'Diamanten (30 T)'));
|
||||||
|
}
|
||||||
|
k.append(zahlen);
|
||||||
|
|
||||||
|
/* Die Pipeline -- nur bei Scouts, denn nur sie haben eine. */
|
||||||
|
if (p.pipeline) {
|
||||||
|
const pl = el('div', 'tpipe');
|
||||||
|
pl.append(el('p', 'tpipe__schild', 'Pipeline'));
|
||||||
|
const r = el('div', 'tpipe__reihe');
|
||||||
|
r.append(kennzahl(String(p.pipeline.offen), 'in Arbeit'));
|
||||||
|
r.append(kennzahl(String(p.pipeline.uebergeben), 'übergeben'));
|
||||||
|
r.append(kennzahl(String(p.pipeline.abgelehnt), 'abgelehnt'));
|
||||||
|
r.append(kennzahl(String(p.pipeline.faellig), 'Kontakt fällig',
|
||||||
|
p.pipeline.faellig ? 'warn' : 'gut'));
|
||||||
|
/* Die Quote sagt mehr als jede Einzelzahl: Viele Kontakte ohne
|
||||||
|
Übergabe sind Arbeit ohne Ergebnis. */
|
||||||
|
const entschieden = p.pipeline.uebergeben + p.pipeline.abgelehnt;
|
||||||
|
if (entschieden) {
|
||||||
|
const q = Math.round(100 * p.pipeline.uebergeben / entschieden);
|
||||||
|
r.append(kennzahl(`${q} %`, 'davon übernommen', q < 20 ? 'warn' : null));
|
||||||
|
}
|
||||||
|
if (p.pipeline.uebergabe_tage !== null) {
|
||||||
|
r.append(kennzahl(`${p.pipeline.uebergabe_tage} T`, 'bis zur Übergabe'));
|
||||||
|
}
|
||||||
|
pl.append(r);
|
||||||
|
k.append(pl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wen genau. Zugeklappt, weil es bei zwanzig Creators die Karte
|
||||||
|
sprengen würde -- aber vorhanden, weil "sieben Creator" ohne
|
||||||
|
Namen keine Auskunft ist. */
|
||||||
|
if (p.creator.length) {
|
||||||
|
const auf = el('details', 'tperson__wer');
|
||||||
|
const z = el('summary', null, `Wen ${p.name} betreut`);
|
||||||
|
auf.append(z);
|
||||||
|
const liste = el('p', 'tperson__namen',
|
||||||
|
p.creator.map((c) => c.name + (c.aktiv ? '' : ' (gesperrt)')).join(' · '));
|
||||||
|
auf.append(liste);
|
||||||
|
k.append(auf);
|
||||||
|
}
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Laden -------------------------------------------------- */
|
||||||
|
async function laden() {
|
||||||
|
let daten;
|
||||||
|
try {
|
||||||
|
const a = await hole('/workspace/api/team');
|
||||||
|
if (!a.ok) { melde('Die Arbeitslage konnte nicht geladen werden.'); return; }
|
||||||
|
daten = await a.json();
|
||||||
|
} catch { return; }
|
||||||
|
|
||||||
|
$('laedt').hidden = true;
|
||||||
|
lueckenZeigen(daten.luecken || {});
|
||||||
|
lageZeigen(daten.leute || [], daten.luecken || {});
|
||||||
|
|
||||||
|
for (const [rolle, block, liste] of [
|
||||||
|
['manager', 'manager-block', 'manager-liste'],
|
||||||
|
['scout', 'scout-block', 'scout-liste'],
|
||||||
|
]) {
|
||||||
|
const eigene = (daten.leute || []).filter((p) => p.rolle === rolle);
|
||||||
|
if (!eigene.length) continue;
|
||||||
|
/* Der Vergleichswert entsteht INNERHALB der Rolle: Ein Manager
|
||||||
|
trägt naturgemäß mehr als ein Scout, und ihn daran zu messen
|
||||||
|
wäre unfair und nutzlos. */
|
||||||
|
const lasten = eigene.map((p) => p.creator.length + p.creator_mittelbar);
|
||||||
|
const mitte = median(lasten);
|
||||||
|
/* "Viel" heisst: die Hälfte über dem Median der eigenen Rolle --
|
||||||
|
und mindestens zwei mehr, damit bei kleinen Zahlen nicht schon
|
||||||
|
ein Creator Unterschied als Überlast gilt. */
|
||||||
|
const vergleich = { viel: Math.max(mitte * 1.5, mitte + 2) };
|
||||||
|
const ziel = $(liste);
|
||||||
|
ziel.textContent = '';
|
||||||
|
for (const p of eigene) ziel.append(karte(p, vergleich));
|
||||||
|
$(block).hidden = false;
|
||||||
|
}
|
||||||
|
melde('');
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const a = await hole('/workspace/api/ich');
|
||||||
|
if (!a.ok) { location.assign('/workspace/'); return; }
|
||||||
|
const ich = await a.json();
|
||||||
|
window.werZeigen?.(ich);
|
||||||
|
/* Die Schranke steht im Server. Hier geht es nur darum, niemanden
|
||||||
|
vor einer leeren Seite stehen zu lassen. */
|
||||||
|
if (ich.rolle !== 'admin' && ich.rolle !== 'spicy') {
|
||||||
|
location.assign('/workspace/start.html');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch { return; }
|
||||||
|
await laden();
|
||||||
|
})();
|
||||||
|
})();
|
||||||
@@ -14,10 +14,10 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -231,10 +231,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609091505" defer></script>
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505" defer></script>
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505" defer></script>
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/aufgaben.js?v=202609091505" defer></script>
|
<script src="assets/js/aufgaben.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+10
-10
@@ -14,11 +14,11 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/automation.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/automation.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -140,10 +140,10 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609091505" defer></script>
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505" defer></script>
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505" defer></script>
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/automation.js?v=202609091505" defer></script>
|
<script src="assets/js/automation.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+14
-14
@@ -14,11 +14,11 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/bereich.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/bereich.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -149,14 +149,14 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609091505" defer></script>
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505" defer></script>
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505" defer></script>
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/checkliste.js?v=202609091505" defer></script>
|
<script src="assets/js/checkliste.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/ampel.js?v=202609091505" defer></script>
|
<script src="assets/js/ampel.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/schulung.js?v=202609091505" defer></script>
|
<script src="assets/js/schulung.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/vorlagen.js?v=202609091505" defer></script>
|
<script src="assets/js/vorlagen.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/bereich.js?v=202609091505" defer></script>
|
<script src="assets/js/bereich.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+11
-11
@@ -14,11 +14,11 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/calls.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/calls.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -82,11 +82,11 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/ki.js?v=202609091505" defer></script>
|
<script src="assets/js/ki.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609091505" defer></script>
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505" defer></script>
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505" defer></script>
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/calls.js?v=202609091505" defer></script>
|
<script src="assets/js/calls.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+10
-10
@@ -14,17 +14,17 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
|
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
|
||||||
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
|
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
|
||||||
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
|
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
|
||||||
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
|
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
|
||||||
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
|
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
|
||||||
Dialog zu sehen bekam. -->
|
Dialog zu sehen bekam. -->
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/chat.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/chat.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -247,10 +247,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script src="assets/js/bereiche.js?v=202609091505"></script>
|
<script src="assets/js/bereiche.js?v=202609091541"></script>
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505"></script>
|
<script src="assets/js/kopf.js?v=202609091541"></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505"></script>
|
<script src="assets/js/glocke.js?v=202609091541"></script>
|
||||||
<script src="assets/js/chat.js?v=202609091505"></script>
|
<script src="assets/js/chat.js?v=202609091541"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+12
-12
@@ -14,11 +14,11 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/content.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/content.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -172,12 +172,12 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609091505" defer></script>
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505" defer></script>
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505" defer></script>
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/checkliste.js?v=202609091505" defer></script>
|
<script src="assets/js/checkliste.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/vorlagen.js?v=202609091505" defer></script>
|
<script src="assets/js/vorlagen.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/content.js?v=202609091505" defer></script>
|
<script src="assets/js/content.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+10
-10
@@ -14,11 +14,11 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/dateien.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/dateien.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -101,10 +101,10 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609091505" defer></script>
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505" defer></script>
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505" defer></script>
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/dateien.js?v=202609091505" defer></script>
|
<script src="assets/js/dateien.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="gate">
|
<body class="gate">
|
||||||
@@ -401,6 +401,6 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="assets/js/gate.js?v=202609091505" defer></script>
|
<script src="assets/js/gate.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+11
-11
@@ -14,11 +14,11 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/kalender.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/kalender.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -370,11 +370,11 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609091505" defer></script>
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505" defer></script>
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505" defer></script>
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kalender.js?v=202609091505" defer></script>
|
<script src="assets/js/kalender.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kalender-abo.js?v=202609091505" defer></script>
|
<script src="assets/js/kalender-abo.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+10
-10
@@ -14,11 +14,11 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/leistung.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/leistung.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -217,10 +217,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609091505"></script>
|
<script src="assets/js/wahl.js?v=202609091541"></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609091505"></script>
|
<script src="assets/js/bereiche.js?v=202609091541"></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505"></script>
|
<script src="assets/js/kopf.js?v=202609091541"></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505"></script>
|
<script src="assets/js/glocke.js?v=202609091541"></script>
|
||||||
<script src="assets/js/leistung.js?v=202609091505"></script>
|
<script src="assets/js/leistung.js?v=202609091541"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+10
-10
@@ -14,11 +14,11 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/personen.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/personen.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -335,10 +335,10 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609091505" defer></script>
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505" defer></script>
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505" defer></script>
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/personen.js?v=202609091505" defer></script>
|
<script src="assets/js/personen.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+11
-11
@@ -14,11 +14,11 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/profil.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -207,11 +207,11 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609091505" defer></script>
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505" defer></script>
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505" defer></script>
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/steckbrief.js?v=202609091505" defer></script>
|
<script src="assets/js/steckbrief.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/profil.js?v=202609091505" defer></script>
|
<script src="assets/js/profil.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+11
-11
@@ -14,11 +14,11 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/report.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/report.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -99,11 +99,11 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/ki.js?v=202609091505" defer></script>
|
<script src="assets/js/ki.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609091505" defer></script>
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505" defer></script>
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505" defer></script>
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/report.js?v=202609091505" defer></script>
|
<script src="assets/js/report.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+10
-10
@@ -14,11 +14,11 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/scouting.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/scouting.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -132,10 +132,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609091505" defer></script>
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505" defer></script>
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505" defer></script>
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/scouting.js?v=202609091505" defer></script>
|
<script src="assets/js/scouting.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -14,12 +14,12 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe
|
<!-- Nur die Startseite: Begrüßungskachel, Uhr, Glockenmulde. Siehe
|
||||||
den Hinweis in start.css -- dort stand es bis zum 07.09.2026. -->
|
den Hinweis in start.css -- dort stand es bis zum 07.09.2026. -->
|
||||||
<link rel="stylesheet" href="assets/css/heim.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/heim.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -667,10 +667,10 @@
|
|||||||
</p>
|
</p>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609091505" defer></script>
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505" defer></script>
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505" defer></script>
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/start.js?v=202609091505" defer></script>
|
<script src="assets/js/start.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+11
-11
@@ -14,11 +14,11 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -78,11 +78,11 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/ki.js?v=202609091505" defer></script>
|
<script src="assets/js/ki.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609091505" defer></script>
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505" defer></script>
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505" defer></script>
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/startcheck.js?v=202609091505" defer></script>
|
<script src="assets/js/startcheck.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+10
-10
@@ -14,11 +14,11 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/profil.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/profil.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -119,10 +119,10 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609091505" defer></script>
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505" defer></script>
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505" defer></script>
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/steckbrief.js?v=202609091505" defer></script>
|
<script src="assets/js/steckbrief.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<title>Team · Spicy & Dogi</title>
|
||||||
|
<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" />
|
||||||
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
|
<link rel="stylesheet" href="assets/css/team.css?v=202609091541" />
|
||||||
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="start">
|
||||||
|
|
||||||
|
<div class="schleier" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<header class="kopfleiste">
|
||||||
|
<button type="button" class="zurueck-knopf" id="zurueck" hidden>
|
||||||
|
<svg class="zurueck-knopf__pfeil" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M14.5 5.5 8 12l6.5 6.5" />
|
||||||
|
</svg>
|
||||||
|
<span class="zurueck-knopf__text">Zurück</span>
|
||||||
|
</button>
|
||||||
|
<p class="marke"><span class="marke__text"><a class="zurueck" href="start.html">Creator Workspace</a> · Team</span></p>
|
||||||
|
<div class="kopfleiste__rechts">
|
||||||
|
<span class="wer" id="wer">…</span>
|
||||||
|
<button type="button" class="abmelden" id="abmelden">Abmelden</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="inhalt inhalt--breit">
|
||||||
|
|
||||||
|
<!-- ==== DER KOPF ================================================
|
||||||
|
Er sagt in einem Satz, was diese Seite ist UND was sie nicht
|
||||||
|
ist. Der zweite Teil ist der wichtigere: Eine Seite mit Zahlen
|
||||||
|
über Kollegen wird sonst als Überwachung gelesen -- und dann
|
||||||
|
arbeitet niemand mehr offen damit.
|
||||||
|
|
||||||
|
Aus der Recherche (Fachliteratur zu Arbeitslast-Dashboards):
|
||||||
|
Der Zweck ist, Stellen zu finden, an denen Unterstützung fehlt.
|
||||||
|
Wer das nicht dazuschreibt, baut ein Kontrollwerkzeug, auch wenn
|
||||||
|
er es nicht wollte. -->
|
||||||
|
<section class="t-kopf">
|
||||||
|
<div class="t-kopf__text">
|
||||||
|
<p class="feldschild">Rund um das Team</p>
|
||||||
|
<h1 class="t-kopf__titel">Manager & Scouts</h1>
|
||||||
|
<p class="t-kopf__unter" id="kopf-unter">
|
||||||
|
Wer betreut wen, wo staut sich etwas, wo fehlt jemand.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p class="t-kopf__grenze">
|
||||||
|
Termine, Chats und Dateien kommen hier nicht vor – weder Inhalte
|
||||||
|
noch Zahlen. Das ist eine <strong>Arbeitslage</strong>, keine Überwachung.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p class="fehler" id="fehler" role="alert" aria-live="polite"></p>
|
||||||
|
|
||||||
|
<!-- ==== DIE LÜCKEN ZUERST ======================================
|
||||||
|
Eine Kennzahl sagt, wie es läuft. Eine Lücke sagt, wo etwas
|
||||||
|
fehlt -- und nur das Zweite kann man heute abstellen. Deshalb
|
||||||
|
steht sie oben und nicht unter den Tabellen. -->
|
||||||
|
<section class="t-luecken" id="luecken" hidden></section>
|
||||||
|
|
||||||
|
<!-- ==== DIE LAGE IN ZAHLEN ===================================== -->
|
||||||
|
<section class="t-lage" id="lage" hidden></section>
|
||||||
|
|
||||||
|
<!-- ==== DIE LEUTE ==============================================
|
||||||
|
Jede Person einzeln, nicht als Durchschnitt: Zwei Scouts mit je
|
||||||
|
sechs Creators und einer mit achtzehn ergeben einen Schnitt von
|
||||||
|
zehn -- und der sagt nichts über den, bei dem es klemmt. -->
|
||||||
|
<section class="t-gruppe" id="manager-block" hidden>
|
||||||
|
<h2 class="feldschild">Manager</h2>
|
||||||
|
<div class="t-liste" id="manager-liste"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="t-gruppe" id="scout-block" hidden>
|
||||||
|
<h2 class="feldschild">Scouts</h2>
|
||||||
|
<div class="t-liste" id="scout-liste"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p class="leise" id="laedt">Die Arbeitslage wird geladen …</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
|
<script src="assets/js/team.js?v=202609091541" defer></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+10
-10
@@ -14,13 +14,13 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
|
<!-- Formular- und Dialogstile: seit dem 07.09.2026 steht hier der
|
||||||
Dialog "Creator anlegen". -->
|
Dialog "Creator anlegen". -->
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -142,11 +142,11 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/bereiche.js?v=202609091505" defer></script>
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505" defer></script>
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505" defer></script>
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/uebersicht.js?v=202609091505" defer></script>
|
<script src="assets/js/uebersicht.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+10
-10
@@ -14,11 +14,11 @@
|
|||||||
<meta name="theme-color" content="#06090f" />
|
<meta name="theme-color" content="#06090f" />
|
||||||
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
<link rel="icon" type="image/png" href="/assets/img/app-symbole/workspace-32.png" />
|
||||||
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
<link rel="apple-touch-icon" href="/assets/img/app-symbole/workspace-180.png" />
|
||||||
<link rel="stylesheet" href="assets/css/gate.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/gate.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/start.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/start.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/wissen.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/wissen.css?v=202609091541" />
|
||||||
<link rel="stylesheet" href="assets/css/module.css?v=202609091505" />
|
<link rel="stylesheet" href="assets/css/module.css?v=202609091541" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="start">
|
<body class="start">
|
||||||
@@ -183,10 +183,10 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="assets/js/wahl.js?v=202609091505" defer></script>
|
<script src="assets/js/wahl.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/bereiche.js?v=202609091505" defer></script>
|
<script src="assets/js/bereiche.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/kopf.js?v=202609091505" defer></script>
|
<script src="assets/js/kopf.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/glocke.js?v=202609091505" defer></script>
|
<script src="assets/js/glocke.js?v=202609091541" defer></script>
|
||||||
<script src="assets/js/wissen.js?v=202609091505" defer></script>
|
<script src="assets/js/wissen.js?v=202609091541" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user