64 KiB
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:
- 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). - Mietkonto & Zahlungen (
/mietkonto) – Buchungsverlauf 2026 mit Statusfilter, Nebenkostenguthaben, Zahlungsdaten. Verwaltung: Zahlungsstatus aller Wohnungen, Rückstandsliste, Haus-Filter. - Schadensmeldungen (
/schadensmeldungen) – Tickets mit Kategorie, Dringlichkeit, Status; Neuanlage per Dialog; Verwaltung pflegt Status per Select. - Dokumente (
/dokumente) – Verträge/Abrechnungen/Hausordnung/Protokolle, Suche + Kategorie-Chips. Mieter sieht eigene + Hausdokumente (unitId=null). - Nachrichten (
/nachrichten) – Chat Mieter ↔ Verwaltung, Ungelesen-Markierung, neue Unterhaltungen. - Benutzerverwaltung (
/benutzer, nur Gruppeverwaltung) – 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-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.envauf dem Server (gitignored, liegt nur dort): liefertMAIL_API_TOKEN(für nginx-envsubst und mp-api) undPOSTGRES_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.envplus zufälligesPOSTGRES_PASSWORD=…. Wichtig: compose interpoliert${VARIABLE}nur aus einer.env-Datei im Projektverzeichnis (nicht ausenv_file:!) – daher auf dem Server/opt/mieterportal/.envals 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 undGET /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-portalläuft auf hocks.eu, statischesdist/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-cachegeliefert (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(Vorlagemail.env.exampleim Repo). - Mail-Stack live (seit 02.10.2026): Container
mp-mail+mp-mail-apilaufen auf hocks.eu unter/opt/mp-mail(Relay überhocks-eu.mail.protection.outlook.com). Testmail und API-/send(mitX-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/sendanhttp://mp-mail-api:8080/sendmit Token ausportal.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 inindex.cssfür.darkumrechnen (.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.pyanpassen 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 --noEmitprü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=10000wartet 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-, 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).
2026-10-04 — Fix: Navigation springt bei Seitenwechsel
- Ursache: Inhalts-Spalte im AppLayout hatte min-width:auto und wurde von breitem Tabellen-Inhalt auf ~1645px gedrückt (Viewport 878px) → Header/Navigation wirkten gesprungen.
- Fix: min-w-0 auf Inhalts-Spalte + main (AppLayout.tsx); Tabellen-Wrapper in Zaehler/Bestand/MieterVerwaltung von overflow-hidden auf overflow-x-auto + min-w-[...] umgestellt.
- Verifiziert live: /zaehler und /kosten jeweils scrollWidth == clientWidth == viewport.
- Deploy: Build → pscp → /opt/mieterportal/dist (direktes Kopieren, kein mv). Commit
844657e.
2026-10-04 — Performance: Code-Splitting + WebP
- Alle Seiten in App.tsx via React.lazy geladen (Suspense + Ladebildschirm); jede Seite ist jetzt ein eigener Chunk.
- AssistChat im AssistWidget lazy: Chunk wird erst beim ersten Öffnen geladen, danach gemountet bleiben (Verlauf erhalten).
- Bilder: hero.jpg 526->148 KB (1200px WebP q70), joseph 316->28 KB (400px WebP); lazy loading fuer Joseph/Kontakt.
- Ergebnis: Einstiegs-Bundle 1071 KB -> 285 KB (gzip 294->89 KB). Groesster Rest-Chunk: Dashboard 412 KB (PDF-Bibliothek) - spaeter separat lazybar.
- Deploy + Live-Verifikation (Login-Assets, Zaehler-Chunk, kein Overflow). Commit
38e681d.
2026-10-04 — Deploy-Pipeline (Gitea Actions)
- act_runner-Container aufgesetzt (/opt/act-runner, Compose + config.yaml), als hocks-vps-runner in Gitea registriert, Labels: docker, ubuntu-latest.
- Workflow .gitea/workflows/deploy.yml: Push auf main -> checkout, npm ci, tsc, Build (inkl. Anleitungs-Stand via GITHUB_SHA), Deploy nach /opt/mieterportal/dist (/mp-Mount via valid_volumes), Health-Check.
- Zwei Fehler beim ersten Lauf behoben: valid_volumes braucht nur den Host-Pfad (nicht host:container); anleitung-meta.mjs faellt auf GITHUB_SHA zurueck wenn kein .git.
- Run 2 erfolgreich, Portal live, Setup-Admin-Token aus Gitea-DB geloescht. Doku: DEPLOY.md. Commits
0e889ef,c71d9a8.
2026-10-04 — Sicherheit & Betrieb (Optimierungen 4)
- Kimi-API-Key ausgelagert: Literal im n8n-Code-Node ersetzt durch $env.KIMI_API_KEY; Key liegt jetzt als Env-Var in /opt/n8n/docker-compose.yml. Workflow in n8n-DB aktualisiert (workflow_entity + NEU: workflow_published_version/workflow_history einfuegen und publizieren - n8n 2.x laeuft aktiv auf der Published Version!).
- Bonus-Fix: Tool-Namen im Workflow von Unterstrich auf Bindestrich (offene-schaeden, mieter-kontakt, zaehler-anomalien) - die API-Routen nutzen Bindestriche, vorher 404. Auch 429-Erkennung verbreitert (e.statusCode).
- Assistent End-to-End verifiziert: Mieteinnahmen-Frage lieferte 18.684 EUR Warmmiete aus dem Portal.
- Login-Rate-Limiting in mp-api (server/src/index.js): max 10 Fehlversuche/IP/10min -> 429, Erfolg setzt zurueck. trust proxy auf loopback/linklocal/uniquelocal beschraenkt. Image neu gebaut, verifiziert (exakt 10x401 dann 429; korrekter Login 200).
- Backup-Restore-Test: Dump von heute 03:17 in restoretest eingespielt - importierbar, aber ALTES Schema (vor den Tages-Migrationen). Frischer Dump sofort getestet: identisches Schema + Zeilenzahlen wie Produktion. Backup-Pipeline OK.
- HINWEIS Luecke: mp-api-Quelle (/opt/mieterportal/server) liegt NUR auf dem Server, nicht im Git-Repo - beim naechsten Mal backend/ ins Repo ziehen.
2026-10-04 — nginx-Kompression + Lade-Skeletons
- Brotli-Experiment abgebrochen: fholzer/nginx-brotli verarbeitet das envsubst-Template nicht (Container lief mit Stock-Config, /api waere offline). Rollback auf nginx:1.27-alpine, Backups liegen neben den Dateien (.bak-brotli). Bestand (gzip + 30d immutable Cache) verifiziert: JS 284->103 KB uebertragen.
- Lade-Skeleton: neuer Ladebildschirm (src/components/LadeSkeleton.tsx + shadcn ui/skeleton.tsx) zeigt Seitenstruktur (Sidebar, Kopf, Filter, Listenzeilen) mit Puls-Animation statt Text. Nutzt der RequireAuth/LoginGate/Suspense-Fallback in App.tsx.
- Deploy komplett ueber CI (Run 5 erfolgreich, Bundle index-BY0BkN9E live). Hinweis: CI-Build hat anderen Hash als lokal, weil anleitung-meta.mjs Datum/Hash injiziert.
2026-10-04 — Anleitung aktualisiert
- Neue Punkte: KI-Assistent-Lasche (Schnellzugriff), Markdown-Antworten, Suche/Listenansicht/Löschen in allen Modulen. Live verifiziert: Build-Stand
eeb4645auf der Anleitungsseite. Commiteeb4645, CI-Lauf 7.
2026-10-04 — Demo-Login entfernt, Passwort geaendert
-
Demo-Zugaenge-Block und Demo-Registrierungscode aus der Login-Seite entfernt; DEMO_PASSWORT-Konstante (lib/auth, store) gestrichen. Commit
9845466, CI-Lauf 9. -
Passwort von j.hinterstein auf gosodo88 gesetzt (sha256 66ce7600...), Login verifiziert (200).
-
HINWEIS: a.hocks hat weiterhin das Erst-Passwort 0815 - bei Bedarf ebenfalls aendern.
-
a.hocks ebenfalls auf gosodo88 gesetzt und verifiziert (Login 200). Damit ist 0815 im Portal komplett abgeschafft.
2026-10-07 — Registrierungscodes: genau einer pro Wohnung (v1.35)
- Backend (
/opt/mieterportal/server/src/index.js, RoutePOST /api/codes):einheitIdjetzt Pflicht (400 ohne Wohnung),anzahl-Parameter entfernt — es wird immer genau 1 Code erzeugt. Vor dem INSERT werden alle bisherigen UNBENUTZTEN Codes derselben Wohnung gelöscht (DELETE ... WHERE einheit_id=$1 AND verwendet_am IS NULL); verwendete Codes bleiben als Historie. - Frontend: Anzahl-Selektor (1/5/10/20) entfernt, Button deaktiviert ohne Wohnwahl, Hinweistext ergänzt. Store-Signatur
generiereCodes(einheitId: string). - Verifiziert live: 2× Code für altekrefelder-we1 → nur der neueste in der DB; Aufruf ohne Wohnung → 400. Testcode + Session-Dateien aufgeräumt. Commit
5cd7686, CI-Lauf 12 (Status 1), mp-api manuell neu gebaut. - Einmal-Bereinigung: 39 überschüssige unbenutzte Codes gelöscht (prinzferdinand-we7 hatte 35, 6 codes ohne Wohnung). Pro Gruppe bleibt der neueste Code erhalten (MP-2C23-3CD2 / MP-BFEC-9849).
- Anleitung erweitert: „Registrierungscodes erzeugen" (Verwaltung) und „Erstregistrierung mit Code" (Mieter). Achtung bei Hash-Verifikation: CI-Build enthält aktuellen Zeitstempel (anleitung-meta), daher abweichende Chunk-Hashes zum lokalen Build. Commit
d9d1151, CI-Lauf 15, live verifiziert. - Fix: Backend sendet jetzt
Cache-Control: no-storeauf allen API-Responses — der heuristische Browser-Cache lieferte sonst nach Reload veraltete Daten (aufgetreten: gelöschte Registrierungscodes wurden weiter angezeigt). mp-api manuell neu gebaut, Header live verifiziert. - nginx-Cache-Optimierung: Bilder/Schriftarten im Root (logo.png etc., nicht hash-benannt) erhalten jetzt
Cache-Control: public, max-age=86400(vorher keine Header → heuristischer Cache)./assets/mit^~markiert, damit das neue Regex die immutable-Regel nicht überschreibt. Template im Repo und auf dem Server synchron, mp-portal neu erstellt, alle Header live verifiziert.
2026-10-07 — E2E-Test Registrierung + Bounce-Loop-Fix im Mail-Relay
- UAT durchgespielt (laut Anleitung): Admin-Login → Code erzeugen (st-toeniser-we5/we6, prinzferdinand-we7) → Registrierung als Test-Mieter → Benutzer mit Wohnungszuordnung angelegt, Code verbraucht, Login mit Wahl-Passwort ok, Benachrichtigung an axel@hocks.eu UND joseph@hinterstein-immo.de jeweils
status=sent(M365). Testdaten (3 User, Sessions, Codes) aufgeräumt. - Dabei entdeckt: chronischer Bounce-Loop im mp-postfix — NDRs an die nicht existente Absenderadresse noreply@hocks.eu wurden an M365 relayt (550 Access denied), erzeugten neue NDRs; Dutzende Logzeilen pro Sekunde, json-File des Containers rotierte weg (deshalb waren frühere Mails nicht mehr nachweisbar). Vermutlich seit 4.10. (letzte saubere Zustellungen 14:29).
- Fix im mp-postfix-Image (
/opt/mp-mail/postfix/, nur auf Server):virtual_alias_maps = lmdb:/etc/postfix/virtualmitnoreply@hocks.eu → axel@hocks.eu— NDRs landen im echten Postfach, Loop unterbrochen. Alpine-Gotcha: postfix erzeugtvirtual.lmdb, nichtvirtual.db(hash nicht ohne Paket postfix-hash; Default-Dict ist lmdb). Queue geleert, Container neu gebaut. - Bekannte Lücke (nicht behoben): Mail-Versand aus der API ist fire-and-forget ohne Retry bei RCPT-Zeit-Fehlern (mail-api fängt nur OSError; smtplib.SMTPRecipientsRefused → Mail geht verloren, 451/550 beim Relay). Beim Test mit kaputter virtual-Map aufgetreten.
- HINWEIS: sfg-mail (Schulportal, /opt/sfg-mail/postfix) hat denselben Aufbau (sender_canonical → noreply@hocks.eu) — dort ggf. denselben Loop prüfen/nachrüsten. Queue aktuell leer.
- Erledigt: sfg-postfix identisch nachgerüstet —
nichtantworten@fv-sfg.de → axel@hocks.eu(lmdb-Virtual-Alias). Kein Loop-Verkehr in 72 h sichtbar, SMTP-EHLO von sfg-mail-api ok, Queue leer, 45 s Beobachtung ohne Bounces. Env/Netz/Restart-Policy des Containers übernommen. - Retry-Härtung mp-mail-api (
/opt/mp-mail/api/server.py, nur Server):/sendlegt bei JEDEM Fehler (statt nur OSError) in die Outbox; Worker loggt VERSANDT OK / Fehlversuche / ALARM nach 10 Versuchen (sichtbar in docker logs); erfolgreiche und endgültig gescheiterte Einträge werden entfernt. Race-Fix: Outbox-Laden/Speichern unter Lock — zuvor rasten 2 parallele /send-Aufrufe (Registrierung sendet 2 Verwaltungs-Mails gleichzeitig) zu korruptem outbox.json mit Eintragverlust (real aufgetreten 17:38, „Extra data"). Korrupte Datei archiviert (/data/outbox.json.corrupt-20261007). Retry-Mechanik isoliert getestet (Fehler→Outbox→Zähler→Erfolg-Entfernung: alle Schritte bestanden); Live-Sofortversand weiterhin ok (status=sent). sfg-Mail-API hat denselben Mechanismus bereits eingebaut — dort nichts zu tun.