Files
Mieterportal/kimi.md
T

45 KiB
Raw Blame History

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)

# 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.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).