diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ffd3fb..e9f7c15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). ### Sicherheit +## [0.12.1] – 2026-09-17 + +### Hinzugefügt +- Symbole für Browser-Tab (SVG und ICO), iOS-Startbildschirm und Web-Manifest, erzeugt mit `npm run icons` aus den SVG-Vorlagen in `assets/` (#017) + ## [0.12.0] – 2026-09-17 ### Hinzugefügt diff --git a/DEVLOG.md b/DEVLOG.md index a7fe2f5..6533345 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,7 +2,7 @@ ## Zeitübersicht -**Gesamt:** 7 h 08 min +**Gesamt:** 7 h 23 min | Issue | Zeit | |---|---| @@ -23,6 +23,7 @@ | #014 Deployment | 0 h 40 min | | #015 Feinschliff | 0 h 35 min | | #016 Handbuch | 0 h 30 min | +| #017 Favicons | 0 h 15 min | | Meilenstein | Zeit | |---|---| @@ -45,7 +46,7 @@ ## 2026-09-17 · Sitzung 1 · 19:36–laufend **Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0) -**Version:** 0.12.0 +**Version:** 0.12.1 ### Erledigt - Auftrag gelesen, Rückfragen gestellt @@ -88,6 +89,7 @@ - Als v0.11.0 veröffentlicht statt 1.0.0: Feldtest, Handbuch, Favicons, CI und Verschlüsselung der Zugangsdaten stehen noch aus - 0.11.1: Anmeldefehler behoben (Secure-Cookie über http) - #016 abgeschlossen: Handbuch mit elf Kapiteln und 16 Bildern, dazu `npm run screenshots` zum Auffrischen +- #017 abgeschlossen: Symbole für Tab, Startbildschirm und Manifest, erzeugt mit `npm run icons` aus SVG-Vorlagen ### Entscheidungen - **Warnungen sammelweise anlegen:** Einzelne Upserts kosteten bei 737 neuen Warnungen 3,8 s; mit `createMany` sind es 0,3 s. diff --git a/assets/logo-16.svg b/assets/logo-16.svg new file mode 100644 index 0000000..9af2ead --- /dev/null +++ b/assets/logo-16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/logo-32.svg b/assets/logo-32.svg new file mode 100644 index 0000000..6527c00 --- /dev/null +++ b/assets/logo-32.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/logo-kachel.svg b/assets/logo-kachel.svg new file mode 100644 index 0000000..bb61aa1 --- /dev/null +++ b/assets/logo-kachel.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/logo.svg b/assets/logo.svg new file mode 100644 index 0000000..b805a6b --- /dev/null +++ b/assets/logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/issues/017-favicon-dateien.md b/docs/issues/017-favicon-dateien.md index 7900508..6e70bc7 100644 --- a/docs/issues/017-favicon-dateien.md +++ b/docs/issues/017-favicon-dateien.md @@ -1,23 +1,23 @@ # #017 Favicon-Dateien erzeugen -**Status:** offen -**Meilenstein:** v0.6.0 +**Status:** erledigt +**Meilenstein:** v0.12.1 **Labels:** ui -**Geschätzt:** 1 h **Tatsächlich:** – +**Geschätzt:** 1 h **Tatsächlich:** 0 h 15 min ## Ziel Das Monogramm liegt als React-Komponente vor. Es fehlen die abgeleiteten Dateien für Browser-Tab, Startbildschirm und E-Mails. ## Aufgaben -- [ ] `favicon.svg` (32-px-Variante mit einer Linie) unter `src/app/icon.svg` -- [ ] `favicon.ico` und PNG-Größen (16, 32, 180, 192, 512) erzeugen -- [ ] `apple-touch-icon` über `apple-icon.tsx` (ImageResponse) oder als PNG -- [ ] Erzeugung als Skript (`npm run icons`) festhalten, damit Änderungen am Logo nachziehbar sind -- [ ] Einfarbige Variante für PDF-Export und Rechnungen bereitstellen +- [x] `favicon.svg` (32-px-Variante mit einer Linie) unter `src/app/icon.svg` +- [x] `favicon.ico` und PNG-Größen (16, 32, 180, 192, 512) erzeugen +- [x] `apple-touch-icon` über `apple-icon.tsx` (ImageResponse) oder als PNG +- [x] Erzeugung als Skript (`npm run icons`) festhalten, damit Änderungen am Logo nachziehbar sind +- [x] Einfarbige Variante für PDF-Export und Rechnungen bereitstellen ## Akzeptanzkriterien -- [ ] Browser-Tab zeigt das Monogramm in Hell und Dunkel -- [ ] Beim Ablegen auf dem iOS-Startbildschirm erscheint das Logo ohne weißen Rand +- [x] Browser-Tab zeigt das Monogramm in Hell und Dunkel +- [x] Beim Ablegen auf dem iOS-Startbildschirm erscheint das Logo ohne weißen Rand ## Abhängigkeiten #002 @@ -25,3 +25,13 @@ Startbildschirm und E-Mails. ## Notizen / Entscheidungen - Auf dem Entwicklungsrechner ist kein SVG-Konverter installiert (kein rsvg-convert, kein ImageMagick). Entweder `sharp` als Entwicklungsabhängigkeit aufnehmen oder Next.js `ImageResponse` nutzen. +- Die SVG-Vorlagen liegen in `assets/`; `npm run icons` erzeugt daraus alle Dateien. Für 16 und 32 Pixel + kommen die vereinfachten Fassungen zum Einsatz (nur das P bzw. eine Linie), sonst verschwimmen die feinen + Linien. +- `favicon.ico` wird im Skript selbst zusammengesetzt (Kopf, Verzeichnis, PNG-Daten) – dafür braucht es + kein weiteres Werkzeug. +- Die Symbole für den Startbildschirm stehen auf dunklem Grund mit abgerundeten Ecken, weil iOS sonst + einen weißen Kasten zeigt. +- Dazu ein `manifest.webmanifest`; Symbole und Manifest sind bewusst ohne Anmeldung erreichbar, weil der + Browser sie vor der Anmeldung lädt. +- Die einfarbige Fassung für PDF und Rechnungen steckt bereits in der Logo-Komponente (Variante `mono`). diff --git a/docs/issues/README.md b/docs/issues/README.md index 6a9b768..959b30b 100644 --- a/docs/issues/README.md +++ b/docs/issues/README.md @@ -20,9 +20,9 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a | [#014](014-deployment.md) | Deployment und Dokumentation | erledigt | v0.10.0 | 8 h | 0 h 40 min | | [#015](015-feinschliff-v1.md) | Feinschliff für 1.0 | erledigt | v0.11.0 | 6 h | 0 h 35 min | | [#016](016-anleitung-screenshots.md) | Benutzerhandbuch mit Screenshots | erledigt | v0.12.0 | 5 h | 0 h 30 min | -| [#017](017-favicon-dateien.md) | Favicon-Dateien erzeugen | in Arbeit | v0.6.0 | 1 h | – | +| [#017](017-favicon-dateien.md) | Favicon-Dateien erzeugen | erledigt | v0.12.1 | 1 h | 0 h 15 min | | [#018](018-agent-feldtest.md) | Agent auf echten Proxmox-Hosts prüfen | offen | v0.2.1 | 2 h | – | -| [#019](019-ci-pipeline.md) | CI-Pipeline | offen | v0.10.0 | 3 h | – | +| [#019](019-ci-pipeline.md) | CI-Pipeline | in Arbeit | v0.10.0 | 3 h | – | | [#020](020-gitea-issue-abgleich.md) | Issues nach Gitea übertragen | erledigt | v0.4.1 | 2 h | 0 h 10 min | | [#021](021-geheimnisse-verschluesseln.md) | Zugangsdaten verschlüsselt speichern | offen | v0.9.0 | 2 h | – | -| | **Summe** | | | **116 h** | **7 h 08 min** | +| | **Summe** | | | **116 h** | **7 h 23 min** | diff --git a/package-lock.json b/package-lock.json index e6210bb..e3faa79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "proxledger", - "version": "0.9.0", + "version": "0.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "proxledger", - "version": "0.9.0", + "version": "0.12.0", "hasInstallScript": true, "dependencies": { "@node-rs/argon2": "^2.2.1", @@ -37,6 +37,7 @@ "eslint-config-prettier": "^10.1.8", "prettier": "^3.9.7", "prisma": "^7.10.0", + "sharp": "^0.35.4", "tsx": "^4.23.13", "typescript": "^5", "vitest": "^5.0.1" @@ -1013,8 +1014,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=18" } @@ -9117,8 +9118,8 @@ "version": "0.35.4", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "devOptional": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", @@ -9167,8 +9168,8 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "devOptional": true, "license": "ISC", - "optional": true, "bin": { "semver": "bin/semver.js" }, diff --git a/package.json b/package.json index 510d08f..7865aff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxledger", - "version": "0.12.0", + "version": "0.12.1", "private": true, "scripts": { "dev": "next dev", @@ -24,7 +24,8 @@ "build:worker": "esbuild src/worker/index.ts --bundle --platform=node --target=node24 --format=esm --outfile=dist/worker.mjs --banner:js=\"$(cat deploy/worker-banner.js)\" --external:@prisma/client --external:@node-rs/argon2 --external:pg-native", "build:all": "npm run build && npm run build:worker", "lasttest": "tsx scripts/lasttest.ts", - "screenshots": "node scripts/screenshots.mjs" + "screenshots": "node scripts/screenshots.mjs", + "icons": "node scripts/icons.mjs" }, "dependencies": { "@node-rs/argon2": "^2.2.1", @@ -55,6 +56,7 @@ "eslint-config-prettier": "^10.1.8", "prettier": "^3.9.7", "prisma": "^7.10.0", + "sharp": "^0.35.4", "tsx": "^4.23.13", "typescript": "^5", "vitest": "^5.0.1" diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000..f2ec82c Binary files /dev/null and b/public/icon-192.png differ diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000..c352ac4 Binary files /dev/null and b/public/icon-512.png differ diff --git a/scripts/icons.mjs b/scripts/icons.mjs new file mode 100644 index 0000000..1f000c4 --- /dev/null +++ b/scripts/icons.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node +/** + * Erzeugt alle Symboldateien aus den SVG-Vorlagen in `assets/`. + * + * npm run icons + * + * Ergebnis: + * src/app/icon.svg – Symbol im Browser-Tab (moderne Browser) + * src/app/favicon.ico – Rückfall für ältere Browser (16, 32, 48 px) + * public/icon-192.png – Startbildschirm und Fortschrittsanzeigen + * public/icon-512.png + * src/app/apple-icon.png – iOS-Startbildschirm (180 px, mit Hintergrund) + * + * Die Größen 16 und 32 nutzen vereinfachte Vorlagen, weil die feinen Linien sonst verschwimmen. + */ + +import { readFile, writeFile, mkdir } from "node:fs/promises"; +import path from "node:path"; +import sharp from "sharp"; + +const WURZEL = process.cwd(); +const ASSETS = path.join(WURZEL, "assets"); + +async function png(vorlage, groesse) { + const svg = await readFile(path.join(ASSETS, vorlage)); + return sharp(svg, { density: 384 }).resize(groesse, groesse).png({ compressionLevel: 9 }).toBuffer(); +} + +/** + * Baut eine ICO-Datei aus PNG-Bildern. Das Format ist schlicht: ein Kopf, je Bild ein + * Verzeichniseintrag, danach die Bilddaten – PNG als Inhalt ist seit Vista erlaubt. + */ +function icoBauen(bilder) { + const kopf = Buffer.alloc(6); + kopf.writeUInt16LE(0, 0); // reserviert + kopf.writeUInt16LE(1, 2); // Typ 1 = Symbol + kopf.writeUInt16LE(bilder.length, 4); + + let versatz = 6 + bilder.length * 16; + const eintraege = []; + for (const bild of bilder) { + const eintrag = Buffer.alloc(16); + eintrag.writeUInt8(bild.groesse >= 256 ? 0 : bild.groesse, 0); // Breite (0 = 256) + eintrag.writeUInt8(bild.groesse >= 256 ? 0 : bild.groesse, 1); // Höhe + eintrag.writeUInt8(0, 2); // Farben in der Palette + eintrag.writeUInt8(0, 3); // reserviert + eintrag.writeUInt16LE(1, 4); // Farbebenen + eintrag.writeUInt16LE(32, 6); // Bits je Bildpunkt + eintrag.writeUInt32LE(bild.daten.length, 8); + eintrag.writeUInt32LE(versatz, 12); + eintraege.push(eintrag); + versatz += bild.daten.length; + } + + return Buffer.concat([kopf, ...eintraege, ...bilder.map((b) => b.daten)]); +} + +async function main() { + await mkdir(path.join(WURZEL, "public"), { recursive: true }); + + // Tab-Symbol: das SVG selbst, damit es in jeder Größe scharf bleibt. + const svg = await readFile(path.join(ASSETS, "logo-32.svg")); + await writeFile(path.join(WURZEL, "src", "app", "icon.svg"), svg); + console.log(" src/app/icon.svg"); + + const ico = icoBauen([ + { groesse: 16, daten: await png("logo-16.svg", 16) }, + { groesse: 32, daten: await png("logo-32.svg", 32) }, + { groesse: 48, daten: await png("logo-32.svg", 48) }, + ]); + await writeFile(path.join(WURZEL, "src", "app", "favicon.ico"), ico); + console.log(` src/app/favicon.ico (${(ico.length / 1024).toFixed(1)} KB)`); + + for (const groesse of [192, 512]) { + const daten = await png("logo-kachel.svg", groesse); + await writeFile(path.join(WURZEL, "public", `icon-${groesse}.png`), daten); + console.log(` public/icon-${groesse}.png`); + } + + const apple = await png("logo-kachel.svg", 180); + await writeFile(path.join(WURZEL, "src", "app", "apple-icon.png"), apple); + console.log(" src/app/apple-icon.png"); +} + +console.log("Erzeuge Symbole aus assets/:"); +main() + .then(() => console.log("Fertig.")) + .catch((fehler) => { + console.error(fehler.message ?? fehler); + process.exit(1); + }); diff --git a/src/app/apple-icon.png b/src/app/apple-icon.png new file mode 100644 index 0000000..8d2bb8e Binary files /dev/null and b/src/app/apple-icon.png differ diff --git a/src/app/favicon.ico b/src/app/favicon.ico new file mode 100644 index 0000000..56ca26b Binary files /dev/null and b/src/app/favicon.ico differ diff --git a/src/app/icon.svg b/src/app/icon.svg new file mode 100644 index 0000000..6527c00 --- /dev/null +++ b/src/app/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/manifest.ts b/src/app/manifest.ts new file mode 100644 index 0000000..6ab25d4 --- /dev/null +++ b/src/app/manifest.ts @@ -0,0 +1,19 @@ +import type { MetadataRoute } from "next"; + +/** Angaben für den Startbildschirm, wenn das Dashboard als Verknüpfung abgelegt wird. */ +export default function manifest(): MetadataRoute.Manifest { + return { + name: "ProxLedger – Proxmox Subscription Dashboard", + short_name: "ProxLedger", + description: "Subscription-Status aller Proxmox-Hosts im Blick", + lang: "de", + start_url: "/", + display: "standalone", + background_color: "#F4F6F9", + theme_color: "#1E2A44", + icons: [ + { src: "/icon-192.png", sizes: "192x192", type: "image/png" }, + { src: "/icon-512.png", sizes: "512x512", type: "image/png", purpose: "any" }, + ], + }; +} diff --git a/src/proxy.ts b/src/proxy.ts index 95c747c..1e21023 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -14,8 +14,13 @@ const OEFFENTLICH = [ "/api/gesundheit", "/install.sh", "/agent.sh", + // Symbole und Manifest: Der Browser holt sie auch vor der Anmeldung. "/favicon.ico", "/icon.svg", + "/apple-icon.png", + "/icon-192.png", + "/icon-512.png", + "/manifest.webmanifest", ]; export async function proxy(request: NextRequest) {