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" }] + } + ] +}