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 @@

- + + +
+ + Der Stand deiner Zentrale + Ein Segment je Person, eingefärbt nach Lage. + + +
+ + – + wird geladen +
+
+ +
-

Willkommen

+ + + + +

Willkommen

+ +
+ +
+ + + + + Dogfather Universe + + +

Die Zentrale

…

-
- + Versteckt, solange nichts geladen ist: Drei Striche in + einer Fassung sehen aus wie ein Fehler. --> +
+