Neues Feld EnergyContractDetails.noBonusDesired (Boolean, default false) +
Migration. Checkbox im Vertragsformular (Strom/Gas, bei den Bonus-Feldern),
Anzeige im Vertragsdetail wenn gesetzt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MaLo-ID (Marktlokation) gehoert zur (Liefer-)Adresse, nicht zum Vertrag.
Address bekommt maloIdElectricity + maloIdGas (getrennte Marktlokationen je
Sparte), pflegbar im AddressModal (nur Lieferadresse). Im Vertrag ist die
MaLo-ID jetzt ein Lesefeld, das je nach Vertragstyp die MaLo der gewaehlten
Lieferadresse zeigt; ContractDetail/-Modal ebenso.
Schema + Migration 20260814100000: 2 Spalten (idempotent) + Daten-Migration
(bestehende EnergyContractDetails.maloId -> jeweilige Lieferadresse,
ELECTRICITY->maloIdElectricity / GAS->maloIdGas). Migrationslogik verifiziert.
Dabei einen selbst verursachten Regressions-Bug gefixt: beim R156-Umbau waren
die 10 owner*-Adressfelder aus der Address-Whitelist gefallen -> Eigentuemer-
Sektion speicherte seit cb21a2c nicht mehr. Address-Whitelist jetzt via
Pick-Helper, programmatisch gegen alle DB-Spalten abgeglichen (owner* + MaLo
drin, id/customerId/Timestamps raus). BankCard/Document gegengeprueft: ok
(nur documentPath bewusst upload-only ausgeschlossen).
Verifiziert: tsc+build gruen; owner + maloId speichern wieder, Injection
(id/customerId) blockiert; Daten-Migration Strom->Strom / Gas->Gas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Schema: BankCard.cardNumber (String?, optional) + Migration
20260813100000_bank_card_number (ADD COLUMN IF NOT EXISTS), auf Dev
angewandt + prisma generate; Prod via migrate deploy. Eingabefeld
"Kartennummer" im Bankkarten-Modal (Kundenakte).
Vertragsansicht (ContractDetail) und Vertrag bearbeiten (ContractForm)
zeigen jetzt bei Bankkarte zusaetzlich BIC/Bank/Kartennummer/Ablaufdatum
und bei Ausweis Behoerde/Ausstellung/Ablaufdatum sowie Geburtsort +
Geburtsdatum des Kunden - jeweils mit Copy-Button und nur wenn gesetzt.
Im Form je Select in eigenem div gewrappt (Grid-Alignment).
Verifiziert: tsc + vite build gruen, cardNumber Round-Trip (update->read),
Contract-Include liefert alle Felder inkl. customer.birthDate/birthPlace.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neue Komponente PdfViewerModal (Modal + iframe auf viewUrl(documentPath),
inkl. Neuer-Tab/Download-Links). Buttons:
- Vertragsansicht (ContractDetail): im Card-Header von Bankkarte/Ausweis
- Vertrag bearbeiten (ContractForm): neben dem Label Bankkarte/Ausweis
Button erscheint NUR wenn ein documentPath hinterlegt/gewaehlt ist. Nutzt den
bestehenden /api/files/download-Endpoint (Per-File-Ownership-Check) - kein
neuer Zugriffspfad. iframe (nicht embed/object wegen object-src 'none');
same-origin via CSP default-src 'self' erlaubt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der vorige Commit ergaenzte nur die Baum-Render-Pfade (Kundenakte + Portal).
Die Staff-Ansicht unter /contracts rendert aber eine eigene Tabelle aus der
flachen getAll-Liste - dort fehlten die neuen Felder noch. Jetzt zeigt auch
die Staff-Tabelle Karteninhaber + Netz hinter der Rufnummer und die
Kuendigungsbestaetigung in roter Schrift. Backend liefert die Felder via
getAllContracts bereits mit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Beide Vertragslisten (Kundenakte-Baum + Hauptmenue /contracts) zeigen bei
Mobilfunkvertraegen mit Rufnummer jetzt zusaetzlich Karteninhaber
(SimCard.cardUser der angezeigten SIM, nur wenn gesetzt) und Netz
(mobileNetwork -> Telekom/Vodafone/Telefonica (o2)) inline. Vertraege mit
Kuendigungsbestaetigung (cancellationConfirmationDate) bekommen eine eigene
Zeile in roter, fetter Schrift.
Backend: getContractTreeForCustomer + getAllContracts selektieren nun
mobileNetwork + cardUser (Tree zusaetzlich cancellationConfirmationDate).
Shared-Helper getContractTypeInfo um cardUser/network + mobileNetworkLabel
erweitert. Beide Listen rendern ueber den Tree-Endpoint.
Verifiziert: tsc + vite build gruen, Tree-Service liefert die Felder,
Helper-Mapping getestet (Vodafone/Telekom/o2, leerer Karteninhaber ausgelassen).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Die Modals in der Kundenakte (Bankkarte, Adresse, Ausweis, Zaehler,
Zaehlerstand) bleiben dauerhaft gemountet und wurden nur ueber isOpen
umgeschaltet. Der Reset-Effekt haengte an <entity>?.id - bei Neuanlage
immer undefined, also kein Reset beim erneuten Oeffnen: nach "Abbrechen"
standen die vorher getippten Daten noch drin.
Jetzt Reset beim Oeffnen (Guard if(!isOpen), Deps [isOpen, <entity>?.id]),
weiterhin kein Reset bei jedem Tastendruck. Ausserdem den fehlerhaften
useState(()=>{})-Init-Missbrauch im Bankkarten-Modal entfernt.
StressfreiEmail-/AdditionalForwards-Modal waren bereits korrekt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Statt npm-Rebuild-Wartung aktualisiert sich der Bankleitzahlen-Datensatz
jetzt zur Laufzeit. Ein Scheduler (taeglich 03:30 + Catch-up 90s) prueft
gemaess konfigurierbarem Intervall und laedt current.json/next.json von
npm/jsDelivr (Paket bankdata-germany) in das neue Bind-Mount-Volume
BANKDATA_DIR (./data/bankdata -> /app/bankdata).
Lookup bevorzugt den Volume-Datensatz vor den ins Image gebackenen Daten
(Fallback). Es wird kein Fremdcode ausgefuehrt - nur JSON gelesen und die
current+next-Delta-Logik nachgebaut. Validierung (>=1000 Eintraege, Format)
+ atomarer Write (tmp+rename) schuetzen den guten Stand vor Muell.
Datenschutz: Der Updater sendet keine Kundendaten, laedt nur eine
oeffentliche Datendatei; abschaltbar; bei Fehler/ohne Egress greift Builtin.
Neue Einstellungen-Seite /settings/bank-data zeigt Datenstand, Update-
Verfuegbarkeit und bietet "Jetzt aktualisieren" + Auto-Update-Schalter +
Intervall. Endpoints GET /api/settings/blz, POST /api/settings/blz/update-now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neuer Button "BIC & Bank aus IBAN abrufen" im Bankkarten-Modal fuellt BIC +
Banknamen automatisch und validiert dabei die IBAN-Pruefziffer (mod-97).
Leeres IBAN-Feld -> OK-Messagebox statt Anfrage.
Datenschutzfreundlich/offline: kein Dritt-Dienst. Nachschlag im eigenen
Backend ueber die Bundesbank-Bankleitzahlendatei (bankdata-germany) +
ibantools fuer die Pruefziffer. Die IBAN verlaesst nie den Server; zurueck
kommen nur oeffentliche Bankverzeichnis-Daten.
Endpoint: POST /api/bank-cards/iban-lookup (nur eingeloggt).
Wartung: bankdata-germany/ibantools ~quartalsweise per npm update ziehen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der Hinweis "Neue Version verfuegbar" scrollte bisher weg. Jetzt sticky an
der Viewport-Oberkante; das Banner meldet seine gemessene Hoehe als CSS-Var
--app-banner-h, an der sich die Sticky-Header von ContractDetail/ContractForm
ausrichten (top-[var(--app-banner-h,0px)]), damit sie nicht dahinter
verschwinden.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neuer Menuepunkt 'Gutschriften' -> Seite /credit-notes mit Tabelle
aller Belege (Beleg-Nr, Art, Kunde, Vertrag, Betrag, Datum, PDF),
Suche + Pagination.
Neuer Endpoint GET /credit-notes (NICHT staff-only wie die uebrigen
Credit-Note-Endpoints): Staff sieht alle Belege aller Kunden, Portal-
Kunden nur eigene + vertretene (Vollmacht via hasAuthorization).
customerIds kommt aus dem JWT, nicht aus Query/Body -> nicht
manipulierbar. Fuer Portal wird receiptPath aus der Response entfernt
(Belege bleiben staff-only). Route requirePermission contracts:read.
Verifiziert: Staff -> alle Belege; Portal-scoped -> nur eigene, korrekt
zugeordnet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Eine Gutschrift/ein Lieferschein braucht eine Empfaengeradresse aufs
Dokument. Rechnungsadresse hat Vorrang, sonst Lieferadresse. Ist keine
von beiden hinterlegt -> Anlegen blockiert.
- Frontend: Klick auf 'Gutschrift anlegen' prueft defaults.hasRecipient-
Address; wenn false -> Modal-OK-Meldung statt Formular.
- Backend Defense-in-Depth: createCreditNote wirft 400, wenn weder
billingAddressId noch addressId gesetzt. getCreditNoteDefaults liefert
hasRecipientAddress.
Verifiziert: ohne Adresse -> hasRecipientAddress false + create 400;
mit Adresse -> ok.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Beleg-Upload jetzt fuer beide Arten: bei Geld die Ueberweisungs-
bestaetigung, bei Sachwert das unterschriebene Dokument. ReceiptControls
in der Liste fuer Geld UND Sachwert (Label je nach Typ). Endpoint war
schon typ-agnostisch.
- PDF-Unterschriftsblock nur noch bei Sachwert - eine Ueberweisung wird
nicht unterschrieben (Beleg = hochgeladene Ueberweisungsbestaetigung).
Bei Geld entfaellt der Unterschrift/Ort-Block; im Formular sind Ort +
'Unterschrift am' bei Geld ausgeblendet.
Verifiziert: beide PDFs erzeugen sauber (je 1 Seite).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pentester R142: Uebergang Geld->betragsloser Sachwert setzte die schon
vergebene Gutschriftsnummer auf null -> Luecke in der GS-Serie.
Loesung: betragsloser Sachwert = Lieferschein mit eigener
Lieferscheinnummer aus separatem Nummernkreis.
- Schema: CreditNote.deliveryNoteNumber (nullbar, unique) + neues Model
DeliveryNoteNumberRange (Default-Praefix 'LS-') + Migration.
- deliveryNoteNumberRange.service (mirror, eigener Zaehler, FOR UPDATE).
- Nummern lazy pro Serie, NIE freigeben: Uebergaenge behalten die
jeweils vergebene Nummer der anderen Serie reserviert -> kein
Doppelverbrauch, keine Luecke. effectiveNumber() liefert je nach Typ
die passende (LS/GS) fuer Anzeige/PDF/Audit.
- Endpunkte GET/PUT /credit-notes/delivery-note-number-range; Settings-
Seite verwaltet jetzt beide Nummernkreise. PDF-Titel 'Sachwert-
Uebergabe', Dateiname lieferschein-...
- Frontend: Typ + displayNumber in Liste/Modal.
Verifiziert: Sachwert 0 -> LS-Nr, GS-Zaehler unberuehrt; Geld -> GS-Nr;
Uebergaenge behalten beide Nummern (kein Neuverbrauch, keine Luecke).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ein betragsloser Sachwert ist eher ein Lieferschein als eine Gutschrift
-> er soll KEINE Gutschriftsnummer aus dem Nummernkreis verbrauchen.
- Schema: CreditNote.number nullbar (Migration MODIFY ... NULL, UNIQUE
bleibt - MySQL erlaubt mehrere NULLs).
- createCreditNote: betragsloser Sachwert -> number=null, assignNextNumber
wird NICHT aufgerufen (Zaehler unangetastet).
- updateCreditNote: Uebergaenge - wird betragslos -> Nummer entfernen;
bekommt nachtraeglich Betrag & hatte keine -> jetzt Nummer vergeben.
- PDF/Liste/Modal/Audit: Fallback 'Sachwert-Uebergabe'/'Beleg #id' wenn
keine Nummer; PDF-Titel 'Sachwert-Uebergabe', kein ZUGFeRD (schon vorher).
Verifiziert: Sachwert 0 -> number null + Zaehler bleibt; Geld -> Nummer
+ Zaehler +1; Sachwert nachtraeglich mit Betrag -> Nummer vergeben.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Eine Sachwert-Gutschrift darf betragslos sein (Betrag leer/0): dann
findet keine Rechnungsstellung statt - der Kunde hat den Gegenstand
einfach als Subvention erhalten.
- Backend: leeres amount -> 0; Sachwert erlaubt 0, Geld verlangt > 0
(400 sonst). vatRelevant bei 0 erzwungen-false.
- PDF: betragsloser Sachwert -> Titel 'Sachwert-Uebergabe', kein
Betrags-/USt-Block (Hinweis keine Rechnungsstellung), KEIN ZUGFeRD-
Embedding. Mit Betrag -> unveraendert ZUGFeRD.
- Frontend: Wert-Feld bei Sachwert optional; USt-Block ausgeblendet ohne
Betrag; Liste zeigt 'Sachwert ohne Betrag (keine Rechnung)'.
Verifiziert: Sachwert 0 -> kein factur-x.xml; Geld 0 -> 400;
Sachwert 200 -> ZUGFeRD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wie bei Bankkarte/Ausweis/Adresse: das 'Zaehler'-Label im Energie-
Vertragsformular bekommt ein LabelWithLink zum Zaehler-Tab der
Kundenakte (/customers/:id?tab=meters), oeffnet im neuen Tab.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1) Folgevertrag-Bug: Backend legt den Folgevertrag korrekt mit
previousContractId an. Der Verlust passierte im Frontend - das
Vorgaenger-Dropdown holt Vertraege ueber getAll, das DEACTIVATED
standardmaessig ausblendet. Beim Bearbeiten des Folgevertrags war der
deaktivierte Vorgaenger nicht als Option da -> Verknuepfung ging beim
Speichern verloren.
Fix: getAllContracts + Controller + contractApi.getAll um
includeDeactivated erweitert; Vorgaenger-Dropdown nutzt es und markiert
deaktivierte Vertraege mit '· deaktiviert'. Verifiziert (Flag inkludiert
deaktivierte; Folgevertrag setzt previousContractId).
2) Kundendaten-Modal: zeigt jetzt zusaetzlich Lieferadresse + (falls
abweichend) Rechnungsadresse des geoeffneten Vertrags, die Stressfrei-
Adresse des Vertrags einzeln und darunter alle weiteren Stressfrei-
Adressen des Kunden. CustomerInfoModal nimmt optionale Vertragskontext-
Props; ContractDetail + ContractForm uebergeben sie.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1) Auto-Status: Wird eine Kuendigungsbestaetigung hinzugefuegt (Datum
ueber Formular und/oder Dokument-Upload), wird der Vertrag von ACTIVE
auf CANCELLED gesetzt und das Vertragsende = Kuendigungsdatum.
Zentrale Funktion maybeCancelOnCancellationConfirmation (idempotent,
nur aus ACTIVE). Upload-Route ersetzt die alte Inline-Logik (setzt
jetzt auch endDate); Update-Controller triggert nur bei neu/geaendertem
Bestaetigungsdatum (manuelle Status-Korrekturen bleiben unangetastet).
2) Cockpit-Filter 'Kuendigungsbestaetigung': neue Liste
cancellationConfirmations (Vertraege mit Bestaetigung in Status
ACTIVE/DRAFT/CANCELLED) + Filter-Option im Cockpit-Dropdown. Eigene
Liste, weil bereits CANCELLED-Vertraege mangels Issue sonst nicht in
der Cockpit-Liste auftauchen.
Beides lokal verifiziert (Helper: ACTIVE->CANCELLED + endDate; Cockpit:
Vertrag erscheint in der Liste mit korrektem Status).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bei Geld-Gutschriften kann die Auszahlung auf ein anderes Konto gehen
als das Vertrags-Abbuchkonto:
- CreditNote.payoutBankCardId + Migration (FK ON DELETE SET NULL, damit
Loeschen einer Bankkarte die Gutschrift nicht mitreisst).
- Formular: Dropdown mit allen Bankkonten des Kunden (Default =
Vertrags-Abbuchkonto). getCreditNoteDefaults liefert bankCards +
contractBankCardId.
- Server prueft, dass die gewaehlte Bankkarte dem Kunden des Vertrags
gehoert (kein Fremdkonto unterschieben).
- PDF: bei Ueberweisung 'Unsere Bankverbindung' (Absender) + darunter
'an Bankkonto: <Kunden-IBAN> (<Inhaber>)'. Section-Zeile zeigt das
Auszahlungskonto.
Lokal verifiziert (Anlage + PDF).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PDF-Erzeugung fuer Gutschriften mit Firmenstammdaten als Absender,
Empfaenger (Kunde+Adresse), Netto/USt/Brutto (bzw. ohne USt-Ausweis),
Sachwert/'Ware erhalten' bzw. Ueberweisungs-Bankverbindung,
Unterschriftsblock, Firmen-Fusszeile.
Endpoint POST /credit-notes/:id/pdf; 'PDF'-Button in CreditNotesSection
(erzeugen + im Tab oeffnen bzw. vorhandenes ansehen). PDF-Pfad wird bei
inhaltlicher Aenderung geleert -> Neu-Erzeugung. Download ueber
fileDownload (credit-notes subDir, Vertrags-Ownership).
Lokal verifiziert (valides %PDF, korrekte Betraege).
ZUGFeRD-XML-Embedding (PDF/A-3, EN 16931) folgt als Teil 2 mit
Validator-Pruefung.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kunden-Feld vatExempt (Kleinunternehmer/USt-befreit §19) + Migration
(ADD COLUMN IF NOT EXISTS). Checkbox im Kundenformular nur fuer
Firmenkunden.
Gutschrift-Vorbelegung vatRelevant wird aus dem Kunden abgeleitet:
Firmenkunde ohne USt-Befreiung -> USt-relevant an (Netto), sonst aus
(wie Privat). Jede Gutschrift speichert ihren eigenen Snapshot, ein
spaeterer Statuswechsel des Kunden aendert bestehende Gutschriften
nicht.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pentester-Fund: die Legacy-Scalar-Felder mobileDetails.phoneNumber und
simCardNumber (kein sichtbares Formularfeld mehr) wurden beim Kopieren
noch uebernommen - anders als die SIM-Karten-Liste und anders als
'Rufnummern werden nicht uebernommen'. Zwei getrennte Vertraege
desselben Kunden haetten so unbemerkt dieselbe Rufnummer getragen.
Datenintegritaet, kein Security-Loch. Beide Felder jetzt im Kopier-
Leer-Block.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auf Wunsch: Portal-Benutzername + Stressfrei-Verknuepfung/'nicht
benoetigt'-Flag werden im Kopier-Modus behalten (bei gleichem Anbieter
oft identisch). Nur das Passwort bleibt leer, weil es verschluesselt
gespeichert und - wie beim Bearbeiten - nicht im Klartext ins Feld
geladen wird. Banner-Hinweis ergaenzt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kopieren-Button in der Vertragsansicht oeffnet das Neu-Formular unter
/contracts/new?copyFrom=<id> mit allen Daten des Quellvertrags
vorbefuellt. Beim Speichern entsteht ein frischer, unabhaengiger
Vertrag (kein previousContractId-Link, kein VVL).
Use-Case: neuer Mobilfunk-/o.ae. Vertrag ist meist fast identisch;
nur Preis, Laufzeit, Kunden-/Vertragsnummer, Anbieter/Tarif aendern
sich - der Rest bleibt gleich.
Im Kopier-Modus geleert (Integritaet/Eindeutigkeit): Status->DRAFT,
Vorgaenger-Link, alle Datumsfelder, Kunden-/Vertragsnummer beim
Anbieter + Plattform-Nummern, Portal-Zugang. SIM-Karten & Rufnummern
werden nicht uebernommen. Rest (Anbieter/Tarif/Preise/Detailfelder/
Notizen) bleibt als Vorlage.
Rein Frontend (ContractForm copy-Mode + Button in ContractDetail),
kein neuer Endpoint - nutzt bestehendes Create.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Test-Ergebnis: Browser kann einer fremden Desktop-App keine echte
lokale Datei zum Anhaengen uebergeben. URL-Drag -> Thunderbird nur Link
(0 Bytes, Fehler beim Senden); Datei-Drop in den Text -> nur Dateiname
als Text; auf die Anhang-Leiste (Thunderbird/Linux) -> ebenfalls kein
echter Anhang; Webmail -> gar nicht moeglich.
Entscheidung: Feature raus. Download- und Anzeigen-Button decken den
Bedarf zuverlaessig ab. PdfDragButton geloescht, fileUrl()-Token-Param
zurueckgebaut. Pentest R131 damit gegenstandslos (kein Drag mehr).
Die separaten Copy-Buttons fuer IBAN + Ausweisnummer im Vertrags-
formular bleiben erhalten.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Thunderbird & Co. hatten beim URL-basierten Drag nur einen LINK als
Anhang gespeichert (0 Bytes) und die Datei erst beim Senden nachgeladen
-> Fehler, weil kurzlebiger Token abgelaufen. Der User erwartet, dass
die Datei direkt angehaengt wird, nicht der Link.
Loesung: Datei wird vorab per Bearer-Auth (Axios-Instanz) als Blob
geladen und beim dragstart als echter Datei-Inhalt uebergeben:
dataTransfer.items.add(File) + DownloadURL mit lokaler blob:-URL.
-> Datei-INHALT wird uebertragen, kein Link.
Blob wird beim Mount vorgeladen (Ladezustand 'laedt ...'); der
Endpoint /files/download schreibt kein Audit-Log, daher kein Spam.
Security R131 damit vollstaendig erledigt: im Drag steckt weder eine
Server-URL noch ein Token -> ein Fehl-Drop kann gar nichts mehr leaken
(kein Access-, kein Download-Token). Token-Cache/getDownloadToken im
Drag-Pfad entfernt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Zusaetzliches Mozilla-Drag-Format (URL\nTitel), damit Thunderbird und
Firefox als Drop-Ziel zuverlaessiger auf den Drag reagieren. Weiterhin
mit dem 60s-Download-Token.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Das Weglassen von text/uri-list + text/plain (optionale R131-Haertung
#2) hatte den Drop ins Mail-Fenster gebrochen: web-basierte Ziele
verstehen DownloadURL (Nativ-Format) nicht und reagierten gar nicht.
Jetzt werden im Normalfall wieder alle drei Formate gesetzt - aber mit
dem kurzlebigen 60s-Download-Token (R131-Empfehlung #1, der eigentliche
Fix des Token-SCOPE). Ein Fehl-Drop zeigt damit hoechstens eine 60s
gueltige Downloads-only-URL statt des 15-Min-Voll-Access-Tokens.
Nur im seltenen Warm-up-Rennen wird ausschliesslich DownloadURL mit
Access-Token-Fallback gesetzt (kein Klartext-Leak).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Der R131-Fix hatte den Drag per preventDefault() abgebrochen, wenn der
60s-Download-Token beim Griff noch nicht vorgewaermt war -> im Mail-
Fenster kam gar keine Datei mehr an.
Jetzt wird der Drag nie abgebrochen: bevorzugt der vorgewaermte
Download-Token, im seltenen Rennen Fallback auf den Access-Token, aber
weiterhin NUR in DownloadURL (kein text/plain) -> kein Klartext-Leak.
DownloadURL ist ein Nativ-Format und wird bei Fehl-Drop in Web-Text-
felder nicht als lesbarer Text ausgegeben, daher bleibt der R131-Fix
(kein Token-Leak als Text) erhalten.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pentest R131 (LOW-MEDIUM): Der Drag-Button haengte den 15-Min-Access-
Token an die URL und schrieb ihn per text/plain + text/uri-list beim
Drag mit. Ein Fehl-Drop in ein Text-/Chat-/URL-Feld haette den vollen
Access-Token (alle Berechtigungen, 15 Min) als lesbaren Text geleakt.
Fix:
- fileUrl() akzeptiert jetzt optionalen expliziten Token.
- PdfDragButton nutzt den kurzlebigen 60s-Download-Token
(authApi.getDownloadToken(), type:download, nur ?token=) statt des
Access-Tokens. Modul-weiter Cache mit Dedup, auf mount + hover
vorgewaermt (dragstart ist synchron, kann nicht awaiten).
- Es wird NUR noch DownloadURL im DataTransfer gesetzt, keine
text/plain- oder text/uri-list-Repraesentation -> Fehl-Drop in ein
Textfeld erzeugt gar keinen sichtbaren Text.
- Klick-Vorschau nutzt ebenfalls bevorzugt den Download-Token.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kleines Info-Icon hinter dem Drag-Element mit Tooltip, dass das Ziehen
nur in Chrome/Edge funktioniert und man in Firefox stattdessen klicken
soll. Gilt automatisch an allen Einbaustellen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Neue wiederverwendbare Komponente PdfDragButton: ziehbares Element,
mit dem der hinterlegte Scan direkt aus dem Browser in ein Mail-Fenster
(Anhang) oder den Datei-Explorer gezogen werden kann (Chromium-
DownloadURL, Format <mime>:<name>:<absolute-url>).
Bewusste Plattform-Grenze: PDF per Strg+V als Datei einfuegen geht im
Browser nicht (Web-Clipboard darf keine OS-Datei-Zwischenablage
befuellen) -> Drag-and-Drop. DownloadURL nur Chrome/Edge, nicht
Firefox (dort Klick-Fallback: Datei im Tab oeffnen).
Eingebaut an: ContractForm (neben IBAN/Ausweisnummer-Copy), Vertrags-
ansicht (Bankkarte/Ausweis-Card), Kundenakte-Tabs Bankkarten+Ausweise.
Nur bei vorhandenem documentPath. Nutzt bestehende fileUrl()-Download-
URL (Token als Query, Per-File-Ownership-Check unveraendert) - kein
neuer Endpoint, keine neue Angriffsflaeche.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In Kundendaten verknuepfen je ein Kopieren-Button neben dem Label.
Kopiert nur den reinen Wert der aktuell gewaehlten Option: bei
Bankkarte die IBAN ohne Namen, bei Ausweis die Ausweisnummer ohne
(TYP). Button erscheint nur bei ausgewaehltem Eintrag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tab-Leiste ist mit dem zusaetzlichen Geworben/angeworben-Tab zu breit
geworden; der letzte Tab lief aus dem Karten-Rahmen. nav auf flex-wrap
umgestellt (gap-x-6 gap-y-1), damit ueberzaehlige Tabs in eine zweite
Zeile umbrechen und innerhalb der Karte bleiben.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beziehungs-Dropdown erweitert um Schwiegertochter/Schwiegersohn,
Schwiegermutter/Schwiegervater, Oma/Opa, Uroma/Uropa (Whitelist
front- und backend synchron).
Bearbeiten-Stift pro Zeile (vor der Muelltonne): Beziehung und/oder
Gegen-Kunde aenderbar. Neuer PUT /:customerId/referrals/:referralId
mit Whitelist-Pruefung, Doppel-Werber-409 (Self-Ausschluss),
Portal-Block und UPDATE-Auditeintrag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Der Toggle "Deaktivierte anzeigen" saß im obersten Seiten-Header.
Verschoben in den Gruppen-Header "Meine Verträge" (rechtsbündig,
nur beim eigenen Kunden-Block), damit er direkt bei den Verträgen
sitzt statt losgelöst ganz oben.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Neuer Tab vor "Datenschutz", nur Mitarbeiter/Admin (nicht Portal),
ohne Consent-Pflicht. Zwei Abschnitte:
1. "<Kunde> wurde an Board geholt durch:" – max. 1 Werber
(DB-Unique auf recruitedId).
2. "<Kunde> hat folgende Kunden an Board geholt:" – beliebig viele.
Jede Zeile: Kunde per Lupe-Such-Modal (breite Suche über Name/
Kundennr./Firma/E-Mail/Telefon) + Beziehungs-Dropdown. Löschen +
Externtab-Link zur Kundenakte pro Zeile.
Bidirektional aus EINEM Datensatz: "A geworben durch B" erscheint
automatisch bei B unter "hat geworben"; von beiden Akten
hinzufügbar/löschbar.
Backend: neues Model CustomerReferral (recruiter/recruited FKs,
recruitedId @unique, relationship) + Migration. Beziehungs-Whitelist
serverseitig; Self-Werbung + Doppel-Werber (409) abgefangen.
Portal-Token wird explizit geblockt (Defense-in-Depth, nicht nur
UI-Ausblendung). CREATE/DELETE auditiert.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Die Cache-Header waren schon optimal (index.html no-store, Assets
immutable) – aber eine bereits laufende SPA holt sich nach einem
Deploy keinen neuen Code, bis der Nutzer neu lädt. Das fängt kein
Cache-Header ab.
useVersionCheck holt die (server-seitig no-store) index.html
periodisch (5 min) + beim Zurückkehren zum Tab, extrahiert die Menge
der referenzierten Vite-Asset-Hashes als Signatur und vergleicht sie
mit dem Startstand. Ändert sie sich (= neuer Build deployt), zeigt
UpdateBanner oben im Layout einen dezenten Hinweis mit "Jetzt neu
laden".
Kein Backend-/Build-Change; im Dev-Modus No-op (Vite-Dev liefert
keine /assets/<hash>-Dateien → leere Signatur → kein Alarm).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Der Admin-/Mitarbeiter-Blick (CustomerDetail) hatte den Toggle schon;
jetzt auch in der Kundenportal-Vertragsübersicht (ContractList,
Portal-Zweig). Button nur bei isCustomerPortal, reicht
includeDeactivated an die getTreeForCustomer-Queries durch
(showDeactivated im Query-Key → frischer Fetch beim Umschalten).
Security: derselbe Endpoint hinter canAccessCustomer (R120) – der
Portal-Kunde bekommt nur eigene/vertretene Bäume, das Flag weitet
nur den Status-Filter innerhalb der erlaubten Daten. Keine neue
Exposition.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Neuer Tab "Spam" (zwischen Gesendet und Papierkorb), zeigt den Junk-/
Spam-Ordner des gewählten Postfachs – damit fälschlich als Spam
einsortierte Mails auffindbar sind.
Backend:
- EmailFolder-Enum um SPAM erweitert (Migration, Wert angehängt →
kein Rewrite bestehender Zeilen).
- imapService.findJunkFolderPath: ermittelt den Junk-Ordner per
Special-Use-Flag \Junk + üblicher Namensliste (Junk/Spam/…).
- syncAllFoldersForAccount synct den Junk-Ordner zusätzlich als
dbFolder=SPAM (syncEmailsForAccount bekommt dbFolder-Option).
- getCachedEmails + getFolderCountsForAccount um SPAM erweitert.
- Papierkorb-Move/Restore für Spam-Mails nutzt den echten Junk-Pfad
als Quell-/Zielordner.
Frontend: Tab + Badge (ungelesen/gesamt), gleicher List-/Detail-Pfad
wie INBOX; Zuordnen-zu-Vertrag auch aus Spam möglich.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Neues Dropdown "Mobilfunknetz" in der Anbieter-&-Tarif-Karte, nur
sichtbar bei Vertragstyp Mobilfunk. Optionen: Bitte auswählen (leer),
Telekom, Vodafone, Telefónica.
Neues Feld MobileContractDetails.mobileNetwork (String nullable,
speichert TELEKOM/VODAFONE/TELEFONICA) + idempotente Migration
(ADD COLUMN IF NOT EXISTS). String statt Enum, damit weitere Netze
ohne Migration ergänzbar sind. Anzeige in der Vertragsansicht mit
lesbarem Netz-Namen.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Die Geburtstags-Sektion gab es bisher nur im Vertrags-Cockpit.
Jetzt zusätzlich auf dem Dashboard für Mitarbeiter/Admins – gleiche
birthdayApi.getUpcoming(7, 30)-Query und identische Karten-
Darstellung (Heute/vergangen/kommend, Link zur Kundenakte). Kein
Backend-Change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Der Vertragsbaum beim Kunden blendete DEACTIVATED-Verträge
komplett aus. Da der aktuellste Vertrag die Baumwurzel ist und
Vorgänger als Children hängen, verschwand eine ganze Kette aus
der Ansicht, sobald die Wurzel deaktiviert wurde – so „verschwand"
ein Vertrag scheinbar, als ein aktiver Folgevertrag gelöscht und
der Vorgänger vorher deaktiviert worden war.
getContractTreeForCustomer bekommt ein optionales
includeDeactivated-Flag (Default false = bisheriges Verhalten),
durchgereicht per Query-Param includeDeactivated=true. Toggle-
Button (Eye/EyeOff) im Kunden-Vertragstab; showDeactivated ist
Teil des Query-Keys → frischer Fetch beim Umschalten. Deaktivierte
behalten ihr graues DEACTIVATED-Badge.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Zwei zusammenspielende Bugs:
1) Backend: deleteContract-Reihenfolge scheiterte am UNIQUE-
Constraint auf Contract.previousContractId. Beim Middle-Delete
(A → B → C, B löschen) hielt B im UPDATE-Moment noch selbst
previousContractId=A – der Versuch, C ebenfalls auf A
umzubiegen, warf MariaDB Duplicate-Entry, das UPDATE brach ab.
Fix: Transaction, umgedrehte Reihenfolge – erst B löschen
(DB-Cascade ON DELETE SET NULL räumt C.previousContractId ab,
gibt A-Slot frei), dann C sauber auf A umbiegen.
2) Frontend: deleteMutation invalidierte weder Contract-Listen
noch Kunden-Vertragsbaum und hatte keinen onError-Handler.
Nach dem Delete wurde nach /contracts navigiert, dort zeigte
der stale Cache noch den gelöschten Vertrag – Eindruck: „ganze
Historie weg". Bei Bug 1 fehlgeschlagen brach das schweigend
ab.
Fix: queryClient.invalidateQueries für ['contracts'],
['contract-tree', customerId], ['customer', customerId] nach
Erfolg. onError-Toast bei Fehler.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Dritter Radio-Button in der Zugangsdaten-Card des Vertragsformulars.
Wenn gesetzt, unterdrückt das Cockpit die Warnung „Portal-Zugangs-
daten fehlen" für diesen Vertrag – für Anbieter ohne Portal oder
Kunden, die bewusst keine Zugangsdaten pflegen. Verstopft das
Cockpit sonst dauerhaft.
Neues Feld Contract.portalCredentialsNotRequired (Boolean, default
false) + idempotente Migration (ADD COLUMN IF NOT EXISTS). Bestand
bleibt auf false, Warnung greift wie bisher.
Beim Umschalten auf Opt-out werden portalUsername,
portalPasswordEncrypted und stressfreiEmailId server-seitig
explizit auf NULL gesetzt – Datenhygiene, damit keine verwaisten
Anmeldedaten in der DB stehen bleiben.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>