Filipe: "ich will dass es eine eigene app wird" -- und dann "subdomain
machen jetzt sofort". Name von ihm gewaehlt: crew. (unauffaellig).
Auf einem Ursprung laesst sich genau EINE App installieren (w3c/manifest
Nr. 1180). Derselbe Grund, aus dem der Workspace am 06.09. umgezogen ist.
Getrennt wird ueber den HOSTNAMEN: ein Dienst, eine Datenbank, ein
Verzeichnis wie bisher.
DIE REGEL STEHT AN EINER STELLE
crew-adresse.js beantwortet: Darf diese Rolle auf dieser Adresse
angemeldet sein? Eingehaengt in sitzungLesen() -- durch die Funktion geht
jedes der 26 Fachmodule und jede Seitenschranke. Eine Middleware daneben
kann man in einem neuen Modul vergessen, und ein vergessener Rechteschutz
faellt nicht auf, weil dann alles geht.
crew. nur Modis; jeder andere bekommt 401 wie bei einem Tippfehler
workspace. keine Modis mehr; sie bekommen den Weg zur neuen Adresse
localhost UNVERAENDERT
Die dritte Zeile ist der Kern: Die Regel ist eine AUFZAEHLUNG der drei
echten Adressen, nicht "alles ausser crew". Sonst wuerden sieben andere
Pruefdateien ab sofort messen, dass ein Modi nirgends hereinkommt -- gruen,
weil sie nichts mehr finden.
ZWEI APPS, ZWEI NAMEN
Beide Adressen liefern dieselben HTML-Dateien. Auf crew. wird
/workspace/app.webmanifest serverseitig auf crew.webmanifest umgebogen --
so braucht keine der 30 Seiten eine zweite Zeile, die man bei der 31.
vergisst. "Team Dogi" statt "Creator Workspace", eigenes Zeichen:
derselbe Husky, aber ohne Chili (die gehoert zu Spicy Media, nicht zu
ihnen) und im Modi-Ton #5f8a9f.
tools/crew-symbol.mjs erzeugt die sechs Symbole und bricht ab, wenn sie
sich zu weniger als 10 % vom Workspace-Symbol unterscheiden. Gemessen:
43 bis 53 %.
GEMESSEN
pruef-crew-adresse 74 Pruefungen, 0 Fehler -- mit Gegenprobe: der Modi
bekommt in der Datenbank die Rolle 'manager', danach
MUSS dieselbe Sitzung auf crew. ins Leere laufen
pruef-rollen 245, unveraendert (die Regel ist lokal wirkungslos)
pruef-workspace-umzug 31 Faelle
pruef-zwischenspeicher 21 -- ein Stempel im Haus, jetzt ueber 23 Dateien
Der Server kennt die Adresse damit. DNS und Caddy bleiben Filipes Schritt.
Co-Authored-By: Claude Opus 5 <[email protected]>
261 lines
12 KiB
JavaScript
261 lines
12 KiB
JavaScript
/* ===================================================================
|
|
Das App-Symbol der Modi-App ("Team Dogi", 10.09.2026).
|
|
|
|
DER HUSKY IST DERSELBE. Filipe am 09.09.2026 zum Rollenbild:
|
|
"das modi symbol soll das gleiche sein wie bei dogfather." Das gilt
|
|
hier genauso -- es ist dieselbe Vorlage, dieselbe Technik.
|
|
|
|
DIE CHILI IST ES NICHT, UND ZWAR ABSICHTLICH. Sie steht fuer Spicy
|
|
Media. Die Modis gehoeren nicht dorthin; ihre Marke heisst "Team
|
|
Dogi". Eine Chili auf ihrem Startbildschirm waere kein Schmuck,
|
|
sondern eine falsche Aussage. An ihre Stelle tritt der Ton, der auch
|
|
ihre Kacheln traegt: das gedeckte Stahlblau #5f8a9f.
|
|
|
|
WARUM EINE EIGENE DATEI UND KEIN SCHALTER IN workspace-symbol.mjs:
|
|
Jenes Werkzeug PRUEFT, dass die Chili in jeder Fassung sichtbar ist.
|
|
Ein Schalter haette diese Pruefung fuer den halben Aufruf abschalten
|
|
muessen -- und eine Pruefung, die man abschalten kann, ist bald
|
|
abgeschaltet. Zwei Bilder mit zwei Regeln bekommen zwei Werkzeuge,
|
|
die jeweils ihre eigenen Regeln durchsetzen.
|
|
|
|
DIE DRITTE GEGENPROBE, DIE ES DORT NICHT GIBT: Am Ende wird das
|
|
erzeugte Symbol gegen das des Workspace gehalten. Waeren beide
|
|
gleich, haette der ganze Umzug seinen Sinn verloren -- zwei Apps mit
|
|
demselben Zeichen sind auf einem Startbildschirm dasselbe Symbol
|
|
zweimal. Gemessen wird das, nicht behauptet.
|
|
|
|
Aufrufen mit: node tools/crew-symbol.mjs
|
|
=================================================================== */
|
|
|
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const WURZEL = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const HUSKY = join(WURZEL, "workspace", "assets", "img", "marke-husky.webp");
|
|
/* Umlenkbar wie beim Schwesterwerkzeug: Ein Symbol beurteilt man, indem
|
|
man es ansieht -- und dafuer darf man die echten Dateien nicht bei
|
|
jedem Versuch ueberschreiben. */
|
|
const ZIEL = process.env.SYMBOL_ZIEL || join(WURZEL, "assets", "img", "app-symbole");
|
|
/* Zum Vergleichen -- immer die echten Symbole, auch wenn das Ziel
|
|
umgelenkt ist. Fehlt eines, ist die dritte Gegenprobe NICHT
|
|
durchfuehrbar, und das ist ein Fehler, kein Ueberspringen. */
|
|
const VERGLEICH = join(WURZEL, "assets", "img", "app-symbole");
|
|
|
|
if (!existsSync(HUSKY)) { console.error(`Fehlt: ${HUSKY}`); process.exit(1); }
|
|
const alsDaten = (p) => "data:image/webp;base64," + readFileSync(p).toString("base64");
|
|
|
|
const GROESSEN = [
|
|
{ n: 512, endung: "512" },
|
|
{ n: 192, endung: "192" },
|
|
{ n: 512, endung: "512-maskable", maskable: true },
|
|
{ n: 192, endung: "192-maskable", maskable: true },
|
|
{ n: 180, endung: "180" },
|
|
{ n: 32, endung: "32" },
|
|
];
|
|
|
|
function html(n, maskable, husky, weglassen = "") {
|
|
/* KLEIN = alles unter 64 px. Dort faellt die Gesichtszeichnung weg:
|
|
achtfach vergroessert verglichen ist der Kopf mit Zeichnung bei
|
|
32 px ein dunkler Fleck, als reine Silhouette ein Hund. Dieselbe
|
|
Messung wie beim Workspace-Symbol, dasselbe Ergebnis. */
|
|
const klein = n < 64;
|
|
const rand = Math.round(n * 0.22);
|
|
/* ALLES IN EINER GRUPPE, damit fuer die maskable-Fassung genau EINE
|
|
Zahl kleiner wird und nicht mehrere auseinanderlaufen. 0,74 haelt
|
|
alles im 80-%-Sicherheitskreis, den Android als einziges garantiert.
|
|
Ohne Chili darf der Kopf etwas groesser stehen -- er ist hier allein
|
|
und muss die Flaeche selbst tragen. */
|
|
const g = Math.round(n * (maskable ? 0.74 : 0.96));
|
|
const ecke = maskable ? "" : `border-radius:${rand}px;`;
|
|
|
|
return `<!doctype html><html><head><meta charset="utf-8"><style>
|
|
html,body{margin:0;width:${n}px;height:${n}px;overflow:hidden}
|
|
.feld{position:relative;width:${n}px;height:${n}px;${ecke}
|
|
/* DER GRUND. Dieselbe Nacht wie beim Workspace -- es ist dasselbe
|
|
Haus --, aber das warme Licht unten ist durch den Modi-Ton
|
|
ersetzt. Beide Lichter sind damit kalt, das Zeichen wirkt
|
|
ruhiger und ist trotzdem sofort ein anderes.
|
|
Gedeckt, nicht leuchtend: augenschonend gilt auch fuer ein
|
|
Symbol, das auf einem hellen Startbildschirm liegt. */
|
|
background:
|
|
radial-gradient(76% 56% at 50% 24%, rgba(95,189,255,.20) 0%, transparent 68%),
|
|
radial-gradient(90% 56% at 50% 104%, rgba(95,138,159,.46) 0%, transparent 70%),
|
|
linear-gradient(180deg,#1a2f47 0%,#0d1a29 52%,#060c14 100%);
|
|
box-shadow:inset 0 ${Math.max(1, Math.round(n * .004))}px 0 rgba(255,255,255,.24),
|
|
inset 0 0 ${Math.round(n * .06)}px rgba(0,0,0,.38);
|
|
display:grid;place-items:center}
|
|
.gruppe{position:relative;width:${g}px;height:${g}px}
|
|
.gruppe > i{position:absolute;left:50%;transform:translate(-50%,-50%)}
|
|
|
|
/* DER KOPF -- zwei Lagen, und der Grund dafuer ist teuer bezahlt:
|
|
|
|
Als blosse MASKE zaehlt nur der Alphakanal, und die Vorlage ist
|
|
rundum freigestellt. Uebrig bliebe eine geschlossene Flaeche in
|
|
Hundeform -- ohne Auge, ohne Schnauze, ohne Ohrinneres. Genau das
|
|
war am 09.09.2026 der Fehler beim Workspace-Symbol.
|
|
|
|
Jetzt unten die Silhouette, darueber die Originalzeichnung im
|
|
Mischmodus "screen": Screen laesst Schwarz stehen (das Fell
|
|
bleibt) und hebt Weiss heraus (Gesicht, Ohrinneres, Auge).
|
|
"mask-mode: luminance" kann Chromium zwar, aber eine
|
|
Kurzschreibweise in einer spezifischeren Regel setzt es
|
|
stillschweigend auf "alpha" zurueck -- der Mischmodus hat diese
|
|
Falle nicht.
|
|
|
|
DIE ZWEITE MASKE blendet den Hals nach unten aus. Die Vorlage ist
|
|
dort angeschnitten; als Symbol sah die gerade Kante nach Fehler
|
|
aus. Bei 32 px faellt der Auslauf weg, weil er dann gerade die
|
|
Schnauze frisst -- die Kante ist dort drei Bildpunkte hoch. */
|
|
.gruppe > i.kopf{width:${klein ? 78 : 90}%;height:${klein ? 78 : 90}%;
|
|
top:${klein ? 46 : 45}%;
|
|
z-index:2;isolation:isolate;
|
|
background:linear-gradient(162deg,#6f9fbe 0%,#3a6a91 44%,#14344f 100%);
|
|
${klein
|
|
? `-webkit-mask:url("${husky}") center/contain no-repeat;
|
|
mask:url("${husky}") center/contain no-repeat;`
|
|
: `-webkit-mask:url("${husky}") center/contain no-repeat,
|
|
linear-gradient(180deg,#000 58%, transparent 86%);
|
|
-webkit-mask-composite:source-in;
|
|
mask:url("${husky}") center/contain no-repeat,
|
|
linear-gradient(180deg,#000 58%, transparent 86%);
|
|
mask-composite:intersect;`}
|
|
filter:drop-shadow(0 ${Math.round(n * .006)}px ${Math.round(n * .016)}px rgba(0,0,0,.7))
|
|
drop-shadow(0 0 ${Math.round(n * .024)}px rgba(95,138,159,.44))}
|
|
.gruppe > i.kopf .zeichnung{position:absolute;inset:0;display:block;
|
|
background:url("${husky}") center/contain no-repeat;
|
|
mix-blend-mode:screen;
|
|
/* Ohne Daempfer wird das Weiss der Vorlage zu reinem Weiss und
|
|
blendet auf dunklem Grund. Hell genug zum Erkennen, nicht
|
|
heller. */
|
|
filter:brightness(.88)}
|
|
.gruppe > i.kopf.flach{background:linear-gradient(168deg,#ffffff 4%,#e2edf5 38%,#a8bfcd 100%)}
|
|
.gruppe > i.kopf.flach .zeichnung{display:none}
|
|
${weglassen === "zeichnung" ? ".gruppe > i.kopf .zeichnung{display:none}" : ""}
|
|
</style></head><body><div class="feld"><div class="gruppe">
|
|
<i class="kopf${klein ? " flach" : ""}"><b class="zeichnung"></b></i>
|
|
</div></div></body></html>`;
|
|
}
|
|
|
|
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({ deviceScaleFactor: 1 })).newPage();
|
|
if (!existsSync(ZIEL)) mkdirSync(ZIEL, { recursive: true });
|
|
|
|
const husky = alsDaten(HUSKY);
|
|
let gebaut = 0;
|
|
const maengel = [];
|
|
|
|
async function aufnehmen(n, maskable, weglassen = "") {
|
|
await seite.setViewportSize({ width: n, height: n });
|
|
await seite.setContent(html(n, maskable, husky, weglassen), { waitUntil: "networkidle" });
|
|
return await seite.screenshot({ omitBackground: false });
|
|
}
|
|
|
|
/* Streuung der Helligkeit. Ein leeres Feld hat keine -- das beweist,
|
|
dass ueberhaupt etwas draufsteht, mehr nicht. Die Bytezahl beweist
|
|
auch das nicht: ein einfarbiges PNG ist genauso gross wie ein volles. */
|
|
async function streuungVon(bild) {
|
|
return await seite.evaluate(async (b64) => {
|
|
const b = new Image(); b.src = "data:image/png;base64," + b64; await b.decode();
|
|
const c = document.createElement("canvas");
|
|
c.width = b.width; c.height = b.height;
|
|
const g2 = c.getContext("2d"); g2.drawImage(b, 0, 0);
|
|
const d = g2.getImageData(0, 0, c.width, c.height).data;
|
|
let s = 0, s2 = 0, m = 0;
|
|
for (let i = 0; i < d.length; i += 4) {
|
|
const h = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2];
|
|
s += h; s2 += h * h; m++;
|
|
}
|
|
return Math.sqrt(s2 / m - (s / m) ** 2);
|
|
}, bild.toString("base64"));
|
|
}
|
|
|
|
/* Anteil der Bildpunkte, die sich sichtbar unterscheiden. Acht Stufen
|
|
Abstand, damit Kantenglaettung allein den Wert nicht hochtreibt.
|
|
-1 heisst "nicht vergleichbar" (verschiedene Groessen) und ist
|
|
ausdruecklich etwas anderes als 0. */
|
|
async function unterschied(a, b) {
|
|
return await seite.evaluate(async ([x, y]) => {
|
|
const laden = async (b64) => {
|
|
const b = new Image(); b.src = "data:image/png;base64," + b64; await b.decode();
|
|
const c = document.createElement("canvas");
|
|
c.width = b.width; c.height = b.height;
|
|
c.getContext("2d").drawImage(b, 0, 0);
|
|
return c.getContext("2d").getImageData(0, 0, c.width, c.height).data;
|
|
};
|
|
const [p, q] = [await laden(x), await laden(y)];
|
|
if (p.length !== q.length) return -1;
|
|
let anders = 0, m = 0;
|
|
for (let i = 0; i < p.length; i += 4) {
|
|
const w = Math.max(Math.abs(p[i] - q[i]), Math.abs(p[i + 1] - q[i + 1]),
|
|
Math.abs(p[i + 2] - q[i + 2]), Math.abs(p[i + 3] - q[i + 3]));
|
|
if (w > 8) anders++;
|
|
m++;
|
|
}
|
|
return anders / m;
|
|
}, [a.toString("base64"), b.toString("base64")]);
|
|
}
|
|
|
|
let verglichen = 0;
|
|
|
|
for (const g of GROESSEN) {
|
|
const bild = await aufnehmen(g.n, !!g.maskable);
|
|
writeFileSync(join(ZIEL, `crew-${g.endung}.png`), bild);
|
|
gebaut++;
|
|
|
|
const streuung = await streuungVon(bild);
|
|
if (streuung < 12) maengel.push(`${g.endung}: kaum Zeichnung (Streuung ${streuung.toFixed(1)})`);
|
|
|
|
const klein = g.n < 64;
|
|
const ohneZeichnung = await unterschied(bild, await aufnehmen(g.n, !!g.maskable, "zeichnung"));
|
|
|
|
if (klein) {
|
|
/* Unter 64 px ist die schlichte Silhouette Absicht. Geprueft wird,
|
|
dass sie es auch bleibt -- sonst waere die Regel bloss ein
|
|
Kommentar, der beim naechsten Umbau stillschweigend kippt. */
|
|
if (ohneZeichnung > 0.001) maengel.push(`${g.endung}: klein soll ohne Gesichtszeichnung sein`);
|
|
} else if (ohneZeichnung < 0.04) {
|
|
maengel.push(`${g.endung}: das Gesicht kommt nicht durch (${(ohneZeichnung * 100).toFixed(1)} %)`);
|
|
}
|
|
|
|
/* DRITTE GEGENPROBE: anders als das Workspace-Symbol. */
|
|
const gegen = join(VERGLEICH, `workspace-${g.endung}.png`);
|
|
let anders = -2;
|
|
if (!existsSync(gegen)) {
|
|
maengel.push(`${g.endung}: Vergleichsbild workspace-${g.endung}.png fehlt `
|
|
+ `-- nicht nachsehbar, ob sich die Symbole unterscheiden`);
|
|
} else {
|
|
anders = await unterschied(bild, readFileSync(gegen));
|
|
verglichen++;
|
|
if (anders < 0) {
|
|
maengel.push(`${g.endung}: Vergleichsbild hat eine andere Groesse -- nicht vergleichbar`);
|
|
} else if (anders < 0.10) {
|
|
maengel.push(`${g.endung}: kaum vom Workspace-Symbol zu unterscheiden `
|
|
+ `(${(anders * 100).toFixed(1)} %)`);
|
|
}
|
|
}
|
|
|
|
console.log(` crew-${g.endung}.png`.padEnd(30)
|
|
+ `${(bild.length / 1024).toFixed(1)} KB · Streuung ${streuung.toFixed(1)}`
|
|
+ ` · Gesicht ${(ohneZeichnung * 100).toFixed(1)} %`
|
|
+ ` · anders als Workspace ${anders < 0 ? "?" : (anders * 100).toFixed(1) + " %"}`);
|
|
}
|
|
await browser.close();
|
|
|
|
/* Die Zahlen gehoeren in die Bedingung, nicht nur in den Bericht. */
|
|
if (gebaut !== GROESSEN.length) {
|
|
console.error(`\nFEHLER: ${gebaut} von ${GROESSEN.length} erzeugt.`); process.exit(1);
|
|
}
|
|
if (verglichen !== GROESSEN.length) {
|
|
console.error(`\nFEHLER: nur ${verglichen} von ${GROESSEN.length} gegen das `
|
|
+ `Workspace-Symbol gehalten.`); process.exit(1);
|
|
}
|
|
if (maengel.length) {
|
|
console.error("\nFEHLER:\n " + maengel.join("\n ")); process.exit(1);
|
|
}
|
|
console.log(`\n${gebaut} Symbole erzeugt. Der Husky ist derselbe wie bei DogFather,`
|
|
+ ` das Gesicht ist in jeder grossen Fassung nachweisbar sichtbar, und alle`
|
|
+ ` ${verglichen} unterscheiden sich messbar vom Workspace-Symbol.`);
|