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
+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();