Zahlen bekommen eine Oberflaeche, das Dashboard bekommt Zahlen

Der Server konnte seit gestern rechnen -- und niemand konnte etwas
eintragen. Ein Rechenwerk ohne Oberflaeche ist kein Modul, sondern ein
Versprechen. Das ist jetzt eingeloest:

  * leistung.html: Wochenkarten (Wert, Vergleich zur Vorwoche,
    Verlaufslinie), Ziele als Fortschrittsbalken, die letzten 14 Tage
    zum Eintragen -- auch die LEEREN, denn eine Luecke ist selbst die
    Auskunft. Import aus dem TikTok-Export mit Vorschau VOR dem
    Schreiben.
  * Kachel "Zahlen" als erste der Gruppe "Rund um den Creator", eigenes
    Zeichen (steigende Linie, kein zweites Balkendiagramm).
  * Dashboard: jede Creator-Karte traegt jetzt Diamanten, LIVE-Tage und
    Verweildauer. Bis heute zeigte sie ausschliesslich ARBEIT -- ein
    Creator konnte null ueberfaellige Aufgaben haben und gleichzeitig
    seit drei Wochen einbrechen, und die Karte sah tadellos aus.
  * Die Fruehwarnung steht oben im Dashboard. Sechs benannte Signale im
    Klartext mit Beleg, bewusst OHNE Punktzahl -- ein Wert wie
    "Abwanderungsrisiko 73" klingt nach Wissenschaft und ist eine
    Behauptung, der man nicht widersprechen kann.
  * chat.html?mit=<person>: der Weg von der Warnung ins Gespraech. Gab
    es vorher nicht; ein Hinweis ohne Weg dahin ist nur ein schlechtes
    Gewissen.

GEFUNDEN VON DEN PRUEFUNGEN, nicht von der Theorie:

  1. Die Verlaufslinie zeichnete fuer vier der sieben Groessen NICHTS.
     Der Server liefert im Verlauf nur drei Reihen; fuer den Rest kam
     undefined an. `=== null` faengt das nicht, Number(undefined) ist
     NaN, und ein <polyline points="NaN,NaN"> ist im DOM vorhanden und
     auf dem Bildschirm unsichtbar -- ohne Fehlermeldung. Die Pruefung
     misst deshalb die PUNKTE, nicht die Anwesenheit des Elements.
  2. Fuenf Schriftgroessen im CHAT lagen unter 11,5 px (.68/.7 rem) und
     waren seit vorgestern drin. Die Grundlinie der Groessenpruefung
     stand auf 43, gemessen wurden 48. Nicht die Grundlinie angehoben,
     sondern die fuenf Stellen behoben: Eine Grundlinie, die mitwaechst,
     ist keine mehr.

