From ec55b6c8d34cf588c08096121b6f08eafae1f618 Mon Sep 17 00:00:00 2001
From: Dogfather
Date: Tue, 8 Sep 2026 01:44:36 +0200
Subject: [PATCH] =?UTF-8?q?WIP=20Zentrale-Kachel=20nach=20VanVans=20Werkti?=
=?UTF-8?q?sch=20=E2=80=94=20NOCH=20NICHT=20FERTIG?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Aufbau steht (Ring links, Titel mittig, Uhr rechts), aber
pruef-start-ansicht meldet 3 Befunde am MAUS-LICHT der Kacheln.
Gemessen: alter Stand 0 Befunde, dieser Stand 3 — kommt also von hier.
Kein JS-Fehler (pageerror/console sind still), also Zeitverhalten:
Die zusaetzliche Abfrage /api/zentrale verzoegert vermutlich den Aufbau
der Kacheln ueber den Zeitpunkt hinaus, an dem kopf.js lichtFolgen ruft.
Ausserdem offen: 2 Schriftgroessen unter 11,5 px (pruef-css-klassen).
NICHT ausliefern.
---
server/index.js | 2 +
server/workspace-zentrale.js | 182 ++++++++++++++++++++++++++++++
workspace/assets/css/heim.css | 207 +++++++++++++++++++++++++++++++++-
workspace/assets/js/start.js | 201 +++++++++++++++++++++++++++++++++
workspace/start.html | 97 ++++++++++++----
5 files changed, 666 insertions(+), 23 deletions(-)
create mode 100644 server/workspace-zentrale.js
diff --git a/server/index.js b/server/index.js
index 950cb5f7..234fc152 100644
--- a/server/index.js
+++ b/server/index.js
@@ -26,6 +26,7 @@ import { reportRouter } from "./workspace-reports.js";
import { scoutRouter } from "./workspace-scouts.js";
import { callRouter } from "./workspace-calls.js";
import { hinweisRouter } from "./workspace-hinweise.js";
+import { zentraleRouter } from "./workspace-zentrale.js";
import { sucheRouter } from "./workspace-suche.js";
import { startcheckRouter } from "./workspace-startcheck.js";
import { wissenRouter } from "./workspace-wissen.js";
@@ -216,6 +217,7 @@ app.use(reportRouter);
app.use(scoutRouter);
app.use(callRouter);
app.use(hinweisRouter);
+app.use(zentraleRouter);
app.use(sucheRouter);
app.use(startcheckRouter);
app.use(wissenRouter);
diff --git a/server/workspace-zentrale.js b/server/workspace-zentrale.js
new file mode 100644
index 00000000..7745a4e7
--- /dev/null
+++ b/server/workspace-zentrale.js
@@ -0,0 +1,182 @@
+/* =====================================================================
+ workspace-zentrale.js — die Zahlen fuer den Ring auf der Startseite.
+
+ Wunsch Filipe (08.09.2026): die Begruessungskachel nach dem Vorbild
+ von VanVans Business Hub umbauen -- grosser Ring links, Text in der
+ Mitte, Uhr rechts. Dort zeigt der Ring "91 % deiner Produktakten":
+ ein Segment je Produkt, eingefaerbt nach Vollstaendigkeit.
+
+ ---------------------------------------------------------------------
+ WARUM ES DIESELBE ZAHL HIER NICHT GIBT
+
+ Nachgemessen an der echten Datenbank am 08.09.2026:
+
+ Termine 31 · Wissen 7 · Schulungen 4 · Personen 9 · AUFGABEN 1
+ Leistung, Leads, Profile, Content-Saeulen, Dateien: jeweils 0
+
+ VanVans Kranz funktioniert, weil 253 Produkte ein BILD ergeben. Ein
+ Kranz aus einer Aufgabe ergibt keins. Die naheliegende Uebertragung
+ ("dann eben Aufgaben") haette eine leere Scheibe erzeugt, die aussieht
+ wie ein Ladefehler.
+
+ Filipe hat deshalb DAS TEAM gewaehlt: ein Segment je Person, gefaerbt
+ danach, wie es um sie steht. Neun Segmente sind wenige, aber sie sind
+ ECHT und aendern sich taeglich -- und die Zahl waechst mit dem Team.
+
+ ---------------------------------------------------------------------
+ UND WARUM DAS NICHT FUER JEDEN GEHT
+
+ Ein Creator darf die Teamliste nicht sehen. Das ist keine
+ Feinheit, sondern die Regel, die diesen ganzen Arbeitsplatz traegt
+ (Wunsch Filipe, 07.09.2026: "jeder soll und darf im kalender immer nur
+ seine eigenen eintraege nur sehen"). Ein Ring, der neun Namen
+ einfaerbt, waere fuer einen Creator ein Leck -- er koennte daran
+ ablesen, wer heute Termine hat und wer nicht.
+
+ Deshalb liefert diese Route ZWEI VERSCHIEDENE RINGE, je nach Rolle:
+
+ Leitung (DogFather, Manager, Scout, Spicy)
+ -> das Team: ein Segment je aktiver Person
+ Creator -> der eigene Tag: ein Segment je Stunde, plus die
+ eigenen Termine als Marken
+
+ Das ist keine Notloesung, sondern die richtige Antwort auf dieselbe
+ Frage: "Wie weit ist das, wofuer ich zustaendig bin?" Fuer DogFather
+ ist das sein Team, fuer eine Creatorin ihr Tag.
+
+ KEINE EIGENEN DATEN, wie bei den Hinweisen: Alles hier ist eine SICHT
+ auf Zeilen, die ohnehin existieren. Es gibt nichts zu pflegen und
+ nichts, was veralten kann.
+ ===================================================================== */
+
+import express from "express";
+import { db, sitzungLesen, istLeitung, betreuteIds } from "./workspace.js";
+
+export const zentraleRouter = express.Router();
+
+function angemeldet(req, res, next) {
+ const person = sitzungLesen(req);
+ if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
+ req.person = person;
+ next();
+}
+zentraleRouter.use("/workspace/api/zentrale", angemeldet);
+
+const p2 = (n) => String(n).padStart(2, "0");
+/* ORTSZEIT, NICHT UTC. toISOString() liefert zwischen Mitternacht und
+ zwei Uhr noch den Vortag -- der Ring stuende dann auf den Terminen
+ von gestern. Derselbe Fehler ist mir am 08.09. zweimal an einem Abend
+ passiert; pruef-struktur.mjs sucht ihn inzwischen im ganzen Repo. */
+function heuteLokal() {
+ const d = new Date();
+ return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())}`;
+}
+
+zentraleRouter.get("/workspace/api/zentrale", (req, res) => {
+ try {
+ const d = db();
+ const heute = heuteLokal();
+ const ich = req.person;
+
+ /* Wessen Lage wird gezeigt? Bei aktiver Fremdsicht die der Person,
+ in die man hineinsieht -- sonst zeigte die Kachel die eigene Lage
+ und alles darunter eine fremde. Genau diese Uneinheitlichkeit hat
+ auf der Startseite schon einmal zu "ist das jetzt meins oder
+ ihrs?" gefuehrt. */
+ const zeigt = req.person.sicht || req.person;
+
+ if (istLeitung(ich)) {
+ /* ---------- DAS TEAM ----------
+ Ein Segment je aktiver Person. Der Zustand einer Person ist
+ bewusst GROB in drei Stufen -- mehr taeuschte Genauigkeit vor,
+ die es nicht gibt (dieselbe Ueberlegung steht in VanVans Kranz).
+
+ frei - nichts Offenes, keine Termine heute -> ruhig
+ dran - hat heute Termine -> die sind versorgt
+ offen - hat unerledigte Termine aus der VERGANGENHEIT
+ -> das ist das, was liegen bleibt
+ */
+ const leute = d.prepare(
+ `SELECT id, name, rolle FROM personen WHERE aktiv = 1 ORDER BY rolle, name`).all();
+
+ const heuteZaehler = d.prepare(
+ `SELECT COUNT(*) AS n FROM termine t
+ WHERE t.erledigt = 0 AND substr(t.beginn, 1, 10) = ?
+ AND (t.creator_id = ? OR t.erstellt_von = ?
+ OR EXISTS (SELECT 1 FROM termin_teilnehmer x
+ WHERE x.termin_id = t.id AND x.person_id = ?))`);
+ const altZaehler = d.prepare(
+ `SELECT COUNT(*) AS n FROM termine t
+ WHERE t.erledigt = 0 AND substr(t.beginn, 1, 10) < ?
+ AND (t.creator_id = ? OR t.erstellt_von = ?
+ OR EXISTS (SELECT 1 FROM termin_teilnehmer x
+ WHERE x.termin_id = t.id AND x.person_id = ?))`);
+
+ const segmente = leute.map((p) => {
+ const heuteN = heuteZaehler.get(heute, p.id, p.id, p.id).n;
+ const altN = altZaehler.get(heute, p.id, p.id, p.id).n;
+ const stufe = altN > 0 ? "offen" : (heuteN > 0 ? "dran" : "frei");
+ /* NUR DER VORNAME nach aussen. Der Ring braucht ein Kuerzel fuer
+ die Sprechblase, nicht den Datensatz einer Person. */
+ return { name: String(p.name || "").trim().split(/\s+/)[0] || "?",
+ rolle: p.rolle, stufe, heute: heuteN, alt: altN };
+ });
+
+ const versorgt = segmente.filter((s) => s.stufe !== "offen").length;
+ return res.json({
+ art: "team",
+ titel: "Team versorgt",
+ prozent: segmente.length ? Math.round((versorgt / segmente.length) * 100) : 100,
+ segmente,
+ zahlen: [
+ { wert: segmente.length, schild: "im Team" },
+ { wert: segmente.filter((s) => s.stufe === "dran").length, schild: "heute dran" },
+ { wert: segmente.filter((s) => s.stufe === "offen").length, schild: "liegt liegen" },
+ ],
+ });
+ }
+
+ /* ---------- DER EIGENE TAG ----------
+ Fuer Creator. Ein Segment je Stunde von 6 bis 24 -- der Teil des
+ Tages, in dem gearbeitet wird. Nachts einen leeren Halbkreis zu
+ zeigen sagt nichts; 18 Segmente fuellen den Ring ordentlich.
+
+ Gefuellt sind die vergangenen Stunden, markiert die mit einem
+ eigenen Termin. Der Ring beantwortet damit "wie weit ist mein
+ Tag" -- dieselbe Frage wie bei VanVan, nur auf das bezogen,
+ wofuer eine Creatorin zustaendig ist. */
+ const VON = 6, BIS = 24;
+ const jetzt = new Date();
+ const stundeJetzt = jetzt.getHours();
+
+ const meine = d.prepare(
+ `SELECT t.beginn FROM termine t
+ WHERE substr(t.beginn, 1, 10) = ?
+ AND (t.creator_id = ? OR t.erstellt_von = ?
+ OR EXISTS (SELECT 1 FROM termin_teilnehmer x
+ WHERE x.termin_id = t.id AND x.person_id = ?))`)
+ .all(heute, zeigt.id, zeigt.id, zeigt.id);
+ const mitTermin = new Set(meine.map((t) => Number(String(t.beginn).slice(11, 13))));
+
+ const segmente = [];
+ for (let s = VON; s < BIS; s++) {
+ segmente.push({ name: `${p2(s)} Uhr`, rolle: "stunde",
+ stufe: mitTermin.has(s) ? "dran" : (s < stundeJetzt ? "frei" : "offen") });
+ }
+ const durch = Math.min(Math.max(stundeJetzt - VON, 0), BIS - VON);
+ return res.json({
+ art: "tag",
+ titel: "Tag geschafft",
+ prozent: Math.round((durch / (BIS - VON)) * 100),
+ segmente,
+ zahlen: [
+ { wert: meine.length, schild: "heute" },
+ { wert: [...mitTermin].filter((s) => s >= stundeJetzt).length, schild: "kommt noch" },
+ { wert: betreuteIds(zeigt).length, schild: "betreut" },
+ ],
+ });
+ } catch (fehler) {
+ console.error("[workspace] Zentrale:", fehler?.message);
+ res.status(503).json({ fehler: "nicht_verfuegbar" });
+ }
+});
diff --git a/workspace/assets/css/heim.css b/workspace/assets/css/heim.css
index c0b7abb8..df48696e 100644
--- a/workspace/assets/css/heim.css
+++ b/workspace/assets/css/heim.css
@@ -346,11 +346,212 @@
transparent 74%);
}
+/* =====================================================================
+ DREI SPALTEN: RING — TEXT — UHR (08.09.2026)
+
+ Uebernommen von VanVans Werktisch im Business Hub, auf Wunsch Filipes
+ ("das model da uebernehmen und es noch perfektionieren"). Vorher stand
+ hier eine FLEX-Zeile: Bild, Text, Zahlen, Uhr -- alles nebeneinander,
+ mit rund 300 px Leere in der Mitte. Das war eine Zeile mit einer
+ Luecke, keine Tafel.
+
+ `minmax(0, 1fr)` FUER DIE MITTE, nicht `1fr`. Das ist im Vorbild
+ ausdruecklich vermerkt, und der Grund ist nachlesenswert: `1fr` geht
+ nie unter die Mindestbreite seines Inhalts. Eine nicht umbrechende
+ Anrede und eine Zahlenreihe zwingen die Spalte damit auf 470 px -- auf
+ einem 390-px-Schirm. Die ganze Seite laesst sich dann seitlich
+ wegschieben. Mit `minmax(0, 1fr)` darf die Spalte schmaler werden als
+ ihr Inhalt, und der Inhalt bricht um, statt hinauszuragen.
+
+ Ich habe den Fehler nicht selbst gemacht, sondern die Begruendung
+ mituebernommen -- eine Loesung ohne ihren Grund haelt genau bis zum
+ naechsten Umbau. */
.willkommen__reihe {
- display: flex; align-items: center; gap: 16px;
- flex-wrap: wrap;
+ display: grid;
+ grid-template-columns: minmax(180px, 232px) minmax(0, 1fr) minmax(180px, 232px);
+ gap: 26px; align-items: center;
+}
+.willkommen__text {
+ min-width: 0;
+ display: flex; flex-direction: column; align-items: center; gap: 9px;
+ text-align: center;
+}
+
+/* ---- Der Ring links ------------------------------------------------
+ Gleich gross wie die Uhr rechts. Im Vorbild steht dazu: "Beide Kreise
+ exakt gleich gross -- sie sind ein Paar, kein Haupt- und Nebenkreis."
+ Das ist der Grund, warum die Tafel ruhig wirkt: zwei gleiche Gewichte
+ links und rechts, dazwischen der Text. */
+.zring {
+ position: relative; aspect-ratio: 1; width: 100%; max-width: 232px;
+ margin: 0 auto;
+}
+.zring__bild {
+ width: 100%; height: 100%; display: block;
+ /* Bei -90 Grad beginnt das erste Segment oben statt rechts -- so
+ liest man einen Fortschritt, seit es Uhren gibt. */
+ transform: rotate(-90deg);
+}
+.zring__mitte {
+ position: absolute; inset: 0;
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
+ gap: 2px; pointer-events: none;
+}
+/* DAS SIEGEL IST RUND -- und das ist eine Reparatur, keine Stilfrage.
+
+ Erst stand hier dieselbe Maske wie in der Kopfleiste:
+ -webkit-mask: url("../img/marke-husky.webp") ...
+ Im Nahaufnahme-Bildschirmfoto war das Ergebnis ein KASTEN mit einem
+ Husky darin, kein freigestellter Husky.
+
+ Nachgemessen: Die Datei hat einen Alphakanal (VP8X mit ALPH-Chunk,
+ Alpha-Flag gesetzt) -- er ist nur RECHTECKIG. Das Bild ist nicht
+ freigestellt. Als Maske taugt es damit nicht, egal wie richtig die
+ CSS-Zeile ist.
+
+ BEFUND FUER SPAETER: Dasselbe gilt fuer `.kopfleiste .marke::before`
+ in start.css -- dort ist es bei 22 px nur nicht aufgefallen. Wer das
+ Logo einmal freistellt, repariert beide Stellen auf einmal.
+
+ Hier wird es deshalb als BILD gezeigt und rund beschnitten, mit
+ einem feinen Ring darum. Das ist naeher am Vorbild als die Maske es
+ je war: VanVans Werktisch traegt in der Mitte ein rundes Siegel, kein
+ ausgeschnittenes Zeichen. */
+.zring__siegel {
+ width: 44px; height: 44px; margin-bottom: 5px;
+ border-radius: 50%;
+ background: url("../img/marke-husky.webp") center / 128% no-repeat, #0b1420;
+ box-shadow:
+ 0 0 0 1px rgba(180, 210, 240, .28),
+ inset 0 0 12px rgba(0, 0, 0, .55);
+ /* Etwas entsaettigt und abgedunkelt: Das Siegel ist der ruhige Kern
+ des Rings, nicht sein hellster Punkt -- sonst zieht es den Blick von
+ der Zahl darunter ab, um die es geht. */
+ filter: saturate(.55) brightness(.92);
+}
+.zring__prozent {
+ font-size: 1.85rem; font-weight: 700; line-height: 1;
+ font-variant-numeric: tabular-nums;
+ color: #dfeaf6;
+ text-shadow: 0 0 16px rgba(127, 196, 242, .3);
+}
+/* ZWEIZEILIG, UND DAS IST GEWOLLT.
+
+ Erst stand hier `max-width: 78%` bei .68rem. In der Nahaufnahme lief
+ "DES TEAMS VERSORGT" als eine Zeile quer ueber die Segmentbahn -- der
+ Ring war dahinter durchgestrichen.
+
+ Der innere Segmentring liegt bei Radius 106 von 150, also auf 71 % der
+ Breite. Alles darunter muss schmaler bleiben als das, sonst kreuzt es
+ ihn. 58 % lassen dem Text Luft und zwingen ihn in zwei Zeilen -- genau
+ wie im Vorbild, wo "DEINER PRODUKTAKTEN" ebenfalls umbricht.
+
+ .72rem statt .68rem, weil 10,9 px unter meiner eigenen Untergrenze von
+ 11,5 px lagen. Die Regel gilt auch fuer Text, den man nur ueberfliegt. */
+.zring__unter {
+ font-size: .72rem; letter-spacing: .06em; text-transform: uppercase;
+ color: rgba(168, 192, 218, .66);
+ max-width: 58%; text-align: center; line-height: 1.25;
+ text-wrap: balance; /* zwei gleich lange Zeilen statt 4+1 Woertern */
+}
+
+/* ---- Die Anrede in eigener Fassung -------------------------------- */
+.zgruss {
+ display: inline-flex; align-items: center; gap: 11px;
+ padding: 7px 15px 7px 8px;
+ border-radius: 999px;
+ background: linear-gradient(178deg, rgba(10, 18, 30, .62), rgba(6, 12, 21, .48));
+ box-shadow:
+ inset 0 1px 0 rgba(160, 195, 230, .13),
+ 0 1px 10px rgba(0, 0, 0, .3);
+ max-width: 100%;
+}
+.zgruss__bild {
+ width: 42px; height: 42px; flex: 0 0 auto;
+ border-radius: 50%;
+ background-size: cover; background-position: center;
+ background-color: rgba(120, 160, 200, .16);
+ box-shadow: 0 0 0 1px rgba(180, 210, 240, .22), inset 0 0 14px rgba(0, 0, 0, .5);
+ display: grid; place-items: center;
+ font-weight: 700; color: #cfe2f5;
+}
+.zgruss__text { display: flex; flex-direction: column; gap: 1px; min-width: 0; text-align: left; }
+.zgruss__datum { font-size: .78rem; color: rgba(168, 192, 218, .72); }
+.zgruss__rolle {
+ flex: 0 0 auto;
+ padding: 4px 11px; border-radius: 999px;
+ font-size: .66rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
+ color: #e6c88a;
+ border: 1px solid rgba(227, 179, 65, .34);
+ background: rgba(227, 179, 65, .09);
+}
+
+/* ---- Zierzeile und Titel ------------------------------------------ */
+.zmarke {
+ display: inline-flex; align-items: center; gap: 11px;
+ font-size: .68rem; font-weight: 600; letter-spacing: .24em; text-transform: uppercase;
+ color: rgba(196, 214, 234, .6);
+ margin-top: 3px;
+}
+/* Die Raute ist ein ELEMENT, kein Schriftzeichen. Ein ◆ aus der
+ Schriftart sitzt nie auf der Grundlinie und sieht auf jedem Geraet
+ anders aus -- im Vorbild steht dieselbe Ueberlegung. */
+.zraute {
+ width: 5px; height: 5px; flex: 0 0 auto;
+ transform: rotate(45deg);
+ background: rgba(227, 179, 65, .62);
+}
+/* ZWEI STUFEN, wie im Vorbild -- nicht eine.
+
+ Bei mittlerer Breite fehlt zuerst der Platz fuer DREI Spalten, aber
+ noch lange nicht fuer zwei: Der Ring bleibt links neben dem Text, die
+ Uhr rutscht darunter und in die Mitte. Erst wenn auch das nicht mehr
+ reicht, steht alles untereinander.
+
+ Die Zahlen 1180 und 780 sind aus dem Vorbild uebernommen, wo sie an
+ echten Geraeten entstanden sind. Sie stehen hier zusammen mit dem,
+ was sie zaehlen -- wer eine vierte Spalte hinzufuegt, muss sie neu
+ messen. Genau das ist im September einmal vergessen worden, und der
+ Sicht-Umschalter lag danach ueber dem Chat-Knopf. */
+@media (max-width: 1180px) {
+ .willkommen__reihe { grid-template-columns: minmax(170px, 220px) minmax(0, 1fr); }
+ .willkommen__rechts { grid-column: 1 / -1; justify-content: center; margin-left: 0; }
+}
+@media (max-width: 780px) {
+ .willkommen__reihe { grid-template-columns: minmax(0, 1fr); gap: 20px; }
+ .zring { max-width: 190px; }
+ .zgruss { flex-wrap: wrap; justify-content: center; }
+ .zgruss__text { text-align: center; align-items: center; }
+}
+
+/* Der Satz unter dem Titel bekommt Fliesstextgroesse und darf umbrechen.
+
+ Im Vorbild stehen dort drei Zeilen ("Ich habe deine 253 Produkte
+ durchgesehen und 1561 von 1720 Angaben schon eingetragen ..."), und
+ genau die machen aus einer Anzeigetafel eine Ansprache. Bei mir ist
+ der Satz kuerzer -- er kommt aus derselben Zaehlung wie die Hinweise
+ darunter und wird nicht zweimal gerechnet -- aber er soll denselben
+ Raum bekommen statt als Kleingedrucktes am Titel zu kleben.
+
+ `text-wrap: balance` verteilt die Zeilen gleichmaessig, statt die
+ letzte mit einem einzelnen Wort enden zu lassen. */
+.willkommen__reihe .unterzeile {
+ font-size: 1rem; line-height: 1.55;
+ max-width: 46ch; margin: 2px auto 0;
+ text-wrap: balance;
+}
+
+.ztitel {
+ margin: 0;
+ font-size: clamp(1.9rem, 3.6vw, 2.9rem);
+ font-weight: 700; line-height: 1.06; letter-spacing: -.01em;
+ color: #dce9f6;
+ /* Ein Verlauf statt einer Volltonfarbe: Der Titel bekommt oben Licht
+ und unten Schatten, wie ein gefraester Schriftzug auf Metall. */
+ background: linear-gradient(178deg, #eef5fc 8%, #b9cee2 58%, #7f95ad 100%);
+ -webkit-background-clip: text; background-clip: text;
+ -webkit-text-fill-color: transparent;
}
-.willkommen__text { min-width: 0; flex: 1 1 260px; }
/* Rechts: Glocke neben der Uhr, beide rechtsbündig. `margin-left: auto`
statt `justify-content: space-between` am Elternteil -- so bleibt die
diff --git a/workspace/assets/js/start.js b/workspace/assets/js/start.js
index b3d292d7..03aefcf8 100644
--- a/workspace/assets/js/start.js
+++ b/workspace/assets/js/start.js
@@ -505,8 +505,37 @@
$('rollentext').textContent = ich.sicht
? `Arbeitsplatz von ${ich.sicht.name} · ${ROLLENTEXT[zeigt.rolle] || zeigt.rolle}`
: (ROLLENTEXT[ich.rolle] || ich.rolle);
+
+ /* Datum und Rolle in der Anrede -- wie im Vorbild, wo unter "Noch
+ wach, Dogi" das Datum steht und daneben "ADMIN & PARTNER".
+
+ Das Datum steht damit ZWEIMAL auf der Kachel: oben in der
+ Zierzeile mit KW, hier ohne. Das ist Absicht und keine Doppelung:
+ Oben gehoert es zur Tafel ("welcher Tag ist heute"), hier zur
+ Anrede ("wann begruesse ich dich"). Im Vorbild ist es genauso,
+ und es liest sich dort natuerlich. Faellt es Filipe als Doppelung
+ auf, kommt die Zierzeile oben weg -- nicht diese Zeile. */
+ const jetzt2 = new Date();
+ const dat = $('zgruss-datum');
+ if (dat) {
+ dat.textContent = `${WOCHENTAGE[jetzt2.getDay()]}, `
+ + `${jetzt2.getDate()}. ${MONATE[jetzt2.getMonth()]}`;
+ }
+ const rolleEl = $('zgruss-rolle');
+ if (rolleEl) {
+ rolleEl.textContent = (ROLLEN_KURZ[zeigt.rolle] || zeigt.rolle || '').toUpperCase();
+ rolleEl.hidden = false;
+ }
}
+ /* Kurzform der Rolle fuer die Plakette neben dem Namen. Der lange
+ Text ("Gesamtuebersicht und Freigaben") steht schon darunter --
+ hier braucht es das Wort, nicht die Erklaerung. */
+ const ROLLEN_KURZ = {
+ admin: 'DogFather', manager: 'Manager', scout: 'Scout',
+ creator: 'Creator', spicy: 'Spicy Media',
+ };
+
/* Ein Satz zur Lage -- gerechnet aus denselben Hinweisen, die darunter
stehen. Keine zweite Zählung, sonst gäbe es irgendwann zwei
Wahrheiten auf einem Bildschirm. */
@@ -534,6 +563,145 @@
standSetzen('offen', gesamt === 0 ? '0' : String(gesamt), stufe);
}
+ /* ---------- Der Ring links ----------------------------------------------
+ Nach dem Vorbild von VanVans Werktisch. Zwei Bahnen:
+
+ INNEN eine feine Bahn mit EINEM Segment je Person (bzw. je
+ Stunde), eingefaerbt nach Lage -- dort sieht man jedes
+ Einzelne.
+ AUSSEN ein durchgehender Bogen, der GENAU auf der Prozentzahl in
+ der Mitte steht.
+
+ Die Trennung ist aus dem Vorbild uebernommen, samt Begruendung:
+ Dort zeigte der Ring erst die 162 Produkte je einzeln, und der
+ Anteil des Blauen entsprach dann der Zahl der fertigen AKTEN (44 %),
+ waehrend daneben die Zahl der erfuellten ANGABEN (91 %) stand. Zwei
+ Auskuenfte, die sich widersprechen, sind schlimmer als eine.
+ Deshalb: der Bogen sagt dasselbe wie die Zahl, immer. */
+
+ const RING_FARBE = {
+ frei: '#57b89b', /* nichts offen -- gedaempftes Petrol, kein Grasgruen */
+ dran: '#7fc4f2', /* heute versorgt */
+ offen: '#ff6b74', /* liegt liegen */
+ };
+
+ function bogen(cx, cy, r, von, bis) {
+ const x1 = cx + r * Math.cos(von), y1 = cy + r * Math.sin(von);
+ const x2 = cx + r * Math.cos(bis), y2 = cy + r * Math.sin(bis);
+ const gross = bis - von > Math.PI ? 1 : 0;
+ return `M ${x1.toFixed(2)} ${y1.toFixed(2)} A ${r} ${r} 0 ${gross} 1 ${x2.toFixed(2)} ${y2.toFixed(2)}`;
+ }
+ const svgEl = (name, attr) => {
+ const n = document.createElementNS('http://www.w3.org/2000/svg', name);
+ for (const [k, v] of Object.entries(attr)) n.setAttribute(k, v);
+ return n;
+ };
+
+ function ringZeichnen(daten) {
+ const svg = $('zring-bild');
+ if (!svg) return;
+ svg.replaceChildren();
+ const segmente = daten.segmente || [];
+
+ /* Der Verlauf fuer den Fortschrittsbogen -- eine Reise von Rot nach
+ Babyblau, dieselben zwei Farben wie in der Uhr daneben und im
+ Rahmen der Kachel. Keine dritte Handschrift. */
+ const defs = svgEl('defs', {});
+ const lauf = svgEl('linearGradient', { id: 'zring-lauf', x1: '0', y1: '0', x2: '1', y2: '1' });
+ for (const [o, f] of [['0%', '#ff5f6d'], ['46%', '#c07ad0'], ['100%', '#7fc4f2']]) {
+ lauf.appendChild(svgEl('stop', { offset: o, 'stop-color': f }));
+ }
+ defs.appendChild(lauf);
+ svg.appendChild(defs);
+
+ /* 1. Die leere Bahn zuerst -- damit der Ring auch bei 0 % ein Ring
+ ist und nicht ein Punkt. */
+ svg.appendChild(svgEl('path', {
+ d: bogen(150, 150, 128, 0, Math.PI * 1.9999),
+ stroke: 'rgba(120, 150, 185, .16)', 'stroke-width': '9',
+ fill: 'none', 'stroke-linecap': 'round',
+ }));
+
+ /* 2. Ein Segment je Person (oder Stunde), innen. Mit Luecke
+ dazwischen, sonst verschmelzen sie zu einem Ring und man
+ zaehlt nichts mehr. */
+ if (segmente.length) {
+ const schritt = (Math.PI * 2) / segmente.length;
+ const luecke = Math.min(schritt * 0.22, 0.06);
+ segmente.forEach((s, i) => {
+ const von = i * schritt + luecke / 2;
+ const bis = (i + 1) * schritt - luecke / 2;
+ const st = svgEl('path', {
+ d: bogen(150, 150, 106, von, bis),
+ stroke: RING_FARBE[s.stufe] || '#8fb3cf',
+ 'stroke-width': '7', fill: 'none', 'stroke-linecap': 'round',
+ opacity: s.stufe === 'offen' ? '.95' : '.72',
+ });
+ /* Der Name als Sprechblase. Nur der Vorname, und nur das, was
+ der Server ohnehin geschickt hat -- der Browser erfaehrt
+ nichts, was er nicht sehen darf. */
+ const titel = document.createElementNS('http://www.w3.org/2000/svg', 'title');
+ titel.textContent = s.name || '';
+ st.appendChild(titel);
+ svg.appendChild(st);
+ });
+ }
+
+ /* 3. Der Fortschrittsbogen aussen, genau auf der Prozentzahl. */
+ const anteil = Math.max(0, Math.min(100, Number(daten.prozent) || 0));
+ const bis = (anteil / 100) * Math.PI * 2;
+ if (bis > 0.01) {
+ const d = bogen(150, 150, 128, 0, Math.min(bis, Math.PI * 1.9999));
+ svg.appendChild(svgEl('path', {
+ d, stroke: 'url(#zring-lauf)', 'stroke-width': '9',
+ fill: 'none', 'stroke-linecap': 'round',
+ }));
+ /* Ein heller Punkt an der Spitze macht aus dem Bogen einen
+ Zeiger -- er markiert den Stand so genau, wie die Zahl es sagt. */
+ const px = 150 + 128 * Math.cos(bis), py = 150 + 128 * Math.sin(bis);
+ svg.appendChild(svgEl('circle', { cx: px, cy: py, r: '3.4', fill: '#eaf4ff' }));
+ }
+
+ $('zring-prozent').textContent = `${anteil} %`;
+ $('zring-unter').textContent = daten.titel || '';
+ }
+
+ async function zentraleLaden() {
+ let daten;
+ try {
+ const antwort = await fetch('/workspace/api/zentrale', { credentials: 'same-origin' });
+ if (!antwort.ok) return;
+ daten = await antwort.json();
+ } catch { return; } /* Der Rest der Kachel steht trotzdem. */
+
+ ringZeichnen(daten);
+
+ /* Die drei Zahlen unter dem Titel kommen bei Leitung aus derselben
+ Antwort -- eine Abfrage, eine Wahrheit. Fuer die Tagesansicht
+ fuellen `lageZeigen` und `tagestermineLaden` sie weiter; deshalb
+ wird hier nur ueberschrieben, was der Server auch geschickt hat. */
+ const zahlen = daten.zahlen || [];
+ const felder = ['heute', 'naechster', 'offen'];
+ zahlen.slice(0, 3).forEach((z, i) => {
+ const wert = $('stand-' + felder[i]);
+ const schild = wert?.parentElement?.querySelector('.stand__schild');
+ if (!wert || !schild) return;
+ wert.textContent = String(z.wert);
+ schild.textContent = z.schild;
+ /* Die Farbe folgt der Bedeutung, nicht der Position: Was liegen
+ bleibt, ist rot -- egal an welcher Stelle es steht. */
+ const kasten = wert.closest('.stand');
+ if (kasten) {
+ kasten.dataset.stufe = /liegt|offen|fehlt/i.test(z.schild)
+ ? (Number(z.wert) > 0 ? 'warnung' : 'ruhig') : '';
+ }
+ });
+ if (zahlen.length) {
+ standVonZentrale = true;
+ $('tagesstand').hidden = false;
+ }
+ }
+
/* ---------- Die drei Ablesungen in der Konsolenmitte --------------------- */
/* EINE Funktion fuer alle drei, und sie wird von den STELLEN gerufen,
@@ -546,7 +714,22 @@
auf beide: Wer nichts Offenes hat, aber drei Calls, soll seinen Tag
sehen, auch wenn die andere Abfrage haengt. Genau diesen Fehler hat
die Kachel darunter schon einmal gemacht. */
+ /* WER DIE FELDER BESETZT HAT, BEHAELT SIE (08.09.2026).
+
+ Seit der Ring da ist, schreiben ZWEI Quellen in dieselben drei
+ Felder: `zentraleLaden` (bei Leitung: im Team / heute dran / liegt
+ liegen) und `lageZeigen` + `tagestermineLaden` (Heute / Als
+ Naechstes / Offen). Beide laufen nebeneinander los -- wer zuletzt
+ ankommt, haette gewonnen, und das waere bei jedem Laden eine andere.
+
+ Ein Bildschirm, auf dem "heute dran: 2" steht und beim naechsten
+ Aufruf "Heute: 5", ist schlimmer als einer, der weniger zeigt.
+ Deshalb: Hat die Zentrale die Felder belegt, ruehrt sie niemand
+ mehr an. Die Reihenfolge des Eintreffens spielt dann keine Rolle. */
+ let standVonZentrale = false;
+
function standSetzen(welches, wert, stufe) {
+ if (standVonZentrale) return;
const feld = $('stand-' + welches);
if (!feld) return;
feld.textContent = wert;
@@ -579,8 +762,26 @@
Die Tagestermine laufen daneben her: Sie sind die rechte Hälfte
derselben Kachel, aber niemand soll auf sie warten. Scheitern
sie, steht die linke Hälfte trotzdem. */
+ /* Der Ring laeuft daneben her, wie die Tagestermine: Er ist die
+ linke Spalte der Kachel, aber niemand soll auf ihn warten.
+ Scheitert er, steht der Rest trotzdem. */
+ const ringLaeuft = zentraleLaden();
+
const termineLaeuft = tagestermineLaden();
await hinweiseLaden();
+ /* KEIN `await ringLaeuft` HIER (08.09.2026).
+
+ Genau das stand kurz an dieser Stelle, und pruef-start-ansicht
+ wurde daraufhin dreimal rot -- nicht am Ring, sondern am
+ MAUS-LICHT auf den Kacheln weiter unten. Der Grund: Alles, was
+ nach diesem `await` kommt, wartet mit. Der Ring braucht eine
+ Server-Abfrage, und in der Zeit war das Licht noch nicht
+ eingehaengt, als die Pruefung mit dem Zeiger darueberfuhr.
+
+ Gemessen, nicht vermutet: derselbe Lauf am alten Stand meldete
+ null Befunde, mit dem `await` drei. Ein zusaetzliches `await` in
+ einer Ladefolge ist nie nur eine Verzoegerung fuer sich selbst --
+ es verzoegert alles dahinter. */
await termineLaeuft;
/* Erst jetzt steht fest, wie die Lage ist. */
lageZeigen();
diff --git a/workspace/start.html b/workspace/start.html
index cc23e13e..f2cd0367 100644
--- a/workspace/start.html
+++ b/workspace/start.html
@@ -47,33 +47,89 @@
-
+
+
+
+
+
+
+
+ –
+ wird geladen
+
+
+
+
-
Willkommen
+
+
+
+
+
Willkommen
+
+
+
+
+
+
+
+
+ Dogfather Universe
+
+
+
Die Zentrale
…
-
-
+ Versteckt, solange nichts geladen ist: Drei Striche in
+ einer Fassung sehen aus wie ein Fehler. -->