Files
Mieterportal/kimi.md
T

829 lines
55 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.36):** Mieterverwaltung. Mieter ist ein eigenes Objekt
1:1 zur Wohnung (Tabelle mieter: Name, Vorname, Telefon, E-Mail, Kalt-/Warmmiete,
Vertragsstatus aktiv|befristet|gekuendigt inkl. „gekündigt zum/von", Kaution
Höhe + Art bar|ueberwiesen|sonstiges (Freitext), Zahlung komplett|raten inkl.
Ratenanzahl, Mietzeitraum Start/Ende). Units schlank: Kaltmiete/Warmmiete/Mieter
aus API + Formularen entfernt (Spalten bleiben physisch), Status jetzt
vermietet|leerstand|unklar (Migration: frei→leerstand, units.tenant→mieter-Rows,
läuft bei Start automatisch). Server: Mieter-CRUD adminOnly (POST/PATCH/DELETE
/api/mieter, Anlage/Löschung setzt Unit-Status), Dokument-Upload pro Mieter +
Kategorie (POST /api/mieter/:id/dokumente?kategorie=, 1 Slot je Kategorie:
Personalausweis, SCHUFA-Auskunft, Bankauszug, Sonstiges; PDF+JPG/PNG; Upload
ersetzt, DELETE kaskadiert Dateien), Preview liefert Content-Type per Endung
(Bilder rendern in Lightbox/Thumbnails). Bootstrap: Mieter (Admin alle, Mieter
nur eigene Wohnung – PII-Schutz). Abrechnung + Mietvertrags-Aktivierung ziehen
Mietdaten aus mieter-Tabelle. Frontend: Seite /mieter (Listenansicht + Suche,
Vertrags-/Kaution-Badges), einspaltiger Dialog mit Dokumenten-Thumbnails
(Mini-iframe → PdfLightbox), Bestand/Wohnungsformular schlank, Dashboard/
Payments/BenutzerVerwaltung/Mietvertraege umgestellt. Deployt, E2E-getestet
(CRUD, Sonderfelder, Upload PDF+PNG, Kaskaden, Migration 44 Mieter live).
**Erledigt 04.10. (v1.37):** KI-Assistent (Verwaltung fragt, N8N + Kimi
antworten). Server (server/src/assist.js): POST /api/assist (auth+adminOnly,
leitet Frage+Verlauf an N8N_ASSIST_WEBHOOK weiter, x-assist-secret-Header,
90 s Timeout, erwartet JSON {antwort}) + 6 Tool-Endpunkte
/api/assist/tools/{leerstaende,mieteinnahmen,offene-schaeden,mieter-kontakt,
zaehler-anomalien,kosten} geschützt per Header x-assist-key=ASSIST_API_KEY,
kompakte JSON-Antworten direkt aus Postgres. Frontend: Seite /assistent
(Chat-UI, Verlauf nur Session, Beispielfragen, Admin-only). Env
(docker-compose + portal.env auf Server): N8N_ASSIST_WEBHOOK,
ASSIST_SHARED_SECRET, ASSIST_API_KEY (letztere zwei auf Server gesetzt).
N8N: importierbarer Workflow n8n/assist-workflow.json (Webhook → AI-Agent mit
Kimi OpenAI-kompatibel https://api.moonshot.ai/v1, Modell kimi-k2.6 → 6
HTTP-Tools → Respond {antwort}); Setup-Doku n8n/README-assistent.md.
Deployt, E2E-getestet (Tools 401/200, /api/assist sauberer 503 ohne Webhook).
**Erledigt 04.10. Abend (v1.37 fertig):** N8N-Workflow „Mieterportal – KI-Assistent
(Kimi)" serverseitig vollständig eingerichtet und aktiviert – direkt in die
n8n-SQLite-DB eingespielt (n8n 2.x API-Import unzuverlässig, 401). Finaler Aufbau
(Code-Node-Ansatz statt langchain, keine Credentials): Webhook POST mp-assist
(responseNode, wichtig: Property webhookId = Node-ID auf Root-Ebene, sonst 404
„not registered") → Code-Node prüft x-assist-secret, Kimi-Tool-Calling per
this.helpers.httpRequest (max. 6 Runden, kein temperature-Param – k2.6 erlaubt
nur 1, kein URLSearchParams in der Sandbox, 429 wird abgefangen) → Respond
{antwort}. PORTAL im Code = http://mp-api:3000 (internes npm_default-Netz).
Drei Geheimnisse als Literale im Code-Node (nicht im Repo, siehe
n8n/README-assistent.md). N8N_API_KEY aus der n8n-Compose entfernt (ungenutzt).
E2E live: „Wie hoch sind die monatlichen Mieteinnahmen?" → korrekte Antwort
(Warm 18.684 € / Kalt 17.686 €) via Portal-Login.
**Erledigt 04.10. Abend (Design):** Alle Formulare 2-spaltig auf shared Layout
umgebaut: Neue Komponente src/components/forms/FormSection.tsx (FormSection =
Karte mit Teal-Icon-Chip + Sora-Überschrift, Kind-Elemente in
grid md:grid-cols-2; Wide = md:col-span-2 für Checkboxen/Textareas/Uploads;
FormFehler). Umgebaut: Bestand (Haus+Wohnung), MieterVerwaltung,
Zaehler (Zähler+Zählerstand), Kosten, Tickets, BenutzerVerwaltung (Benutzer+
E-Mail), Mietvertraege (Vermieterdaten+Vertrag), Documents, Profil (Karten-
Raster 2-spaltig). Dialoge auf sm:max-w-2xl verbreitert. Fonts: Inter (Body) +
Sora (Headings, font-display) via Google Fonts in index.html + tailwind.config
fontFamily. Deployt (dist-Tausch auf Server, mp-portal-Neustart nötig wegen
Bind-Mount-Inode) und verifiziert (CSS/JS 200, Fonts im HTML, Login 200).
**Erledigt 04.10. Abend (v1.38):** KI-Assistent zusätzlich als einblendbares
Widget: AssistChat (Chat-Logik aus Assistent.tsx extrahiert, shared) +
AssistWidget (vertikale Teal-Lasche fixed rechts, mittig, oeffnet Slide-over-
Panel 430px mit Teal-Header; schließt per Escape und bei Seitenwechsel; nur
admin, auf /assistent ausgeblendet). Widget global in AppLayout eingebunden.
Deployt (direktes Kopieren ins bestehende dist-Verzeichnis – mv bricht den
Bind-Mount-Inode!) und verifiziert (CSS/JS/Login 200).
**Erledigt 04.10. (v1.35):** Mietvertragsmodul (Verwaltung, Navigation
„Mietverträge", Admin-only). Workflow: anlegen → Entwurf-PDF → Versand an
zukünftigen Mieter → Upload unterschriebenen Vertrag → manuell aktivieren
(Mieterkonto wird angelegt, Wohnung als vermietet geführt, Zugangsdaten
per E-Mail). DB (db.js): einstellungen (Schlüssel-Wert, Vermieterdaten:
vermieter_name/strasse/hausnummer/plz/ort/telefon/email/bankname/iban) +
mietvertraege (Status entwurf|versendet|unterschrieben|aktiv, Dokument-
und Benutzerbezüge als TEXT ohne harten FK, unit_id mit FK). Server:
server/src/vertrag.js (pdfkit-Mustervertrag: Parteien, §1 Mietobjekt aus
Bestand, §2 Mietzeit, §3 Miete/Nebenkosten/Zahlung, §4 Kaution ≤3 Nettokalt-
mieten, §5 Gebrauch/Instandhaltung/Schönheitsreparaturen/Rauchwarnmelder,
§6 Schlussbestimmungen, Unterschriftsfelder; erzeugeVertragsPdf legt/ersetzt
Dokument „Mietvertrag" der Wohnung). Endpunkte (alle adminOnly): CRUD
/api/mietvertraege[n], POST /:id/entwurf, /:id/versenden (PDF-Anhang via
mail.sendeMail + CC Verwaltung, Status→versendet), /:id/unterschrift
(multer single('file'), Dokument d-mvu-<id>, Status→unterschrieben),
/:id/aktivieren (Benutzername aus Vor-/Nachname [Umlaute→ae/oe/ue, unique
mit Zähler], Passwort sha256("0815"), users+einheit_id+telefon anlegen,
units.tenant/status setzen, Mail mit Zugangsdaten, Status→aktiv),
GET/PUT /api/einstellungen (Whitelist vermieter_*). Bootstrap liefert
mietvertraege + einstellungen (via vermieterDaten, kurze Keys).
DELETE /api/documents löst Verweise in mietvertraege; Wohnungs-Blockerliste
um mietvertraege erweitert. Frontend: types (Mietvertrag/MietvertragForm/
VermieterDaten), store (Bootstrap-Felder + 8 Aktionen, apiPut neu in
lib/api.ts), Seite src/pages/Mietvertraege.tsx (Listenansicht Standard +
Suche, Vermieterdaten-Dialog, Vertrags-Dialog mit Miet-Vorbefüllung aus
der Wohnung, Aktionen: Entwurf/PdfLightbox, Senden, Upload, Aktivieren
mit Confirm + Benutzername-Alert, Löschen; Warnbanner bei unvollständigen
Vermieterdaten), Anleitung neuer Abschnitt „Mietverträge".
E2E: Vertrag anlegen→Entwurf-PDF (Vorschau lädt)→versenden (CC-Verwaltung
in Mail-API-Log)→Unterschrift-Upload (Test-PDF)→aktivieren (Benutzer
vorhanden, Wohnung vermietet, Login mit 0815 möglich)→Aufräumen.
**Erledigt 04.10. (v1.34):** SparkReceipt-Zuordnung nutzt Mietobjekt-Adresse.
Problem: auf eingehenden Rechnungen steht als Adresse der Rechnungsempfänger
(Rott 250 = eigene Verwaltungsadresse, als Haus gepflegt) – die sekundäre
Adress-Zuordnung matchte damit immer das falsche Haus. Lösung: Gebäude
bekommen das Kennzeichen eigenobjekt (DB: buildings.eigenobjekt BOOLEAN,
Default FALSE); belegAutoZuordnung schließt solche Häuser aus
(WHERE eigenobjekt IS NOT TRUE) – primäre Stromzählernummer-Matchung
bleibt unverändert. POST/PATCH /api/buildings und mapBuilding liefern/
speichern eigenobjekt; PATCH merged via mapBuilding-Altbestand, bleibt
also erhalten wenn nicht mitsendet. Bestand.tsx: neuer Abschnitt
„Zuordnung Belege" mit Checkbox „Eigenobjekt / Rechnungsempfänger"
(Erklärung im Hint), Badge „Eigenobjekt" in Haus-Karte und Listenzeile.
Verwaltung muss Rott 250 einmalig als Eigenobjekt markieren; bereits
falsch zugeordnete Belege lassen sich einzeln per „Analysieren" neu
zuordnen (Schutz nur für 'manuell'). E2E: Webhook mit nur Empfängeradresse
→ keine Zuordnung statt Rott 250; mit Mietobjekt-Adresse im Text →
Zuordnung aufs Mietobjekt.
Fix in v1.33 mitnachgetragen: DELETE /api/tickets/:id löscht jetzt in
einer Transaktion zuerst messages, dann threads (threads.ticket_id hat
KEIN ON DELETE CASCADE), dann das Ticket – vorher 500 durch FK-Verletzung.
Blocker-Liste von DELETE /api/units/:id um transactions, tickets,
threads, registration_codes und belege erweitert (alle FKs auf units.id
ohne Cascade, vorher möglicher 500 statt 400er-Hinweis).
**Erledigt 04.10. (v1.33):** Einträge in allen Listen löschbar. Neue
adminOnly-DELETE-Endpunkte in server/src/index.js: /api/tickets/:id
(kaskadiert threads + messages), /api/meters/:id (kaskadiert meter_readings),
/api/belege/:id, /api/buildings/:name und /api/units/:id – letztere zwei
verweigern mit 400 + deutscher Blocker-Liste (Haus: Wohnungen/Zähler/Kosten;
Wohnung: Zähler/Dokumente/Kosten/Mieter), /api/users/:id (eigenes Konto und
letzten aktiven Verwalter geschützt, sessions vorher gelöscht). Store:
deleteTicket, deleteZaehler, deleteHaus, deleteWohnung, deleteBeleg,
loescheBenutzer (apiDelete + lokaler State-Filter, threads bei Ticket-Delete
mitgefiltert). Frontend: rote Trash2-Löschbuttons mit window.confirm +
alert(err) in Bestand (Haus Karte+Liste, Wohnungszeile), Zaehler (Karte+Liste),
Kosten BelegeListe, Tickets (admin, warnt vor Mitlöschung der Kommunikation),
BenutzerVerwaltung (eigenes Konto deaktiviert); Buttons nur bei role=admin,
in Mieter-sichtbaren Seiten (Zaehler, Tickets) zusätzlich gegated. E2E: 400-
Blocker beim Haus mit Wohnung/Zähler, Kaskaden-Löschung bis Haus weg,
Ticket anlegen+löschen, fremden Testbenutzer löschen, eigenes Konto → 400.
**Erledigt 04.10. (v1.32):** Deutsche Datumsformate in allen Modulen.
NEU src/lib/format.ts: fmtDatum (ISO YYYY-MM-DD → TT.MM.JJJJ, deutsche Werte
durchreichen) + fmtDatumZeit (mit Uhrzeit) – eingesetzt in Zaehler (Karte,
Liste, Verlauf), Dashboard (Ankündigungen, Tickets), Documents, Kosten
(Belegdatum), Messages, Tickets, BenutzerVerwaltung (letzterLogin), Profil,
PdfLightbox. Eingabe-Fix: StandEintragenDialog wandelt date-Feld jetzt
ISO→deutsch (vorher roh gesendet); Server normalisiert zusätzlich in
POST /api/meters und POST /api/meters/:id/readings via datumDeutsch()
(ISO → TT.MM.JJJJ vor Validierung/Speicherung). E2E: ISO-Eintrag
„2026-01-15" wird als „15.01.2026" gespeichert.
**Erledigt 04.10. (v1.31):** PDF-Lightbox für erzeugte/gespeicherte PDFs mit
Druck-, E-Mail- und Download-Option. Server: dokumentMitDatei-Helper
(Rechte/Dateiprüfung, basename gegen Pfad-Traversal) für download + NEU
GET /api/documents/:id/preview (inline, Content-Type application/pdf) und
POST /api/documents/:id/email (admin; Empfänger „an" oder automatisch Mieter
der Wohnung, immer CC Verwaltung, HTML-Karte + PDF als base64-Anhang via
mail.sendeMail). mail-server/api/server.py: Anhang-Support (attachment_b64/
attachment_filename/attachment_mime, Limit ~20 MB, Outbox-tauglich) – Datei
liegt BIND-MOUNTED in /opt/mp-mail/api/server.py, Deploy = pscp + docker
restart mp-mail-api (KEIN Image-Rebuild). Achtung: wget im Container auf
„localhost" → IPv6 refused, immer 127.0.0.1 verwenden; Port 8080 ist NICHT
auf dem Host veröffentlicht. Frontend: src/components/PdfLightbox.tsx
(iframe-Preview, iframe.contentWindow.print(), E-Mail-Formular), Store
sendeDokumentEmail; eingebunden in Kosten.tsx AbrechnungDialog (Vorschau-
Button, ergebnis speichert jetzt DocumentItem statt nur id) und Documents.tsx
(Vorschau-Button je Zeile mit hasFile).
**Erledigt 04.10. (v1.30):** Wohnungsformular einspaltig (Analogie Haus-Formular
v1.26): Abschnitte Zuordnung (Haus, Wohnungsnummer) / Ausstattung (Etage,
Wohnfläche, Heizungsart) / Miete (Kalt-/Warmmiete) / Mieter (Name, Status),
gemeinsame Helfer Abschnitt + Feld, Dialog max-h-90vh overflow-y-auto.
**Erledigt 04.10. (v1.29):** Heizungsart-Dropdown in Haus- und Wohnungsverwaltung;
Listenansicht ist jetzt Standard in allen Modulen. DB: buildings + units je
ALTER heizungsart TEXT DEFAULT ''; Server HEIZUNGSARTEN-Liste + heizungsartValid
(leer oder Listenwert), POST/PATCH /api/buildings und /api/units persistieren
das Feld, mapBuilding/mapUnit liefern es (Fallback ''). Frontend:
HEIZUNGSARTEN/Heizungsart in types (9 gängige Formen), Select „Nicht angegeben"
Sentinel '__keine' in beiden Dialogen; Wohnungs-Neuanlage übernimmt die
Heizungsart automatisch aus dem gewählten Haus (waehleHaus). Anzeige: Haus-Karte
(dl-Zeile mit Flame-Icon, nur wenn gesetzt) + Listen-Detailzeile.
Listenansicht-Default: Bestand.tsx + Zaehler.tsx ansicht-useState 'liste'.
Anleitung aktualisiert.
**Erledigt 04.10. (v1.28):** Zählerübersicht mit Suche + Listenansicht
(Analogie zur Hausübersicht v1.26): Suchfeld über Nummer/Art/Haus/Wohneinheit
(mit Trefferzähler), Toggle Karten/Liste; Liste = Tabelle (Art-Badge mit Icon,
Nummer, Haus, Wohneinheit, Stand rechts tabular-nums, Stand am, Anomalie-Icon,
Aktionen), responsive via hidden sm/md/lg. Bestehende Filter Haus/Art bleiben
und kombinieren mit der Suche.
**Erledigt 04.10. (v1.27):** Zähleranlage ohne Haus-Feld, mit Ablesedatum.
POST /api/meters akzeptiert standAm (Validierung via zaehlerVerlauf.parseDatum,
Default heute); stand_am + erster Verlaufseintrag (erstbefund) nutzen das Datum.
Frontend ZaehlerDialog: Haus-Textfeld entfernt – Haus folgt der Wohneinheit,
nur bei „Allgemeinzähler (ohne Wohnung)" erscheint eine Haus-Auswahl
(buildings-Select, Anzeige b.adresse). Ablesedatum (type=date, Default heute,
Pflicht) nur bei Neuanlage; Frontend wandelt YYYY-MM-DD → TT.MM.JJJJ. WICHTIG: parseDatum ist ein
lenient Parser (Überlauf 32.13. → Feb Folgejahr!) – für Benutzereingaben
zusätzlich zaehlerVerlauf.datumGueltig (Exaktheits-Roundtrip, nur
dd.mm.yyyy/yyyy-mm-dd) – eingesetzt in POST /api/meters und
POST /api/meters/:id/readings.
**Erledigt 04.10. (v1.26):** Haus-Formular ohne Namensfeld, Adresse in Einzelfelder.
buildings um strasse/hausnummer/plz/ort/lage/wohnflaeche/grundstuecksflaeche
erweitert (7 idempotente ALTERs, quadratmeter bleibt Alt-Spalte); name (PK) und
adresse (Anzeige) werden serverseitig aus Straße+Hausnummer bzw. komplett
abgeleitet (hausNameAus/hausAdresseAus, POST + PATCH). Start-Migration
haeuserFelderMigrieren zerlegt Alt-Adressen („Straße Nr., PLZ Ort") via
adresseZerlegen und zieht wohnflaeche aus quadratmeter nach (Log-Zeile
„Häuser: N Adresse(n) in Einzelfelder zerlegt"). pruefeHaus validiert PLZ
(5 Ziffern), Pflicht: Straße/Hausnummer/PLZ/Ort/Baujahr/Stockwerke/Etagenhöhe/
Anzahl Wohneinheiten/Wohnfläche; Lage + Grundstücksfläche optional.
Haus-Formular einspaltig in Abschnitten (Adresse/Gebäude/Flächen), Karte und
Liste zeigen h.adresse als Überschrift; NEU: Suche (Adresse/Ort/PLZ/Lage) und
Listenansicht (Tabelle, Zeilenklick öffnet Wohneinheiten, Kosten-Selects
zeigen b.adresse). Typ: Building (neu) + HausForm = Omit<name|adresse>.
Anleitung aktualisiert.
**Erledigt 04.10. (v1.25):** Zählerverlauf + Anomalitätserkennung. Neue Tabelle
meter_readings (seq BIGSERIAL als Sortierung – erstellt_am ist Text und sortiert
lexikografisch falsch!): jeder erfasste Stand bleibt als Eintrag bestehen
(quelle erstbefund/manuel/mieter, erfasst_von, anomalie + anomalie_grund);
meters.stand bleibt aktuellster Wert (Cache). Modul server/src/zaehlerVerlauf.js:
pruefeAnomalie (Ruecklauf | Datum in Zukunft | rueckdatiert | Stillstand ≥60 Tage
| Verbrauchsspitze >4× Median-Tagesrate, nur ≥2 Intervalle + Delta ≥10),
standEintragen (insert + Mail an Verwaltung bei Anomalie, HTML-Karte mit
Button), erstbefundFehlende (Start-Einmal-Backfill bestehender Zähler).
Endpunkte: GET/POST /api/meters/:id/readings (Mieter nur eigene Einheit,
meterFuer-Helper prüft), DELETE …/readings/:rid (admin, baut meters-Cache neu).
Bootstrap: letzte_anomalie via Subquery (ORDER BY seq DESC). Frontend
src/pages/Zaehler.tsx: VerlaufDialog (Liste neueste zuerst, Auffälligkeits-
Badge, Löschen für Verwaltung), StandEintragenDialog (Mieter + Verwaltung,
Warnbox bei Anomalie statt sofortigem Schließen), Anomalie-Badge auf der Karte.
Achtung Git-Bash: Pfad-Argumente an node werden von MSYS umgewandelt –
Pfade an node-Skripte OHNE führenden / übergeben. AUCH BEHOBEN: nginx
pufferte Container-IPs beim Start → nach mp-api-Recreate lieferte jede API-
Anfrage 502 (root cause mehrerer „schwarzer Bildschirm“-/Reload-Probleme).
Fix: resolver 127.0.0.11 + Variablen-proxy_pass in
docker/templates/default.conf.template; deploy.sh prüft Health jetzt inhaltlich
(curl -sf + grep datenbank:true).
**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).