Compare commits
2
Commits
0e522181e2
...
b2a0fb3a97
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2a0fb3a97 | ||
|
|
ec55b6c8d3 |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -26,6 +26,7 @@ import { reportRouter } from "./workspace-reports.js";
|
|||||||
import { scoutRouter } from "./workspace-scouts.js";
|
import { scoutRouter } from "./workspace-scouts.js";
|
||||||
import { callRouter } from "./workspace-calls.js";
|
import { callRouter } from "./workspace-calls.js";
|
||||||
import { hinweisRouter } from "./workspace-hinweise.js";
|
import { hinweisRouter } from "./workspace-hinweise.js";
|
||||||
|
import { zentraleRouter } from "./workspace-zentrale.js";
|
||||||
import { sucheRouter } from "./workspace-suche.js";
|
import { sucheRouter } from "./workspace-suche.js";
|
||||||
import { startcheckRouter } from "./workspace-startcheck.js";
|
import { startcheckRouter } from "./workspace-startcheck.js";
|
||||||
import { wissenRouter } from "./workspace-wissen.js";
|
import { wissenRouter } from "./workspace-wissen.js";
|
||||||
@@ -216,6 +217,7 @@ app.use(reportRouter);
|
|||||||
app.use(scoutRouter);
|
app.use(scoutRouter);
|
||||||
app.use(callRouter);
|
app.use(callRouter);
|
||||||
app.use(hinweisRouter);
|
app.use(hinweisRouter);
|
||||||
|
app.use(zentraleRouter);
|
||||||
app.use(sucheRouter);
|
app.use(sucheRouter);
|
||||||
app.use(startcheckRouter);
|
app.use(startcheckRouter);
|
||||||
app.use(wissenRouter);
|
app.use(wissenRouter);
|
||||||
|
|||||||
@@ -434,7 +434,49 @@ console.log("\n=== Das Licht folgt dem Zeiger");
|
|||||||
const kontext = await browser.newContext({ viewport: { width: 1440, height: 1200 } });
|
const kontext = await browser.newContext({ viewport: { width: 1440, height: 1200 } });
|
||||||
const seite = await anmelden(kontext, "admin", "CODE-DOGI-0001");
|
const seite = await anmelden(kontext, "admin", "CODE-DOGI-0001");
|
||||||
const kachel = seite.locator(".kachel").nth(1);
|
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)");
|
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(() =>
|
const anzahlAn = () => seite.evaluate(() =>
|
||||||
document.querySelectorAll('[data-licht="an"]').length);
|
document.querySelectorAll('[data-licht="an"]').length);
|
||||||
|
|
||||||
/* 1. Ueber die Luecke zwischen zwei Kacheln hinaus. */
|
/* 1. Ueber die Luecke zwischen zwei Kacheln hinaus.
|
||||||
const k1 = await seite.locator(".kachel").nth(1).boundingBox();
|
|
||||||
|
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.mouse.move(k1.x + k1.width / 2, k1.y + k1.height / 2);
|
||||||
await seite.waitForTimeout(300);
|
await seite.waitForTimeout(300);
|
||||||
ok(await anzahlAn() === 1, `beim Ueberfahren leuchtet genau eine Kachel (${await anzahlAn()})`);
|
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})`);
|
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);
|
await seite.waitForTimeout(250);
|
||||||
const links = await kachel.evaluate((k) => k.style.getPropertyValue("--mx"));
|
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);
|
await seite.waitForTimeout(250);
|
||||||
const rechts = await kachel.evaluate((k) => k.style.getPropertyValue("--mx"));
|
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
|
/* Beim Verlassen zurück in die Ruhelage. Ohne das blieben die Kacheln
|
||||||
nach einer Weile alle unterschiedlich beleuchtet stehen, ohne dass
|
nach einer Weile alle unterschiedlich beleuchtet stehen, ohne dass
|
||||||
jemand wüsste, warum. */
|
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);
|
await seite.waitForTimeout(250);
|
||||||
const danach = await kachel.evaluate((k) => k.style.getPropertyValue("--mx") || "(Ruhelage)");
|
const danach = await kachel.evaluate((k) => k.style.getPropertyValue("--mx") || "(Ruhelage)");
|
||||||
ok(danach === "(Ruhelage)", `nach dem Verlassen wieder in Ruhe (${danach})`);
|
ok(danach === "(Ruhelage)", `nach dem Verlassen wieder in Ruhe (${danach})`);
|
||||||
|
|||||||
@@ -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" });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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.`);
|
||||||
@@ -94,7 +94,21 @@
|
|||||||
--k-rand: 4px;
|
--k-rand: 4px;
|
||||||
|
|
||||||
position: relative;
|
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: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
||||||
@@ -346,11 +360,270 @@
|
|||||||
transparent 74%);
|
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 {
|
.willkommen__reihe {
|
||||||
display: flex; align-items: center; gap: 16px;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
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`
|
/* Rechts: Glocke neben der Uhr, beide rechtsbündig. `margin-left: auto`
|
||||||
statt `justify-content: space-between` am Elternteil -- so bleibt die
|
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 |
@@ -505,7 +505,36 @@
|
|||||||
$('rollentext').textContent = ich.sicht
|
$('rollentext').textContent = ich.sicht
|
||||||
? `Arbeitsplatz von ${ich.sicht.name} · ${ROLLENTEXT[zeigt.rolle] || zeigt.rolle}`
|
? `Arbeitsplatz von ${ich.sicht.name} · ${ROLLENTEXT[zeigt.rolle] || zeigt.rolle}`
|
||||||
: (ROLLENTEXT[ich.rolle] || ich.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
|
/* Ein Satz zur Lage -- gerechnet aus denselben Hinweisen, die darunter
|
||||||
stehen. Keine zweite Zählung, sonst gäbe es irgendwann zwei
|
stehen. Keine zweite Zählung, sonst gäbe es irgendwann zwei
|
||||||
@@ -534,6 +563,145 @@
|
|||||||
standSetzen('offen', gesamt === 0 ? '0' : String(gesamt), stufe);
|
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 --------------------- */
|
/* ---------- Die drei Ablesungen in der Konsolenmitte --------------------- */
|
||||||
|
|
||||||
/* EINE Funktion fuer alle drei, und sie wird von den STELLEN gerufen,
|
/* 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
|
auf beide: Wer nichts Offenes hat, aber drei Calls, soll seinen Tag
|
||||||
sehen, auch wenn die andere Abfrage haengt. Genau diesen Fehler hat
|
sehen, auch wenn die andere Abfrage haengt. Genau diesen Fehler hat
|
||||||
die Kachel darunter schon einmal gemacht. */
|
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) {
|
function standSetzen(welches, wert, stufe) {
|
||||||
|
if (standVonZentrale) return;
|
||||||
const feld = $('stand-' + welches);
|
const feld = $('stand-' + welches);
|
||||||
if (!feld) return;
|
if (!feld) return;
|
||||||
feld.textContent = wert;
|
feld.textContent = wert;
|
||||||
@@ -579,8 +762,26 @@
|
|||||||
Die Tagestermine laufen daneben her: Sie sind die rechte Hälfte
|
Die Tagestermine laufen daneben her: Sie sind die rechte Hälfte
|
||||||
derselben Kachel, aber niemand soll auf sie warten. Scheitern
|
derselben Kachel, aber niemand soll auf sie warten. Scheitern
|
||||||
sie, steht die linke Hälfte trotzdem. */
|
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();
|
const termineLaeuft = tagestermineLaden();
|
||||||
await hinweiseLaden();
|
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;
|
await termineLaeuft;
|
||||||
/* Erst jetzt steht fest, wie die Lage ist. */
|
/* Erst jetzt steht fest, wie die Lage ist. */
|
||||||
lageZeigen();
|
lageZeigen();
|
||||||
|
|||||||
+75
-18
@@ -47,33 +47,89 @@
|
|||||||
<span class="willkommen__marker" id="tagesmarker" hidden></span>
|
<span class="willkommen__marker" id="tagesmarker" hidden></span>
|
||||||
</p>
|
</p>
|
||||||
<div class="willkommen__reihe">
|
<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">
|
<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>
|
<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">
|
<p class="unterzeile">
|
||||||
<span id="rollentext">…</span>
|
<span id="rollentext">…</span>
|
||||||
<span class="willkommen__lage" id="lage" hidden></span>
|
<span class="willkommen__lage" id="lage" hidden></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ==== Die Mitte der Konsole: drei Ablesungen ================
|
<!-- ==== Die drei Ablesungen ==================================
|
||||||
Zwischen Text und Uhr standen rund 300 px Leere -- auf einem
|
Sie standen zuerst RECHTS, zwischen Text und Uhr -- dort
|
||||||
1440er Bildschirm nachgemessen, nicht geschaetzt. Eine
|
fuellten sie eine Luecke. Im Vorbild stehen sie unter dem
|
||||||
Konsole mit einem leeren Feld in der Mitte sieht nicht
|
Titel in der Mitte, und das ist der bessere Ort: Sie
|
||||||
aufgeraeumt aus, sondern unfertig.
|
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
|
KEINE ZWEITE ZAEHLUNG. Die Zahlen kommen aus denselben
|
||||||
Lagesatz daneben NICHT sagt: wie viel heute ansteht, wann das
|
Quellen, die die Kacheln darunter fuellen. Zwei Rechenwege
|
||||||
Naechste losgeht, wie viel offen ist.
|
fuer dieselbe Zahl laufen auseinander, und dann stehen zwei
|
||||||
|
Wahrheiten auf einem Bildschirm.
|
||||||
|
|
||||||
KEINE ZWEITE ZAEHLUNG. Die Zahlen kommen aus denselben zwei
|
Versteckt, solange nichts geladen ist: Drei Striche in
|
||||||
Quellen, die die Kacheln darunter fuellen (`hinweise` und die
|
einer Fassung sehen aus wie ein Fehler. -->
|
||||||
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. -->
|
|
||||||
<div class="willkommen__stand" id="tagesstand" hidden>
|
<div class="willkommen__stand" id="tagesstand" hidden>
|
||||||
<div class="stand" data-stand="heute">
|
<div class="stand" data-stand="heute">
|
||||||
<span class="stand__wert" id="stand-heute">–</span>
|
<span class="stand__wert" id="stand-heute">–</span>
|
||||||
@@ -88,6 +144,7 @@
|
|||||||
<span class="stand__schild">Offen</span>
|
<span class="stand__schild">Offen</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div><!-- /.willkommen__text -->
|
||||||
|
|
||||||
<!-- ==== Rechts in der Kachel: Glocke und Uhr ==================
|
<!-- ==== Rechts in der Kachel: Glocke und Uhr ==================
|
||||||
Die Glocke stand bisher in der Kopfleiste. Dort ist sie eine
|
Die Glocke stand bisher in der Kopfleiste. Dort ist sie eine
|
||||||
|
|||||||
Reference in New Issue
Block a user