WIP Zentrale-Kachel nach VanVans Werktisch — NOCH NICHT FERTIG

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.
This commit is contained in:
2026-09-08 01:44:36 +02:00
parent 0e522181e2
commit ec55b6c8d3
5 changed files with 666 additions and 23 deletions
+2
View File
@@ -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);
+182
View File
@@ -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" });
}
});
+204 -3
View File
@@ -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
+201
View File
@@ -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();
+77 -20
View File
@@ -47,33 +47,89 @@
<span class="willkommen__marker" id="tagesmarker" hidden></span>
</p>
<div class="willkommen__reihe">
<span class="willkommen__plakette" id="plakette" aria-hidden="true"></span>
<!-- ==== LINKS: DER RING ======================================
Nach dem Vorbild von VanVans Werktisch (Business Hub), auf
Wunsch Filipes uebernommen und weitergebaut.
Dort steht "91 % deiner Produktakten" -- ein Segment je
Produkt. Diese Zahl gibt es hier nicht: Nachgemessen hat der
Workspace 31 Termine, 9 Personen und GENAU EINE Aufgabe. Ein
Kranz aus einer Aufgabe ist kein Bild, sondern sieht aus wie
ein Ladefehler.
Filipe hat DAS TEAM gewaehlt. Fuer Creator zeigt derselbe
Ring den eigenen Tag -- nicht als Notloesung, sondern weil
ein Creator die Teamliste nicht sehen darf und die Frage
dieselbe bleibt: "Wie weit ist das, wofuer ich zustaendig
bin?" Die Entscheidung faellt im Server
(workspace-zentrale.js), nicht hier: Was der Browser nicht
bekommt, kann er auch nicht verraten. -->
<div class="zring" id="zring">
<svg class="zring__bild" id="zring-bild" viewBox="0 0 300 300"
role="img" aria-labelledby="zring-titel zring-text">
<title id="zring-titel">Der Stand deiner Zentrale</title>
<desc id="zring-text">Ein Segment je Person, eingefärbt nach Lage.</desc>
</svg>
<!-- IN DER MITTE STEHT DAS SIEGEL, NICHT DAS PERSONENBILD.
Im Vorbild sitzt dort VanVans Firmenlogo, und das Bild von
Dogi steckt in der Anrede daneben. Das ist kein Zufall: Der
Ring gehoert dem BETRIEB, die Anrede der PERSON. Haette ich
das Personenbild in die Mitte gesetzt, staende dasselbe
Gesicht zweimal auf einer Kachel -- und der Ring saehe aus,
als messe er die Person. -->
<div class="zring__mitte">
<span class="zring__siegel" aria-hidden="true"></span>
<strong class="zring__prozent" id="zring-prozent">–</strong>
<span class="zring__unter" id="zring-unter">wird geladen</span>
</div>
</div>
<!-- ==== MITTE: WER, WO, WAS ================================== -->
<div class="willkommen__text">
<h1 class="titel" id="gruss">Willkommen</h1>
<!-- Die Anrede sitzt in einer eigenen Fassung, wie im Vorbild:
Bild, Name und Rolle gehoeren zusammen und heben sich
dadurch vom Titel darunter ab, statt mit ihm zu ringen. -->
<span class="zgruss">
<span class="zgruss__bild willkommen__plakette" id="plakette" aria-hidden="true"></span>
<span class="zgruss__text">
<h1 class="titel" id="gruss">Willkommen</h1>
<span class="zgruss__datum" id="zgruss-datum"></span>
</span>
<span class="zgruss__rolle" id="zgruss-rolle" hidden></span>
</span>
<!-- Die Zierzeile traegt die Marke, wie "◆ VAN'S DIY ◆" im
Vorbild. Die Rauten sind eigene Elemente und kein Zeichen im
Text: Ein ◆ aus der Schriftart sitzt nie auf der Grundlinie
und sieht auf jedem Geraet anders aus. -->
<span class="zmarke">
<i class="zraute" aria-hidden="true"></i>
Dogfather Universe
<i class="zraute" aria-hidden="true"></i>
</span>
<p class="ztitel" id="ztitel">Die Zentrale</p>
<p class="unterzeile">
<span id="rollentext">…</span>
<span class="willkommen__lage" id="lage" hidden></span>
</p>
</div>
<!-- ==== Die Mitte der Konsole: drei Ablesungen ================
Zwischen Text und Uhr standen rund 300 px Leere -- auf einem
1440er Bildschirm nachgemessen, nicht geschaetzt. Eine
Konsole mit einem leeren Feld in der Mitte sieht nicht
aufgeraeumt aus, sondern unfertig.
<!-- ==== Die drei Ablesungen ==================================
Sie standen zuerst RECHTS, zwischen Text und Uhr -- dort
fuellten sie eine Luecke. Im Vorbild stehen sie unter dem
Titel in der Mitte, und das ist der bessere Ort: Sie
gehoeren zum Satz darueber ("253 Produkte, 100 Akten
fertig, 159 Angaben offen" liest sich als Fortsetzung des
Textes, nicht als Anzeige am Rand).
Was hier steht, ist deshalb keine Deko, sondern das, was der
Lagesatz daneben NICHT sagt: wie viel heute ansteht, wann das
Naechste losgeht, wie viel offen ist.
KEINE ZWEITE ZAEHLUNG. Die Zahlen kommen aus denselben
Quellen, die die Kacheln darunter fuellen. Zwei Rechenwege
fuer dieselbe Zahl laufen auseinander, und dann stehen zwei
Wahrheiten auf einem Bildschirm.
KEINE ZWEITE ZAEHLUNG. Die Zahlen kommen aus denselben zwei
Quellen, die die Kacheln darunter fuellen (`hinweise` und die
Tagestermine). Zwei Rechenwege fuer dieselbe Zahl laufen
auseinander, und dann stehen zwei Wahrheiten auf einem
Bildschirm.
Versteckt, solange nichts geladen ist: Drei Striche in einer
Fassung sehen aus wie ein Fehler. -->
Versteckt, solange nichts geladen ist: Drei Striche in
einer Fassung sehen aus wie ein Fehler. -->
<div class="willkommen__stand" id="tagesstand" hidden>
<div class="stand" data-stand="heute">
<span class="stand__wert" id="stand-heute">–</span>
@@ -87,7 +143,8 @@
<span class="stand__wert" id="stand-offen">–</span>
<span class="stand__schild">Offen</span>
</div>
</div>
</div>
</div><!-- /.willkommen__text -->
<!-- ==== Rechts in der Kachel: Glocke und Uhr ==================
Die Glocke stand bisher in der Kopfleiste. Dort ist sie eine