Compare commits

...
2 Commits
Author SHA1 Message Date
DogFatherGitandClaude Opus 5 b2a0fb3a97 Die Zentrale bekommt die echten Marken -- und drei rote Pruefungen waren keine
Die drei Befunde in pruef-start-ansicht kamen NICHT vom Licht. Sie hingen
alle an einem boundingBox(), das einmal am Anfang ohne Vorrollen gemessen
wurde. Der Umbau zur Zentrale hatte die Begruessungskachel von 244 auf
404 px wachsen lassen, die zweite Kachel rutschte von y=971 auf y=1131,
ihre Mitte lag bei 1207 -- ausserhalb eines 1200 px hohen Fensters. Dorthin
faehrt kein Zeiger, also entstand kein Licht.

Verraten hat es die Mischung aus gruen und rot: "links" (30 % der Hoehe)
bestand, "rechts" (60 %) nicht. Eine Kachel, die nur zur Haelfte getroffen
wird, ist nicht kaputt -- sie haengt halb aus dem Bild.

Beides ist jetzt behoben, nicht nur eines:

  * Die Pruefung holt die Kachel ueber scrollIntoView({block:"center"})
    ins Bild und misst DANACH, vor jeder Benutzung. Passt sie trotzdem
    nicht ins Fenster, ist das ein harter Fehler statt einer stillen
    Fehlmessung.
  * Die Kachel selbst faellt von 404 auf 344 px. Groesster Posten war die
    Anrede-Pille mit 88 px: In ihr steckt <h1 class="titel">, und
    .willkommen .titel ist die grosse Seitenueberschrift -- es standen
    also zwei Ueberschriften in Titelgroesse uebereinander. Der Rang von
    #gruss aendert sich nicht, nur die Groesse.

Nebenbefund, den die Reparatur mit aufgedeckt hat: Die Randmessung stand
auf "nah 51 gegen fern 0". Diese 0 war kein Messwert, sondern der
Bildpunkt ausserhalb des Fensters. Jetzt "nah 43 gegen fern 7" -- dieselbe
Pruefung misst zum ersten Mal wirklich.

DIE MARKEN. marke-husky.webp war nie freigestellt (0,3 % durchsichtig,
alle vier Ecken Alpha 255) -- als Maske ergab das einen Kasten mit einem
Husky darin. Ersetzt durch das echte Original, damit repariert sich die
Kopfleiste ohne eine einzige geaenderte CSS-Zeile mit.

In der Mitte des Rings steht jetzt Spicy Media, nicht der Husky: Der Ring
zeigt DAS TEAM, ein Segment je Person. Der DogFather-Kopf in seiner Mitte
haette Filipe bildlich ins Zentrum seines eigenen Teams gesetzt.

Und davon nur die Chili: Das volle Siegel war bei 44 px unlesbarer Matsch.
Beim ersten Ausschneiden meldete das Werkzeug "60,8 % deckend, Ecken
0/0/0/0" -- klang tadellos und war ein schwarzes RECHTECK mit Chili darin.
Die Flutfuellung laeuft von aussen und kommt nie hinter den weissen Ring.
Gefunden hat das kein Kennwert, sondern das Hinsehen.

