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) {