diff --git a/CHANGELOG.md b/CHANGELOG.md index f96c993..e9b20e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,15 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). ### Sicherheit +## [0.22.1] – 2026-10-03 + +### Behoben +- **Die ganze Seite wurde auf dem Telefon herausgezoomt**, sobald die Warnungen geöffnet wurden oder eine Host-Detailansicht eine lange Server-ID enthielt. Ursache war jeweils ein Element breiter als das Fenster (#038) +- Die Laufzeit-Detailseite rechnete noch mit dem Listenpreis statt mit dem Betrag nach Rabatt – Marge und der Vorschlag für die Kundenrechnung waren um den Rabatt zu hoch (#037) + +### Hinzugefügt +- `npm run screenshots -- --mobil name=/pfad` für gezielte Aufnahmen in Telefonbreite (#038) + ## [0.22.0] – 2026-10-03 ### Geändert diff --git a/DEVLOG.md b/DEVLOG.md index 972b994..4e8cd68 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,7 +2,7 @@ ## Zeitübersicht -**Gesamt:** 19 h 53 min +**Gesamt:** 20 h 33 min | Issue | Zeit | |---|---| @@ -43,6 +43,7 @@ | #035 Tabellen als Karten | 0 h 50 min | | #036 Standort am Host | 0 h 30 min | | #037 Rabatt je Position | 0 h 55 min | +| #038 Überbreite auf dem Handy | 0 h 40 min | | Meilenstein | Zeit | |---|---| @@ -65,7 +66,7 @@ ## 2026-09-17 · Sitzung 1 · 19:36–laufend **Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0) -**Version:** 0.22.0 +**Version:** 0.22.1 ### Erledigt - Auftrag gelesen, Rückfragen gestellt @@ -194,6 +195,10 @@ - Florians Schilderung des Ablaufs (unbezahlte Rechnung → Kundenrechnung → Kunde zahlt → selbst zahlen → PAID-Fassung) ergab die zwölfte Warnregel - Lehre: Ein Beleg aus der Praxis sagt mehr über das Datenmodell als jede Annahme darüber, wie Rechnungen aussehen +- Überbreite auf dem Handy (#038): Drei Verursacher, ein Muster – ein Element breiter als das Fenster zoomt die ganze Seite heraus +- Dabei einen eigenen Fehler aus #037 gefunden: Die Detailseite rechnete weiter mit dem Listenpreis. Beim Ändern einer Rechengröße reicht es nicht, die Abfragen anzupassen – auch die Seiten rechnen selbst +- Das Aufnahme-Skript kann jetzt gezielt in Telefonbreite aufnehmen; damit ließ sich die Behebung belegen statt nur behaupten + ### Nächster Schritt - #016 Benutzerhandbuch mit Screenshots, danach #017 Favicons und #019 CI-Pipeline - Für 1.0.0 fehlt noch: PMG (kein Host verfügbar) und der Fall „aktive Subscription auf PBS“ (#023) diff --git a/docs/anleitung/bilder/benutzer.png b/docs/anleitung/bilder/benutzer.png index 312a7a4..a9a07b1 100644 Binary files a/docs/anleitung/bilder/benutzer.png and b/docs/anleitung/bilder/benutzer.png differ diff --git a/docs/anleitung/bilder/host-detail.png b/docs/anleitung/bilder/host-detail.png index c8dc817..f1b31f2 100644 Binary files a/docs/anleitung/bilder/host-detail.png and b/docs/anleitung/bilder/host-detail.png differ diff --git a/docs/anleitung/bilder/kunden.png b/docs/anleitung/bilder/kunden.png index 2fc37f7..270aa61 100644 Binary files a/docs/anleitung/bilder/kunden.png and b/docs/anleitung/bilder/kunden.png differ diff --git a/docs/anleitung/bilder/nicht-zugeordnet.png b/docs/anleitung/bilder/nicht-zugeordnet.png index 866504e..73f7b4a 100644 Binary files a/docs/anleitung/bilder/nicht-zugeordnet.png and b/docs/anleitung/bilder/nicht-zugeordnet.png differ diff --git a/docs/anleitung/bilder/passkeys.png b/docs/anleitung/bilder/passkeys.png index d425d0e..e0b3fa9 100644 Binary files a/docs/anleitung/bilder/passkeys.png and b/docs/anleitung/bilder/passkeys.png differ diff --git a/docs/anleitung/bilder/rechnungen.png b/docs/anleitung/bilder/rechnungen.png index 7f979ae..374f1b3 100644 Binary files a/docs/anleitung/bilder/rechnungen.png and b/docs/anleitung/bilder/rechnungen.png differ diff --git a/docs/anleitung/bilder/system.png b/docs/anleitung/bilder/system.png index d823aa9..56857a2 100644 Binary files a/docs/anleitung/bilder/system.png and b/docs/anleitung/bilder/system.png differ diff --git a/docs/anleitung/bilder/tokens.png b/docs/anleitung/bilder/tokens.png index 41da4a4..652df75 100644 Binary files a/docs/anleitung/bilder/tokens.png and b/docs/anleitung/bilder/tokens.png differ diff --git a/docs/anleitung/bilder/warnregeln.png b/docs/anleitung/bilder/warnregeln.png index 08d1c3e..ada45fc 100644 Binary files a/docs/anleitung/bilder/warnregeln.png and b/docs/anleitung/bilder/warnregeln.png differ diff --git a/docs/issues/038-ueberbreite-handy.md b/docs/issues/038-ueberbreite-handy.md new file mode 100644 index 0000000..f93258a --- /dev/null +++ b/docs/issues/038-ueberbreite-handy.md @@ -0,0 +1,31 @@ +# #038 Seite wird auf dem Handy herausgezoomt +**Status:** erledigt +**Meilenstein:** v0.22.1 +**Labels:** bug, ui +**Geschätzt:** 2 h **Tatsächlich:** 0 h 40 min + +## Auslöser +Zwei Meldungen aus dem Einsatz: Beim Öffnen der Warnungen verkleinerte sich die ganze Seite, und die +Host-Detailansicht saß verschoben im Bild. + +## Ursache +Beides derselbe Mechanismus: Ein Element ist breiter als das Fenster, also zoomt der Browser die +**ganze Seite** heraus, damit sie hineinpasst. Drei Verursacher: + +1. Die Klappliste der Warnungen war mit 22 rem fest breiter als ein 390-px-Fenster. +2. Drei Tabellen in der Host-Detailansicht (Keys, Server-IDs, Tokens) standen ohne + `table-responsive` – anders als die übrigen. +3. Eine 32-stellige Server-ID bricht nicht von selbst um. + +## Behebung +- Klapplisten richten sich auf dem Telefon am Fenster aus (`position: fixed`, links und rechts 0,5 rem, + höchstens 70 % Fensterhöhe mit eigenem Scroll) statt am Knopf +- Die drei Tabellen bekommen ihre Hülle +- Lange Schlüssel und IDs dürfen umbrechen (`overflow-wrap: anywhere`) +- `npm run screenshots -- --mobil name=/pfad` nimmt gezielt eine Seite in Telefonbreite auf; damit + wurde die Host-Detailansicht gegengeprüft + +## Nachtrag zu #037 +Beim Prüfen fiel auf, dass die Laufzeit-Detailseite noch mit dem **Listenpreis** rechnete statt mit +dem Betrag nach Rabatt – Marge und Vorschlag für die Kundenrechnung waren dort um den Rabatt zu hoch. +Die Seite zeigt jetzt „990,00 € · 1.100,00 € − 110,00 € Rabatt“ und rechnet mit 990. diff --git a/docs/issues/README.md b/docs/issues/README.md index 41e8747..82cf4c7 100644 --- a/docs/issues/README.md +++ b/docs/issues/README.md @@ -41,4 +41,5 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a | [#035](035-mobile-tabellen.md) | Tabellen auf dem Handy als Karten | erledigt | v0.21.0 | 3 h | 0 h 50 min | | [#036](036-standort-am-host.md) | Standort am Host setzen | erledigt | v0.21.0 | 1 h | 0 h 30 min | | [#037](037-rabatt-und-ablauf.md) | Rabatt je Position und der tatsächliche Ablauf | erledigt | v0.22.0 | 3 h | 0 h 55 min | -| | **Summe** | | | **160 h** | **19 h 53 min** | +| [#038](038-ueberbreite-handy.md) | Seite wird auf dem Handy herausgezoomt | erledigt | v0.22.1 | 2 h | 0 h 40 min | +| | **Summe** | | | **162 h** | **20 h 33 min** | diff --git a/package.json b/package.json index d929da0..fe01a3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxledger", - "version": "0.22.0", + "version": "0.22.1", "private": true, "scripts": { "dev": "next dev", diff --git a/scripts/screenshots.mjs b/scripts/screenshots.mjs index 6f9ccd8..6a1f639 100644 --- a/scripts/screenshots.mjs +++ b/scripts/screenshots.mjs @@ -25,6 +25,14 @@ const BASIS = process.env.SCREENSHOT_URL ?? "http://localhost:3000"; const ZIEL = path.join(process.cwd(), "docs", "anleitung", "bilder"); const ALLE = process.argv.includes("--alle"); +/** Zusätzliche Handy-Aufnahmen von der Befehlszeile: --mobil name=/pfad */ +const MOBIL_ZUSATZ = process.argv + .filter((wert, i) => process.argv[i - 1] === "--mobil") + .map((eintrag) => { + const [name, ...rest] = eintrag.split("="); + return { name, pfad: rest.join("="), titel: "Handy" }; + }); + /** Zusätzliche Seiten von der Befehlszeile: --seite name=/pfad */ const ZUSATZ = process.argv .filter((wert, i) => process.argv[i - 1] === "--seite") @@ -187,6 +195,11 @@ async function main() { console.log(` ${seite.name}.png – ${seite.titel}`); } + for (const seite of MOBIL_ZUSATZ) { + await aufnehmen(hell, seite.pfad, path.join(ZIEL, `${seite.name}.png`), 390, 1800, adresse); + console.log(` ${seite.name}.png – ${seite.titel}`); + } + if (ALLE) { const dunkel = profilVorbereiten("dunkel", true); for (const seite of ["dashboard", "hosts", "rechnungen", "warnungen"]) { diff --git a/src/app/(dashboard)/hosts/[id]/page.tsx b/src/app/(dashboard)/hosts/[id]/page.tsx index c082e3b..19c400e 100644 --- a/src/app/(dashboard)/hosts/[id]/page.tsx +++ b/src/app/(dashboard)/hosts/[id]/page.tsx @@ -225,62 +225,66 @@ export default async function HostDetailSeite({ params }: { params: Promise<{ id inhalt: ( <>