Gemessen: pruef-start-ansicht EXIT=0 (137 -> 140 Pruefungen, die drei
roten sind gruen, keine ist verschwunden), pruef-css-klassen EXIT=0
(472 Groessen), pruef-handy EXIT=0.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 02:39:32 +02:00
DogFatherGit ec55b6c8d3 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.
2026-09-08 01:44:36 +02:00
11 changed files with 1186 additions and 30 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+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);
+62 -6
View File
@@ -434,7 +434,49 @@ console.log("\n=== Das Licht folgt dem Zeiger");
const kontext = await browser.newContext({ viewport: { width: 1440, height: 1200 } });
const seite = await anmelden(kontext, "admin", "CODE-DOGI-0001");
const kachel = seite.locator(".kachel").nth(1);
const kasten = await kachel.boundingBox();
/* ERST INS BILD ROLLEN, DANN MESSEN -- und zwar VOR JEDER Benutzung.
Hier stand `const kasten = await kachel.boundingBox()` ein einziges
Mal, ganz am Anfang, ohne vorher zu rollen. Am 08.09.2026 hat das
drei Pruefungen dieses Blocks umgeworfen, ohne dass am Licht
irgendetwas kaputt war: Die Begruessungskachel wuchs beim Umbau zur
Zentrale von 244 auf 404 px, die zweite Kachel rutschte damit von
y=971 auf y=1131, und ihre Mitte lag bei 1207 -- ausserhalb eines
1200 px hohen Fensters. Dorthin kann kein Zeiger fahren, also
entstand kein Licht.
Verraten hat es die MISCHUNG aus gruen und rot: "links" (30 % der
Hoehe) bestand, "rechts" (60 %) nicht. Eine Kachel, die nur zur
Haelfte getroffen wird, ist nicht kaputt -- sie haengt halb aus dem
Bild. Genau dieser Fehler steht zwanzig Zeilen weiter unten schon
einmal beschrieben, fuer den 07.09.2026. Dort wurde er an der
einzelnen Stelle geflickt, hier nicht.
Deshalb jetzt nicht noch einmal eine feste Annahme, sondern eine
Funktion: Sie holt die Kachel MITTIG ins Bild und misst danach.
`block: "center"` statt `scrollIntoViewIfNeeded()` mit Absicht --
letzteres rollt nur so weit, dass die Kante gerade eben anliegt,
und dann liegt der 60-%-Punkt wieder am Rand. Wer die Kachel spaeter
noch hoeher macht, merkt davon nichts mehr. */
const kachelHolen = async () => {
await kachel.evaluate((k) => k.scrollIntoView({ block: "center", behavior: "instant" }));
await seite.waitForTimeout(150);
const k = await kachel.boundingBox();
/* Die Anzahl gehoert in die Bedingung, nicht in den Meldetext: Eine
Kachel, die trotz Rollen nicht ganz ins Fenster passt, wuerde die
naechsten Messungen still verfaelschen. Dann lieber hier lautstark
scheitern als drei Zeilen weiter unten raetselhaft. */
const fenster = await seite.evaluate(() => window.innerHeight);
if (k.y < 0 || k.y + k.height > fenster) {
throw new Error(`Die Kachel passt auch nach dem Rollen nicht ins Fenster `
+ `(y=${Math.round(k.y)}, Hoehe=${Math.round(k.height)}, Fenster=${fenster}). `
+ `Ist sie groesser geworden?`);
}
return k;
};
const kasten = await kachelHolen();
const ruhe = await kachel.evaluate((k) => k.style.getPropertyValue("--mx") || "(Ruhelage)");
@@ -491,8 +533,14 @@ console.log("\n=== Das Licht folgt dem Zeiger");
const anzahlAn = () => seite.evaluate(() =>
document.querySelectorAll('[data-licht="an"]').length);
/* 1. Ueber die Luecke zwischen zwei Kacheln hinaus. */
const k1 = await seite.locator(".kachel").nth(1).boundingBox();
/* 1. Ueber die Luecke zwischen zwei Kacheln hinaus.
Auch hier ueber `kachelHolen()` statt ueber ein blankes
`boundingBox()`: Die Mitte dieser Kachel lag am 08.09.2026
ausserhalb des Fensters, der Zeiger kam nie an, und die Pruefung
meldete "leuchtet genau eine Kachel (0)" -- als waere das Licht
kaputt. Es war nur niemand da, der es haette ausloesen koennen. */
const k1 = await kachelHolen();
await seite.mouse.move(k1.x + k1.width / 2, k1.y + k1.height / 2);
await seite.waitForTimeout(300);
ok(await anzahlAn() === 1, `beim Ueberfahren leuchtet genau eine Kachel (${await anzahlAn()})`);
@@ -719,11 +767,19 @@ console.log("\n=== Das Licht folgt dem Zeiger");
}
ok(ruhe === "(Ruhelage)", `ruhend keine eigene Position gesetzt (${ruhe})`);
await seite.mouse.move(kasten.x + kasten.width * 0.2, kasten.y + kasten.height * 0.3);
/* NEU GEMESSEN, nicht der Kasten von oben. Der Block darueber endet
mit `window.scrollTo({ top: 0 })` -- ab dieser Zeile steht die Seite
also wieder ganz oben, und jede Koordinate von vorhin zeigt um die
gerollte Strecke daneben. Genau daran ist "rechts" gescheitert. */
const kastenJetzt = await kachelHolen();
await seite.mouse.move(kastenJetzt.x + kastenJetzt.width * 0.2,
kastenJetzt.y + kastenJetzt.height * 0.3);
await seite.waitForTimeout(250);
const links = await kachel.evaluate((k) => k.style.getPropertyValue("--mx"));
await seite.mouse.move(kasten.x + kasten.width * 0.85, kasten.y + kasten.height * 0.6);
await seite.mouse.move(kastenJetzt.x + kastenJetzt.width * 0.85,
kastenJetzt.y + kastenJetzt.height * 0.6);
await seite.waitForTimeout(250);
const rechts = await kachel.evaluate((k) => k.style.getPropertyValue("--mx"));
@@ -734,7 +790,7 @@ console.log("\n=== Das Licht folgt dem Zeiger");
/* Beim Verlassen zurück in die Ruhelage. Ohne das blieben die Kacheln
nach einer Weile alle unterschiedlich beleuchtet stehen, ohne dass
jemand wüsste, warum. */
await seite.mouse.move(kasten.x - 250, kasten.y - 250);
await seite.mouse.move(kastenJetzt.x - 250, kastenJetzt.y - 250);
await seite.waitForTimeout(250);
const danach = await kachel.evaluate((k) => k.style.getPropertyValue("--mx") || "(Ruhelage)");
ok(danach === "(Ruhelage)", `nach dem Verlassen wieder in Ruhe (${danach})`);
+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" });
}
});
+385
View File
@@ -0,0 +1,385 @@
/* ===================================================================
Stellt die beiden echten Marken frei: den DogFather-Husky und das
Spicy-Media-Siegel.
ANLASS (08.09.2026). Im Workspace lag `marke-husky.webp` als Maske
hinter dem Schriftzug und in der Mitte des Zentrale-Rings. Nachgemessen
war die Datei NICHT freigestellt: 0,3 % durchsichtige Punkte, alle vier
Ecken mit Alpha 255. Als Maske ergibt so etwas einen KASTEN mit einem
Husky darin -- egal wie richtig die CSS-Zeile ist. Filipe hat daraufhin
die beiden Originale geliefert.
WOHIN WAS KOMMT, und warum nicht andersherum:
Husky -> Kopfleiste (`.kopfleiste .marke::before`, 22 px)
Dort wird er als MASKE benutzt und mit dem Akzentverlauf
eingefaerbt. Von einer Maske zaehlt nur der Alphakanal,
also die Silhouette -- Ohren, Schnauze, Nackenlinie. Bei
22 px ist das erkennbar; die feinen schwarzen Linien
waeren es nicht.
Spicy -> Siegel in der Mitte des Zentrale-Rings (44 px), und
davon NUR DIE CHILI (Begruendung unten beim Auftrag).
Warum ueberhaupt Spicy Media und nicht der Husky: Dieser
Ring zeigt DAS TEAM, ein Segment je Person. Der
DogFather-Kopf in seiner Mitte haette Filipe bildlich ins
Zentrum seines Teams gesetzt, mit allen anderen als Kranz
darum herum. Genau das soll nicht sein (Nutzeranweisung
18.08.2026: nie hierarchisch ueber das Team stellen).
Spicy Media ist die Marke, unter der alle gleich
arbeiten, Filipe eingeschlossen.
DER ZWEITE DURCHGANG AUS `logo-freistellen.mjs` LAEUFT HIER NICHT --
und das ist der wichtigste Unterschied. Dort werden nach der Flutfuellung
auch die EINGESCHLOSSENEN Flaechen entfernt, die dem Grundton nahe
kommen. Beim damaligen Symbol war der Grund hellblau und alles
Hellblaue Hintergrund. Hier waere derselbe Schritt zerstoerisch:
* Der Husky liegt auf Fast-Weiss (247,248,247) und ist SCHWARZ-WEISS
gezeichnet. Schnauze, Wange, Ohrinnenseite und Brust sind weiss.
Ein Farbvergleich gegen den Grundton haette dem Hund das halbe
Gesicht ausgeschnitten.
* Das Spicy-Siegel liegt auf reinem Schwarz und ist innen ebenfalls
schwarz. Der Durchgang haette den Kreis ausgehoehlt und vom
Wappen einen blossen Ring uebriggelassen.
Es bleibt also bei der reinen Flutfuellung von aussen: Sie kommt nur
an das heran, was mit dem Bildrand zusammenhaengt, und stoppt an der
ersten Kontur. Genau das ist hier gewollt.
Aufrufen mit: node tools/marken-freistellen.mjs
=================================================================== */
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const WURZEL = join(dirname(fileURLToPath(import.meta.url)), "..");
const ZIEL_ORDNER = join(WURZEL, "workspace", "assets", "img");
/* Die Quellen liegen MIT IM REPO, nicht nur in Filipes Bilderordner.
Ein Werkzeug, dessen Eingangsdatei auf einem fremden Rechner liegt,
laesst sich genau einmal ausfuehren -- danach ist es Dekoration. Wer
die Marke spaeter in einer anderen Groesse oder mit anderer Kante
braucht, muss die Originale haben. */
const AUFTRAEGE = [
{
name: "Husky",
quelle: join(WURZEL, "assets", "img", "marke", "quelle-dogfather.jpg"),
ziel: join(ZIEL_ORDNER, "marke-husky.webp"),
/* Unter dem Kopf steht der Graffiti-Schriftzug "DOGFATHER". Bei
22 px waere er ein grauer Fleck, und als Maske zieht er die
Silhouette in die Breite. Er wird deshalb abgeschnitten -- die
Trennlinie wird gemessen, nicht geraten (siehe kopfTrennen). */
kopfTrennen: true,
},
{
name: "Spicy Media",
quelle: join(WURZEL, "assets", "img", "marke", "quelle-spicy-media.jpg"),
ziel: join(ZIEL_ORDNER, "marke-spicy.webp"),
kopfTrennen: false,
/* NUR DIE CHILI, NICHT DAS GANZE SIEGEL -- und das ist gemessen,
nicht Geschmack.
Der erste Versuch nahm das vollstaendige Logo: Doppelring, Chili,
"SPICY MEDIA", "FATIH TRABZON". Im Bildschirmfoto bei 44 px war
davon ein dunkler Fleck mit einem roten Strich uebrig, der
Schriftzug unlesbarer Matsch. Ein Logo MIT TEXT funktioniert in
dieser Groesse nicht -- kein Kniff aendert daran etwas, es sind
schlicht zu wenige Bildpunkte fuer Buchstaben.
Die Chili mit der Flamme ist das eigentliche Erkennungszeichen und
traegt auch klein: eine kraeftige rote Form mit gruenem Stiel.
Gefunden wird sie ueber die FARBE statt ueber feste Koordinaten --
alles, was bunt ist (max-min der Kanaele > 40), gehoert dazu; der
weisse Ring, die weisse Schrift und der schwarze Grund sind grau
und fallen von selbst heraus. Wer das Logo spaeter austauscht,
muss deshalb keine Zahlen nachtragen. */
nurFarbmotiv: true,
},
];
const KANTE = 512; // Ausgabegroesse, quadratisch
for (const auftrag of AUFTRAEGE) {
if (!existsSync(auftrag.quelle)) {
console.error(`Quelle fehlt: ${auftrag.quelle}`);
process.exit(1);
}
}
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
const seite = await (await browser.newContext()).newPage();
const arbeiten = async (auftrag) => await seite.evaluate(async ({ quelle, kopfTrennen, nurFarbmotiv, KANTE }) => {
const bild = new Image();
bild.src = quelle;
await bild.decode();
const b = bild.naturalWidth, h = bild.naturalHeight;
const leinwand = document.createElement("canvas");
leinwand.width = b; leinwand.height = h;
const stift = leinwand.getContext("2d", { willReadFrequently: true });
stift.drawImage(bild, 0, 0);
const daten = stift.getImageData(0, 0, b, h);
const p = daten.data;
/* GRUNDTON AUS DEM HAEUFIGSTEN RANDTON, nicht aus den vier Ecken.
Uebernommen aus logo-freistellen.mjs, wo die Eckenmittelung an
abgerundeten Ecken gescheitert ist. Der Weg ueber den ganzen Rand
ueberlebt Rahmen, runde Ecken und einzelne Ausreisser. */
const haufen = new Map();
const merke = (x, y) => {
const i = (y * b + x) * 4;
if (p[i + 3] < 200) return;
const schluessel = `${p[i] >> 3},${p[i + 1] >> 3},${p[i + 2] >> 3}`;
const e = haufen.get(schluessel) || { n: 0, r: 0, g: 0, bl: 0 };
e.n++; e.r += p[i]; e.g += p[i + 1]; e.bl += p[i + 2];
haufen.set(schluessel, e);
};
for (let x = 0; x < b; x++) { merke(x, 0); merke(x, h - 1); }
for (let y = 0; y < h; y++) { merke(0, y); merke(b - 1, y); }
if (!haufen.size) return { fehler: "Der ganze Rand ist durchsichtig." };
const groesster = [...haufen.values()].sort((a, c) => c.n - a.n)[0];
const randPunkte = [...haufen.values()].reduce((s, e) => s + e.n, 0);
const randAnteil = groesster.n / randPunkte;
if (randAnteil < 0.5) {
return { fehler: `Der Rand ist nicht einfarbig genug `
+ `(haeufigster Ton nur ${Math.round(randAnteil * 100)} % des Randes).` };
}
const grund = [
Math.round(groesster.r / groesster.n),
Math.round(groesster.g / groesster.n),
Math.round(groesster.bl / groesster.n),
];
const abstand = (i) => Math.sqrt(
(p[i] - grund[0]) ** 2 + (p[i + 1] - grund[1]) ** 2 + (p[i + 2] - grund[2]) ** 2);
/* NAH bis hierher gilt ein Punkt als Hintergrund
WEIT ab hier gehoert er zum Motiv; dazwischen wird weich abgestuft.
Etwas grosszuegiger als die 42/96 der Symbolfassung: Beide Quellen
sind JPEG-Fotos, keine erzeugten PNG. Um jede Kontur liegt ein Saum
aus Kompressionsartefakten, der weder Grund noch Motiv ist. Wer ihn
stehen laesst, bekommt einen schmutzigen Rand -- bei einer Maske
faellt das als ausgefranste Silhouette auf. */
const NAH = 58, WEIT = 118;
const besucht = new Uint8Array(b * h);
const liste = [];
for (let x = 0; x < b; x++) { liste.push(x, x + (h - 1) * b); }
for (let y = 0; y < h; y++) { liste.push(y * b, b - 1 + y * b); }
let entfernt = 0, weich = 0;
while (liste.length) {
const feld = liste.pop();
if (besucht[feld]) continue;
besucht[feld] = 1;
const i = feld * 4;
const a = abstand(i);
if (a > WEIT) continue; // Motivkante erreicht, hier ist Schluss
if (a <= NAH) { p[i + 3] = 0; entfernt++; }
else { p[i + 3] = Math.round(255 * ((a - NAH) / (WEIT - NAH))); weich++; }
const x = feld % b, y = (feld - x) / b;
if (x > 0) liste.push(feld - 1);
if (x < b - 1) liste.push(feld + 1);
if (y > 0) liste.push(feld - b);
if (y < h - 1) liste.push(feld + b);
}
/* ---- Wo steht das Motiv? ---------------------------------------- */
const zeile = new Array(h).fill(0);
let x0 = b, y0 = h, x1 = -1, y1 = -1, sichtbar = 0;
for (let y = 0; y < h; y++) {
for (let x = 0; x < b; x++) {
if (p[(y * b + x) * 4 + 3] > 40) {
zeile[y]++; sichtbar++;
if (x < x0) x0 = x; if (x > x1) x1 = x;
if (y < y0) y0 = y; if (y > y1) y1 = y;
}
}
}
if (x1 < 0) return { fehler: "Nach dem Freistellen ist nichts uebrig." };
/* ---- Kopf vom Schriftzug trennen -------------------------------- */
let trennung = null;
if (kopfTrennen) {
/* GEMESSEN, NICHT GERATEN. Gesucht wird die duennste Stelle des
Motivs im mittleren Drittel -- zwischen Kinn/Nacken und der
Oberkante der Buchstaben liegt eine Einschnuerung. Der Bereich
ist bewusst eng gefasst (45-78 % der Motivhoehe): Weiter oben
liegt die Taille zwischen den Ohren, weiter unten die Luecke
zwischen zwei Buchstabenreihen -- beide waeren falsch. */
const hoehe = y1 - y0 + 1;
const von = y0 + Math.round(hoehe * 0.45);
const bis = y0 + Math.round(hoehe * 0.78);
let besteZeile = von, bestesMass = Infinity;
for (let y = von; y <= bis; y++) {
if (zeile[y] < bestesMass) { bestesMass = zeile[y]; besteZeile = y; }
}
trennung = { y: besteZeile, punkte: bestesMass, von, bis,
profil: [von, Math.round((von + bis) / 2), bis].map((y) => `${y}:${zeile[y]}`) };
/* Alles ab der Trennlinie wegwerfen und den Rahmen neu bestimmen. */
for (let y = besteZeile; y < h; y++)
for (let x = 0; x < b; x++) p[(y * b + x) * 4 + 3] = 0;
x0 = b; y0 = h; x1 = -1; y1 = -1; sichtbar = 0;
for (let y = 0; y < h; y++) for (let x = 0; x < b; x++) {
if (p[(y * b + x) * 4 + 3] > 40) {
sichtbar++;
if (x < x0) x0 = x; if (x > x1) x1 = x;
if (y < y0) y0 = y; if (y > y1) y1 = y;
}
}
if (x1 < 0) return { fehler: "Nach dem Abschneiden des Schriftzugs ist nichts uebrig." };
}
/* ---- Nur das farbige Motiv (siehe `nurFarbmotiv` oben) ----------- */
let farbrahmen = null;
if (nurFarbmotiv) {
let fx0 = b, fy0 = h, fx1 = -1, fy1 = -1, bunt = 0;
for (let y = 0; y < h; y++) for (let x = 0; x < b; x++) {
const i = (y * b + x) * 4;
if (p[i + 3] < 60) continue;
const max = Math.max(p[i], p[i + 1], p[i + 2]);
const min = Math.min(p[i], p[i + 1], p[i + 2]);
if (max - min > 40) {
bunt++;
if (x < fx0) fx0 = x; if (x > fx1) fx1 = x;
if (y < fy0) fy0 = y; if (y > fy1) fy1 = y;
}
}
/* Die Anzahl gehoert in die Bedingung: Ein Logo ohne bunte Punkte
haette sonst einen leeren Rahmen ergeben, und das Ergebnis waere
ein leeres Bild gewesen -- das sauberste von allen. */
if (bunt < 500) return { fehler: `Nur ${bunt} bunte Punkte gefunden -- zu wenig fuer ein `
+ `Farbmotiv. Ist das Logo einfarbig?` };
/* Alles ausserhalb des Farbrahmens verschwindet, damit Ring und
Schriftzug nicht als Reste am Bildrand stehenbleiben. */
for (let y = 0; y < h; y++) for (let x = 0; x < b; x++) {
if (x < fx0 || x > fx1 || y < fy0 || y > fy1) p[(y * b + x) * 4 + 3] = 0;
}
/* UND DER GRUND INNERHALB DES RAHMENS MUSS AUCH WEG.
Das war der erste Versuch nicht, und die Zahlen haben es NICHT
verraten: "60,8 % deckend, Ecken-Alpha 0/0/0/0" klang tadellos.
Im Bild war es ein schwarzes RECHTECK mit einer Chili darin. Der
Grund ist die Flutfuellung: Sie laeuft von aussen und bleibt am
weissen Doppelring stehen -- hinter den Ring, wo die Chili sitzt,
kommt sie nie. Dort ist der schwarze Grund also unberuehrt.
Hier ist ein reiner Farbvergleich gefahrlos, und das laesst sich
nachrechnen statt hoffen: Der Grundton ist rgb(0,0,0), die Chili
liegt bei rund (200,30,30) -- Abstand ueber 200 --, der gruene
Stiel bei rund (40,80,30) mit Abstand 94, die weisse Flamme bei
441. Alle drei liegen weit ueber der Schwelle NAH (58). Genau
deshalb steht diese Rechnung hier und nicht nur das Ergebnis:
Wer die Marke austauscht, muss sie nachrechnen koennen.
Fuer das VOLLE Siegel waere derselbe Schritt zerstoerisch (er
hoehlte den schwarzen Kreis aus) -- deshalb haengt er an
`nurFarbmotiv` und nicht am Werkzeug allgemein. */
let kern = 0;
for (let y = fy0; y <= fy1; y++) for (let x = fx0; x <= fx1; x++) {
const i = (y * b + x) * 4;
if (p[i + 3] === 0) continue;
const a = abstand(i);
if (a <= NAH) { p[i + 3] = 0; kern++; }
else if (a < WEIT) p[i + 3] = Math.min(p[i + 3],
Math.round(255 * ((a - NAH) / (WEIT - NAH))));
}
if (kern < 500) return { fehler: `Im Farbrahmen wurden nur ${kern} Grundton-Punkte `
+ `entfernt -- der Hintergrund hinter dem Motiv steht vermutlich noch.` };
x0 = fx0; y0 = fy0; x1 = fx1; y1 = fy1;
sichtbar = bunt;
farbrahmen = `${fx1 - fx0 + 1}x${fy1 - fy0 + 1} (${bunt} bunte Punkte)`;
}
stift.putImageData(daten, 0, 0);
/* ---- Quadratisch und zentriert ausgeben --------------------------
Beide Einsatzorte sind Quadrate (44 px Siegel, 22 px Maske). Wer
ein nicht-quadratisches Bild mit `contain` hineinlegt, bekommt es
kleiner als gedacht und aus der Mitte geruecht. Deshalb wird der
Motivrahmen hier einmal sauber quadriert -- mit einem schmalen
Rand, damit die weiche Kante nicht am Bildrand abgeschnitten wird. */
const mb = x1 - x0 + 1, mh = y1 - y0 + 1;
/* Etwas mehr Luft um ein Farbmotiv (14 % statt 6 %): Die Chili ist
eine schraege Form mit Stiel und Flamme, die bis an den Rahmen
stossen -- ohne Rand klebt sie an der Kante des Siegels. */
const seiteQ = Math.round(Math.max(mb, mh) * (nurFarbmotiv ? 1.14 : 1.06));
const mx = x0 + mb / 2, my = y0 + mh / 2;
const ziel = document.createElement("canvas");
ziel.width = KANTE; ziel.height = KANTE;
const zs = ziel.getContext("2d");
zs.imageSmoothingQuality = "high";
zs.drawImage(leinwand, mx - seiteQ / 2, my - seiteQ / 2, seiteQ, seiteQ, 0, 0, KANTE, KANTE);
/* Wie viel ist im FERTIGEN Bild noch sichtbar? Die Zahl unten
entscheidet, ob gespeichert wird -- ein leeres Bild waere sonst
das sauberste Ergebnis von allen. */
const zp = zs.getImageData(0, 0, KANTE, KANTE).data;
let deckend = 0, halb = 0;
for (let i = 3; i < zp.length; i += 4) {
if (zp[i] > 200) deckend++; else if (zp[i] > 10) halb++;
}
const eckenAlpha = [[0, 0], [KANTE - 1, 0], [0, KANTE - 1], [KANTE - 1, KANTE - 1]]
.map(([x, y]) => zp[(y * KANTE + x) * 4 + 3]);
return {
webp: ziel.toDataURL("image/webp", 0.94),
grund, randAnteil: Math.round(randAnteil * 100),
entfernt, weich, trennung, farbrahmen,
rahmen: `${mb}x${mh}`, quelle: `${b}x${h}`,
deckend, halb, eckenAlpha,
anteil: deckend / (KANTE * KANTE),
};
}, { quelle: "data:image/jpeg;base64," + readFileSync(auftrag.quelle).toString("base64"),
kopfTrennen: auftrag.kopfTrennen, nurFarbmotiv: !!auftrag.nurFarbmotiv, KANTE });
let fehlt = 0;
for (const auftrag of AUFTRAEGE) {
console.log(`\n=== ${auftrag.name} ===`);
const e = await arbeiten(auftrag);
if (e.fehler) { console.error(` Abgebrochen: ${e.fehler}`); fehlt++; continue; }
console.log(` Quelle: ${e.quelle}, Grundton rgb(${e.grund.join(",")}) `
+ `auf ${e.randAnteil} % des Randes`);
console.log(` Freigestellt: ${e.entfernt} Punkte ganz, ${e.weich} weich`);
if (e.trennung) {
console.log(` Schnitt bei y=${e.trennung.y} (${e.trennung.punkte} Punkte breit, `
+ `gesucht in ${e.trennung.von}..${e.trennung.bis}; Profil ${e.trennung.profil.join(" ")})`);
}
console.log(` Motivrahmen: ${e.rahmen}`);
console.log(` Ergebnis: ${e.deckend} deckende Punkte (${(e.anteil * 100).toFixed(1)} %), `
+ `${e.halb} weiche Kante, Ecken-Alpha ${e.eckenAlpha.join("/")}`);
/* DREI BEDINGUNGEN, und alle drei muessen halten. Eine Pruefung, die
immer bestaetigt, bestaetigt nichts -- deshalb steht die Anzahl in
der Bedingung und nicht nur im Meldetext. */
if (e.anteil < 0.06) {
console.error(` FEHLER: nur ${(e.anteil * 100).toFixed(1)} % uebrig -- die Fuellung hat das `
+ `Motiv mitgenommen. Nichts gespeichert.`); fehlt++; continue;
}
if (e.anteil > 0.94) {
console.error(` FEHLER: ${(e.anteil * 100).toFixed(1)} % uebrig -- es wurde praktisch nichts `
+ `entfernt. Nichts gespeichert.`); fehlt++; continue;
}
if (e.eckenAlpha.some((a) => a > 20)) {
console.error(` FEHLER: eine Ecke ist noch deckend (${e.eckenAlpha.join("/")}) -- genau `
+ `daran ist die alte Datei gescheitert. Nichts gespeichert.`); fehlt++; continue;
}
writeFileSync(auftrag.ziel, Buffer.from(e.webp.split(",")[1], "base64"));
const kb = (readFileSync(auftrag.ziel).length / 1024).toFixed(1);
console.log(` Gespeichert: ${auftrag.ziel.replace(WURZEL, ".")} (${KANTE}x${KANTE}, ${kb} KB)`);
}
await browser.close();
if (fehlt) { console.error(`\n${fehlt} von ${AUFTRAEGE.length} nicht gespeichert.`); process.exit(1); }
console.log(`\nBeide Marken freigestellt.`);
+277 -4
View File
@@ -94,7 +94,21 @@
--k-rand: 4px;
position: relative;
padding: 26px 30px;
/* 20px oben/unten statt 26.
JEDER PIXEL HIER SCHIEBT DIE KACHELN DARUNTER NACH UNTEN, und das
ist keine Feinheit: Mit dem dreispaltigen Umbau wuchs diese Kachel
von 244 auf 404 px. Die erste Kachelreihe rutschte dadurch von
y=971 auf y=1131 -- auf einem 1200 px hohen Fenster war damit keine
einzige Kachel mehr zu sehen, ohne zu rollen. Die Startseite BESTEHT
aber aus diesen Kacheln.
Zusammen mit der kleineren Anrede (siehe `.zgruss .titel`), dem
engeren Zeilenabstand und der Titelgroesse sind es rund 60 px
weniger. Die Kachel bleibt dem Vorbild treu -- VanVans Werktisch
hat mit minmax(200px, 260px) sogar eine breitere Ringspalte als
unsere 180-232 --, steht der Seite darunter aber nicht mehr im Weg. */
padding: 20px 30px;
border: 0;
border-radius: 0;
@@ -346,11 +360,270 @@
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;
/* 7px statt 9: vier Luecken uebereinander, also 8 px weniger Hoehe.
Siehe die Rechnung ueber `.willkommen`, warum jeder Pixel hier
unten bei den Kacheln ankommt. */
display: flex; flex-direction: column; align-items: center; gap: 7px;
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;
}
/* IN DER MITTE STEHT SPICY MEDIA, NICHT DER DOGFATHER-KOPF.
Das ist keine Stilfrage, sondern eine inhaltliche. Dieser Ring zeigt
DAS TEAM -- ein Segment je Person. Der DogFather-Kopf in seiner Mitte
haette Filipe bildlich ins Zentrum seines eigenen Teams gesetzt, mit
allen anderen als Kranz darum herum. Genau das soll nicht sein
(Anweisung 18.08.2026: nie hierarchisch ueber das Team stellen). Das
Spicy-Media-Siegel steht fuer alle gleich -- es ist die Marke, unter
der das ganze Team arbeitet, Filipe eingeschlossen.
Optisch trifft es ausserdem genau, was ein Siegel hier braucht: Es
ist von sich aus rund, mit hellem Doppelring auf dunklem Kern. Auf
dem dunklen Untergrund des Workspace leuchtet der Ring ruhig, ohne zu
blenden -- der Kern bleibt dunkel und zieht den Blick nicht von der
Zahl darunter ab, um die es geht.
VORGESCHICHTE, damit sie niemand zweimal macht: Hier stand
`marke-husky.webp`, und in der Kopfleiste dieselbe Datei als Maske.
Die Datei hatte zwar einen Alphakanal, der war aber RECHTECKIG -- als
Maske ergab das einen Kasten mit einem Husky darin. Seit dem
08.09.2026 ist sie durch das echte, freigestellte Original ersetzt
(`tools/marken-freistellen.mjs`, Ecken-Alpha nachgemessen 0/0/0/0),
und die Kopfleiste ist damit ohne eine einzige geaenderte CSS-Zeile
mitrepariert.
`contain` statt der frueheren 128 %: Das Werkzeug gibt das Motiv
bereits quadratisch gerahmt aus, ein Nachskalieren wuerde den
Doppelring nur wieder anschneiden. */
.zring__siegel {
width: 44px; height: 44px; margin-bottom: 5px;
border-radius: 50%;
/* 74 % statt `contain`: Bei voller Breite stiess die Chili an den
Rand des Siegels und wirkte hineingezwaengt. Mit etwas Luft
ringsum sitzt sie im Kreis, statt ihn auszufuellen. */
background: url("../img/marke-spicy.webp") center / 74% no-repeat, #0b1420;
box-shadow:
0 0 0 1px rgba(180, 210, 240, .28),
inset 0 0 12px rgba(0, 0, 0, .55);
/* Nur leicht zurueckgenommen. Die alte Fassung stand auf
saturate(.55) brightness(.92) -- das war fuer den schwarzweissen
Husky gedacht. Dem Spicy-Siegel nimmt so viel Entsaettigung das
Rot der Chili, also bleibt es bei einem sanften Daempfer: ruhig
genug, um nicht mit der Zahl zu konkurrieren, farbig genug, um als
Marke erkennbar zu sein. */
filter: saturate(.86) brightness(.95);
}
.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); }
/* DIE ANREDE IST EINE PILLE, KEINE ZWEITE UEBERSCHRIFT.
In der Pille steckt `<h1 class="titel" id="gruss">`, und
`.willkommen .titel` (start.css) ist mit clamp(1.9rem, 3.4vw, 2.6rem)
die grosse Seitenueberschrift. Nachgemessen war die Pille dadurch
88 px hoch -- der groesste Einzelposten der ganzen Kachel, fuer eine
Zeile "Guten Abend, Filipe".
Damit standen ZWEI Ueberschriften in Titelgroesse uebereinander: die
Anrede und "Die Zentrale" direkt darunter. Im Vorbild ist genau das
nicht so -- dort ist die Anrede klein und ruhig, und der Titel traegt
allein. Die Pille faellt damit von 88 auf rund 56 px, ohne dass
irgendwo Polsterung weggenommen werden musste.
Die Schriftgroesse aendert die Gliederung NICHT: `#gruss` bleibt das
h1 der Seite. Wie gross eine Ueberschrift gesetzt ist und welchen
Rang sie hat, sind zwei verschiedene Dinge. */
.zgruss .titel {
font-size: 1.05rem; font-weight: 700; line-height: 1.25;
letter-spacing: -.005em;
margin: 0;
/* Der Verlauf aus `.willkommen .titel` wuerde hier mitkommen und die
Anrede in dieselbe gefraeste Metalloptik setzen wie den Titel. Bei
17 px ist das nur unruhig -- also zurueck auf eine feste Farbe. */
background: none; -webkit-text-fill-color: currentColor;
color: #e8f1fa; text-shadow: none;
}
/* .72rem ist die Hausuntergrenze (11,52 px), nicht Geschmack: Darunter
liest auf einem Telefon niemand mehr mit. Hier stand .66rem = 10,6 px.
Beim Bau des Rings war dieselbe Regel bei `.zring__unter` schon
angewandt worden -- diese beiden Stellen sind dabei durchgerutscht,
weil sie in einem anderen Abschnitt der Datei stehen. */
.zgruss__rolle {
flex: 0 0 auto;
padding: 4px 11px; border-radius: 999px;
font-size: .72rem; 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 ------------------------------------------ */
/* Ebenfalls auf die Hausuntergrenze gehoben (.68rem waren 10,9 px).
Das breite letter-spacing macht die Zeile dadurch etwas laenger --
sie steht in der mittleren Spalte, die mit `minmax(0, 1fr)` mitgeht,
und bricht daher um statt ueberzulaufen. */
.zmarke {
display: inline-flex; align-items: center; gap: 11px;
font-size: .72rem; 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;
/* Obergrenze von 2.9 auf 2.45rem. Bei 1440 px lief der Titel vorher
auf 46 px hinaus und war damit fast so hoch wie die Anrede daneben.
Die untere Grenze bleibt, damit er auf dem Telefon nicht schrumpft
-- dort ist er die einzige Ueberschrift im Bild. */
font-size: clamp(1.9rem, 3.6vw, 2.45rem);
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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+201
View File
@@ -505,7 +505,36 @@
$('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
@@ -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();
+75 -18
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">
<!-- 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>
@@ -88,6 +144,7 @@
<span class="stand__schild">Offen</span>
</div>
</div>
</div><!-- /.willkommen__text -->
<!-- ==== Rechts in der Kachel: Glocke und Uhr ==================
Die Glocke stand bisher in der Kopfleiste. Dort ist sie eine