Files
Mieterportal/kimi.md
T

583 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Kimi-Kontext: Hinterstein Immobilien · Mieterportal (mieterportal.hocks.eu)
> Projekt-Kontextdatei für Kimi Work. Stand: 02.10.2026, 13:00 Uhr.
> Bitte bei größeren Änderungen am Projekt aktualisieren.
> Konventionen und Workflow sind am Projekt Schulportal (sfg.hocks.eu) orientiert.
## 1. Projektüberblick
Mieterportal der **Hinterstein Immobilien** (Demo, Inhaber Joseph Hinterstein) für
**50 Wohnungen in 3 Häusern** (Hauptstraße 12 · Ringweg 3 · Am Park 5). Analog zum
Schulportal: alle Mietangelegenheiten digital an einem Ort.
**Datenhaltung (seit v1.3):** zentral serverseitig in **PostgreSQL** über eine
**Node-API** (Express) – kein localStorage mehr für Mietdaten. Mehrere Nutzer
sehen denselben Datenstand in Echtzeit; Rechte werden serverseitig erzwungen
(Mieter nur eigene Wohnung, Verwaltung alles).
**Authentifizierung:** Login-Pflicht auf `/login` (Route-Guard in `App.tsx`).
Gruppen `verwaltung` und `mieter`; Mieter registrieren sich per einmaligem
**Registrierungscode** (`/registrieren`). Passwörter als SHA-256-Hash (im Browser
gerechnet, Vergleich serverseitig), Sitzung als HttpOnly-Cookie `mp_session`
(8 h, Tabelle `sessions`). Demo-Zugänge (Passwort `0815`): `j.hinterstein`
(Verwaltung), `a.berger` (Mieterin, Hauptstraße 12 Whg. 7), `m.wolf` (Mieter,
Ringweg 3 Whg. 4). Demo-Registrierungscode: `MP-2026-0815`.
**Die 6 Module:**
1. **Dashboard** (`/`) – Mieter: Begrüßung, nächste Zahlung, Mietdiagramm, Ankündigungen,
offene Meldungen. Verwaltung: Kennzahlen (Vermietungsquote, Miet-Soll, Rückstände),
Einnahmenverlauf + Belegung pro Haus (Recharts).
2. **Mietkonto & Zahlungen** (`/mietkonto`) – Buchungsverlauf 2026 mit Statusfilter,
Nebenkostenguthaben, Zahlungsdaten. Verwaltung: Zahlungsstatus aller Wohnungen,
Rückstandsliste, Haus-Filter.
3. **Schadensmeldungen** (`/schadensmeldungen`) – Tickets mit Kategorie, Dringlichkeit,
Status; Neuanlage per Dialog; Verwaltung pflegt Status per Select.
4. **Dokumente** (`/dokumente`) – Verträge/Abrechnungen/Hausordnung/Protokolle,
Suche + Kategorie-Chips. Mieter sieht eigene + Hausdokumente (unitId=null).
5. **Nachrichten** (`/nachrichten`) – Chat Mieter ↔ Verwaltung, Ungelesen-Markierung,
neue Unterhaltungen.
6. **Benutzerverwaltung** (`/benutzer`, nur Gruppe `verwaltung`) – Nutzerliste mit
Gruppen und Wohnungsbezug, Registrierungscodes erzeugen/deaktivieren,
Demo-Zugänge zurücksetzen.
## 2. Technischer Stack
| Teil | Technologie |
|---|---|
| Frontend | React 18 + TypeScript, Vite 7, Tailwind v3.4 + shadcn/ui (Radix), React Router 7 (BrowserRouter), Recharts |
| State | Store-Kontext im Browser, **alle Daten serverseitig** (kein localStorage mehr); Ladezustand `bereit`, Schreibzugriff async |
| API | `server/`: Node 20 + Express + pg, Session-Cookie, Rollenrechte, serverseitige Mail-Benachrichtigungen |
| Datenbank | PostgreSQL 16 (`mp-db`), Schema/Seed in `server/src/db.js` + `seed.js`, Volume `mp_pgdata` |
| Daten | `server/seed/generated.json` (50 Wohnungen + 552 Buchungen via `beispieldaten.py`) + `server/src/seed-data.js` (handschriftlich) |
| Auth | SHA-256-Hash (Browser) + HttpOnly-Cookie-Session (8 h), `src/lib/api.ts`, `src/lib/auth.ts` |
| Branding | Kunde: `src/assets/hinterstein/` (logo.png, joseph-hinterstein.jpg, kontakt.jpg); KI-generiert: `src/assets/logo.png`, `hero.jpg`, `modules.jpg` |
| Webserver | Docker-Container `mp-portal` (nginx), statisches `dist/`, proxiet `/api/` → `mp-api:3000` |
| Mail | `mail-server/`: Postfix-Relay (`mp-mail`) + Python-API (`mp-mail-api`) – **live**, `/opt/mp-mail`; Portal-nginx proxyt `POST /api/mail/send` mit Token aus `portal.env` |
| Server | Root-VPS hocks.eu (IONOS), Docker + Portainer, Subdomain via NPM (Nginx Proxy Manager, Let's Encrypt) |
## 3. Wichtige Dateien
Projektverzeichnis: `C:\Users\ah\Documents\Kimi\Workspaces\Mieterportal`
**Git:** `https://git.hocks.eu/ah-admin/Mieterportal` (Branch `main`). Secrets
ausgesperrt via `.gitignore` (`mail-server/mail.env` etc.).
| Datei | Inhalt |
|---|---|
| `src/store/store.tsx` | Store-Kontext: lädt `/api/bootstrap`, stellt Collections + `me`/`role`/`meineEinheit` bereit, async-Methoden für alle Schreibaktionen, `fmtEUR` |
| `src/lib/api.ts` | Fetch-Client (same-origin, JSON, `ApiFehler` mit deutscher Meldung) |
| `src/lib/auth.ts` | SHA-256 für Passwörter, `DEMO_PASSWORT` |
| `server/src/index.js` | Express-API: Auth (Login/Logout/me/Register), Bootstrap, Tickets, Threads/Messages, Benutzerverwaltung, Codes, `/api/healthz` |
| `server/src/db.js` | pg-Pool + idempotentes Schema (11 Tabellen, snake_case) |
| `server/src/seed.js` | Idempotenter Seed (läuft nur bei leerer users-Tabelle) |
| `server/src/seed-data.js` | Handschriftliche Demo-Daten (Tickets, Dokumente, Threads, Ankündigungen, Benutzer, Codes) |
| `server/src/mail.js` | Serverseitiger Versand über mp-mail-api (fire-and-forget) |
| `server/seed/generated.json` | 50 Wohnungen + 552 Buchungen, erzeugt von `beispieldaten.py` (Seed 20261002) |
| `beispieldaten.py` | Deterministischer Generator: `python beispieldaten.py [seed]` → `server/seed/generated.json` |
| `src/types/index.ts` | Alle Typen (Unit, Transaction, Ticket, DocumentItem, Thread, Announcement, Benutzer, RegistrierungsCode) |
| `src/components/AppLayout.tsx` | Sidebar (Desktop) + Mobile Top-Bar/Nav, Logout, Ungelesen-Badge, ThemeToggle |
| `src/components/ThemeToggle.tsx` | Dark-Mode-Umschalter (Sonne/Mond) |
| `src/lib/theme.ts` | `mp-theme` in localStorage (nur noch Theme!), Init vor erstem Render (main.tsx) |
| `src/pages/Login.tsx` | Login-Pflicht: Hero mit Joseph-Hinterstein-Portrait, Logo, Feature-Liste, Login + Registrierung + Demo-Einstiege |
| `src/pages/BenutzerVerwaltung.tsx` | Benutzerverwaltung (nur Verwaltung): Nutzer anlegen/pflegen, Registrierungscodes erzeugen |
| `docker/templates/default.conf.template` | nginx-Site als envsubst-Template: SPA, `/api/` → mp-api, `POST /api/mail/send` → mp-mail-api mit `${MAIL_API_TOKEN}` |
| `docker-compose.yml` | Stack: `mp-portal` (nginx), `mp-api` (Node, Build ./server), `mp-db` (PostgreSQL 16, Volume mp_pgdata), Netz npm_default |
| `deploy.sh` | Deploy-Workflow: Build, Paket (dist + compose + nginx + server), Upload, `docker compose up -d --build`, Health-Check |
| `mail-server/` | Postfix-Relay + schlanke Mail-API (Muster Schulportal, angepasst) – separater Stack unter /opt/mp-mail |
## 4. Deploy-Workflow (bewährt, aus Schulportal übernommen)
```bash
# 1. Bauen (im Projektverzeichnis)
npm run build
# 2. Paketieren + Hochladen (pscp braucht cwd = Projektverzeichnis)
tar -czf mp-dist.tar.gz -C dist .
"/c/Program Files/PuTTY/pscp.exe" -batch \
-hostkey "SHA256:4G3UxsugNkHH6BrPXScLXh+vX8P1sLgWKn8cZBKSRxw" \
-i "Z:\Devel\ssh\priv_ionos_hocks_vps.ppk" -P 38592 \
mp-dist.tar.gz root@hocks.eu:/tmp/
# 3. Entpacken + Container neu starten + Hash prüfen
"/c/Program Files/PuTTY/plink.exe" -batch -hostkey "SHA256:4G3UxsugNkHH6BrPXScLXh+vX8P1sLgWKn8cZBKSRxw" \
-i "Z:\Devel\ssh\priv_ionos_hocks_vps.ppk" -P 38592 root@hocks.eu \
"rm -rf /opt/mieterportal/dist && mkdir -p /opt/mieterportal/dist && \
tar -xzf /tmp/mp-dist.tar.gz -C /opt/mieterportal/dist && \
cd /opt/mieterportal && docker compose up -d && sleep 2 && \
curl -s https://mieterportal.hocks.eu/ | grep -o 'index-[^\"]*\.js'"
# Alternativ alles in einem Schritt:
./deploy.sh
```
- SSH: Port **38592**, Key `Z:\Devel\ssh\priv_ionos_hocks_vps.ppk` (Pageant), Hostkey-Flag zwingend.
- **`portal.env` auf dem Server** (gitignored, liegt nur dort): liefert
`MAIL_API_TOKEN` (für nginx-envsubst und mp-api) und `POSTGRES_PASSWORD`
(für mp-db und die DATABASE_URL des mp-api). Einmalig angelegt:
`grep '^MAIL_API_TOKEN=' /opt/mp-mail/mail.env > /opt/mieterportal/portal.env`
plus zufälliges `POSTGRES_PASSWORD=…`. Wichtig: compose interpoliert
`${VARIABLE}` nur aus einer `.env`-Datei im Projektverzeichnis (nicht aus
`env_file:`!) – daher auf dem Server `/opt/mieterportal/.env` als
**Symlink auf portal.env**:
`cd /opt/mieterportal && ln -sf portal.env .env`
- deploy.sh macht `docker compose up -d --build` (baut das mp-api-Image auf
dem Server, npm ci im Container) und prüft danach Container-Status,
Portal-Hash und `GET /api/healthz`.
- **Vorsicht Fail2Ban:** zu viele gescheiterte SSH-Versuche von einer IP führen zu einem
Ban (Timeout-Verbindungsbild). Ban aufheben auf dem Server: `fail2ban-client unban --all`.
- **Live (seit 02.10.2026):** Container `mp-portal` läuft auf hocks.eu, statisches `dist/`
unter `/opt/mieterportal/dist` (gemountet). NPM-Proxy-Host **id 7** (Datei
`/data/compose/1/data/nginx/proxy_host/7.conf`, DB-Tabelle proxy_host), Zertifikat
**npm-9** (Let's Encrypt, läuft bis 31.12.2026; via certbot/webroot erstellt, liegt im
npm-Container unter `/etc/letsencrypt/live/npm-9/`). HTTP→HTTPS-Redirect aktiv
(HSTS, Block-Exploits, HTTP/2). Änderungen am Proxy-Host im NPM-UI werden die
DB-Konfiguration konsistent halten; manuelle Conf-Edits werden beim nächsten
UI-Speichern überschrieben.
- HTML wird mit `no-cache` geliefert (nginx-Config), Assets hash-benannt → Reload genügt.
Der eingebettete Kimi-Browser cached hart → beim Testen Cache-Buster anhängen: `?v=X`.
## 5. Zugänge & Konfiguration
- **Demo-Zugänge (Passwort `0815`):** `j.hinterstein` (Verwaltung), `a.berger`
(Mieterin, Hauptstraße 12 Whg. 7), `m.wolf` (Mieter, Ringweg 3 Whg. 4).
Demo-Registrierungscode: `MP-2026-0815`. Passwörter liegen als SHA-256-Hash
in der users-Tabelle; Sitzung als HttpOnly-Cookie (Tabelle sessions).
Abmelden reicht zum „Reset" – nichts liegt mehr im Browser-localStorage
(außer dem Dark-Mode-Theme).
- **Verwaltungs-E-Mail:** `axel@hocks.eu` (echtes, zustellbares Postfach, damit
Benachrichtigungen ankommen) – im Portal änderbar.
- **Mail-API-Token:** bewusst nicht hier dokumentiert – liegt auf dem Server in
`/opt/mp-mail/mail.env` (Vorlage `mail.env.example` im Repo).
- **Mail-Stack live (seit 02.10.2026):** Container `mp-mail` + `mp-mail-api` laufen
auf hocks.eu unter `/opt/mp-mail` (Relay über `hocks-eu.mail.protection.outlook.com`).
Testmail und API-`/send` (mit `X-Api-Token`) verifiziert (status=sent); ohne Token 401.
- **Mail-Benachrichtigungen (seit v1.2 nginx-Proxy, seit v1.3 serverseitig,
seit v1.4 im HTML-Format wie im Schulportal):** Der Portal-nginx proxiet
`POST /api/mail/send` an `http://mp-mail-api:8080/send` mit Token aus
`portal.env` (403 für alles außer POST). **Versand aus der Node-API**
(`server/src/mail.js`, fire-and-forget): neue Schadensmeldung → alle aktiven
Verwaltungsbenutzer, neue Nachricht/Antwort → Gegenstelle. HTML im
Schulportal-Stil (`baueHtmlMail`: 600-px-Karte, Kopfzeile in Hinterstein-
Farben #22333b/Amber, Zeilen-Tabelle, Status-Box, Zitat-Box, Button „Zum
Mieterportal“, Footer) immer mit Plain-Text-Fallback (multipart/alternative).
E2E verifiziert: Zustellung an axel@hocks.eu, Postfix status=sent.
## 6. Konventionen & Besonderheiten
- **Dark Mode:** class-Strategie, Präferenz `mp-theme`, Init vor erstem Render.
Feste Farbklassen werden in `index.css` für `.dark` umrechnen
(`.dark .bg-white`/`.bg-slate-50`/`.bg-teal-50`); Sidebar ist eigenes Dunkel-Teal.
- **Barrierefreiheit:** aria-pressed/labels am ThemeToggle und Rollenwechsel, Fokus-Ringe.
- **Formatierung:** Beträge via `fmtEUR` (Intl de-DE), Datumsformate TT.MM.JJJJ.
- **Datenpflege:** Bestandsdaten ändern → `beispieldaten.py` anpassen und neu ausführen,
nicht generated.json von Hand editieren.
- **Sicherheit:** kein offenes Relay (Postfix mynetworks loopback + Docker-Netz,
sender_canonical zentral). Upload/Download-Funktionen sind Demo-Darstellungen.
- **Keine destruktiven Aktionen auf dem Server ohne ausdrückliche Bestätigung.**
## 7. Testing
- `npm run build` (`tsc -b`) ist die echte Typprüfung; `npx tsc --noEmit` prüft wegen
Solution-tsconfig ggf. nichts. Server-Syntax: `node --check server/src/*.js`.
- API-Livetests mit curl (Cookie-Jar): Login → `/api/auth/me` → `/api/bootstrap`
(Mieter-Sicht zählt kleiner!) → Ticket/Thread schreiben → Postfix-Log auf
Zustellung prüfen (`docker logs mp-mail | grep to=<empfaenger>`).
- Headless-Screenshots (Dark Mode = Systempräferenz, `--virtual-time-budget=10000`
wartet auf den Session-Check):
`"/c/Program Files/Google/Chrome/Application/chrome.exe" --headless --disable-gpu --screenshot=... --window-size=WxH "URL"`
- Screenshots ausgeliefert nach `preview/`.
## 8. Verlauf / offene Punkte
**Erledigt 04.10. (v1.24):** Anleitungsseite /anleitung (Seite „Anleitung" im
Menü, sichtbar für Mieter und Verwaltung). Inhalte rollenbasiert in
src/data/anleitung.ts (Abschnitte je gruppe 'mieter'|'verwaltung'); Verwaltung
hat Reiter „Für die Verwaltung" / „Mieter-Ansicht". Stand-Badge (Datum + Build-
Hash) kommt aus src/data/anleitung-meta.ts, die von scripts/anleitung-meta.mjs
erzeugt wird – Aufruf vor jedem Build: als package.json „prebuild"-Hook (läuft
also bei JEDEM npm run build mit) plus eigener Schritt 1 in deploy.sh (1/5…5/5).
Meta-Datei ist gitignored (sonst Hash-Henne-Ei: Committen ändert den Hash).
Dunkelmodus-taugliche Klassen beachten.
**Erledigt 04.10. (v1.23):** Nebenkostenabrechnung als PDF direkt aus dem
Kosten- und Zählerstandsmodul. Neues Modul `server/src/abrechnung.js`
(pdfkit, neue Dependency – Achtung: Standardfont Helvetica/WinAnsi, kein
U+2212-Minus; ASCII '-' verwenden): lädt Wohnung, Hauseinheiten
(Flächenanteil = Wohnungsgröße/Hausgesamtfläche), umlegbare Kosten des Hauses
(jahr + (unit_id = Wohnung ODER NULL)), gruppiert nach Kostenart; Vorauszahlung
= (Warm- − Kaltmiete) × 12; Differenz → Nachzahlung (rot) oder Erstattung
(grün). PDF nach UPLOAD_DIR als `ab-<unit>-<jahr>.pdf`, idempotentes
documents-Row (Titel `Nebenkostenabrechnung <Jahr> – <Haus>, Whg. <Nr>`,
Kategorie „Nebenkostenabrechnung“, alte Datei wird ersetzt). Endpoint
`POST /api/abrechnungen` (auth, adminOnly) → `{dokument, zusammenfassung}`.
Frontend: `AbrechnungDialog` in src/pages/Kosten.tsx (Button im Kosten-Header
neben KostenDialog), Ergebnisbox mit Download-Link. E2E live: Testkosten
(hausweit + wohnungsbezogen) → Abrechnung we1/2026 → PDF geprüft (Umlaute,
€, ² ok) → Testdaten bereinigt. UAT-Liste: neue Spalte „Ziel-Version" (Spalte
K), COUNTIF-Bugfix (zählte fälschlich Spalte I statt J).
**Erledigt 04.10. (v1.22):** Direkt-E-Mail der Verwaltung an Mieter mit
automatischem CC an die Verwaltung. Mail-API (mail-server/api/server.py,
auf dem Server manuell ausgerollt, Container mp-mail-api per docker restart –
NICHT Teil von deploy.sh) um cc-Feld erweitert: Body cc kommagetrennt,
einzeln per EMAIL_RE validiert, versenden setzt msg["Cc"]. ACHTUNG Repo-
Stand vorher von Server frisch gezogen (from_address/from_name war nur
serverseitig). API: POST /api/users/:id/email (adminOnly) – Betreff/Text
Pflicht, Mieter muss E-Mail hinterlegt haben (sonst 400 mit Hinweis),
HTML-Karte baueHtmlMail, CC = mail.verwaltungEmails() (alle aktiven
Verwaltungs-Accounts mit E-Mail), Versand fire-and-forget via sendeMail(…, cc).
PATCH /api/users/:id akzeptiert jetzt auch email (Format- + Eindeutigkeits-
prüfung, LEERE E-Mail darf mehrfach vorkommen – Eindeutigkeit nur bei
nicht-leer, sonst Fehler beim Leeren). Frontend Benutzerverwaltung:
EmailDialog pro Mieter (Empfängeranzeige, Betreff, Nachricht, Hinweis auf
CC, Senden deaktiviert ohne Mieter-E-Mail), E-Mail-Adresse jetzt inline im
Benutzer-Eintrag editierbar (onBlur → setzeBenutzerEmail). Store:
sendeBenutzerEmail + setzeBenutzerEmail. E2E verifiziert: E-Mail setzen,
senden → Postfix eine queue-ID mit 3 Empfängern (Testadresse bounced
erwartet, joseph@hinterstein-immo.de + axel@hocks.eu status=sent),
Fehlerfall ohne E-Mail sauber, Testdaten bereinigt.
**Erledigt 04.10. (v1.21):** Live-Stammdaten aus SharePoint-Export-CSV
importiert (KEIN Dauersync – Nutzer-Entscheidung: einmaliger Import,
Pflege danach nur noch im Portal). CSV „Master Data V" (53 Zeilen,
37 Spalten, 12 Häuser in Krefeld, 46 Wohneinheiten inkl. Garagen/Zimmer,
43 Mietparteien) liegt NUR lokal im Workspace (PII, in .gitignore).
Import-Skript server/import/import-masterdata.py (stdin CSV → stdout SQL,
deterministische IDs slug(haus)-slug(we), deutsche Zahlenformate
'2.700'→2700 / '71,4'→71.4 / Baujahr '1.905'→1905, PLZ '47.829'→47829,
Etage aus 'Lage im Objekt' EG/1. OG/DG, WE01→'1'). buildings-NOT-NULL
(baujahr/stockwerke/etagenhoehe/wohneinheiten/quadratmeter) per ALTER
gelockert. Import ersetzt komplett: threads/tickets/transactions/costs/
belege/documents/meters/codes + Mieter-User + units + buildings;
Verwaltungs-User, Ankündigungen bleiben. Mieter bekommen Zufalls-Passwort-
Hash (unloggbar bis Passwort-Reset auf 0815 durch Verwaltung in /benutzer),
email='' (Mailversand überspringt leere Adressen, v1.20-Profil zum
Nachpflegen). Zähler NICHT importiert (CSV-Spalten nur '0').
Sync-Infrastruktur (v1.21-Vorentwicklung) komplett entfernt: mp-sync-cron
Container gelöscht, portal.env von SP_*/INTERN_SYNC_TOKEN bereinigt,
Code aus Arbeitsbaum entfernt (nie gepusht). WICHTIG seed.js-Fix:
Demo-Nachseeds (seedeZaehler/Haeuser/Kosten) prüften nur Tabellen-Leere
→ FK-Crashloop nach Import (hs-7 fehlt). Fix: demoBestandVorhanden()
gate auf units.id='hs-7'. Frontend: hartkodierte Demo-Zahlen entfernt –
AppLayout-Footer + Dashboard (Bestandszählung, Belegungsquote) jetzt
dynamisch aus buildings/units, Hauslisten in Payments/Zaehler/Benutzer-
Verwaltung dynamisch, Login-Demo-Zugang a.berger entfernt (existiert
nicht mehr), DEMO_UNIT_ID='' (Fallback = erste Einheit). E2E: Bootstrap
12 Häuser/46 Einheiten/43 Mieter, API healthy.
**Erledigt 04.10. (v1.20):** Stammdaten-Selbstpflege. users-Tabelle um
telefon/kontoinhaber/iban/bankname erweitert (ALTER ... ADD COLUMN IF NOT
EXISTS). PATCH /api/auth/profil (auth, eigene Daten): E-Mail (Pflicht +
Eindeutigkeitsprüfung case-insensitiv), Telefon, Kontoinhaber, IBAN
(Leerzeichen werden entfernt/großgeschrieben, Formatprüfung
^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$), Bankname. oeffentlicherBenutzer liefert die
neuen Felder mit (gelten damit auch für /api/auth/me, Bootstrap benutzer,
Benutzerverwaltung). Frontend: neue Seite /profil (Nav „Mein Profil", beide
Rollen): Ansichtskarte Konto & Wohnung (Name/Benutzername/Wohneit/ Mitglied
seit, readonly), editierbare Kontaktdaten (E-Mail, Telefon) und
Bankverbindung (nur Mieter, Hinweis auf Kaution). Store: aktualisiereProfil
→ PATCH, setMe mit Server-Antwort; Erfolgs-/Fehlermeldung inline. E2E
verifiziert: Speichern als a.berger (IBAN normalisiert), Auslesen via
/api/auth/me, Negativtests ungültige IBAN + fremde E-Mail werden sauber
abgelehnt. Demo-Daten der a.berger bleiben als Schaufenster-Datensatz.
**Erledigt 04.10. (v1.19):** Schadensmeldungen mit Kommunikation verknüpft.
threads-Tabelle um `ticket_id TEXT REFERENCES tickets(id)` erweitert (ALTER +
Index idx_threads_ticket). POST /api/tickets legt den Auto-Thread bereits mit
ticket_id an (Verknüpfung kam zur ursprünglichen Thread-Anlage nachträglich
dazu – bestehende Threads ohne ticket_id bleiben unverknüpft, nur NEUE
Meldungen). mapThread liefert ticketId mit. PATCH /api/tickets/:id schreibt
bei Statuswechsel eine Systemnachricht in den verknüpften Thread
(„Status … geändert: alt → neu", from_role 'admin', unread_by_tenant=TRUE)
und sendet eine HTML-Mail an den Mieter (Titel + alt/neu). Frontend:
Ticket-Karten zeigen einen „Kommunikation"-Bereich (Nachrichtenzahl, letzte
Nachricht, Link „Unterhaltung öffnen →" nach `/nachrichten?thread=<id>`);
Messages.tsx wertet den ?thread=-Parameter aus und selektiert die Unterhaltung.
E2E verifiziert: Ticket als a.berger → Thread mit ticketId + Erstnachricht +
KI-Antwort (N8N) im selben Thread; Statuswechsel als Admin → Systemnachricht
+ Mail-Versuch (Bounce nur wegen Demo-Adresse example.de, erwartet);
Testdaten bereinigt.
**Erledigt 04.10. (v1.17 + v1.18):** Auto-Zuordnung der Belege + eigener
OCR-Container. ZUORDNUNGSREGEL (Vorgabe des Nutzers): PRIMÄR Stromzählernummer,
SEKUNDÄR Adresse. belege-Tabelle um unit_id/gebaude/zuordnung/rohtext erweitert
(ALTER ... ADD COLUMN IF NOT EXISTS). `belegAutoZuordnung(text)`: Stromzähler
aus meters (längste Nummer zuerst) als Substring im Volltext, dann Hausname/
Adresse aus buildings; WICHTIG: Umlaut-Normalisierung (ß↔ss, Umlaute↔Ae/Oe/Ue)
auf beiden Seiten – sonst findet „Hauptstrasse" nicht „Hauptstraße".
Webhook prüft zuerst den Event-Volltext (eventVolltext = rekursiv alle
String-/Zahlenwerte); ohne Treffer + datei_url → belegAnalysieren() asynchron
nach dem Response (fire-and-forget). `zuordnung='manuell'` wird bei Updates/
OCR NIEMALS überschrieben (CASE-Expressions im ON CONFLICT und im UPDATE).
E2E verifiziert: Zählernummer→rw-4, Adresse→Hauptstraße 12, Testdaten weg.
OCR-CONTAINER mp-ocr (v1.18): server/ocr/ (FastAPI + Tesseract deu + PyMuPDF;
PDF→200dpi-PNG→pytesseract; Bilder direkt; /ocr {url} und /ocr/upload;
data:-URLs werden lokal dekodiert (httpx kann keine data:-URLs); 20 MB Limit).
Compose-Service ohne öffentlichen Port, nur im npm_default-Netz; deploy.sh
baut ihn serverseitig mit (`compose up -d --build`). mp-api: OCR_URL env
(default http://mp-ocr:8081), belegAnalysieren(id) (OCR-Text → Zuordnung →
rohtext ersetzen; Fallback rohtext), POST /api/belege/:id/analysieren
(adminOnly). Frontend: Belege-Tabelle mit Zuordnungs-Spalte (Ziel + „erkannt
per Zählernummer/Adresse/Manuell"), „Analysieren"-Button pro Beleg
(analysiereBeleg im Store), KostenDialog-Vorbefüllung um unitId/building
erweitert. E2E verifiziert: OCR über data-URL-PDF (reportlab-Test-PDF mit
„Zaehler-Nr: ST-0815-4711") → hs-7 zugeordnet; mp-ocr healthy.
**Erledigt 04.10. (v1.16):** SparkReceipt-Schnittstelle für gescannte Belege.
SparkReceipt bietet Webhooks (Doku: sparkreceipt.com/integrations/webhooks)
– POST auf eine URL bei document created/updated/deleted, Standard-Verzögerung
10 min, Event-Log mit „Send now". Setup beim Nutzer: SparkReceipt →
Integrationen → Webhooks → URL
`https://mieterportal.hocks.eu/api/belege/webhook?token=<SPARKRECEIPT_WEBHOOK_TOKEN>`
(Token liegt in /opt/mieterportal/portal.env, Stand 04.10.:
0e8c671ca1c4df3668bac6b2b57602f49ad0f4db8366cc42 – bei Rotation Eintrag in
portal.env ersetzen + `docker compose up -d`). NEUE TABELLE `belege`
(quelle, externe_id, haendler, betrag, waehrung, datum, jahr, kategorie,
notiz, datei_url, zugeordnet, erfasst_am; UNIQUE(quelle, externe_id)).
API: POST /api/belege/webhook (KEINE Session – Token per ?token= ODER
x-webhook-token-Header; ohne gesetztes Env → 503; toleranten Feld-Aliasen –
Payload ggf. unter body.document verschachtelt, Aliase merchant/vendor/total/
amount/date/category/file_url u. a.; delete-Events werden ignoriert;
INSERT ... ON CONFLICT (quelle, externe_id) DO UPDATE → idempotent).
PATCH /api/belege/:id (adminOnly) setzt zugeordnet. Bootstrap liefert belege
nur an die Verwaltung. Frontend: Tabs auf /kosten („Kostenpositionen" /
„Spark-Receipt-Belege", Admin-only) – Info-Box mit der Webhook-URL, Filter
Alle/Nicht zugeordnet, Tabelle mit Übernehmen-Button (öffnet KostenDialog
vorbelegt mit Betrag/Jahr/Notiz „SparkReceipt · Händler (#ID)";
onGespeichert markiert den Beleg zugeordnet) plus Beleg-Link (datei_url).
KostenDialog hat neue Props vorbefuellung/onGespeichert/triggerLabel.
E2E verifiziert (simulierte SR-Payloads): 401 ohne Token, Create, Update
selbe externe_id ohne Duplikat, Jahr-Ableitung aus Datum, Mieter 403, Admin
zugeordnet, Testbeleg per SQL entfernt. OFFEN: echtes Feld-Mapping gegen
ein echtes SparkReceipt-Event prüfen (Event-Log in SR zeigt das Payload –
ggf. Aliase in belegFelder() nachpflegen); Beleg-PDF-Download aus SR
nachrüsten, falls die datei_url ein öffentlicher Link ist.
**Erledigt 04.10. (v1.15):** Struktur „1 Haus → mehrere Wohneinheiten" in der
Verwaltung verankert + Bugfix Haus-Umbenennung. BUGFIX (wichtig): der FK
`costs_building_fkey` aus v1.14 hatte kein ON UPDATE CASCADE → jede
Haus-Umbenennung (PATCH /api/buildings/:name) schlug mit FK-Verletzung fehl
(„Interner Serverfehler"), sobald Kostenpositionen existieren. Fix in db.js:
Constraint per DO-Block idempotent auf ON UPDATE CASCADE neu gesetzt (DROP +
ADD; SCHEMA läuft als eine Simple-Query ohne Parameter, DO-Blöcke sind darin
erlaubt). E2E-Rundreise verifiziert: Umbenennen zieht units + meters +
costs mit, Rückbenennung sauber. UI (Bestand.tsx komplett überarbeitet):
Haus-Karten enthalten jetzt die 1:n-Struktur sichtbar – pro Haus
Belegungsfortschritt (Balken „X von Y Wohneinheiten angelegt", amber bei
Über-Soll, Hinweistext), eingebettete sortierte Wohnungsliste (Etage, dann
Nummer numerisch) mit Status/Mieter/Warmmiete/Bearbeiten, „+ Wohnung"
direkt pro Haus (festesHaus-Prop → Haus-Select vorbelegt und disabled,
Dialogtitel passt sich an). Globaler „Neue Wohnung"-Button oben bleibt mit
freier Hauswahl. Verwaiste Wohnungen (Haus existiert nicht) werden separat
mit Korrekturhinweis gelistet. Server: unverändert – POST/PATCH /api/units
prüft bereits, dass das Ziel-Haus in buildings existiert.
**Erledigt 04.10. (v1.14):** Steuermodul „Steuer & Kosten" (/kosten, Nav zwischen
Zählerstände und Dokumente, für beide Rollen sichtbar). Neue Tabelle `costs`
(building NOT NULL → buildings.name, unit_id NULL = hausweite Position, jahr,
kostenart, betrag, umlegbar BOOLEAN, notiz, erfasst_am), idempot vorbefüllt
mit 16 Demo-Positionen 2025/2026 (`seedeKosten()`). API (adminOnly):
POST/PATCH/DELETE /api/costs(/:id); Validierung: Kostenart aus KOSTEN_ARTEN
(15 Werte), jahr 2000..aktuell+1, betrag ≥ 0, Haus muss in buildings existieren
– bei gesetzter unitId folgt das Haus IMMER der Wohnung (eingereichtes Haus
wird überschrieben, vgl. meters). Bootstrap: Admin alle (jahr DESC), Mieter nur
eigene Wohnung. Frontend: Summenkarten (umlegbar/nicht umlegbar/Gesamt über die
Filterauswahl), Filterchips Umlegbarkeit + Selects Jahr/Haus (Admin), Tabelle
mit Kostenart/Notiz, Haus, Wohnung, Jahr, Badge, Betrag; Admin-Dialog
(Anlegen/Bearbeiten) mit Kostenart-, Umlegbarkeits-, Jahr-, Betrag-, Haus-
(disabled bei Wohnungswahl) und Wohnung-Select („Ganzes Haus" = Sentinel
'__haus'). Store: addKosten/updateKosten/deleteKosten. E2E verifiziert:
Hausableitung, Patch, Validierung 400 (Kostenart/Haus/Jahr/Betrag), Mieter
403, Löschen, Testdaten aufgeräumt.
**Erledigt 04.10. (v1.13):** Echter PDF-Upload für das Dokumentenmodul. Neue
Dep multer (^2.0.2, server/package-lock.json liegt jetzt im Repo, Docker
baut mit `npm ci`). DB: `documents` um `file_name`/`file_path` erweitert
(idempotent per `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` in db.js SCHEMA –
ACHTUNG: SCHEMA läuft als eine pool.query mit mehreren Statements,
funktioniert nur über das Simple-Query-Protokoll ohne Parameter). API:
POST /api/documents/:id/upload (adminOnly, multipart Feld „file", nur PDF,
25 MB, Datei heißt `<id>.pdf` im UPLOAD_DIR und ERSETZT die alte Datei;
`size` wird aus den echten Bytes gesetzt, `formatBytes` deutsche Anzeige);
GET /api/documents/:id/download (auth: Verwaltung immer, Mieter nur eigene
Wohnung oder Hausdokument; `path.basename` gegen Pfad-Traversal); DELETE
löscht die Datei best-effort mit. Ablage: UPLOAD_DIR env (default
./uploads), im Compose als named Volume `mp_uploads` an /app/uploads –
überlebt Container-Rekreation. nginx: `client_max_body_size 25m` für
location /api/ (war 256k!), proxy_read_timeout 60s. Frontend: Datei-Input
im Dokument-Dialog (Client-Validierung PDF/25 MB), Upload läuft NACH
Anlegen/Patchen über apiUpload (FormData, kein Content-Type setzen),
`syncDokument` ersetzt den State mit der Server-Fassung; PDF-Button ist
jetzt echter Download-Link, ohne Datei deaktiviert mit Hinweis.
E2E verifiziert: Upload/Download Byte-identisch (Admin + Mieter eigene
Wohnung), fremde Wohnung 403, Nicht-PDF 400, Upload ohne Doc 404,
Löschen entfernt Datei, Testdaten aufgeräumt.
**Erledigt 04.10. (v1.12):** Dokumente-CRUD für die Verwaltung (Schwerpunkt
Mietverträge). API (adminOnly): POST/PATCH/DELETE /api/documents(/:id) mit
Validierung (Titel nicht leer, Kategorie aus den 5 erlaubten Werten, unitId
null oder vorhandene Unit, Datum default `deutschesDatum()`, size default '–',
id `d-${Date.now()}`). Frontend: Admin-Dialog auf /dokumente (Titel, Kategorie,
Wohnung inkl. „Alle Häuser", Datum, Größe optional; Radix-Select braucht für
„Alle Häuser" den Sentinel '__alle' – leerer value ist unzulässig), pro Zeile
Bearbeiten/Löschen (window.confirm), Store-Aktionen addDokument/updateDokument/
deleteDokument + apiDelete. Mieter-Ansicht unverändert. E2E verifiziert:
Anlegen/Patchen/Löschen (Umlaute sauber), Validierung 400, Mieter 403,
Testdaten aufgeräumt. Bewusst NICHT gebaut: echter PDF-Upload (kein Storage
im Stack, Demo-Charakter – Download-Button bleibt dekorativ).
**Erledigt 04.10. (v1.11):** KI-Antworten auf Schadenmeldungen via n8n/Ollama.
Kette: Mieter-Ticket → mp-api `kiBenachrichtigen()` (POST an n8n, fire-and-
forget) → n8n-Workflow `mpKiAntwort0001` (Webhook POST `mp-schadensmeldung`,
responseMode onReceived; Knoten: Webhook → Code „KI-Antwort generieren
(Ollama)" → Code „Antwort ins Portal posten") → Ollama
`http://192.168.188.100:11434/api/generate` Modell `qwen2.5:7b` → POST
`https://mieterportal.hocks.eu/api/ki-antwort` (KI_API_TOKEN-Bearer) →
Nachricht als „Hinterstein Immobilien (KI-Assistent)" im Ticket-Thread + Mail
an Mieter. Konfig in /opt/mieterportal/portal.env: KI_API_TOKEN +
N8N_TICKET_WEBHOOK. n8n 2.41-Hürden beim direkten DB-Import: publizierte
Knoten in `workflow_history` (versionId = activeVersionId, authors='{}'
NOT NULL), `shared_workflow`-Zeile (projectId M7v7qD1yefYEQ9Dn, role
workflow:owner), Webhook-Knoten braucht node-level `webhookId`, fehlgeschlagener
`workflow_publication_outbox`-Eintrag blockiert Reaktivierung (Zeile löschen +
`docker restart n8n`). E2E verifiziert: KI-Antwort im Thread + Mail sauber.
**Erledigt 04.10. (v1.10):** Bestandsverwaltung. Neue Tabelle `buildings`
(PK name; Pflicht: Adresse, Baujahr, Stockwerke, Etagenhöhe, Wohneinheiten,
Quadratmeter), idempotent vorbefüllt mit den 3 Demo-Häusern (`seedeHaeuser()`).
Wohnungen bleiben über `units.building` (Text = Hausname) verknüpft; Haus-
Umbenennung zieht units + meters in einer Transaktion mit. API (adminOnly):
POST/PATCH /api/buildings, POST/PATCH /api/units mit Validierung. Frontend:
neue Admin-Seite /bestand („Häuser & Wohnungen", Nav hinter Zählerstände) mit
Haus-Karten (inkl. „angelegt: X von Y Wohneinheiten") und gefilterter
Wohnungsliste, je Anlegen/Bearbeiten-Dialog. Robustheit: uncaughtException/
unhandledRejection-Handler in index.js – defekte JSON-Bodies von außen haben
den mp-api-Prozess gekillt (502er), jetzt 500 + Dienst läuft weiter.
E2E verifiziert: anlegen/editieren/umbenennen (Kaskade), Validierung 400,
Mieter 403. Achtung Testfall: curl verbindet --data-binary + -d mit „&"
→ invalider JSON → 500.
**Erledigt 04.10. (v1.9):** Zähler-Modul + Registrierungs-Mail + getrennter Name.
Neue Tabelle `meters` (Zählernummer, Art Strom/Wasser/Gas/Wärme/Sonstiges,
Stand, Stand-Datum, Wohneinheit nullable, Haus, Pflege-Zeitstempel) mit
idempotentem Demo-Seed (`seedeZaehler()` läuft bei jedem Start, füllt nur bei
leerer Tabelle – auch nachträglich auf Live-DB). API: POST/PATCH /api/meters
(adminOnly), Bootstrap liefert meters (Admin: alle, Mieter: nur eigene Wohnung);
Haus wird aus der Wohneinheit abgeleitet, bei Allgemeinzählern direkt gepflegt.
Frontend: neue Seite /zaehler (Kartenraster, Filter nach Haus + Art, Admin-
Dialog zum Anlegen/Bearbeiten), Nav-Eintrag mit Gauge-Icon, Store-Aktionen
addZaehler/updateZaehler. Registrierung: neuer Mieter löst HTML-Mail an die
Verwaltung aus (Name/Benutzername/E-Mail/Wohnung, Button zur Benutzerverwaltung);
Formular jetzt mit getrennten Feldern Vorname/Nachname + Benutzername.
E2E verifiziert: Zähler-CRUD + Rechte (403 Mieter), Registrierung inkl.
Mail-Zustellung an beide Verwaltungspostfächer, Umlaute sauber.
**Erledigt 02.10. (v1.8):** Schadensmeldungen starten jetzt automatisch eine
Unterhaltung (Thread „Schadensmeldung: …" mit erster Nachricht inkl. Kategorie/
Priorität, ungelesen für die Gegenseite) – Antworten darauf lösen über
`/api/threads/:id/messages` wie gehabt Mails an Verwaltung + Mieter aus.
Zusätzlich Eingangsbestätigungs-Mail an den Mieter bei jeder Meldung.
Verwaltungs-Mails gehen jetzt an zwei echte Postfächer: joseph@hinterstein-immo.de
(Benutzer j.hinterstein, Seed + Live-DB geändert – .de ist laut Betreiber die
richtige Adresse) und axel@hocks.eu (neuer Verwaltungsbenutzer a.hocks, Passwort
0815, Seed + Live-DB). E2E verifiziert:
Meldung + Antwort → je Mail an beide Postfächer zugestellt (Postfix status=sent);
Mieter-Adressen der Demoseeds sind example.de und bouncen bewusst.
**Encoding-Wissen (02.10.):** Umlaute in Mails waren nie kaputt – die früheren
Test-Mails enthielten bereits � (U+FFFD), weil curl unter Git Bash Umlaute in
`-d '…'` über die ANSI-Codepage schickt (0xFC = invalid UTF-8 → ersetzt).
Kette Browser → API → DB → mail-api → Postfix ist UTF-8-sauber (per Hex-Check
c3bc in DB bestätigt). Bei API-Tests mit Umlauten: JSON als UTF-8-Datei schreiben
und `curl --data-binary @datei` verwenden.
**Erledigt 02.10. (v1.7):** Responsive Layout repariert. Sidebar skaliert jetzt in zwei
Stufen (`w-64` ab `lg`, `xl:w-72`) statt fixer 288 px. Mobile Ansicht (Top-Bar +
horizontal scrollende Nav) gilt jetzt bis 1024 px (`md:hidden` → `lg:hidden`) – vorher
stand zwischen 768–1024 px die volle Sidebar und quetschte Inhalt + Fußzeile.
Sidebar-Fußzeile bricht um (`flex-wrap`, `truncate`-sichere Buttons), `main`-Padding
folgt dem `lg`-Breakpoint. Tailwind war bereits das CSS-Framework (kein Wechsel nötig).
Screenshots: `preview/09-mobile-900px.png`, `preview/10-desktop-1280px.png`.
**Erledigt 02.10. (v1.6):** Login-Race behoben: `setMe` läuft jetzt erst nach
`ladeAlles()` (Start/Login/Registrierung) – vorher rendereten die Seiten gegen
noch leere Daten → Absturz → schwarzer Bildschirm, erst Reload half. Zusätzlich
`FehlerGrenze` (ErrorBoundary) in `App.tsx`: Render-Fehler zeigen Hinweis +
Reload-Button statt schwarzem Bildschirm.
**Erledigt 02.10. (v1.5):** Lokales Datenbank-Backup: `backup/vps-db-backup.cmd`
holt nächtlich (Aufgabenplanung „Mieterportal DB-Backup", täglich 03:17 Uhr,
Benutzer ah, interaktiv) per plink ein `pg_dump | gzip` vom Container mp-db und
legt es nach `D:\VPS_Backup\mieterportal\mieterportal-db-YYYY-MM-DD.sql.gz`
(14-tägige Aufbewahrung via forfiles, Log: `backup.log`). Probelauf OK
(11 KB, Dump-Format geprüft). Einschränkung: läuft nur, wenn der Rechner an
und Benutzer ah angemeldet ist (Z:-Laufwerk mit dem SSH-Key, Pageant).
**Erledigt 02.10. (v1.0):** Portal komplett neu aufgesetzt (5 Module, Mieter-/Verwaltungsansicht,
KI-Branding: Logo/Hero/Illustration), Docker-Setup (Dockerfile, nginx, compose),
Repo in git.hocks.eu angelegt, Deploy-Workflow + Dark Mode + beispieldaten.py aus dem
Schulportal übernommen, mail-server-Struktur angelegt (noch nicht live verkabelt).
**Erledigt 02.10. (v1.0 live):** Deployment auf hocks.eu durchgeführt: Container `mp-portal`
läuft (`/opt/mieterportal`, compose + gemountetes dist), NPM-Proxy-Host id 7 + LE-Zertifikat
npm-9 eingerichtet (per certbot/webroot, DB-Insert, 7.conf aus 5.conf abgeleitet, nginx reload).
Live geprüft: `https://mieterportal.hocks.eu/` → HTTP 200, Titel korrekt, SPA-Routing
(`/mietkonto`) 200, HTTP→HTTPS-Redirect 301, Zertifikat CN=mieterportal.hocks.eu gültig
bis 31.12.2026. Zertifikatsdateien liegen im npm-Container unter `/etc/letsencrypt/live/npm-9/`
(Backup der Zertifikate auf dem Host: `/root/le-mp/`). Fail2Ban-Ban nach frühen
Fehlversuchen ist nach ~40 min automatisch ausgelaufen.
**Erledigt 02.10. (v1.1 live):** Rebrand auf „Hinterstein Immobilien" (Kunden-Logo,
Joseph-Hinterstein-Portrait auf der Login-Seite, Kontakt-Banner), echtes
Authentifizierungssystem (Login-Pflicht, Gruppen Verwaltung/Mieter, SHA-256,
Session 8 h), Mieter-Registrierung per einmaligem Code, Benutzerverwaltung als
6. Modul (`/benutzer`), Demo-Zugänge mit Passwort `0815`. Mail-Stack auf dem Server
aktiviert (`/opt/mp-mail`, Testmail + API-/send verifiziert). Store-Key auf
`hinterstein-mieterportal-v2` gehoben. Live deployed und geprüft (HTTP 200, Titel
„Hinterstein Immobilien · Mieterportal").
**Erledigt 02.10. (v1.2 live):** E-Mail-Benachrichtigungen an das Portal
angeschlossen: nginx-Proxy `/api/mail/send` mit serverseitig injiziertem Token
(envsubst-Template + `portal.env` auf dem Server), `src/lib/mail.ts` sendet bei
neuen Schadensmeldungen (→ Verwaltung) und Nachrichten/Antworten (→ Gegenstelle).
E2E-Versand live verifiziert (versandt: true), GET auf den Endpunkt 403.
**Erledigt 02.10. (v1.3 live):** Serverseitige Datenbank: PostgreSQL 16 (`mp-db`,
Volume `mp_pgdata`) + Node-API (`mp-api`, Express + pg, eigenes Image via
deploy.sh `--build`). Schema 11 Tabellen (idempotent), Seed einmalig aus
`server/seed/generated.json` + `seed-data.js` (Verwaltungs-E-Mail auf echtes
Postfach axel@hocks.eu gesetzt). Frontend komplett auf API umgestellt:
`src/lib/api.ts`, Store lädt `/api/bootstrap`, kein localStorage mehr (außer
Theme), Ladebildschirm während Session-Check. Rollenrechte serverseitig
durchgesetzt und getestet (Mieterin sieht nur hs-7: 12 Buchungen/2 Tickets,
403 auf fremde Unterhaltung; Verwaltung sieht alles: 552 Buchungen/8 Tickets).
Registrierung, Code-Erzeugung, Ticket inkl. Mail-Benachrichtigung (status=sent
an axel@hocks.eu) E2E verifiziert; Testdaten danach bereinigt. deploy.sh baut
das API-Image auf dem Server; `.env`-Symlink auf portal.env für
Compose-Interpolation beachten (POSTGRES_PASSWORD!).
**Erledigt 02.10. (v1.4 live):** HTML-E-Mail-Benachrichtigungen im Schulportal-
Stil: `baueHtmlMail`/`buttonHtml` in `server/src/mail.js` (600-px-Karte,
Kopfzeile Hinterstein-Farben, Zeilen, Status-Box, Zitat, Aktions-Button, Footer),
immer mit Plain-Text-Fallback (multipart/alternative). Umgesetzt für
Schadensmeldung, neue Unterhaltung und Antwort. Vorschauen lokal renderbar
(`mail-vorschau*.html`, Screenshots in `preview/08-*.png`). Live getestet:
Zustellung an axel@hocks.eu status=sent, Test-Ticket bereinigt.
**Offen:**
- Zertifikatserneuerung prüfen (per 01.12.2026): NPM-Renewer sollte cert 9 über HTTP-Challenge
erneuern; sonst certbot/webroot + Kopie nach npm-9 wiederholen.
- Demo-Empfängeradressen der Mieter (example.de) durch echte ersetzen, sobald bekannt.
- Backup ggf. serverseitig ergänzen (zweite Sicherungslinie, falls lokaler Rechner
nachts aus ist), Backup-Wiederherstellung einmal testen.
- Sicherheit für Produktion: Passwort-Hash serverseitig salten (bcrypt/argon2), CSRF-Token,
Rate-Limiting auf /api/auth/login.
- Mögliche nächste Schritte: E-Mail-Verifizierung bei Registrierung, Dokumenten-Download
mit echten Dateien.
## 9. Sonstiges
- User kommuniziert auf Deutsch; alle UI-Texte und Antworten auf Deutsch.
- Dateien außerhalb des Workspaces immer erst in den Workspace kopieren.
- Vorsicht mit destruktiven Aktionen (Explizite Bestätigung einholen).