Keys

- - - - - - - - - - {host.keyHistorie.map((eintrag) => ( - - - - - - ))} - {host.keyHistorie.length === 0 ? ( +
+
KeyErstmals gemeldetZuletzt gemeldet
- - {formatDatum(eintrag.ersteMeldung)}{formatDatum(eintrag.letzteMeldung)}
+ - + + + - ) : null} - -
- Noch kein Key gemeldet. - KeyErstmals gemeldetZuletzt gemeldet
+ + + {host.keyHistorie.map((eintrag) => ( + + + + + {formatDatum(eintrag.ersteMeldung)} + {formatDatum(eintrag.letzteMeldung)} + + ))} + {host.keyHistorie.length === 0 ? ( + + + Noch kein Key gemeldet. + + + ) : null} + + +

Server-IDs

- - - - - - - - - - {host.serverIdHistorie.map((eintrag) => ( - - - - - - ))} - {host.serverIdHistorie.length === 0 ? ( +
+
Server-IDErstmals gemeldetZuletzt gemeldet
- - {formatDatum(eintrag.ersteMeldung)}{formatDatum(eintrag.letzteMeldung)}
+ - + + + - ) : null} - -
- Noch keine Server-ID gemeldet. - Server-IDErstmals gemeldetZuletzt gemeldet
+ + + {host.serverIdHistorie.map((eintrag) => ( + + + + + {formatDatum(eintrag.ersteMeldung)} + {formatDatum(eintrag.letzteMeldung)} + + ))} + {host.serverIdHistorie.length === 0 ? ( + + + Noch keine Server-ID gemeldet. + + + ) : null} + + + {host.serverIdHistorie.length > 1 ? (

Mehrere Server-IDs bedeuten Hardwaretausch oder Neuinstallation – der Key muss @@ -429,53 +433,55 @@ export default async function HostDetailSeite({ params }: { params: Promise<{ id titel: "Tokens", icon: "bi-key-fill", inhalt: ( - - - - - - - - - - - - {host.tokens.map((token) => ( - - - - - - - - ))} - {host.tokens.length === 0 ? ( +
+
AnfangErstelltZuletzt benutztZustand - Aktion -
{token.praefix}…{formatDatum(token.erstelltAm)}{formatDatumZeit(token.letzteNutzungAm)} - {token.widerrufenAm ? ( - Widerrufen - ) : ( - Aktiv - )} - - {token.widerrufenAm ? null : ( -
- - - -
- )} -
+ - + + + + + - ) : null} - -
- Kein Token vorhanden – dieser Host stammt aus den Demo-Daten. - AnfangErstelltZuletzt benutztZustand + Aktion +
+ + + {host.tokens.map((token) => ( + + {token.praefix}… + {formatDatum(token.erstelltAm)} + {formatDatumZeit(token.letzteNutzungAm)} + + {token.widerrufenAm ? ( + Widerrufen + ) : ( + Aktiv + )} + + + {token.widerrufenAm ? null : ( +

+ + + +
+ )} + + + ))} + {host.tokens.length === 0 ? ( + + + Kein Token vorhanden – dieser Host stammt aus den Demo-Daten. + + + ) : null} + + + ), }, ]} diff --git a/src/app/(dashboard)/rechnungen/[id]/page.tsx b/src/app/(dashboard)/rechnungen/[id]/page.tsx index 1570e2d..b378d9f 100644 --- a/src/app/(dashboard)/rechnungen/[id]/page.tsx +++ b/src/app/(dashboard)/rechnungen/[id]/page.tsx @@ -25,7 +25,10 @@ export default async function LaufzeitSeite({ params }: { params: Promise<{ id: const einkauf = laufzeit.proxmoxPosition?.rechnung ?? null; const verkauf = laufzeit.kundenPosition?.rechnung ?? null; - const einkaufBetrag = laufzeit.proxmoxPosition ? Number(laufzeit.proxmoxPosition.nettoBetrag) : null; + // Gerechnet wird mit dem Betrag nach Abzug des Rabatts – das ist, was tatsächlich gezahlt wird (#037). + const einkaufListenpreis = laufzeit.proxmoxPosition ? Number(laufzeit.proxmoxPosition.nettoBetrag) : null; + const einkaufRabatt = Number(laufzeit.proxmoxPosition?.rabatt ?? 0); + const einkaufBetrag = einkaufListenpreis === null ? null : einkaufListenpreis - einkaufRabatt; const verkaufBetrag = laufzeit.kundenPosition ? Number(laufzeit.kundenPosition.nettoBetrag) : null; const marge = einkaufBetrag !== null && verkaufBetrag !== null ? verkaufBetrag - einkaufBetrag : null; @@ -109,7 +112,14 @@ export default async function LaufzeitSeite({ params }: { params: Promise<{ id:
Einkauf netto
-
{formatEuro(einkaufBetrag)}
+
+ {formatEuro(einkaufBetrag)} + {einkaufRabatt > 0 ? ( + + {formatEuro(einkaufListenpreis)} − {formatEuro(einkaufRabatt)} Rabatt + + ) : null} +
Verkauf netto
{formatEuro(verkaufBetrag)}
Marge
@@ -164,7 +174,7 @@ export default async function LaufzeitSeite({ params }: { params: Promise<{ id: nummer: einkauf?.nummer ?? null, datum: einkauf?.datum.toISOString() ?? null, faelligAm: einkauf?.faelligAm?.toISOString() ?? faelligVorschlag, - nettoBetrag: einkaufBetrag, + nettoBetrag: einkaufListenpreis, ustSatz: Number(einkauf?.ustSatz ?? vorgaben.steuersatz), rabatt: laufzeit.proxmoxPosition?.rabatt === null || diff --git a/src/app/globals.css b/src/app/globals.css index ee7b97f..31587ef 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -395,6 +395,12 @@ body { width: 13rem; } +/* Klapplisten in der Kopfzeile (Warnungen, Benutzermenü). */ +.pl-klappliste { + width: 22rem; + max-width: calc(100vw - 1rem); +} + /* ---------- Mobil ---------- */ @media (max-width: 575.98px) { @@ -425,6 +431,30 @@ body { * Zellen ohne `data-label` stehen für sich – etwa eine Zeile mit Knöpfen. */ @media (max-width: 767.98px) { + /* + * Auf dem Telefon ist die Klappliste breiter als das Fenster – der Browser zoomt dann die + * **ganze Seite** heraus, damit sie hineinpasst. Deshalb hier fest am Fenster ausrichten + * statt am Knopf (#038). + */ + .pl-klappliste { + position: fixed; + top: 3.5rem; + right: 0.5rem; + left: 0.5rem; + width: auto; + max-width: none; + max-height: 70vh; + overflow-y: auto; + } + + /* + * Lange Schlüssel und Server-IDs dürfen umbrechen. Sonst machen sie die Seite breiter als das + * Fenster – und der Browser zoomt die ganze Seite heraus, damit sie hineinpasst (#038). + */ + .pl-mono { + overflow-wrap: anywhere; + } + .pl-karten-tabelle, .pl-karten-tabelle tbody, .pl-karten-tabelle tr, diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 99a895c..7fa3d09 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -140,8 +140,7 @@ export function Header({ ) : null}
{alerts.length} offene Warnungen
@@ -206,7 +205,9 @@ export function Header({