diff --git a/assets/img/wd-admin-192.jpg b/assets/img/wd-admin-192.jpg
new file mode 100644
index 00000000..f86fb014
Binary files /dev/null and b/assets/img/wd-admin-192.jpg differ
diff --git a/assets/img/wd-admin-512-maskable.jpg b/assets/img/wd-admin-512-maskable.jpg
new file mode 100644
index 00000000..3f7228b3
Binary files /dev/null and b/assets/img/wd-admin-512-maskable.jpg differ
diff --git a/assets/img/wd-admin-512.jpg b/assets/img/wd-admin-512.jpg
new file mode 100644
index 00000000..052d6ee4
Binary files /dev/null and b/assets/img/wd-admin-512.jpg differ
diff --git a/assets/js/wd-core.js b/assets/js/wd-core.js
index 8dae87c6..6526ee2d 100644
--- a/assets/js/wd-core.js
+++ b/assets/js/wd-core.js
@@ -838,7 +838,7 @@
Stand haben. Die Nummer wird zusammen mit CACHE_NAME in sw.js
hochgezaehlt -- beide gehoeren zusammen. */
navigator.serviceWorker
- .register("/webdesign/sw.js?v=58", { scope: "/webdesign/" })
+ .register("/webdesign/sw.js?v=59", { scope: "/webdesign/" })
.catch(function () { /* still schlucken: ohne SW funktioniert alles weiter */ });
});
}
diff --git a/pruef-verwaltung-app.mjs b/pruef-verwaltung-app.mjs
new file mode 100644
index 00000000..09aa1afc
--- /dev/null
+++ b/pruef-verwaltung-app.mjs
@@ -0,0 +1,182 @@
+/* =====================================================================
+ Prueft, ob die Verwaltung als EIGENE App installierbar ist.
+
+ Der Fehler, den man hier am ehesten macht, faellt erst beim zweiten
+ Installieren auf: Haben zwei Manifeste dieselbe "id", halten Browser
+ sie fuer dieselbe App. Die zweite Installation ueberschreibt dann die
+ erste, statt danebenzustehen -- und man sucht den Fehler ueberall,
+ nur nicht in einer Zeile, die man fuer Beiwerk gehalten hat.
+
+ Ebenso still: ein Symbol, das im Manifest steht, aber nicht existiert
+ oder andere Masse hat als angegeben. Der Browser meldet das nicht --
+ er zeigt einfach ein leeres oder verzerrtes Kaestchen.
+ ===================================================================== */
+import { chromium } from "playwright";
+import http from "http";
+import fs from "fs";
+import path from "path";
+
+const TYP = {
+ ".html": "text/html", ".css": "text/css", ".js": "text/javascript",
+ ".png": "image/png", ".svg": "image/svg+xml", ".webp": "image/webp",
+ ".json": "application/json", ".ico": "image/x-icon", ".jpg": "image/jpeg",
+ /* Ohne diesen Eintrag liefert der Testserver den Typ "text/plain", und
+ der Browser ignoriert das Manifest stillschweigend. Genau so wuerde
+ es auch auf einem falsch eingerichteten Server aussehen. */
+ ".webmanifest": "application/manifest+json",
+};
+
+const srv = http.createServer((q, r) => {
+ const p = path.join(process.cwd(), decodeURIComponent(q.url.split("?")[0]));
+ if (fs.existsSync(p) && !p.endsWith("/")) {
+ r.writeHead(200, { "content-type": TYP[path.extname(p)] || "text/plain" });
+ return r.end(fs.readFileSync(p));
+ }
+ r.writeHead(404);
+ r.end("");
+});
+
+let ok = 0, fehl = 0;
+const pruefe = (name, gut, info) => {
+ if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); }
+ else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); }
+};
+
+/* Masse aus dem Dateikopf lesen, ohne Fremdpaket. */
+function masse(datei) {
+ const b = fs.readFileSync(datei);
+ if (b[0] === 0x89 && b[1] === 0x50) {
+ return { breite: b.readUInt32BE(16), hoehe: b.readUInt32BE(20) };
+ }
+ let i = 2;
+ while (i < b.length) {
+ if (b[i] !== 0xFF) { i++; continue; }
+ const m = b[i + 1];
+ if (m >= 0xC0 && m <= 0xCF && m !== 0xC4 && m !== 0xC8 && m !== 0xCC) {
+ return { hoehe: b.readUInt16BE(i + 5), breite: b.readUInt16BE(i + 7) };
+ }
+ i += 2 + b.readUInt16BE(i + 2);
+ }
+ return null;
+}
+
+srv.listen(4410, async () => {
+ const browser = await chromium.launch();
+ const seite = await browser.newPage({ viewport: { width: 1280, height: 900 } });
+
+ console.log("1. DIE VERWALTUNG HAT EIN EIGENES MANIFEST");
+ await seite.goto("http://127.0.0.1:4410/webdesign/verwaltung.html");
+ await seite.waitForTimeout(900);
+
+ const verweis = await seite.evaluate(() =>
+ document.querySelector('link[rel="manifest"]')?.getAttribute("href") || null);
+ pruefe("verwaltung.html verweist auf ihr eigenes Manifest",
+ verweis === "/webdesign/verwaltung.webmanifest", verweis);
+
+ const a = await fetch("http://127.0.0.1:4410/webdesign/verwaltung.webmanifest");
+ pruefe("Manifest ist abrufbar", a.ok, "HTTP " + a.status);
+ pruefe("und wird als Manifest ausgeliefert",
+ (a.headers.get("content-type") || "").includes("manifest"),
+ a.headers.get("content-type"));
+
+ const m = JSON.parse(await a.text());
+
+ console.log("\n2. IST ES WIRKLICH EINE ZWEITE APP?");
+ const alt = JSON.parse(fs.readFileSync("webdesign/app.webmanifest", "utf8"));
+ /* Der eine Wert, an dem alles haengt. */
+ pruefe("die Kennung unterscheidet sich vom Webdesign-Bereich",
+ m.id !== alt.id, `"${m.id}" gegen "${alt.id}"`);
+ pruefe("auch der Startpunkt ist ein anderer",
+ m.start_url !== alt.start_url, m.start_url);
+ pruefe("und der Name", m.name !== alt.name, `"${m.name}"`);
+ pruefe("Kurzname passt auf einen Startbildschirm (max. 12 Zeichen)",
+ m.short_name && m.short_name.length <= 12, `"${m.short_name}" (${m.short_name.length})`);
+
+ console.log("\n3. WAS BROWSER ZUM INSTALLIEREN VERLANGEN");
+ pruefe("Anzeigeart ist eigenstaendig", m.display === "standalone", m.display);
+ pruefe("Startadresse liegt im Geltungsbereich",
+ m.start_url.startsWith(m.scope), `${m.start_url} in ${m.scope}`);
+ pruefe("Hintergrund- und Themenfarbe gesetzt",
+ !!m.background_color && !!m.theme_color, m.theme_color);
+
+ const gross = m.icons.filter((i) => i.sizes === "512x512");
+ pruefe("es gibt ein Symbol mit 512 Punkten", gross.length > 0, gross.length + " Stueck");
+ pruefe("und eines fuers Beschneiden (maskable)",
+ m.icons.some((i) => (i.purpose || "").includes("maskable")));
+
+ console.log("\n4. GIBT ES DIE SYMBOLE AUCH WIRKLICH?");
+ for (const i of m.icons) {
+ const datei = path.join(process.cwd(), i.src);
+ const da = fs.existsSync(datei);
+ if (!da) { pruefe(` ${i.src}`, false, "fehlt"); continue; }
+ const g = masse(datei);
+ const [b, h] = i.sizes.split("x").map(Number);
+ /* Ein Symbol, dessen Masse nicht zur Angabe passen, wird verzerrt
+ dargestellt -- ohne jede Fehlermeldung. */
+ pruefe(` ${i.src}`, g && g.breite === b && g.hoehe === h,
+ g ? `${g.breite}x${g.hoehe}, ${Math.round(fs.statSync(datei).size / 1024)} KB` : "unlesbar");
+ }
+
+ console.log("\n5. DIE VERKNUEPFUNGEN ZEIGEN AUF ECHTE ZIELE");
+ for (const v of (m.shortcuts || [])) {
+ const r = await fetch("http://127.0.0.1:4410" + v.url.split("?")[0]);
+ pruefe(` ${v.short_name} -> ${v.url}`, r.ok, "HTTP " + r.status);
+ }
+
+ console.log("\n5b. TUN DIE VERKNUEPFUNGEN AUCH ETWAS?");
+ /* Dass die Adresse antwortet, heisst noch nicht, dass sie den
+ versprochenen Bereich oeffnet. Genau das war beim Bauen der Fall:
+ "?bereich=anfragen" stand in den Verknuepfungen UND in den
+ Push-Meldungen -- ausgewertet hat den Parameter niemand. Man landete
+ immer auf der Uebersicht, und nichts sah kaputt aus. */
+ for (const wunsch of ["anfragen", "projekte", "postfach"]) {
+ const p = await browser.newPage({ viewport: { width: 1280, height: 900 } });
+ await p.goto(`http://127.0.0.1:4410/webdesign/verwaltung.html?bereich=${wunsch}`);
+ await p.waitForTimeout(600);
+
+ /* Ohne Anmeldung sind die Reiter verborgen. Fuer diese Pruefung wird
+ der Bereich sichtbar geschaltet und die Auswertung angestossen --
+ geprueft wird die Auswertung, nicht die Anmeldung. */
+ const offen = await p.evaluate((w) => {
+ document.getElementById("vw-anmeldung").hidden = true;
+ document.getElementById("vw-bereich").hidden = false;
+ /* dieselbe Funktion, die die Seite nach der Anmeldung aufruft */
+ if (typeof window.vwBereichAusAdresse === "function") window.vwBereichAusAdresse();
+ const aktiv = document.querySelector("[data-tab].vw-an");
+ return aktiv ? aktiv.dataset.tab : null;
+ }, wunsch);
+
+ pruefe(` ?bereich=${wunsch} oeffnet den Reiter`, offen === wunsch,
+ offen === wunsch ? "geoeffnet" : "geoeffnet wurde: " + offen);
+ await p.close();
+ }
+
+ /* Gegenprobe: Unsinn in der Adresse darf nichts kaputtmachen. */
+ {
+ const p = await browser.newPage();
+ const fehler = [];
+ p.on("pageerror", (e) => fehler.push(String(e).slice(0, 70)));
+ await p.goto("http://127.0.0.1:4410/webdesign/verwaltung.html?bereich=");
+ await p.waitForTimeout(500);
+ await p.evaluate(() => {
+ document.getElementById("vw-bereich").hidden = false;
+ if (typeof window.vwBereichAusAdresse === "function") window.vwBereichAusAdresse();
+ });
+ pruefe(" unsinniger Wert richtet keinen Schaden an", fehler.length === 0,
+ fehler[0] || "kein Fehler");
+ await p.close();
+ }
+
+ console.log("\n6. DIE ANDERE APP BLEIBT UNBERUEHRT");
+ const b2 = await seite.goto("http://127.0.0.1:4410/webdesign/index.html");
+ await seite.waitForTimeout(500);
+ const verweis2 = await seite.evaluate(() =>
+ document.querySelector('link[rel="manifest"]')?.getAttribute("href") || null);
+ pruefe("der oeffentliche Bereich nutzt weiterhin sein eigenes",
+ verweis2 === "/webdesign/app.webmanifest", verweis2);
+
+ await browser.close();
+ srv.close();
+ console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`);
+ process.exit(fehl ? 1 : 0);
+});
diff --git a/webdesign/sw.js b/webdesign/sw.js
index 334f5778..b7851d8a 100644
--- a/webdesign/sw.js
+++ b/webdesign/sw.js
@@ -34,7 +34,7 @@
Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den
Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten
Stand fest. */
-const CACHE_NAME = "dogfather-webdesign-v58";
+const CACHE_NAME = "dogfather-webdesign-v59";
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
HTML-Datei in dieser Liste. */
diff --git a/webdesign/verwaltung.html b/webdesign/verwaltung.html
index 5d39c7cb..d957c2b0 100644
--- a/webdesign/verwaltung.html
+++ b/webdesign/verwaltung.html
@@ -12,8 +12,29 @@
-
-
+
+
+
@@ -2469,8 +2490,51 @@
function zeigeBereich() {
document.getElementById("vw-anmeldung").hidden = true;
document.getElementById("vw-bereich").hidden = false;
+ bereichAusAdresse();
}
+ /* ---------------------------------------------------------------------
+ ?bereich=... AUS DER ADRESSE ÖFFNEN
+
+ Gebraucht an zwei Stellen, die das bisher nur VERSPROCHEN haben:
+
+ - die Verknüpfungen der installierten App ("Anfragen", "Projekte",
+ "Postfach" beim langen Antippen des Symbols)
+ - der Verweis in einer Push-Meldung, der zur betreffenden Anfrage
+ führen soll
+
+ Beide zeigten auf "?bereich=anfragen" — ausgewertet hat den Parameter
+ bisher niemand. Man landete immer auf der Übersicht, ohne dass etwas
+ kaputt aussah. Genau die Sorte stiller Wirkungslosigkeit, gegen die
+ hier an anderer Stelle schon einiges gebaut wurde.
+
+ Umgesetzt über einen ausgelösten Klick auf den vorhandenen Reiter,
+ nicht über nachgebaute Logik: Am Reiter hängen Signatur, Leuchtbalken,
+ das Nachladen der Daten und das Verschieben des Wischstreifens auf dem
+ Handy. Ein zweiter Weg dorthin wäre ein zweiter Weg, der irgendwann
+ nicht mehr mitgepflegt wird.
+
+ Läuft NACH der Anmeldung, denn vorher gibt es die Reiter nicht.
+ --------------------------------------------------------------------- */
+ function bereichAusAdresse() {
+ try {
+ var wunsch = new URLSearchParams(window.location.search).get("bereich");
+ if (!wunsch) return;
+ var knopf = document.querySelector('[data-tab="' + wunsch.replace(/[^a-z]/gi, "") + '"]');
+ if (knopf && !knopf.classList.contains("vw-an")) knopf.click();
+ } catch (e) { /* alter Browser: dann eben die Übersicht */ }
+ }
+
+ /* Nach aussen verfuegbar machen — gleiches Vorgehen wie bei
+ window.vwBalkenSetzen weiter unten.
+
+ Grund ist hier die Prüfbarkeit: Der Aufruf sitzt hinter der
+ Anmeldung, und ein Test, der sich erst anmelden müsste, prüft am
+ Ende die Anmeldung statt die Sache. Ohne diesen Zugang war meine
+ eigene Prüfung wirkungslos und meldete "geöffnet wurde: uebersicht"
+ — sie hatte die Funktion schlicht nie erreicht. */
+ window.vwBereichAusAdresse = bereichAusAdresse;
+
document.getElementById("vw-login-form").addEventListener("submit", async function (e) {
e.preventDefault();
var feld = document.getElementById("vw-feld-code");
diff --git a/webdesign/verwaltung.webmanifest b/webdesign/verwaltung.webmanifest
new file mode 100644
index 00000000..94245261
--- /dev/null
+++ b/webdesign/verwaltung.webmanifest
@@ -0,0 +1,47 @@
+{
+ "name": "Dogfather Verwaltung",
+ "short_name": "Verwaltung",
+ "description": "Anfragen, Projekte, Kunden und Zahlungen an einer Stelle. Meldet sich, sobald eine neue Anfrage eingeht.",
+ "lang": "de",
+ "dir": "ltr",
+
+ "start_url": "/webdesign/verwaltung.html?start=app",
+
+ "id": "/webdesign/verwaltung",
+
+ "scope": "/webdesign/",
+
+ "display": "standalone",
+ "display_override": ["window-controls-overlay", "standalone", "minimal-ui"],
+ "orientation": "any",
+ "background_color": "#05070b",
+ "theme_color": "#05070b",
+ "categories": ["business", "productivity"],
+
+ "icons": [
+ { "src": "/assets/img/wd-admin-192.jpg", "sizes": "192x192", "type": "image/jpeg", "purpose": "any" },
+ { "src": "/assets/img/wd-admin-512.jpg", "sizes": "512x512", "type": "image/jpeg", "purpose": "any" },
+ { "src": "/assets/img/wd-admin-512-maskable.jpg", "sizes": "512x512", "type": "image/jpeg", "purpose": "maskable" }
+ ],
+
+ "shortcuts": [
+ {
+ "name": "Anfragen",
+ "short_name": "Anfragen",
+ "url": "/webdesign/verwaltung.html?bereich=anfragen",
+ "icons": [{ "src": "/assets/img/wd-admin-192.jpg", "sizes": "192x192" }]
+ },
+ {
+ "name": "Projekte",
+ "short_name": "Projekte",
+ "url": "/webdesign/verwaltung.html?bereich=projekte",
+ "icons": [{ "src": "/assets/img/wd-admin-192.jpg", "sizes": "192x192" }]
+ },
+ {
+ "name": "Postfach",
+ "short_name": "Postfach",
+ "url": "/webdesign/verwaltung.html?bereich=postfach",
+ "icons": [{ "src": "/assets/img/wd-admin-192.jpg", "sizes": "192x192" }]
+ }
+ ]
+}