pruef-leistung-optik.mjs: 57 Pruefungen, zwei echte Browser, Gegenprobe
zu jeder Aussage -- auch dazu, dass die Vorschau vor dem Bestaetigen
wirklich noch nichts geschrieben hat und dass ein Creator seine Zahlen
zwar SIEHT, sie aber weder eintragen noch die Fruehwarnung ueber sich
lesen kann (auch nicht ueber eine von Hand gebaute Anfrage).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-06 18:27:16 +02:00
co-authored by Claude Opus 5
parent c8a4e7628c
commit 953c3f5721
28 changed files with 2159 additions and 168 deletions
+581
View File
@@ -0,0 +1,581 @@
/* ZAHLEN — KANN MAN SIE EINTRAGEN, UND SAGEN SIE ETWAS?
pruef-leistung.mjs beweist, dass der Server richtig rechnet. Diese
Prüfung fragt das, worauf es danach ankommt: Kommt jemand an die
Zahlen heran, und sagt ihm die Seite etwas, das er ohne sie nicht
wüsste?
Ein Rechenwerk ohne Oberfläche ist kein Modul, sondern ein
Versprechen. Genau deshalb steht diese Prüfung neben der anderen.
GEMESSEN WIRD:
* Ein Tag wird eingetragen und steht danach in der Wochenkarte.
* Der VERGLEICH zur Vorwoche erscheint -- die Zahl allein wäre eine
Behauptung.
* Die kleine Verlaufslinie wird wirklich GEZEICHNET. Nicht "das
Element ist da": Ihre Punkte müssen Zahlen sein. Genau hier saß
ein echter Fehler -- der Server liefert im Verlauf nur drei der
sieben Größen, für die anderen kam `undefined` an, wurde zu NaN,
und der Browser zeichnete wortlos nichts.
* Der Import zeigt ERST die Vorschau. Gegenprobe: vorher darf noch
nichts in der Datenbank stehen.
* Ein Creator SIEHT seine Zahlen und kann sie NICHT eintragen.
* Schriftgrößen und Fingerziele auf dem Handy.
* Das Dashboard zeigt die Zahlen und die Frühwarnung.
GEGENPROBEN durchgehend: Zu jeder Prüfung, die "in Ordnung" sagen
kann, gehört eine, die beweist, dass sie auch "nicht in Ordnung"
sagen könnte.
===================================================================== */
import { mkdtempSync, rmSync, appendFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ordner = mkdtempSync(join(tmpdir(), "ws-leistoptik-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
const { portMussFreiSein } = await import("./helfer-port.mjs");
await portMussFreiSein(4337, "die Zahlen-Optikpruefung");
process.env.PORT = "4337";
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");
await new Promise((r) => setTimeout(r, 1000));
const BASIS = "http://127.0.0.1:4337";
const PROTOKOLL = join(process.cwd(), "pruef-leistung-optik-lauf.txt");
try { rmSync(PROTOKOLL, { force: true }); } catch { /* gab es noch nicht */ }
const melde = (z) => {
console.log(z);
try { appendFileSync(PROTOKOLL, z + "\n"); } catch { /* Zugabe */ }
};
let fehler = 0;
let gemacht = 0;
const ok = (b, t) => { gemacht++; melde((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", {
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 jetzt = new Date().toISOString();
function anlegen(name, rolle, code) {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768, jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
anlegen("Filipe", "admin", "CODE-DOGI-0001");
const idMan = anlegen("Marek", "manager", "CODE-MANA-0001");
const idLuna = anlegen("Luna", "creator", "CODE-CREA-0001");
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idMan, jetzt);
/* Ortszeit-Datum. NIE toISOString(): das ist UTC und liegt zwischen
Mitternacht und zwei Uhr einen Tag daneben -- eine Prüfung, die um
00:30 rot wird, ohne dass sich etwas geändert hat. */
const p2 = (n) => String(n).padStart(2, "0");
const tagVor = (n) => {
const t = new Date(Date.now() - n * 86400000);
return `${t.getFullYear()}-${p2(t.getMonth() + 1)}-${p2(t.getDate())}`;
};
/* VORWOCHE bestücken, damit es überhaupt etwas zu vergleichen gibt --
und damit die Verlaufslinie genug Punkte hat. Die laufende Woche
bleibt leer: Sie wird gleich über die Oberfläche gefüllt, und das ist
der eigentliche Gegenstand dieser Prüfung. */
const setzen = d.prepare(`
INSERT INTO leistung (creator_id, tag, diamanten, dauer_min, gueltiger_tag,
zuschauer_avg, verweildauer_s, erfasst, erfasst_von, quelle)
VALUES (?,?,?,?,1,?,?,?,?, 'hand')`);
for (let i = 7; i <= 27; i++) {
/* Absteigend über drei Wochen -- damit auch die Frühwarnung etwas zu
melden hat und nicht nur "keine Befunde" heißt. */
setzen.run(idLuna, tagVor(i), 400 + i * 12, 90, 30, 200 + i, jetzt, idMan);
}
d.close();
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await pw.chromium.launch();
async function alsRolle(rolle, code, wohin = "leistung.html", groesse = { width: 1280, height: 900 }) {
const ctx = await browser.newContext({ viewport: groesse });
const seite = await ctx.newPage();
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
await seite.click(`.rolle[data-rolle="${rolle}"]`);
await seite.fill("#code", code);
await seite.click("button[type=submit]");
await seite.waitForURL("**/start.html", { timeout: 25000 });
await seite.goto(`${BASIS}/workspace/${wohin}`, { waitUntil: "networkidle" });
await seite.waitForTimeout(900);
return { ctx, seite };
}
/* =======================================================================
1. Die Seite ist überhaupt erreichbar — und über die Kachel
======================================================================= */
melde("\n=== Der Weg zur Seite ===");
const man = await alsRolle("manager", "CODE-MANA-0001", "start.html");
{
const kachel = await man.seite.$$eval(".kachel", (k) =>
k.map((x) => ({
name: x.querySelector(".kachel__name")?.textContent.trim(),
/* Das href sitzt am .kachel__link INNEN, nicht am <li> aussen --
beim ersten Anlauf stand hier `x.getAttribute("href")` und
lieferte brav "null". Eine Prüfung, die am falschen Element
misst, meldet einen Fehler, den es nicht gibt; das ist genauso
schädlich wie einer, den sie übersieht. */
ziel: x.querySelector(".kachel__link")?.getAttribute("href"),
zeichen: !!x.querySelector("svg"),
})).filter((x) => x.name === "Zahlen"));
ok(kachel.length === 1, `die Kachel "Zahlen" steht auf der Startseite (${kachel.length}×)`);
ok(kachel[0]?.ziel === "leistung.html", `sie führt nach "${kachel[0]?.ziel}"`);
ok(kachel[0]?.zeichen === true, "sie hat ein Zeichen");
/* GEGENPROBE zum Selektor: Ein Name, den es nicht gibt, darf NICHT
gefunden werden -- sonst prüfte der Block oben nichts. */
const erfunden = await man.seite.$$eval(".kachel", (k) =>
k.some((x) => x.querySelector(".kachel__name")?.textContent.trim() === "Zahlenzauber"));
ok(!erfunden, "eine erfundene Kachel wird nicht gefunden");
await man.seite.goto(`${BASIS}/workspace/leistung.html`, { waitUntil: "networkidle" });
await man.seite.waitForTimeout(1100);
const titel = await man.seite.$eval(".titel", (e) => e.textContent.trim());
ok(titel.length > 0, `die Seite ist offen: "${titel}"`);
}
/* =======================================================================
2. Einen Tag eintragen — und er steht danach in der Wochenkarte
======================================================================= */
melde("\n=== Einen Tag eintragen ===");
{
const vorher = await man.seite.$$eval(".zahlkarte__wert", (w) => w.map((x) => x.textContent));
ok(vorher.length >= 5,
`die Wochenkarten stehen da (${vorher.length} Werte) — sonst prüft alles Folgende nichts`);
/* Die laufende Woche ist leer: Diamanten müssen 0 sein. Steht hier
schon etwas, ist der Bestand oben falsch aufgebaut und die Prüfung
misst etwas anderes als sie behauptet. */
const diaVorher = await man.seite.$$eval(".zahlkarte", (k) => {
const t = k.find((x) => x.querySelector(".zahlkarte__name")?.textContent === "Diamanten");
return t?.querySelector(".zahlkarte__wert")?.textContent;
});
ok(diaVorher === "0", `vor dem Eintragen stehen ${diaVorher} Diamanten da`);
/* Der erste "eintragen"-Knopf ist der von HEUTE (die Liste läuft
rückwärts). */
const knoepfe = await man.seite.$$(".tagzeile__knopf");
ok(knoepfe.length === 14, `es gibt 14 Tageszeilen mit Knopf (${knoepfe.length})`);
await knoepfe[0].click();
await man.seite.waitForSelector("#tag-dialog[open]", { timeout: 5000 });
await man.seite.fill("#f-diamanten", "1250");
await man.seite.fill("#f-dauer", "120");
await man.seite.fill("#f-verweildauer", "260");
await man.seite.fill("#f-notiz", "PK gegen Mia");
await man.seite.click("#tag-speichern");
await man.seite.waitForTimeout(1400);
const dia = await man.seite.$$eval(".zahlkarte", (k) => {
const t = k.find((x) => x.querySelector(".zahlkarte__name")?.textContent === "Diamanten");
return t?.querySelector(".zahlkarte__wert")?.textContent;
});
ok(dia === "1.250", `die Karte zeigt jetzt "${dia}" (mit Tausenderpunkt)`);
/* DER VERGLEICH. Ohne ihn ist die Zahl eine Behauptung -- das ist die
Regel, wegen der dieses Modul überhaupt so gebaut ist. */
const wandel = await man.seite.$$eval(".zahlkarte", (k) => {
const t = k.find((x) => x.querySelector(".zahlkarte__name")?.textContent === "Diamanten");
const w = t?.querySelector(".zahlkarte__wandel");
return { text: w?.textContent || "", richtung: w?.dataset.richtung || "" };
});
ok(/Vorwoche/.test(wandel.text) && /%/.test(wandel.text),
`daneben steht der Vergleich: "${wandel.text}"`);
/* DAS VORZEICHEN MUSS ZUR RICHTUNG PASSEN, und die Richtung färbt.
Nicht fest auf "hoch" prüfen: Die Vorwoche ist im Bestand oben
absichtlich stärker, es MUSS also ein Minus sein -- eine Prüfung,
die "hoch" erwartet, prüft dann nur noch, ob ich die Testdaten
richtig erinnere. Diese hier prüft den Zusammenhang selbst und
bleibt gültig, wenn sich der Bestand ändert. */
const p = Number((wandel.text.match(/(-?\d+)\s*%/) || [])[1]);
const soll = p > 0 ? "hoch" : p < 0 ? "runter" : "gleich";
ok(Number.isFinite(p) && wandel.richtung === soll,
`${p} % ist als "${wandel.richtung}" gekennzeichnet (erwartet "${soll}")`);
/* Und die Farbe muss der Richtung folgen -- sonst steht ein Minus in
Grün da und niemand merkt es. */
const farbe = await man.seite.$$eval(".zahlkarte", (k) => {
const t = k.find((x) => x.querySelector(".zahlkarte__name")?.textContent === "Diamanten");
return getComputedStyle(t.querySelector(".zahlkarte__wandel")).color;
});
const [r, g] = (farbe.match(/\d+/g) || []).map(Number);
ok(p < 0 ? r > g : g >= r,
`und die Farbe passt dazu (${farbe})`);
/* Die Notiz ist der Kontext, ohne den eine Zahl in die Irre führt. */
const notiz = await man.seite.$$eval(".tagzeile__notiz", (n) => n.map((x) => x.textContent));
ok(notiz.includes("PK gegen Mia"), `die Notiz steht in der Tageszeile (${notiz.length} Notizen)`);
/* GEGENPROBE: Ein Wert, der nie eingetragen wurde, darf nicht
dastehen. */
const erfunden = await man.seite.$$eval(".zahlkarte__wert", (w) =>
w.some((x) => x.textContent === "9.999"));
ok(!erfunden, "ein nie eingetragener Wert erscheint nicht");
}
/* =======================================================================
3. Die Verlaufslinie wird WIRKLICH gezeichnet
======================================================================= */
melde("\n=== Die kleine Verlaufslinie ===");
{
/* "Das Element ist da" reicht NICHT. Genau hier saß ein echter
Fehler: Der Server liefert im Verlauf nur drei der sieben Größen;
für alle anderen kam undefined an, wurde zu Number(undefined) = NaN
und der Browser zeichnete wortlos gar nichts. Ein SVG mit
points="NaN,NaN" ist im DOM vorhanden und auf dem Bildschirm
unsichtbar. Deshalb werden die PUNKTE gemessen. */
const linien = await man.seite.$$eval(".zahlkarte__linie polyline", (ls) =>
ls.map((l) => l.getAttribute("points") || ""));
ok(linien.length >= 3, `${linien.length} Verlaufslinien sind gezeichnet`);
const kaputt = linien.filter((p) => !p || /NaN|undefined|Infinity/.test(p));
ok(linien.length > 0 && kaputt.length === 0,
`keine Linie hat unbrauchbare Punkte (${kaputt.length} von ${linien.length})`);
/* Und sie muss WACHSEN: mindestens drei Punktepaare, sonst ist es
keine Linie, sondern ein Strich. */
const zuKurz = linien.filter((p) => p.trim().split(/\s+/).length < 3);
ok(linien.length > 0 && zuKurz.length === 0,
`jede Linie hat mindestens drei Punkte (${zuKurz.length} zu kurz)`);
/* Auch der Endpunkt-Kreis. */
const kreise = await man.seite.$$eval(".zahlkarte__linie circle", (cs) =>
cs.map((c) => `${c.getAttribute("cx")},${c.getAttribute("cy")}`));
ok(kreise.length === linien.length && !kreise.some((k) => /NaN/.test(k)),
`jede Linie hat einen Endpunkt (${kreise.length})`);
/* GEGENPROBE zum Selektor: Nach dem Entfernen darf nichts mehr
gefunden werden. */
const nachher = await man.seite.evaluate(() => {
document.querySelectorAll(".zahlkarte__linie").forEach((e) => e.remove());
return document.querySelectorAll(".zahlkarte__linie polyline").length;
});
ok(nachher === 0, "der Selektor trifft wirklich die Linien (nach dem Entfernen 0)");
await man.seite.reload({ waitUntil: "networkidle" });
await man.seite.waitForTimeout(1200);
}
/* =======================================================================
4. Ziele setzen — aus einem Satz wird ein Fortschritt
======================================================================= */
melde("\n=== Ziele ===");
{
const leerText = await man.seite.$eval("#zielbalken", (e) => e.textContent.trim());
ok(/keine Ziele/i.test(leerText), `ohne Ziel steht ein Satz da: "${leerText.slice(0, 60)}…"`);
await man.seite.click("#ziele-aendern");
await man.seite.waitForSelector("#ziele-dialog[open]", { timeout: 5000 });
await man.seite.fill("#z-diamanten", "2500");
await man.seite.fill("#z-tage", "5");
await man.seite.click("#ziele-speichern");
await man.seite.waitForTimeout(1300);
const balken = await man.seite.$$eval(".zielbalken__zeile", (zs) =>
zs.map((z) => ({
name: z.querySelector(".zielbalken__name")?.textContent,
zahl: z.querySelector(".zielbalken__zahl")?.textContent,
breite: z.querySelector(".zielbalken__fuell")?.style.width,
})));
ok(balken.length === 2, `zwei Ziele stehen als Balken da (${balken.length})`);
const dia = balken.find((b) => b.name === "Diamanten");
ok(dia?.zahl === "1.250 von 2.500", `der Stand steht daneben: "${dia?.zahl}"`);
ok(dia?.breite === "50%", `der Balken ist zur Hälfte gefüllt (${dia?.breite})`);
/* KEIN BALKEN ÜBER 100 % -- er hinge sonst aus dem Kasten. */
await man.seite.click("#ziele-aendern");
await man.seite.waitForSelector("#ziele-dialog[open]", { timeout: 5000 });
await man.seite.fill("#z-diamanten", "500");
await man.seite.click("#ziele-speichern");
await man.seite.waitForTimeout(1300);
const voll = await man.seite.$$eval(".zielbalken__zeile", (zs) => {
const z = zs.find((x) => x.querySelector(".zielbalken__name")?.textContent === "Diamanten");
return {
breite: z?.querySelector(".zielbalken__fuell")?.style.width,
voll: z?.querySelector(".zielbalken__fuell")?.dataset.voll,
text: z?.querySelector(".zielbalken__anteil")?.textContent,
};
});
ok(voll.breite === "100%", `bei 250 % bleibt der Balken bei ${voll.breite}`);
ok(voll.voll === "ja", "und er ist als erreicht gekennzeichnet");
ok(/250/.test(voll.text || ""), `der wahre Wert steht trotzdem da: "${voll.text}"`);
}
/* =======================================================================
5. Import — ERST die Vorschau, dann das Schreiben
======================================================================= */
melde("\n=== Aus Datei einlesen ===");
{
const csv = [
"Datum;Diamanten;Dauer (Minuten);Ø Zuschauer",
`${tagVor(3)};1.480;95;312`,
`${tagVor(2)};970;80;255`,
"kein-datum;500;60;100",
].join("\n");
await man.seite.click("#import-oeffnen");
await man.seite.waitForSelector("#import-dialog[open]", { timeout: 5000 });
const gesperrt = await man.seite.$eval("#import-los", (b) => b.disabled);
ok(gesperrt, "der Übernehmen-Knopf ist gesperrt, solange keine Datei da ist");
await man.seite.setInputFiles("#datei", {
name: "export.csv", mimeType: "text/csv", buffer: Buffer.from(csv, "utf8"),
});
await man.seite.waitForSelector("#vorschau:not([hidden])", { timeout: 8000 });
const zahlen = await man.seite.$$eval(".import-zahl", (zs) =>
zs.map((z) => `${z.querySelector(".import-zahl__wert")?.textContent} ${z.querySelector(".import-zahl__name")?.textContent}`));
ok(zahlen.some((z) => /^2 neu/.test(z)), `die Vorschau zählt: ${zahlen.join(" · ")}`);
ok(zahlen.some((z) => /^1 unlesbar/.test(z)), "die unlesbare Zeile wird gemeldet, nicht verschluckt");
const spalten = await man.seite.$eval(".import-spalten", (e) => e.textContent);
ok(/Diamanten/.test(spalten) && /LIVE-Dauer/.test(spalten),
`die erkannten Spalten stehen da: "${spalten}"`);
/* DIE GEGENPROBE, auf die es ankommt: Bis hierher darf NICHTS
geschrieben worden sein. Eine Vorschau, die schon schreibt, ist
keine Vorschau. */
const vorAntwort = await fetch(`${BASIS}/workspace/api/leistung/${idLuna}/tage?tage=7`, {
headers: { cookie: (await man.ctx.cookies()).map((c) => `${c.name}=${c.value}`).join("; ") },
});
const vorTage = (await vorAntwort.json()).tage || [];
const schonDa = vorTage.some((t) => t.tag === tagVor(3));
ok(!schonDa, "vor dem Bestätigen steht noch nichts in der Datenbank");
await man.seite.click("#import-los");
await man.seite.waitForTimeout(1600);
const nachTage = await man.seite.$$eval(".tagzeile", (zs) =>
zs.filter((z) => z.dataset.quelle === "import").length);
ok(nachTage === 2, `nach dem Bestätigen sind 2 Zeilen aus dem Import da (${nachTage})`);
/* DER TAUSENDERPUNKT. "1.480" darf nicht als 1,48 ankommen -- Faktor
tausend daneben, und die Zahl sieht trotzdem plausibel aus. */
const werte = await man.seite.$$eval(".tagzeile", (zs) =>
zs.filter((z) => z.dataset.quelle === "import")
.map((z) => z.querySelector(".tagzeile__wertzahl")?.textContent));
ok(werte.includes("1.480"), `"1.480" ist als 1480 angekommen (${werte.join(", ")})`);
ok(!werte.includes("1,48") && !werte.includes("1"),
"und nicht als 1,48 — der Tausenderpunkt wurde richtig gelesen");
}
/* =======================================================================
6. Der Creator sieht seine Zahlen — und darf sie nicht eintragen
======================================================================= */
melde("\n=== Was der Creator sieht ===");
const luna = await alsRolle("creator", "CODE-CREA-0001");
{
const karten = await luna.seite.$$eval(".zahlkarte__wert", (w) => w.map((x) => x.textContent));
ok(karten.length >= 5, `Luna sieht ihre Zahlen (${karten.length} Werte)`);
/* 1250 von Hand + 1480 + 970 aus dem Import = 3700. Die Zahl steht
hier ausgerechnet und nicht abgeschrieben: So fällt auf, wenn der
Import etwas dazuzählt oder verschluckt. */
ok(karten.includes("3.700"),
`darunter ihre Diamanten, 1250+1480+970 (${karten.slice(0, 4).join(", ")})`);
const knoepfe = await luna.seite.$$(".tagzeile__knopf");
ok(knoepfe.length === 0, `sie hat keinen Eintragen-Knopf (${knoepfe.length})`);
const zieleKnopf = await luna.seite.$eval("#ziele-aendern", (b) => b.hidden);
ok(zieleKnopf, "und kann auch keine Ziele setzen");
/* Kein Auswahlfeld: Sie hat nur sich selbst, und eine Liste mit einem
Eintrag ist eine Frage ohne Anlass. */
const wahl = await luna.seite.$eval("#creator-wahl", (e) => e.hidden);
ok(wahl, "es gibt kein Creator-Auswahlfeld für sie");
/* DIE SCHRANKE SITZT AM SERVER, nicht in der Oberfläche. Ein
versteckter Knopf ist keine Sicherung. */
const antwort = await luna.seite.evaluate(async (tag) => {
const a = await fetch(`/workspace/api/leistung/3/${tag}`, {
method: "PUT", credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ diamanten: 99999 }),
});
return a.status;
}, tagVor(1));
ok(antwort === 403, `von Hand gebaut lehnt der Server ab (HTTP ${antwort})`);
}
/* =======================================================================
7. Lesbarkeit und Fingerziele
======================================================================= */
melde("\n=== Lesbar und bedienbar ===");
{
/* Kleiner als 12 px liest niemand auf einem Bildschirm, an dem er
jeden Tag sitzt. Zweimal ist mir das schon durchgerutscht (10,56
und 10,88 px) -- deshalb wird es gemessen, nicht angesehen. */
const klein = await man.seite.$$eval(
".zahlkarte *, .zielbalken *, .tagzeile *, .import-vorschau *",
(es) => es
.filter((e) => e.textContent.trim() && !e.children.length)
.map((e) => ({ t: e.textContent.trim().slice(0, 28), px: parseFloat(getComputedStyle(e).fontSize) }))
.filter((x) => x.px < 12));
ok(klein.length === 0,
`keine Schrift unter 12 px (${klein.length} gefunden: ${klein.slice(0, 3).map((k) => `${k.t} ${k.px}px`).join(" | ")})`);
/* Die ZAHL der geprüften Elemente gehört in die Bedingung, nicht nur
in den Meldetext. Eine leere Auswahl wäre sonst grün, ohne etwas
angesehen zu haben. */
const gezaehlt = await man.seite.$$eval(
".zahlkarte *, .zielbalken *, .tagzeile *",
(es) => es.filter((e) => e.textContent.trim() && !e.children.length).length);
ok(gezaehlt > 40, `und es wurden wirklich Elemente gemessen (${gezaehlt})`);
/* HANDY: Der Eintragen-Knopf muss mit dem Daumen zu treffen sein. */
const handy = await alsRolle("manager", "CODE-MANA-0001", "leistung.html",
{ width: 390, height: 844 });
const masse = await handy.seite.$$eval(".tagzeile__knopf", (bs) =>
bs.map((b) => { const r = b.getBoundingClientRect(); return { h: Math.round(r.height), w: Math.round(r.width) }; }));
const zuKlein = masse.filter((m) => m.h < 38 || m.w < 60);
ok(masse.length > 0 && zuKlein.length === 0,
`alle ${masse.length} Eintragen-Knöpfe sind groß genug (${zuKlein.length} zu klein)`);
/* NICHTS DARF SEITLICH HERAUSHÄNGEN. Waagerechtes Scrollen auf dem
Handy ist der sicherste Weg, eine Seite unbenutzbar zu machen. */
const breite = await handy.seite.evaluate(() => ({
doc: document.documentElement.scrollWidth,
fenster: window.innerWidth,
}));
ok(breite.doc <= breite.fenster + 1,
`nichts hängt seitlich heraus (${breite.doc} px auf ${breite.fenster} px)`);
/* Und die Wochenkarte bleibt einspaltig -- ein vierstelliger Wert
bricht sonst um, und eine umgebrochene Zahl ist keine Zahl mehr. */
const spalten = await handy.seite.$eval("#zahlkarten",
(e) => getComputedStyle(e).gridTemplateColumns.split(" ").length);
ok(spalten === 1, `die Wochenkarten stehen untereinander (${spalten} Spalte)`);
const wertHoehe = await handy.seite.$$eval(".zahlkarte__wert", (ws) =>
ws.map((w) => Math.round(w.getBoundingClientRect().height)));
const umbruch = wertHoehe.filter((h) => h > 42);
ok(wertHoehe.length > 0 && umbruch.length === 0,
`keine Zahl bricht um (${umbruch.length} von ${wertHoehe.length})`);
await handy.ctx.close();
}
/* =======================================================================
8. Das Dashboard: Zahlen in der Karte und die Frühwarnung
======================================================================= */
melde("\n=== Dashboard ===");
{
await man.seite.goto(`${BASIS}/workspace/uebersicht.html`, { waitUntil: "networkidle" });
await man.seite.waitForTimeout(1600);
const zahlen = await man.seite.$$eval(".kzahlen", (zs) =>
zs.map((z) => ({
ziel: z.getAttribute("href"),
felder: [...z.querySelectorAll(".kzahlen__feld")].map((f) =>
`${f.querySelector(".kzahlen__name")?.textContent}=${f.querySelector(".kzahlen__wert")?.textContent}`),
})));
ok(zahlen.length === 1, `jede Creator-Karte trägt ihre Zahlen (${zahlen.length})`);
ok(zahlen[0]?.felder.some((f) => /^Diamanten=3\.700$/.test(f)),
`die Diamanten stehen dort: ${zahlen[0]?.felder.join(" · ")}`);
ok(/leistung\.html\?creator=/.test(zahlen[0]?.ziel || ""),
`und führen zur Zahlenseite ("${zahlen[0]?.ziel}")`);
/* DIE FRÜHWARNUNG. Der Bestand oben fällt über drei Wochen ab und
hat diese Woche weniger LIVE-Tage -- da MUSS etwas stehen. */
const warnung = await man.seite.$eval("#fruehwarnung", (e) => e.hidden);
ok(!warnung, "die Frühwarnung ist sichtbar");
const signale = await man.seite.$$eval(".warnsignal__text", (ts) => ts.map((t) => t.textContent));
ok(signale.length > 0, `${signale.length} Signale: ${signale.join(" | ").slice(0, 140)}`);
/* KEINE PUNKTZAHL. Genau das war die Entscheidung: sechs benannte
Beobachtungen statt eines Werts, dem man nicht widersprechen kann. */
const punktzahl = await man.seite.evaluate(() =>
/risiko|score|punkte?zahl/i.test(document.getElementById("fruehwarnung")?.textContent || ""));
ok(!punktzahl, "und nirgends steht eine Risiko-Punktzahl");
/* Der Weg zum Gespräch. Ein Hinweis ohne Weg dahin ist nur ein
schlechtes Gewissen. */
const wege = await man.seite.$$eval(".warnkarte__weg", (ws) => ws.map((w) => w.getAttribute("href")));
ok(wege.length > 0 && wege.every((w) => /^chat\.html\?mit=\d+$/.test(w)),
`jede Warnkarte führt ins Gespräch (${wege.join(", ")})`);
/* Auch hier die Schriftgrößen -- die Dashboard-Zeile ist eine eigene
Fläche mit eigenem CSS. Beim ersten Lauf stand .kzahlen__name auf
.66 rem = 10,56 px; genau derselbe Wert, der mir schon zweimal
durchgerutscht ist, weil ihn niemand misst. */
const kleinDash = await man.seite.$$eval(".kzahlen *, .warnkarte *", (es) => es
.filter((e) => e.textContent.trim() && !e.children.length)
.map((e) => ({ t: e.textContent.trim().slice(0, 24), px: parseFloat(getComputedStyle(e).fontSize) }))
.filter((x) => x.px < 12));
const gezaehltDash = await man.seite.$$eval(".kzahlen *, .warnkarte *",
(es) => es.filter((e) => e.textContent.trim() && !e.children.length).length);
ok(gezaehltDash > 8 && kleinDash.length === 0,
`im Dashboard keine Schrift unter 12 px (${gezaehltDash} gemessen, ${kleinDash.length} zu klein`
+ `${kleinDash.length ? ": " + kleinDash.slice(0, 3).map((k) => `${k.t} ${k.px}px`).join(" | ") : ""})`);
/* Und dieser Weg muss WIRKLICH funktionieren. ?mit= war bis zum
06.09.2026 gar nicht vorgesehen -- ein Link, der auf einer leeren
Gesprächsliste endet, ist schlimmer als keiner. */
await man.seite.goto(`${BASIS}/workspace/${wege[0]}`, { waitUntil: "networkidle" });
await man.seite.waitForTimeout(1800);
const offen = await man.seite.$eval("#verlauf-titel", (e) => e.textContent.trim());
ok(offen === "Luna", `der Link öffnet das Gespräch mit "${offen}"`);
}
/* =======================================================================
9. Ein Creator sieht die Frühwarnung NIE
======================================================================= */
melde("\n=== Was der Creator NICHT sieht ===");
{
await luna.seite.goto(`${BASIS}/workspace/uebersicht.html`, { waitUntil: "networkidle" });
await luna.seite.waitForTimeout(1600);
const versteckt = await luna.seite.$eval("#fruehwarnung", (e) => e.hidden);
ok(versteckt, "die Frühwarnung bleibt bei ihr verborgen");
/* Auch von Hand abgefragt kommt nichts. Ein verstecktes Element ist
keine Sicherung. */
const roh = await luna.seite.evaluate(async () => {
const a = await fetch("/workspace/api/fruehwarnung", { credentials: "same-origin" });
return a.ok ? await a.json() : { fehler: a.status };
});
ok(Array.isArray(roh.creator) && roh.creator.length === 0 && roh.gilt_nicht === true,
`die Schnittstelle liefert ihr nichts (${JSON.stringify(roh).slice(0, 70)})`);
}
await luna.ctx.close();
await man.ctx.close();
await browser.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
melde(`\n${gemacht} Pruefungen`);
melde(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`);
/* process.exit() ist hier richtig: Diese Prüfung startet einen
Express-Server, der von selbst weiterläuft (offene Verbindungen,
Zeitgeber für die Sicherung). Mit `exitCode` allein wartet Node
darauf, dass all das endet -- der Lauf liefe in die Zeitüberschreitung,
obwohl alle Prüfungen längst durch sind. Die Browser sind zu, bevor
diese Zeile läuft. */
process.exit(fehler ? 1 : 0);
+8
View File
@@ -1311,6 +1311,14 @@ const GESCHUETZT = {
die Seite nicht öffnen dürfte, könnte auch nicht antworten, und
genau das Antworten ist der Zweck. */
"/workspace/chat.html": null,
/* Zahlen (06.09.2026): jede Rolle. WESSEN Zahlen jemand sieht,
entscheidet sichtbareCreatorIds() -- ein Creator sieht genau seine
eigenen. Ihn von der Seite auszusperren waere auch inhaltlich
falsch: Es sind SEINE Zahlen, und ohne sie ist jedes Ziel, das mit
ihm vereinbart wird, eine Behauptung. EINTRAGEN darf er sie
trotzdem nicht (darfEintragen() in workspace-leistung.js) -- eine
Zahl, die man sich selbst gibt, ist keine Messung. */
"/workspace/leistung.html": null,
/* NUR DogFather (02.09.2026). Die Rollenauswahl verspricht das seit
jeher ("Manager -- dieselben Rechte, ausser der
Personenverwaltung"), die Schranke hielt sich nur nicht daran.
+5 -5
View File
@@ -178,7 +178,7 @@
}
.chat-raum__zeit {
flex: none;
font-size: .7rem; color: var(--text-still);
font-size: .75rem; color: var(--text-still);
font-variant-numeric: tabular-nums;
}
.chat-raum__vorschau {
@@ -266,7 +266,7 @@
grid-column: 1;
width: 30px; height: 30px; border-radius: 50%;
display: grid; place-items: center;
font-size: .7rem; font-weight: 700;
font-size: .75rem; font-weight: 700;
color: var(--r);
background: color-mix(in srgb, var(--r) 13%, transparent);
border: 1px solid color-mix(in srgb, var(--r) 28%, transparent);
@@ -326,7 +326,7 @@
margin-top: 4px;
}
.chat-nachricht__zeit {
font-size: .68rem; color: var(--text-still);
font-size: .75rem; color: var(--text-still);
font-variant-numeric: tabular-nums;
}
/* Der Zurücknehmen-Knopf erscheint erst beim Darüberfahren -- er ist
@@ -334,7 +334,7 @@
Geräten ohne Zeiger steht er immer da (siehe unten). */
.chat-nachricht__weg-knopf {
padding: 0; border: 0; background: none;
font: inherit; font-size: .68rem;
font: inherit; font-size: .75rem;
color: var(--text-still); cursor: pointer;
opacity: 0; transition: opacity .16s ease, color .16s ease;
}
@@ -389,7 +389,7 @@
.chat-wahl__titel {
margin: 0 0 7px;
display: flex; align-items: center; gap: 7px;
font-size: .68rem; font-weight: 700;
font-size: .75rem; font-weight: 700;
letter-spacing: .08em; text-transform: uppercase;
color: var(--text-still);
}
+284
View File
@@ -0,0 +1,284 @@
/* ===================================================================
Zahlen. Baut auf gate.css, start.css und aufgaben.css auf.
DER GESTALTERISCHE GRUNDSATZ dieser Seite ist ein anderer als auf den
uebrigen: Hier steht nicht die Flaeche im Vordergrund, sondern die
ZAHL. Alles andere -- Rahmen, Beschriftung, Vergleich, Linie --
ordnet sich ihr unter und wird leiser, je weiter es von ihr weg ist.
Deshalb:
* Die Zahl in Tabellenziffern (font-variant-numeric), damit
untereinanderstehende Werte auf derselben Stelle sitzen. Ohne das
tanzen 1.240 und 987 gegeneinander, und man vergleicht schlechter.
* Der Vergleich in GEDECKTEM Gruen/Rot, nicht in Signalfarbe. Ein
Minus ist eine Auskunft, kein Alarm -- und die Seite soll
augenschonend bleiben (Dauerregel).
* Die Verlaufslinie ohne Achsen, Gitter und Beschriftung. Sie
beantwortet genau eine Frage ("geht es hoch oder runter?"), und
jede weitere Linie darin macht diese Antwort langsamer.
=================================================================== */
/* ---------- Kopf ------------------------------------------------------- */
.leistung__wahl { display: grid; gap: 6px; min-width: 220px; }
/* Wie vollstaendig die Woche erfasst ist. Steht neben der Ueberschrift
und nicht darunter: Wer die Zahlen liest, muss diesen Satz VORHER
gesehen haben -- eine Woche aus zwei Tagen ist etwas anderes als eine
aus sieben. */
.leistung__erfassung {
margin: 0; font-size: 13px; color: var(--text-still);
}
.leistung__erfassung[data-knapp="ja"] { color: #d9a441; }
.leistung__leer {
margin: 0; padding: 18px; text-align: center;
color: var(--text-leise); font-size: 14px; line-height: 1.6;
}
.leistung-woche, .leistung-ziele, .leistung-tage { margin: 0 0 30px; }
/* ---------- Die Wochenkarten -------------------------------------------- */
.zahlkarten {
display: grid; gap: 14px;
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
}
.zahlkarte {
position: relative; overflow: hidden;
padding: 16px 16px 12px;
background: var(--flaeche);
border: 1px solid var(--rand);
border-radius: var(--radius-klein);
display: grid; gap: 4px;
/* Alle Karten gleich hoch, auch die ohne Verlaufslinie. Sonst
entsteht eine Treppe, und eine Treppe liest man als Bedeutung
("die kurze Karte ist weniger wichtig"), obwohl sie nur ein
Nebeneffekt ist. */
align-content: start; min-height: 132px;
}
/* DIE LEITZAHL. Die Verweildauer ist seit 2026 die Groesse, an der der
Algorithmus alles aufhaengt -- sie darf nicht eine von sieben
gleichaussehenden Karten sein. Ein feiner Rand und ein Hauch Farbe
reichen; ein Kasten in Signalfarbe waere Laerm. */
.zahlkarte[data-leit="ja"] {
border-color: color-mix(in srgb, var(--akzent) 34%, transparent);
background:
radial-gradient(120% 100% at 0% 0%,
color-mix(in srgb, var(--akzent) 9%, transparent), transparent 70%),
var(--flaeche);
}
.zahlkarte__name {
margin: 0; font-size: 12px; letter-spacing: .07em; text-transform: uppercase;
color: var(--text-still);
}
.zahlkarte__wert {
margin: 0; font-size: 30px; line-height: 1.1; font-weight: 650;
color: var(--text);
/* Tabellenziffern: gleiche Breite je Ziffer. Damit stehen die Werte
mehrerer Karten auf derselben senkrechten Linie. */
font-variant-numeric: tabular-nums;
}
.zahlkarte__wandel {
margin: 2px 0 0; font-size: 12.5px; line-height: 1.45;
}
/* Gedeckt, nicht grell. Gruen und Rot sind hier Auskunft, nicht Ampel --
und beide bleiben ueber 4,5:1 gegen die Kachelflaeche. */
.zahlkarte__wandel[data-richtung="hoch"] { color: #6fc38a; }
.zahlkarte__wandel[data-richtung="runter"] { color: #e08c93; }
.zahlkarte__wandel[data-richtung="gleich"] { color: var(--text-still); }
/* Die kleine Verlaufslinie. Sie sitzt unten in der Karte und laeuft
ueber die ganze Breite -- als Untergrund, nicht als eigenes Bild. */
.zahlkarte__linie {
width: 100%; height: 30px; margin-top: 8px; display: block;
overflow: visible;
}
.zahlkarte__linie polyline {
fill: none; stroke: var(--akzent); stroke-width: 1.6;
stroke-linejoin: round; stroke-linecap: round;
/* Nicht mitskalieren: preserveAspectRatio="none" streckt das Bild in
die Breite und wuerde die Linie sonst hauchduenn ziehen. */
vector-effect: non-scaling-stroke;
opacity: .8;
}
.zahlkarte__linie circle {
fill: var(--akzent); stroke: none;
/* Der letzte Punkt ist das Jetzt. Er wird durch die Streckung zum
Ei -- deshalb bleibt er klein genug, dass es nicht auffaellt. */
}
.zahlkarte[data-leit="ja"] .zahlkarte__linie polyline { opacity: 1; }
/* ---------- Ziele -------------------------------------------------------- */
.zielbalken { display: grid; gap: 16px; }
.zielbalken__zeile {
padding: 14px 16px;
background: var(--flaeche);
border: 1px solid var(--rand);
border-radius: var(--radius-klein);
}
.zielbalken__kopf {
display: flex; justify-content: space-between; align-items: baseline;
gap: 12px; margin-bottom: 10px; flex-wrap: wrap;
}
.zielbalken__name { font-size: 14px; color: var(--text); }
.zielbalken__zahl {
font-size: 14px; color: var(--text-leise); font-variant-numeric: tabular-nums;
}
.zielbalken__spur {
height: 10px; border-radius: 999px; overflow: hidden;
background: rgba(150, 186, 220, 0.12);
}
.zielbalken__fuell {
height: 100%; border-radius: 999px;
background: linear-gradient(90deg, var(--akzent-tief), var(--akzent));
transition: width var(--tempo);
}
/* Ein erreichtes Ziel wechselt die Farbe, nicht die Groesse. Ein
springender Balken waere Bewegung ohne Aussage. */
.zielbalken__fuell[data-voll="ja"] {
background: linear-gradient(90deg, #2f8f5b, #55c184);
}
.zielbalken__anteil {
margin: 8px 0 0; font-size: 12.5px; color: var(--text-still);
}
@media (prefers-reduced-motion: reduce) {
.zielbalken__fuell { transition: none; }
}
/* ---------- Die Tageszeilen ---------------------------------------------- */
.tagetabelle {
display: grid; gap: 8px;
}
.tagzeile {
display: grid; align-items: center; gap: 6px 16px;
grid-template-columns: 74px 1fr auto;
padding: 12px 16px;
background: var(--flaeche);
border: 1px solid var(--rand);
border-radius: var(--radius-klein);
}
/* EIN TAG OHNE EINTRAG BLEIBT SICHTBAR -- er ist selbst die Auskunft.
Er wird nur leiser, damit die erfassten Tage vorn stehen. */
.tagzeile[data-leer="ja"] {
background: transparent;
border-style: dashed;
border-color: rgba(150, 186, 220, 0.13);
}
/* Woher die Zeile kommt. Ein schmaler Streifen links, kein Etikett:
Man muss es wissen KOENNEN, nicht bei jedem Blick lesen. */
.tagzeile[data-quelle="import"] {
border-left: 3px solid color-mix(in srgb, var(--violett) 55%, transparent);
}
.tagzeile__datum { display: grid; gap: 1px; }
.tagzeile__wochentag {
font-size: 12px; letter-spacing: .08em; text-transform: uppercase;
color: var(--text-still);
}
.tagzeile__zahl {
font-size: 15px; color: var(--text); font-variant-numeric: tabular-nums;
}
.tagzeile__werte {
display: flex; flex-wrap: wrap; gap: 6px 22px; min-width: 0;
}
.tagzeile__wert { display: grid; gap: 1px; min-width: 0; }
.tagzeile__wertname {
font-size: 12px; letter-spacing: .05em; color: var(--text-still);
}
.tagzeile__wertzahl {
font-size: 15px; color: var(--text); font-variant-numeric: tabular-nums;
}
.tagzeile__leer { font-size: 13.5px; color: var(--text-still); }
/* Die Notiz laeuft ueber die ganze Zeilenbreite unter den Werten --
sie ist der Kontext, ohne den eine eingebrochene Zahl in die Irre
fuehrt ("krank", "Technik ausgefallen"). */
.tagzeile__notiz {
grid-column: 2 / -1;
margin: 2px 0 0; font-size: 13px; line-height: 1.5;
color: var(--text-leise);
}
.tagzeile__knopf {
grid-row: 1; grid-column: 3;
padding: 8px 14px; min-height: 38px;
font: inherit; font-size: 13px;
color: var(--text-leise); cursor: pointer;
background: transparent;
border: 1px solid var(--rand); border-radius: 999px;
transition: color var(--tempo), border-color var(--tempo);
}
.tagzeile__knopf:hover, .tagzeile__knopf:focus-visible {
color: var(--text); border-color: var(--rand-hell);
}
/* ---------- Der Import --------------------------------------------------- */
.dialog--breit { max-width: 720px; }
.import-vorschau {
margin: 4px 0 14px; padding: 16px;
background: rgba(150, 186, 220, 0.05);
border: 1px solid var(--rand);
border-radius: var(--radius-klein);
}
.import-zahlen { display: flex; flex-wrap: wrap; gap: 12px 28px; }
.import-zahl { display: grid; gap: 2px; }
.import-zahl__wert {
font-size: 26px; font-weight: 650; line-height: 1.1;
color: var(--text); font-variant-numeric: tabular-nums;
}
.import-zahl__name { font-size: 12px; color: var(--text-still); }
.import-zahl[data-warn="ja"] .import-zahl__wert { color: #d9a441; }
.import-spalten {
margin: 14px 0 0; font-size: 13px; line-height: 1.6;
color: var(--text-leise);
}
.import-probleme {
margin: 10px 0 0; padding-left: 20px;
font-size: 12.5px; line-height: 1.7; color: #d9a441;
}
/* ---------- Handy --------------------------------------------------------- */
@media (max-width: 640px) {
/* Die Wochenkarten werden EINSPALTIG, nicht enger. Zwei Karten
nebeneinander auf 360 px lassen fuer die Zahl 150 px uebrig -- ein
vierstelliger Wert bricht dann um, und eine umgebrochene Zahl ist
keine Zahl mehr. */
.zahlkarten { grid-template-columns: 1fr; }
.zahlkarte { min-height: 0; }
.zahlkarte__wert { font-size: 27px; }
.leistung__wahl { min-width: 0; width: 100%; }
/* Die Tageszeile wird zweizeilig: Datum und Knopf oben, die Werte
darunter. Drei Spalten nebeneinander quetschen den Knopf sonst auf
unter 44 px -- und darunter trifft man ihn mit dem Daumen nicht
mehr zuverlaessig. */
.tagzeile {
grid-template-columns: 1fr auto;
gap: 10px 12px;
}
.tagzeile__datum { grid-row: 1; grid-column: 1; }
.tagzeile__knopf { grid-row: 1; grid-column: 2; }
.tagzeile__werte { grid-row: 2; grid-column: 1 / -1; gap: 8px 18px; }
.tagzeile__notiz { grid-row: 3; grid-column: 1 / -1; }
.import-zahl__wert { font-size: 23px; }
}
+104
View File
@@ -172,6 +172,104 @@ a.wert:hover { border-color: var(--rand-hell); background: rgba(255, 255, 255, .
}
.creator__luecke:hover { color: var(--akzent); border-color: rgba(63, 189, 245, .35); }
/* ---------- Die Zahlen in der Karte (06.09.2026) --------------------------
Eine eigene Zeile unter den Arbeitszahlen, durch eine feine Linie
abgesetzt: Es sind zwei verschiedene Sorten Auskunft. Oben steht, was
WIR schulden (überfällig, im Review), unten, was der Kanal HERGIBT.
Ohne die Trennung liest man sie als eine Reihe und vergleicht Zahlen,
die nichts miteinander zu tun haben. */
.kzahlen {
display: flex; flex-wrap: wrap; gap: 8px 26px;
margin: 12px 0 0; padding: 12px 0 0;
border-top: 1px solid var(--rand);
text-decoration: none; color: inherit;
}
.kzahlen:hover .kzahlen__wert, .kzahlen:focus-visible .kzahlen__wert { color: var(--akzent); }
.kzahlen[data-leer="ja"] { border-top-style: dashed; }
.kzahlen__feld { display: grid; gap: 1px; min-width: 0; }
.kzahlen__name {
font-size: .75rem; letter-spacing: .06em; text-transform: uppercase;
color: var(--text-still);
}
.kzahlen__wert {
font-size: 1.05rem; color: var(--text);
/* Tabellenziffern: gleiche Breite je Ziffer, damit die Werte mehrerer
Karten untereinander auf derselben Stelle sitzen. */
font-variant-numeric: tabular-nums;
transition: color var(--tempo);
}
.kzahlen__wandel { margin-left: 7px; font-size: .8rem; }
/* Gedeckt, nicht grell -- eine Auskunft, kein Alarm. */
.kzahlen__wandel[data-richtung="hoch"] { color: #6fc38a; }
.kzahlen__wandel[data-richtung="runter"] { color: #e08c93; }
.kzahlen__wandel[data-richtung="gleich"] { color: var(--text-still); }
.kzahlen__leer { font-size: .82rem; color: var(--text-still); }
/* ---------- Die Frühwarnung ------------------------------------------------
Bernstein, nicht Rot. "Hier würde ich nachfassen" ist ein Auftrag,
kein Fehler -- und Rot wäre auf einer Seite, die man jeden Morgen
aufmacht, nach drei Tagen Tapete. Dieselbe Farbe wie das Merkzeichen
in den Bereichen, aus demselben Grund. */
.warnblock { margin: 0 0 26px; }
.warnliste {
display: grid; gap: 12px;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}
.warnkarte {
padding: 14px 16px 12px;
background:
radial-gradient(120% 100% at 0% 0%, rgba(217, 164, 65, .07), transparent 68%),
var(--flaeche);
border: 1px solid rgba(217, 164, 65, .28);
border-radius: var(--radius-klein);
}
.warnkarte__kopf {
display: flex; align-items: baseline; justify-content: space-between;
gap: 12px; margin-bottom: 10px;
}
.warnkarte__name {
font-size: 1rem; font-weight: 600; color: var(--text); text-decoration: none;
}
.warnkarte__name:hover, .warnkarte__name:focus-visible { color: var(--akzent); }
/* Der Weg zum Gespräch. Klein und ruhig -- er soll da sein, wenn man
ihn sucht, und nicht mit dem Namen um Aufmerksamkeit ringen. */
.warnkarte__weg {
flex: none;
padding: 5px 12px; min-height: 32px;
display: inline-flex; align-items: center;
font-size: .8rem; color: var(--text-leise); text-decoration: none;
border: 1px solid var(--rand); border-radius: 999px;
transition: color var(--tempo), border-color var(--tempo);
}
.warnkarte__weg:hover, .warnkarte__weg:focus-visible {
color: var(--akzent); border-color: rgba(63, 189, 245, .35);
}
.warnkarte__signale { margin: 0; padding: 0; list-style: none; display: grid; gap: 8px; }
.warnsignal {
display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px;
padding-left: 14px; position: relative;
}
/* Ein kleiner Punkt statt eines Aufzählungszeichens -- sechs Signale
untereinander mit Spiegelstrichen sähen aus wie eine Mängelliste. */
.warnsignal::before {
content: ''; position: absolute; left: 0; top: .55em;
width: 5px; height: 5px; border-radius: 50%;
background: rgba(217, 164, 65, .75);
}
.warnsignal__text { font-size: .84rem; line-height: 1.55; color: var(--text); }
.warnsignal__beleg {
font-size: .8rem; color: var(--text-still); font-variant-numeric: tabular-nums;
}
/* ---------- Rest ----------------------------------------------------------- */
.stand {
@@ -188,6 +286,12 @@ a.wert:hover { border-color: var(--rand-hell); background: rgba(255, 255, 255, .
.naechstes { justify-items: flex-start; text-align: left; }
.summen { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.stand { text-align: left; }
/* Drei Zahlen nebeneinander lassen auf 360 px je 100 px -- "Ø
Verweildauer" bricht dann in drei Zeilen um. Zwei Spalten, die
dritte darunter. */
.kzahlen { gap: 10px 20px; }
.warnliste { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
+35
View File
@@ -125,6 +125,16 @@
],
automation: [
'M13.2 3.2 6 13.4h5.2l-.8 7.4 7.6-10.6h-5.2z'
],
/* Zahlen (06.09.2026): die Flaeche UNTER einem steigenden
Linienzug. Bewusst kein zweites Balkendiagramm -- "Reports &
Review" hat schon eins, und zwei Kacheln mit demselben Zeichen
sind schlimmer als eine ohne: Man klickt zweimal falsch, bevor
man merkt, dass es zwei verschiedene Sachen sind. Eine Linie
ist auch die richtige Aussage: Es geht hier nicht um einen
Bericht, sondern um einen VERLAUF. */
zahlen: [
'M4 16 9.5 11.5 13 14 20 6.5 20 19.5 4 19.5z'
]
};
@@ -247,6 +257,15 @@
fuellung: ['M13.2 3.2 6 13.4h5.2l-.8 7.4 7.6-10.6h-5.2z'],
linien: ['M13.2 3.2 6 13.4h5.2l-.8 7.4 7.6-10.6h-5.2z'],
},
zahlen: {
fuellung: ['M4 16 9.5 11.5 13 14 20 6.5 20 19.5 4 19.5z'],
/* Der Linienzug selbst, die Grundlinie darunter und ein Punkt am
oberen Ende -- der letzte Messwert, das Jetzt. Genau dieselbe
Bildsprache wie die kleine Linie in jeder Zahlkarte: Wer die
Kachel gesehen hat, erkennt die Karten wieder. */
linien: ['M4 16 9.5 11.5 13 14 20 6.5', 'M3.6 19.8h16.8',
'M20 5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z'],
},
};
/* ---------- Die Bereiche -------------------------------------------------
@@ -315,6 +334,22 @@
Ein Creator sieht nur seine eigene Akte (dort steht auch sein
Steckbrief mit drin, er hat ja nur eine Seite). Alle anderen
bekommen beide Kacheln getrennt. */
/* ZAHLEN STEHT VORN (06.09.2026).
Der Workspace organisierte bis dahin ARBEIT hervorragend und
wusste über das GESCHÄFT nichts -- keine Diamanten, keine
LIVE-Tage, keine Verweildauer. Damit hing die ganze Gruppe
hier in der Luft: Der Start-Check bewertet, ohne zu messen,
der Report fragt "was hat funktioniert?" ohne Beleg, und das
90-Tage-Ziel war ein Satz statt eines Fortschritts. Deshalb
die erste Kachel der Gruppe, nicht die letzte.
Ton 4 (Violett) ist von Hand gewählt und nicht gerechnet: Er
liegt weit von seinem Nachbarn 15 (Blaugrün) und von 8
(Orangerot) darunter. Wer diese Gruppe umsortiert, lässt
tools/kachel-farben.mjs neu laufen -- die Nachbarschaften
haben sich durch diese Kachel geändert. */
{ name: 'Zahlen', unter: 'Diamanten, LIVE-Tage, Verweildauer', zeichen: 'zahlen', ton: 4,
rollen: ['admin', 'manager', 'creator', 'scout'], ziel: 'leistung.html', szene: 'arena' },
{ name: 'Mein Steckbrief', unter: 'Dein Bild und deine Kanäle', zeichen: 'steckbrief', ton: 15,
rollen: ['admin', 'manager', 'scout'], ziel: 'steckbrief.html', szene: 'halle' },
{ name: 'Creator-Profile', unter: 'Ziele und 90-Tage-Plan', zeichen: 'profil', ton: 8,
+26 -2
View File
@@ -637,7 +637,31 @@
/* Kommt man aus einer Benachrichtigung, steht das Gespräch in der
Adresse -- dann direkt hinein. */
const wunsch = Number(new URLSearchParams(location.search).get('raum'));
if (Number.isInteger(wunsch) && wunsch > 0) await raumOeffnen(wunsch);
const suche = new URLSearchParams(location.search);
const wunsch = Number(suche.get('raum'));
if (Number.isInteger(wunsch) && wunsch > 0) { await raumOeffnen(wunsch); return; }
/* ?mit=<Person> -- der Weg von woanders her (06.09.2026).
Die Frühwarnung im Dashboard schlägt vor nachzufassen; ohne
diesen Einstieg müsste man danach die Gesprächsliste durchsuchen
oder ein Gespräch von Hand anlegen, das vielleicht schon
existiert. `/direkt` legt beim ersten Mal an und findet beim
zweiten dasselbe wieder -- ein Aufruf für beide Fälle.
Die Berechtigung prüft der Server (darfSchreibenMit). Eine von
Hand getippte Personennummer bringt hier nichts. */
const mit = Number(suche.get('mit'));
if (!Number.isInteger(mit) || mit < 1) return;
try {
const a = await hole('/workspace/api/chat/direkt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mit }),
});
if (!a.ok) return; /* die Liste steht ja */
const { raum_id: raumId } = await a.json();
await raeumeLaden();
await raumOeffnen(raumId);
} catch { /* die Liste steht ja */ }
})();
})();
+590
View File
@@ -0,0 +1,590 @@
/* =====================================================================
leistung.js — die Zahlen über das Geschäft.
Stufe 1 des Plans vom 31.08.2026. Der Server rechnet (siehe
workspace-leistung.js), diese Datei zeigt und nimmt entgegen.
DREI DINGE, die jede Zahl hier mitbringt -- sonst fliegt sie raus:
1. den WERT
2. den VERGLEICH zur Vorwoche ("+23 %")
3. den VERLAUF als kleine Linie
Eine Zahl ohne diese drei ist eine Eitelkeitszahl. Sie sieht nach
Auskunft aus, und man kann nichts entscheiden.
===================================================================== */
(() => {
'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 LEITUNG = new Set(['admin', 'manager']);
let ich = null;
let creator = [];
let offenerCreator = 0;
let stand = null;
let importText = '';
let importSpalten = null;
async function hole(weg, einst) {
const a = await fetch(weg, { credentials: 'same-origin', ...einst });
if (a.status === 401) { location.assign('/workspace/'); throw new Error('weg'); }
return a;
}
const melde = (t) => { $('fehler').textContent = t || ''; };
/* ---------- Zahlen lesbar machen ---------------------------------------- */
/** 1240 → "1.240". Tausenderpunkte, damit man vierstellige Zahlen
* auf einen Blick erfasst. */
const zahl = (n) => (n === null || n === undefined ? '–' : Number(n).toLocaleString('de-DE'));
/** Sekunden → "3:30 Min". Eine Verweildauer von 210 Sekunden liest
* niemand als dreieinhalb Minuten, wenn sie als "210" dasteht. */
function dauer(s) {
if (s === null || s === undefined) return '–';
const m = Math.floor(s / 60);
const rest = Math.round(s % 60);
return m ? `${m}:${String(rest).padStart(2, '0')} Min` : `${rest} Sek`;
}
/** Minuten → "1 Std 35". */
function stunden(min) {
if (min === null || min === undefined) return '–';
const h = Math.floor(min / 60);
const m = min % 60;
return h ? `${h} Std ${m}` : `${m} Min`;
}
/* ---------- Die Wochenkarten -------------------------------------------- */
const FELDER = [
/* Die Verweildauer steht VORN. Sie ist die Leitzahl (Recherche
06.09.2026), nicht eine von acht. */
{ key: 'verweildauer_s', name: 'Ø Verweildauer', form: dauer, leit: true },
{ key: 'diamanten', name: 'Diamanten', form: zahl },
{ key: 'gueltige_tage', name: 'LIVE-Tage', form: (n) => `${n} von 7` },
{ key: 'dauer_min', name: 'LIVE-Zeit', form: stunden },
{ key: 'zuschauer_avg', name: 'Ø Zuschauer', form: zahl },
{ key: 'schenker', name: 'Schenker', form: zahl },
{ key: 'follower_neu', name: 'Neue Follower', form: zahl },
];
function wochenKartenBauen() {
const ziel = $('zahlkarten');
ziel.textContent = '';
if (!stand) return;
for (const f of FELDER) {
const w = stand.woche[f.key];
if (!w) continue;
const karte = el('article', 'zahlkarte');
if (f.leit) karte.dataset.leit = 'ja';
karte.append(el('p', 'zahlkarte__name', f.name));
karte.append(el('p', 'zahlkarte__wert', f.form(w.wert)));
/* DER VERGLEICH. Ohne ihn ist die Zahl eine Behauptung. */
const v = el('p', 'zahlkarte__wandel');
if (w.wandel?.prozent !== undefined) {
const p = w.wandel.prozent;
v.dataset.richtung = p > 0 ? 'hoch' : p < 0 ? 'runter' : 'gleich';
v.textContent = `${p > 0 ? '+' : ''}${p} % gegenüber der Vorwoche`;
} else if (w.wandel?.neu) {
/* "Von null auf 500" -- keine Prozent, sondern die Auskunft,
die dahintersteckt. */
v.dataset.richtung = 'hoch';
v.textContent = 'neu – vorher nichts';
} else {
v.dataset.richtung = 'gleich';
v.textContent = w.vorher === null ? 'kein Vergleich' : 'wie in der Vorwoche';
}
karte.append(v);
/* DER VERLAUF als kleine Linie. Acht Wochen -- ohne Richtung ist
ein Wert nur ein Punkt. */
const linie = verlaufsLinie(stand.verlauf, f.key);
if (linie) karte.append(linie);
ziel.append(karte);
}
}
/** Eine kleine Linie aus den letzten acht Wochen.
*
* Selbst gezeichnet als SVG-Pfad, kein Diagrammpaket: Für acht
* Punkte eine Bibliothek zu laden wäre in einem Haus mit drei
* Abhängigkeiten die vierte -- und sie brächte Farben, Achsen und
* Beschriftungen mit, die hier alle stören würden. */
function verlaufsLinie(verlauf, key) {
if (!Array.isArray(verlauf) || verlauf.length < 3) return null;
/* == null FAENGT AUCH undefined -- und genau darauf kam es an.
Der Server liefert im Verlauf nur drei der sieben Groessen
(Diamanten, LIVE-Tage, Verweildauer); fuer alle anderen ist
w[key] undefined. Mit `=== null` rutschte das durch, wurde zu
Number(undefined) = NaN, und die Linie bekam Punkte wie
"NaN,NaN" -- der Browser zeichnet dann gar nichts und sagt
nichts. Eine leere Stelle in der Karte, ohne Fehlermeldung. */
const werte = verlauf.map((w) => {
const r = w?.[key];
if (r === null || r === undefined) return null;
const n = Number(r);
return Number.isFinite(n) ? n : null;
});
const echte = werte.filter((w) => w !== null);
if (echte.length < 3) return null;
const max = Math.max(...echte);
const min = Math.min(...echte);
const spanne = max - min || 1;
const B = 100, H = 26;
const punkte = werte.map((w, i) => {
if (w === null) return null;
const x = (i / (werte.length - 1)) * B;
/* Von unten nach oben: In SVG wächst y nach unten. */
const y = H - ((w - min) / spanne) * (H - 4) - 2;
return `${x.toFixed(1)},${y.toFixed(1)}`;
}).filter(Boolean);
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('class', 'zahlkarte__linie');
svg.setAttribute('viewBox', `0 0 ${B} ${H}`);
svg.setAttribute('preserveAspectRatio', 'none');
svg.setAttribute('aria-hidden', 'true');
svg.innerHTML = `<polyline points="${punkte.join(' ')}" />`;
/* Der letzte Punkt bekommt einen Kreis -- er ist das Jetzt. */
const letzter = punkte[punkte.length - 1].split(',');
svg.innerHTML += `<circle cx="${letzter[0]}" cy="${letzter[1]}" r="2.2" />`;
return svg;
}
/* ---------- Ziele als Fortschritt --------------------------------------- */
function zieleBauen() {
const ziel = $('zielbalken');
ziel.textContent = '';
const z = stand?.ziele;
$('ziele-aendern').hidden = !darfEintragen();
if (!z || (!z.diamanten && !z.tage && !z.verweildauer)) {
ziel.append(el('p', 'leistung__leer',
darfEintragen()
? 'Noch keine Ziele gesetzt. Aus „mehr Reichweite" wird hier eine prüfbare Größe.'
: 'Noch keine Ziele gesetzt.'));
return;
}
for (const [key, name, form] of [
['diamanten', 'Diamanten', zahl],
['tage', 'LIVE-Tage', (n) => String(n)],
['verweildauer', 'Ø Verweildauer', dauer],
]) {
const t = z[key];
if (!t) continue;
const zeile = el('div', 'zielbalken__zeile');
const kopf = el('div', 'zielbalken__kopf');
kopf.append(el('span', 'zielbalken__name', name));
kopf.append(el('span', 'zielbalken__zahl', `${form(t.ist)} von ${form(t.ziel)}`));
zeile.append(kopf);
const spur = el('div', 'zielbalken__spur');
const fuell = el('div', 'zielbalken__fuell');
/* Über 100 % wird nicht breiter -- der Balken bliebe sonst aus
dem Kasten hängen. Die Zahl daneben sagt es trotzdem. */
fuell.style.width = `${Math.min(100, Math.max(0, t.anteil))}%`;
if (t.anteil >= 100) fuell.dataset.voll = 'ja';
spur.append(fuell);
zeile.append(spur);
zeile.append(el('p', 'zielbalken__anteil',
t.anteil >= 100 ? `Ziel erreicht (${t.anteil} %)` : `${t.anteil} %`));
ziel.append(zeile);
}
}
const darfEintragen = () => LEITUNG.has(ich?.rolle) || ich?.rolle === 'scout';
/* ---------- Die Tageszeilen --------------------------------------------- */
async function tageLaden() {
let tage = [];
try {
const a = await hole(`/workspace/api/leistung/${offenerCreator}/tage?tage=21`);
if (a.ok) tage = (await a.json()).tage || [];
} catch { return; }
const ziel = $('tagetabelle');
ziel.textContent = '';
const nach = new Map(tage.map((t) => [t.tag, t]));
/* DIE LETZTEN 14 TAGE, auch die ohne Eintrag. Nur die vorhandenen
zu zeigen hieße, dass eine Lücke unsichtbar ist -- und genau die
Lücke ist die Auskunft: Da war nichts, oder es wurde nicht
erfasst. */
for (let i = 0; i < 14; i++) {
const d = new Date();
d.setDate(d.getDate() - i);
const p = (n) => String(n).padStart(2, '0');
const tag = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
const z = nach.get(tag);
const zeile = el('div', 'tagzeile');
if (!z) zeile.dataset.leer = 'ja';
if (z?.quelle === 'import') zeile.dataset.quelle = 'import';
const datum = el('div', 'tagzeile__datum');
datum.append(el('span', 'tagzeile__wochentag',
d.toLocaleDateString('de-DE', { weekday: 'short' })));
datum.append(el('span', 'tagzeile__zahl',
d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' })));
zeile.append(datum);
const werte = el('div', 'tagzeile__werte');
if (z) {
werte.append(wert('Diamanten', zahl(z.diamanten)));
werte.append(wert('LIVE', stunden(z.dauer_min)));
werte.append(wert('Verweil', dauer(z.verweildauer_s)));
werte.append(wert('Ø Zusch.', zahl(z.zuschauer_avg)));
} else {
werte.append(el('span', 'tagzeile__leer', 'nichts erfasst'));
}
zeile.append(werte);
if (z?.notiz) {
const n = el('p', 'tagzeile__notiz', z.notiz);
zeile.append(n);
}
if (darfEintragen()) {
const knopf = el('button', 'tagzeile__knopf', z ? 'ändern' : 'eintragen');
knopf.type = 'button';
knopf.setAttribute('aria-label',
`${tag} ${z ? 'ändern' : 'eintragen'}`);
knopf.addEventListener('click', () => tagOeffnen(tag, z));
zeile.append(knopf);
}
ziel.append(zeile);
}
}
function wert(name, text) {
const s = el('span', 'tagzeile__wert');
s.append(el('span', 'tagzeile__wertname', name));
s.append(el('span', 'tagzeile__wertzahl', text));
return s;
}
/* ---------- Eintragen ---------------------------------------------------- */
function tagOeffnen(tag, z) {
const d = $('tag-dialog');
d.dataset.tag = tag;
const datum = new Date(tag + 'T12:00:00');
$('tag-titel').textContent = datum.toLocaleDateString('de-DE',
{ weekday: 'long', day: 'numeric', month: 'long' });
$('tag-unter').textContent = z?.quelle === 'import'
? 'Diese Zeile kam aus einer Datei. Änderungen von Hand bleiben erhalten, bis erneut eingelesen wird.'
: 'Leer lassen heißt: nicht gemessen. Eine Null ist etwas anderes.';
$('f-diamanten').value = z?.diamanten ?? '';
$('f-dauer').value = z?.dauer_min ?? '';
$('f-verweildauer').value = z?.verweildauer_s ?? '';
$('f-zavg').value = z?.zuschauer_avg ?? '';
$('f-zmax').value = z?.zuschauer_max ?? '';
$('f-schenker').value = z?.schenker ?? '';
$('f-follower').value = z?.follower_neu ?? '';
$('f-notiz').value = z?.notiz ?? '';
$('tag-fehler').textContent = '';
$('tag-loeschen').hidden = !z;
d.showModal();
$('f-diamanten').focus();
}
async function tagSpeichern(e) {
e.preventDefault();
const d = $('tag-dialog');
const tag = d.dataset.tag;
const n = (id) => {
const v = $(id).value.trim();
return v === '' ? null : Number(v);
};
const dauerMin = n('f-dauer');
try {
const a = await hole(`/workspace/api/leistung/${offenerCreator}/${tag}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
diamanten: n('f-diamanten'),
dauer_min: dauerMin,
/* Ein gültiger Tag ist ein Tag mit LIVE-Zeit -- daraus
geschlossen, statt ein weiteres Kästchen zu verlangen. */
gueltiger_tag: dauerMin > 0,
verweildauer_s: n('f-verweildauer'),
zuschauer_avg: n('f-zavg'),
zuschauer_max: n('f-zmax'),
schenker: n('f-schenker'),
follower_neu: n('f-follower'),
notiz: $('f-notiz').value,
}),
});
if (!a.ok) {
$('tag-fehler').textContent = (await a.json().catch(() => ({}))).fehler || 'Ging nicht.';
return;
}
d.close();
await allesLaden();
} catch { /* umgeleitet */ }
}
async function tagLoeschen() {
const d = $('tag-dialog');
if (!window.confirm('Diese Zeile wirklich löschen? Die Zahlen des Tages sind dann weg.')) return;
try {
const a = await hole(`/workspace/api/leistung/${offenerCreator}/${d.dataset.tag}`,
{ method: 'DELETE' });
if (!a.ok) { $('tag-fehler').textContent = 'Ging nicht.'; return; }
d.close();
await allesLaden();
} catch { /* umgeleitet */ }
}
/* ---------- Ziele ------------------------------------------------------- */
function zieleOeffnen() {
const z = stand?.ziele;
$('z-diamanten').value = z?.diamanten?.ziel ?? '';
$('z-tage').value = z?.tage?.ziel ?? '';
$('z-verweildauer').value = z?.verweildauer?.ziel ?? '';
$('ziele-fehler').textContent = '';
$('ziele-dialog').showModal();
}
async function zieleSpeichern(e) {
e.preventDefault();
const n = (id) => {
const v = $(id).value.trim();
return v === '' ? null : Number(v);
};
try {
const a = await hole(`/workspace/api/leistung/${offenerCreator}/ziele`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
diamanten_woche: n('z-diamanten'),
tage_woche: n('z-tage'),
verweildauer_s: n('z-verweildauer'),
}),
});
if (!a.ok) { $('ziele-fehler').textContent = 'Ging nicht.'; return; }
$('ziele-dialog').close();
await allesLaden();
} catch { /* umgeleitet */ }
}
/* ---------- Import ------------------------------------------------------ */
async function dateiGewaehlt(e) {
const datei = e.target.files?.[0];
if (!datei) return;
$('import-fehler').textContent = '';
$('vorschau').hidden = true;
$('import-los').disabled = true;
try {
importText = await datei.text();
} catch {
$('import-fehler').textContent = 'Die Datei ließ sich nicht lesen.';
return;
}
/* ERST DIE VORSCHAU. Ein Import, der sofort schreibt, ist bei
einer falsch zugeordneten Spalte nicht mehr zurückzuholen. */
try {
const a = await hole(`/workspace/api/leistung/${offenerCreator}/import/vorschau`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: importText }),
});
const v = await a.json();
if (!a.ok) {
$('import-fehler').textContent = v.fehler || 'Die Datei konnte nicht gelesen werden.';
return;
}
importSpalten = v.spalten;
vorschauZeigen(v);
} catch { /* umgeleitet */ }
}
function vorschauZeigen(v) {
const ziel = $('vorschau');
ziel.textContent = '';
ziel.hidden = false;
/* Die ZAHLEN stehen vorn: Wer übernimmt, will wissen "wie viele",
nicht "welche". */
const zeile = el('div', 'import-zahlen');
zeile.append(importZahl(v.neu, 'neu'));
zeile.append(importZahl(v.ersetzt, v.ersetzt === 1 ? 'wird ersetzt' : 'werden ersetzt'));
if (v.fehlerhaft) zeile.append(importZahl(v.fehlerhaft, 'unlesbar', true));
ziel.append(zeile);
/* Welche Spalten erkannt wurden -- damit man merkt, wenn etwas
fehlt. Eine stillschweigend nicht erkannte Spalte wäre eine
Zahl, die einfach nie ankommt. */
const erkannt = Object.keys(importSpalten || {}).filter((k) => k !== 'tag');
const namen = {
diamanten: 'Diamanten', dauer_min: 'LIVE-Dauer', zuschauer_avg: 'Ø Zuschauer',
zuschauer_max: 'Spitze', verweildauer_s: 'Verweildauer',
schenker: 'Schenker', follower_neu: 'Follower',
};
ziel.append(el('p', 'import-spalten',
erkannt.length
? 'Erkannt: ' + erkannt.map((k) => namen[k] || k).join(' · ')
: 'Außer dem Datum wurde keine Spalte erkannt – die Zahlen bleiben leer.'));
if (v.probleme?.length) {
const liste = el('ul', 'import-probleme');
for (const p of v.probleme) {
liste.append(el('li', null, `Zeile ${p.zeile}: ${p.grund}`));
}
ziel.append(liste);
}
$('import-los').disabled = !(v.neu + v.ersetzt);
}
function importZahl(n, name, warn = false) {
const k = el('div', 'import-zahl');
if (warn) k.dataset.warn = 'ja';
k.append(el('span', 'import-zahl__wert', String(n)));
k.append(el('span', 'import-zahl__name', name));
return k;
}
async function importUebernehmen(e) {
e.preventDefault();
if (!importText) return;
const knopf = $('import-los');
knopf.disabled = true;
try {
const a = await hole(`/workspace/api/leistung/${offenerCreator}/import`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: importText, spalten: importSpalten }),
});
const r = await a.json();
if (!a.ok) { $('import-fehler').textContent = r.fehler || 'Ging nicht.'; return; }
$('import-dialog').close();
$('datei').value = '';
importText = '';
await allesLaden();
melde(`${r.geschrieben} Tage übernommen.`);
setTimeout(() => melde(''), 5000);
} catch { /* umgeleitet */ } finally { knopf.disabled = false; }
}
/* ---------- Laden ------------------------------------------------------- */
async function allesLaden() {
if (!offenerCreator) return;
try {
const a = await hole(`/workspace/api/leistung/${offenerCreator}`);
if (!a.ok) { melde('Die Zahlen konnten nicht geladen werden.'); return; }
stand = await a.json();
} catch { return; }
$('woche').hidden = false;
$('ziele-block').hidden = false;
$('tage-block').hidden = false;
/* WIE VOLLSTÄNDIG IST DIE WOCHE? Eine Zahl aus zwei von sieben
Tagen ist etwas anderes als eine aus sieben -- ohne diesen
Hinweis hält man eine halbe Woche für einen Einbruch. */
const e = stand.erfassung;
$('erfassung').textContent = e.tage >= 7
? 'alle 7 Tage erfasst'
: `${e.tage} von 7 Tagen erfasst`;
$('erfassung').dataset.knapp = e.tage < 4 ? 'ja' : '';
wochenKartenBauen();
zieleBauen();
await tageLaden();
}
/* ---------- Start -------------------------------------------------------- */
$('tag-form').addEventListener('submit', tagSpeichern);
$('tag-zu').addEventListener('click', () => $('tag-dialog').close());
$('tag-loeschen').addEventListener('click', tagLoeschen);
$('ziele-aendern').addEventListener('click', zieleOeffnen);
$('ziele-form').addEventListener('submit', zieleSpeichern);
$('ziele-zu').addEventListener('click', () => $('ziele-dialog').close());
$('import-oeffnen').addEventListener('click', () => {
$('vorschau').hidden = true;
$('import-los').disabled = true;
$('import-fehler').textContent = '';
$('import-dialog').showModal();
});
$('import-zu').addEventListener('click', () => $('import-dialog').close());
$('datei').addEventListener('change', dateiGewaehlt);
$('import-form').addEventListener('submit', importUebernehmen);
(async () => {
try {
const a = await hole('/workspace/api/ich');
if (!a.ok) { location.assign('/workspace/'); return; }
ich = await a.json();
} catch { return; }
window.werZeigen?.(ich);
/* Ein Creator sieht nur sich selbst -- ohne Auswahlfeld, das nur
einen Eintrag hätte. */
if (ich.rolle === 'creator') {
offenerCreator = ich.id;
await allesLaden();
return;
}
try {
const { creator: liste } = await (await hole('/workspace/api/leistung')).json();
creator = liste || [];
} catch { return; }
if (!creator.length) {
melde('Dir ist noch kein Creator zugeordnet.');
return;
}
$('creator-wahl').hidden = false;
for (const c of creator) {
const o = document.createElement('option');
o.value = String(c.id);
/* Die wichtigste Zahl steht gleich in der Auswahl -- so sieht
man beim Umschalten, wo etwas los ist. */
o.textContent = c.tage_erfasst
? `${c.name} — ${zahl(c.diamanten)} Diamanten`
: `${c.name} — noch keine Zahlen`;
$('creator').append(o);
}
/* Vom Dashboard kommend: direkt zur richtigen Person. */
const wunsch = Number(new URLSearchParams(location.search).get('creator'));
offenerCreator = creator.some((c) => c.id === wunsch) ? wunsch : creator[0].id;
$('creator').value = String(offenerCreator);
window.personenwahl?.('creator');
$('creator').addEventListener('change', async () => {
offenerCreator = Number($('creator').value);
history.replaceState(null, '', `leistung.html?creator=${offenerCreator}`);
await allesLaden();
});
await allesLaden();
})();
})();
+137 -2
View File
@@ -66,7 +66,65 @@
return a;
}
function karte(c, checkGesamt, eigen) {
/* ---------- Die Zahlen in der Karte -------------------------------------
Bis zum 06.09.2026 zeigte diese Karte ausschliesslich ARBEIT:
überfällig, offen, in Arbeit, im Review. Über das GESCHÄFT stand
hier nichts -- und damit fehlte ausgerechnet die Zeile, wegen der
man ein Dashboard aufmacht. Ein Creator konnte null überfällige
Aufgaben haben und gleichzeitig seit drei Wochen einbrechen; die
Karte sah dann tadellos aus.
Drei Zahlen, nicht sieben: Diamanten (das Ergebnis), LIVE-Tage (die
Anwesenheit) und die Verweildauer (die Leitgrösse, an der der
Algorithmus alles aufhängt). Alles Weitere steht eine Seite weiter.
Eine Karte, die alles zeigt, zeigt nichts. */
const zahlDe = (n) => Number(n || 0).toLocaleString('de-DE');
function dauerKurz(s) {
if (s === null || s === undefined) return '–';
const m = Math.floor(s / 60);
return m ? `${m}:${String(Math.round(s % 60)).padStart(2, '0')}` : `${Math.round(s)}s`;
}
function zahlenZeile(z, creatorId) {
const a = el('a', 'kzahlen');
a.href = `leistung.html?creator=${creatorId}`;
/* KEINE ZAHLEN IST EINE EIGENE AUSSAGE, keine Null. Wer hier "0
Diamanten" schreibt, behauptet eine Messung, die es nicht gab --
und der Betreuer sucht danach den Einbruch statt den fehlenden
Export. */
if (!z || !z.tage_erfasst) {
a.dataset.leer = 'ja';
a.append(el('span', 'kzahlen__leer', 'Für diese Woche sind noch keine Zahlen erfasst'));
return a;
}
const feld = (name, text, extra) => {
const s = el('span', 'kzahlen__feld');
s.append(el('span', 'kzahlen__name', name));
s.append(el('span', 'kzahlen__wert', text));
if (extra) s.append(extra);
return s;
};
/* Der Vergleich zur Vorwoche steht DIREKT an der Zahl. Ohne ihn ist
"1.240" eine Behauptung; mit ihm ist es eine Auskunft. */
let wandel = null;
if (z.wandel_diamanten?.prozent !== undefined) {
const p = z.wandel_diamanten.prozent;
wandel = el('span', 'kzahlen__wandel', `${p > 0 ? '+' : ''}${p} %`);
wandel.dataset.richtung = p > 0 ? 'hoch' : p < 0 ? 'runter' : 'gleich';
}
a.append(feld('Diamanten', zahlDe(z.diamanten), wandel));
a.append(feld('LIVE-Tage', `${z.gueltige_tage} / 7`));
a.append(feld('Ø Verweildauer', dauerKurz(z.verweildauer_s)));
return a;
}
function karte(c, checkGesamt, eigen, zahlen) {
const k = el('article', 'creator');
if (c.ueberfaellig > 0 || c.dringend > 0) k.dataset.achtung = 'ja';
@@ -114,6 +172,8 @@
werte.append(wert(c.erledigt30, 'erledigt 30 T.', 'aufgaben.html'));
k.append(werte);
k.append(zahlenZeile(zahlen, c.id));
/* Start-Check als Balken statt als Zahl -- "4 von 16" liest man,
ein Viertel gefuellt sieht man. */
const check = el('a', 'fortschritt');
@@ -173,6 +233,57 @@
s.hidden = false;
}
/* ---------- Die Frühwarnung ---------------------------------------------
Sechs benannte Signale, jedes im Klartext und mit Beleg. Bewusst
keine Punktzahl: "Abwanderungsrisiko 73" klingt nach Wissenschaft,
ist aber eine Behauptung, der man nicht widersprechen kann. Sechs
Sätze, die man einzeln bestreiten kann, sind ehrlicher -- und
handlungsfähiger, denn jeder sagt schon, was zu tun wäre. */
async function fruehwarnungLaden() {
let d;
try {
const a = await hole('/workspace/api/fruehwarnung');
if (!a.ok) return; /* still bleiben, nicht stören */
d = await a.json();
} catch { return; }
const leute = d.creator || [];
if (!leute.length) return; /* nichts zu melden ist kein Block */
const ziel = $('warnliste');
ziel.textContent = '';
for (const c of leute) {
const k = el('article', 'warnkarte');
const kopf = el('div', 'warnkarte__kopf');
const name = el('a', 'warnkarte__name', c.name);
name.href = `leistung.html?creator=${c.id}`;
kopf.append(name);
/* Der Weg zum Gespräch steht gleich daneben. Bei vier der sechs
Signale ist "einmal schreiben" die richtige nächste Handlung --
und ein Hinweis ohne Weg dahin ist nur ein schlechtes
Gewissen. */
const schreiben = el('a', 'warnkarte__weg', 'Schreiben');
schreiben.href = `chat.html?mit=${c.id}`;
kopf.append(schreiben);
k.append(kopf);
const liste = el('ul', 'warnkarte__signale');
for (const s of c.signale || []) {
const li = el('li', 'warnsignal');
li.dataset.art = s.art;
li.append(el('span', 'warnsignal__text', s.text));
if (s.beleg) li.append(el('span', 'warnsignal__beleg', s.beleg));
liste.append(li);
}
k.append(liste);
ziel.append(k);
}
$('fruehwarnung').hidden = false;
}
(async () => {
let ich;
try {
@@ -229,6 +340,23 @@
summenZeigen(creator);
/* Die Zahlen kommen aus EINER zusätzlichen Abfrage für alle Creator,
nicht aus einer je Karte. Bei zwölf Creators wären das zwölf
Anfragen, die alle dasselbe tun -- und die Seite stünde so lange
halb leer.
Und sie darf nicht scheitern dürfen: Wenn das Zahlen-Modul
stolpert, bleibt die Übersicht wie vorher stehen, nur ohne die
drei Zeilen. Eine leere Seite wäre der schlechtere Ausgang. */
let zahlenNach = new Map();
try {
const a = await hole('/workspace/api/leistung');
if (a.ok) {
const z = await a.json();
zahlenNach = new Map((z.creator || []).map((c) => [c.id, c]));
}
} catch { /* die Karten stehen auch ohne */ }
/* Wo etwas brennt, steht oben. Sortiert wird nach dem, was Handlung
verlangt -- nicht alphabetisch. Bei Gleichstand dann doch nach
Namen, damit die Reihenfolge stabil bleibt. */
@@ -237,9 +365,16 @@
|| String(a.name).localeCompare(String(b.name), 'de'));
const box = el('div', 'creator-liste');
for (const c of sortiert) box.append(karte(c, d.check_gesamt || 16, eigen));
for (const c of sortiert) {
box.append(karte(c, d.check_gesamt || 16, eigen, zahlenNach.get(c.id)));
}
ziel.append(box);
/* Zum Schluss, und ohne await davor: Die Karten sollen nicht auf die
Frühwarnung warten. Sie schiebt sich oben ein, wenn sie da ist --
für einen Creator kommt sie nie (der Server liefert ihm nichts). */
if (!eigen) fruehwarnungLaden();
if (d.stand) {
/* Der Server schickt UTC. zeitpunkt() darf hier NICHT genommen
werden -- das schneidet die Zeichenkette nur auf und zeigte im
+8 -8
View File
@@ -9,9 +9,9 @@
<meta name="theme-color" content="#0674b9" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061826" />
</head>
<body class="start">
@@ -211,10 +211,10 @@
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609061746" defer></script>
<script src="assets/js/bereiche.js?v=202609061746" defer></script>
<script src="assets/js/kopf.js?v=202609061746" defer></script>
<script src="assets/js/glocke.js?v=202609061746" defer></script>
<script src="assets/js/aufgaben.js?v=202609061746" defer></script>
<script src="assets/js/wahl.js?v=202609061826" defer></script>
<script src="assets/js/bereiche.js?v=202609061826" defer></script>
<script src="assets/js/kopf.js?v=202609061826" defer></script>
<script src="assets/js/glocke.js?v=202609061826" defer></script>
<script src="assets/js/aufgaben.js?v=202609061826" defer></script>
</body>
</html>
+9 -9
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/automation.css?v=202609061826" />
</head>
<body class="start">
@@ -134,10 +134,10 @@
</main>
<script src="assets/js/wahl.js?v=202609061746" defer></script>
<script src="assets/js/bereiche.js?v=202609061746" defer></script>
<script src="assets/js/kopf.js?v=202609061746" defer></script>
<script src="assets/js/glocke.js?v=202609061746" defer></script>
<script src="assets/js/automation.js?v=202609061746" defer></script>
<script src="assets/js/wahl.js?v=202609061826" defer></script>
<script src="assets/js/bereiche.js?v=202609061826" defer></script>
<script src="assets/js/kopf.js?v=202609061826" defer></script>
<script src="assets/js/glocke.js?v=202609061826" defer></script>
<script src="assets/js/automation.js?v=202609061826" defer></script>
</body>
</html>
+13 -13
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/bereich.css?v=202609061826" />
</head>
<body class="start">
@@ -117,14 +117,14 @@
</main>
<script src="assets/js/wahl.js?v=202609061746" defer></script>
<script src="assets/js/bereiche.js?v=202609061746" defer></script>
<script src="assets/js/kopf.js?v=202609061746" defer></script>
<script src="assets/js/glocke.js?v=202609061746" defer></script>
<script src="assets/js/checkliste.js?v=202609061746" defer></script>
<script src="assets/js/ampel.js?v=202609061746" defer></script>
<script src="assets/js/schulung.js?v=202609061746" defer></script>
<script src="assets/js/vorlagen.js?v=202609061746" defer></script>
<script src="assets/js/bereich.js?v=202609061746" defer></script>
<script src="assets/js/wahl.js?v=202609061826" defer></script>
<script src="assets/js/bereiche.js?v=202609061826" defer></script>
<script src="assets/js/kopf.js?v=202609061826" defer></script>
<script src="assets/js/glocke.js?v=202609061826" defer></script>
<script src="assets/js/checkliste.js?v=202609061826" defer></script>
<script src="assets/js/ampel.js?v=202609061826" defer></script>
<script src="assets/js/schulung.js?v=202609061826" defer></script>
<script src="assets/js/vorlagen.js?v=202609061826" defer></script>
<script src="assets/js/bereich.js?v=202609061826" defer></script>
</body>
</html>
+10 -10
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/calls.css?v=202609061826" />
</head>
<body class="start">
@@ -76,11 +76,11 @@
</main>
<script src="assets/js/ki.js?v=202609061746" defer></script>
<script src="assets/js/wahl.js?v=202609061746" defer></script>
<script src="assets/js/bereiche.js?v=202609061746" defer></script>
<script src="assets/js/kopf.js?v=202609061746" defer></script>
<script src="assets/js/glocke.js?v=202609061746" defer></script>
<script src="assets/js/calls.js?v=202609061746" defer></script>
<script src="assets/js/ki.js?v=202609061826" defer></script>
<script src="assets/js/wahl.js?v=202609061826" defer></script>
<script src="assets/js/bereiche.js?v=202609061826" defer></script>
<script src="assets/js/kopf.js?v=202609061826" defer></script>
<script src="assets/js/glocke.js?v=202609061826" defer></script>
<script src="assets/js/calls.js?v=202609061826" defer></script>
</body>
</html>
+8 -8
View File
@@ -8,16 +8,16 @@
<meta name="theme-color" content="#05070d" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<!-- aufgaben.css bringt die Dialog- und Formularstile mit (.dialog,
.neu__zeile, .dialog__knoepfe). Sie stehen dort seit dem
Aufgabenbrett; sie hierher zu kopieren hiesse, sie an zwei Stellen
zu pflegen. Dieselbe Lösung nutzen kalender.html und content.html.
Gemeldet von pruef-css-klassen.mjs, bevor jemand einen ungestylten
Dialog zu sehen bekam. -->
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/chat.css?v=202609061826" />
</head>
<body class="start">
@@ -153,9 +153,9 @@
</form>
</dialog>
<script src="assets/js/bereiche.js?v=202609061746"></script>
<script src="assets/js/kopf.js?v=202609061746"></script>
<script src="assets/js/glocke.js?v=202609061746"></script>
<script src="assets/js/chat.js?v=202609061746"></script>
<script src="assets/js/bereiche.js?v=202609061826"></script>
<script src="assets/js/kopf.js?v=202609061826"></script>
<script src="assets/js/glocke.js?v=202609061826"></script>
<script src="assets/js/chat.js?v=202609061826"></script>
</body>
</html>
+11 -11
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/content.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/content.css?v=202609061826" />
</head>
<body class="start">
@@ -166,12 +166,12 @@
</main>
<script src="assets/js/wahl.js?v=202609061746" defer></script>
<script src="assets/js/bereiche.js?v=202609061746" defer></script>
<script src="assets/js/kopf.js?v=202609061746" defer></script>
<script src="assets/js/glocke.js?v=202609061746" defer></script>
<script src="assets/js/checkliste.js?v=202609061746" defer></script>
<script src="assets/js/vorlagen.js?v=202609061746" defer></script>
<script src="assets/js/content.js?v=202609061746" defer></script>
<script src="assets/js/wahl.js?v=202609061826" defer></script>
<script src="assets/js/bereiche.js?v=202609061826" defer></script>
<script src="assets/js/kopf.js?v=202609061826" defer></script>
<script src="assets/js/glocke.js?v=202609061826" defer></script>
<script src="assets/js/checkliste.js?v=202609061826" defer></script>
<script src="assets/js/vorlagen.js?v=202609061826" defer></script>
<script src="assets/js/content.js?v=202609061826" defer></script>
</body>
</html>
+9 -9
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/dateien.css?v=202609061826" />
</head>
<body class="start">
@@ -95,10 +95,10 @@
</main>
<script src="assets/js/wahl.js?v=202609061746" defer></script>
<script src="assets/js/bereiche.js?v=202609061746" defer></script>
<script src="assets/js/kopf.js?v=202609061746" defer></script>
<script src="assets/js/glocke.js?v=202609061746" defer></script>
<script src="assets/js/dateien.js?v=202609061746" defer></script>
<script src="assets/js/wahl.js?v=202609061826" defer></script>
<script src="assets/js/bereiche.js?v=202609061826" defer></script>
<script src="assets/js/kopf.js?v=202609061826" defer></script>
<script src="assets/js/glocke.js?v=202609061826" defer></script>
<script src="assets/js/dateien.js?v=202609061826" defer></script>
</body>
</html>
+2 -2
View File
@@ -27,7 +27,7 @@
<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=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
</head>
<body class="gate">
@@ -145,6 +145,6 @@
</p>
</main>
<script src="assets/js/gate.js?v=202609061746" defer></script>
<script src="assets/js/gate.js?v=202609061826" defer></script>
</body>
</html>
+9 -9
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/kalender.css?v=202609061826" />
</head>
<body class="start">
@@ -206,10 +206,10 @@
</main>
<script src="assets/js/wahl.js?v=202609061746" defer></script>
<script src="assets/js/bereiche.js?v=202609061746" defer></script>
<script src="assets/js/kopf.js?v=202609061746" defer></script>
<script src="assets/js/glocke.js?v=202609061746" defer></script>
<script src="assets/js/kalender.js?v=202609061746" defer></script>
<script src="assets/js/wahl.js?v=202609061826" defer></script>
<script src="assets/js/bereiche.js?v=202609061826" defer></script>
<script src="assets/js/kopf.js?v=202609061826" defer></script>
<script src="assets/js/glocke.js?v=202609061826" defer></script>
<script src="assets/js/kalender.js?v=202609061826" defer></script>
</body>
</html>
+207
View File
@@ -0,0 +1,207 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Zahlen · Creator Workspace</title>
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#05070d" />
<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=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/leistung.css?v=202609061826" />
</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&nbsp;Workspace</a> · Zahlen</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">
<section class="kopf-zeile">
<div>
<p class="marke">Zahlen</p>
<h1 class="titel">Was wirklich passiert ist.</h1>
</div>
<div class="leistung__wahl" id="creator-wahl" hidden>
<label class="feld-schild" for="creator">Creator</label>
<select id="creator"></select>
</div>
</section>
<p class="fehler" id="fehler" role="alert" aria-live="polite"></p>
<!-- DIE WOCHE IN ZAHLEN.
Jede Karte trägt die drei Dinge, die eine Kennzahl braucht
(Recherche 31.08./06.09.2026): den WERT, den VERGLEICH zur
Vorwoche und den VERLAUF als kleine Linie. Eine Zahl ohne diese
drei ist eine Eitelkeitszahl -- sie sieht nach Auskunft aus, und
man kann nichts damit entscheiden. -->
<section class="leistung-woche" id="woche" hidden>
<div class="klapp-kopf">
<p class="feldschild">Diese Woche</p>
<p class="leistung__erfassung" id="erfassung"></p>
</div>
<div class="zahlkarten" id="zahlkarten"></div>
</section>
<!-- Die Ziele als Fortschritt, nicht als Satz. -->
<section class="leistung-ziele" id="ziele-block" hidden>
<div class="klapp-kopf">
<p class="feldschild">Ziele</p>
<button type="button" class="knopf-still" id="ziele-aendern" hidden>Ziele setzen</button>
</div>
<div class="zielbalken" id="zielbalken"></div>
</section>
<!-- Die Tageszeilen. Tastaturfreundlich: Eine Woche soll in unter
einer Minute eingetragen sein. -->
<section class="leistung-tage" id="tage-block" hidden>
<div class="klapp-kopf">
<p class="feldschild">Die letzten Tage</p>
<button type="button" class="knopf knopf--klein" id="import-oeffnen">Aus Datei einlesen</button>
</div>
<div class="tagetabelle" id="tagetabelle"></div>
</section>
<p class="fuss">
Die Zahlen kommen aus dem offiziellen Export von TikTok LIVE Backstage –
entweder von Hand eingetragen oder als Datei eingelesen. Es wird nichts
automatisch abgerufen: Dafür gibt es keine offene Schnittstelle.
</p>
</main>
<!-- Einen Tag eintragen -->
<dialog class="dialog" id="tag-dialog">
<form method="dialog" id="tag-form" class="neu neu--blank">
<h2 class="dialog__titel" id="tag-titel">Tag eintragen</h2>
<p class="dialog__unter" id="tag-unter"></p>
<div class="neu__raster">
<div>
<label class="feld-schild" for="f-diamanten">Diamanten</label>
<input id="f-diamanten" type="number" min="0" inputmode="numeric" />
</div>
<div>
<label class="feld-schild" for="f-dauer">LIVE-Dauer <span class="leise">Minuten</span></label>
<input id="f-dauer" type="number" min="0" inputmode="numeric" />
</div>
<div>
<label class="feld-schild" for="f-verweildauer">Ø Verweildauer <span class="leise">Sekunden</span></label>
<input id="f-verweildauer" type="number" min="0" inputmode="numeric" />
</div>
<div>
<label class="feld-schild" for="f-zavg">Ø Zuschauer</label>
<input id="f-zavg" type="number" min="0" inputmode="numeric" />
</div>
<div>
<label class="feld-schild" for="f-zmax">Spitze</label>
<input id="f-zmax" type="number" min="0" inputmode="numeric" />
</div>
<div>
<label class="feld-schild" for="f-schenker">Schenker</label>
<input id="f-schenker" type="number" min="0" inputmode="numeric" />
</div>
<div>
<label class="feld-schild" for="f-follower">Neue Follower</label>
<input id="f-follower" type="number" min="0" inputmode="numeric" />
</div>
</div>
<!-- Der Kontext, ohne den Zahlen in die Irre führen. -->
<div class="neu__zeile">
<label class="feld-schild" for="f-notiz">
Was war an dem Tag besonders? <span class="leise">(optional)</span>
</label>
<input id="f-notiz" maxlength="500" placeholder="z. B. PK gegen Mia, krank, Technik ausgefallen" />
</div>
<p class="fehler" id="tag-fehler" role="alert" aria-live="polite"></p>
<div class="dialog__knoepfe">
<button type="submit" class="knopf knopf--klein" id="tag-speichern">Speichern</button>
<button type="button" class="abmelden" id="tag-zu">Abbrechen</button>
<button type="button" class="abmelden abmelden--warnung" id="tag-loeschen" hidden>Zeile löschen</button>
</div>
</form>
</dialog>
<!-- Ziele setzen -->
<dialog class="dialog" id="ziele-dialog">
<form method="dialog" id="ziele-form" class="neu neu--blank">
<h2 class="dialog__titel">Ziele je Woche</h2>
<p class="dialog__unter">
Aus „mehr Reichweite" wird eine prüfbare Größe. Leer lassen heißt: kein Ziel.
</p>
<div class="neu__raster">
<div>
<label class="feld-schild" for="z-diamanten">Diamanten pro Woche</label>
<input id="z-diamanten" type="number" min="0" inputmode="numeric" />
</div>
<div>
<label class="feld-schild" for="z-tage">LIVE-Tage pro Woche</label>
<input id="z-tage" type="number" min="0" max="7" inputmode="numeric" />
</div>
<div>
<label class="feld-schild" for="z-verweildauer">Ø Verweildauer <span class="leise">Sekunden</span></label>
<input id="z-verweildauer" type="number" min="0" inputmode="numeric" />
</div>
</div>
<p class="fehler" id="ziele-fehler" role="alert" aria-live="polite"></p>
<div class="dialog__knoepfe">
<button type="submit" class="knopf knopf--klein" id="ziele-speichern">Speichern</button>
<button type="button" class="abmelden" id="ziele-zu">Abbrechen</button>
</div>
</form>
</dialog>
<!-- Aus Datei einlesen. ERST VORSCHAU, DANN ÜBERNEHMEN -->
<dialog class="dialog dialog--breit" id="import-dialog">
<form method="dialog" id="import-form" class="neu neu--blank">
<h2 class="dialog__titel">Aus dem Export einlesen</h2>
<p class="dialog__unter">
Die CSV-Datei aus TikTok LIVE Backstage. Erst siehst du, was passieren
würde – geschrieben wird erst, wenn du bestätigst. Dieselbe Datei
zweimal einzulesen richtet keinen Schaden an.
</p>
<div class="neu__zeile">
<label class="feld-schild" for="datei">Datei wählen</label>
<input id="datei" type="file" accept=".csv,text/csv,text/plain" />
</div>
<div class="import-vorschau" id="vorschau" hidden></div>
<p class="fehler" id="import-fehler" role="alert" aria-live="polite"></p>
<div class="dialog__knoepfe">
<button type="submit" class="knopf knopf--klein" id="import-los" disabled>Übernehmen</button>
<button type="button" class="abmelden" id="import-zu">Abbrechen</button>
</div>
</form>
</dialog>
<script src="assets/js/wahl.js?v=202609061826"></script>
<script src="assets/js/bereiche.js?v=202609061826"></script>
<script src="assets/js/kopf.js?v=202609061826"></script>
<script src="assets/js/glocke.js?v=202609061826"></script>
<script src="assets/js/leistung.js?v=202609061826"></script>
</body>
</html>
+9 -9
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/personen.css?v=202609061826" />
</head>
<body class="start">
@@ -123,10 +123,10 @@
</main>
<script src="assets/js/wahl.js?v=202609061746" defer></script>
<script src="assets/js/bereiche.js?v=202609061746" defer></script>
<script src="assets/js/kopf.js?v=202609061746" defer></script>
<script src="assets/js/glocke.js?v=202609061746" defer></script>
<script src="assets/js/personen.js?v=202609061746" defer></script>
<script src="assets/js/wahl.js?v=202609061826" defer></script>
<script src="assets/js/bereiche.js?v=202609061826" defer></script>
<script src="assets/js/kopf.js?v=202609061826" defer></script>
<script src="assets/js/glocke.js?v=202609061826" defer></script>
<script src="assets/js/personen.js?v=202609061826" defer></script>
</body>
</html>
+10 -10
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609061826" />
</head>
<body class="start">
@@ -201,11 +201,11 @@
</main>
<script src="assets/js/wahl.js?v=202609061746" defer></script>
<script src="assets/js/bereiche.js?v=202609061746" defer></script>
<script src="assets/js/kopf.js?v=202609061746" defer></script>
<script src="assets/js/glocke.js?v=202609061746" defer></script>
<script src="assets/js/steckbrief.js?v=202609061746" defer></script>
<script src="assets/js/profil.js?v=202609061746" defer></script>
<script src="assets/js/wahl.js?v=202609061826" defer></script>
<script src="assets/js/bereiche.js?v=202609061826" defer></script>
<script src="assets/js/kopf.js?v=202609061826" defer></script>
<script src="assets/js/glocke.js?v=202609061826" defer></script>
<script src="assets/js/steckbrief.js?v=202609061826" defer></script>
<script src="assets/js/profil.js?v=202609061826" defer></script>
</body>
</html>
+10 -10
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/report.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/report.css?v=202609061826" />
</head>
<body class="start">
@@ -93,11 +93,11 @@
</main>
<script src="assets/js/ki.js?v=202609061746" defer></script>
<script src="assets/js/wahl.js?v=202609061746" defer></script>
<script src="assets/js/bereiche.js?v=202609061746" defer></script>
<script src="assets/js/kopf.js?v=202609061746" defer></script>
<script src="assets/js/glocke.js?v=202609061746" defer></script>
<script src="assets/js/report.js?v=202609061746" defer></script>
<script src="assets/js/ki.js?v=202609061826" defer></script>
<script src="assets/js/wahl.js?v=202609061826" defer></script>
<script src="assets/js/bereiche.js?v=202609061826" defer></script>
<script src="assets/js/kopf.js?v=202609061826" defer></script>
<script src="assets/js/glocke.js?v=202609061826" defer></script>
<script src="assets/js/report.js?v=202609061826" defer></script>
</body>
</html>
+9 -9
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/scouting.css?v=202609061826" />
</head>
<body class="start">
@@ -126,10 +126,10 @@
</div>
</div>
<script src="assets/js/wahl.js?v=202609061746" defer></script>
<script src="assets/js/bereiche.js?v=202609061746" defer></script>
<script src="assets/js/kopf.js?v=202609061746" defer></script>
<script src="assets/js/glocke.js?v=202609061746" defer></script>
<script src="assets/js/scouting.js?v=202609061746" defer></script>
<script src="assets/js/wahl.js?v=202609061826" defer></script>
<script src="assets/js/bereiche.js?v=202609061826" defer></script>
<script src="assets/js/kopf.js?v=202609061826" defer></script>
<script src="assets/js/glocke.js?v=202609061826" defer></script>
<script src="assets/js/scouting.js?v=202609061826" defer></script>
</body>
</html>
+7 -7
View File
@@ -9,8 +9,8 @@
<meta name="theme-color" content="#0674b9" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
</head>
<body class="start">
@@ -160,10 +160,10 @@
</p>
</main>
<script src="assets/js/wahl.js?v=202609061746" defer></script>
<script src="assets/js/bereiche.js?v=202609061746" defer></script>
<script src="assets/js/kopf.js?v=202609061746" defer></script>
<script src="assets/js/glocke.js?v=202609061746" defer></script>
<script src="assets/js/start.js?v=202609061746" defer></script>
<script src="assets/js/wahl.js?v=202609061826" defer></script>
<script src="assets/js/bereiche.js?v=202609061826" defer></script>
<script src="assets/js/kopf.js?v=202609061826" defer></script>
<script src="assets/js/glocke.js?v=202609061826" defer></script>
<script src="assets/js/start.js?v=202609061826" defer></script>
</body>
</html>
+10 -10
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/startcheck.css?v=202609061826" />
</head>
<body class="start">
@@ -72,11 +72,11 @@
</main>
<script src="assets/js/ki.js?v=202609061746" defer></script>
<script src="assets/js/wahl.js?v=202609061746" defer></script>
<script src="assets/js/bereiche.js?v=202609061746" defer></script>
<script src="assets/js/kopf.js?v=202609061746" defer></script>
<script src="assets/js/glocke.js?v=202609061746" defer></script>
<script src="assets/js/startcheck.js?v=202609061746" defer></script>
<script src="assets/js/ki.js?v=202609061826" defer></script>
<script src="assets/js/wahl.js?v=202609061826" defer></script>
<script src="assets/js/bereiche.js?v=202609061826" defer></script>
<script src="assets/js/kopf.js?v=202609061826" defer></script>
<script src="assets/js/glocke.js?v=202609061826" defer></script>
<script src="assets/js/startcheck.js?v=202609061826" defer></script>
</body>
</html>
+9 -9
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/profil.css?v=202609061826" />
</head>
<body class="start">
@@ -113,10 +113,10 @@
</main>
<script src="assets/js/wahl.js?v=202609061746" defer></script>
<script src="assets/js/bereiche.js?v=202609061746" defer></script>
<script src="assets/js/kopf.js?v=202609061746" defer></script>
<script src="assets/js/glocke.js?v=202609061746" defer></script>
<script src="assets/js/steckbrief.js?v=202609061746" defer></script>
<script src="assets/js/wahl.js?v=202609061826" defer></script>
<script src="assets/js/bereiche.js?v=202609061826" defer></script>
<script src="assets/js/kopf.js?v=202609061826" defer></script>
<script src="assets/js/glocke.js?v=202609061826" defer></script>
<script src="assets/js/steckbrief.js?v=202609061826" defer></script>
</body>
</html>
+30 -7
View File
@@ -9,9 +9,9 @@
<meta name="theme-color" content="#0674b9" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/uebersicht.css?v=202609061826" />
</head>
<body class="start">
@@ -49,6 +49,29 @@
<p class="fehler" id="fehler" role="alert" aria-live="polite"></p>
<!-- FRÜHWARNUNG (06.09.2026).
Steht GANZ OBEN, noch vor den Summen. Die Summen beantworten "wie
steht es insgesamt", die Karten "wie steht es bei jedem" – aber
die Frage, mit der ein Betreuer morgens ankommt, ist eine dritte:
"wo muss ich HEUTE hin?" Sie stand bis jetzt nirgends.
Bewusst OHNE Punktzahl. Ein Wert wie "Abwanderungsrisiko 73"
klingt nach Wissenschaft und ist eine Behauptung: Man kann ihn
nicht prüfen, nicht widersprechen und nicht erklären. Hier stehen
stattdessen die einzelnen Beobachtungen im Klartext, jede mit
ihrem Beleg – dann entscheidet ein Mensch.
Ein Creator sieht diesen Block nie (der Server liefert ihm gar
keine Daten). Zu lesen, dass ein System einen für gefährdet hält,
ist keine Mitteilung an die betroffene Person. -->
<section class="warnblock" id="fruehwarnung" hidden aria-labelledby="warn-titel">
<div class="klapp-kopf">
<h2 class="feldschild" id="warn-titel">Wo ich diese Woche nachfassen würde</h2>
</div>
<div class="warnliste" id="warnliste"></div>
</section>
<!-- Summen über alle sichtbaren Creator. Steht oben, weil die Frage
"wie steht es insgesamt" vor der Frage "bei wem" kommt. -->
<section class="summen" id="summen" hidden></section>
@@ -59,10 +82,10 @@
</main>
<script src="assets/js/bereiche.js?v=202609061746" defer></script>
<script src="assets/js/bereiche.js?v=202609061826" defer></script>
<script src="assets/js/kopf.js?v=202609061746" defer></script>
<script src="assets/js/glocke.js?v=202609061746" defer></script>
<script src="assets/js/uebersicht.js?v=202609061746" defer></script>
<script src="assets/js/kopf.js?v=202609061826" defer></script>
<script src="assets/js/glocke.js?v=202609061826" defer></script>
<script src="assets/js/uebersicht.js?v=202609061826" defer></script>
</body>
</html>
+9 -9
View File
@@ -9,10 +9,10 @@
<meta name="theme-color" content="#0674b9" />
<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=202609061746" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609061746" />
<link rel="stylesheet" href="assets/css/gate.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/start.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/aufgaben.css?v=202609061826" />
<link rel="stylesheet" href="assets/css/wissen.css?v=202609061826" />
</head>
<body class="start">
@@ -177,10 +177,10 @@
</main>
<script src="assets/js/wahl.js?v=202609061746" defer></script>
<script src="assets/js/bereiche.js?v=202609061746" defer></script>
<script src="assets/js/kopf.js?v=202609061746" defer></script>
<script src="assets/js/glocke.js?v=202609061746" defer></script>
<script src="assets/js/wissen.js?v=202609061746" defer></script>
<script src="assets/js/wahl.js?v=202609061826" defer></script>
<script src="assets/js/bereiche.js?v=202609061826" defer></script>
<script src="assets/js/kopf.js?v=202609061826" defer></script>
<script src="assets/js/glocke.js?v=202609061826" defer></script>
<script src="assets/js/wissen.js?v=202609061826" defer></script>
</body>
</html>