836 lines
56 KiB
Markdown
836 lines
56 KiB
Markdown
# Kimi-Kontext: Hinterstein Immobilien · Mieterportal (mieterportal.hocks.eu)
|
||
|
||
> Projekt-Kontextdatei für Kimi Work. Stand: 02.10.2026, 13:00 Uhr.
|
||
> Bitte bei größeren Änderungen am Projekt aktualisieren.
|
||
> Konventionen und Workflow sind am Projekt Schulportal (sfg.hocks.eu) orientiert.
|
||
|
||
## 1. Projektüberblick
|
||
|
||
Mieterportal der **Hinterstein Immobilien** (Demo, Inhaber Joseph Hinterstein) für
|
||
**50 Wohnungen in 3 Häusern** (Hauptstraße 12 · Ringweg 3 · Am Park 5). Analog zum
|
||
Schulportal: alle Mietangelegenheiten digital an einem Ort.
|
||
|
||
**Datenhaltung (seit v1.3):** zentral serverseitig in **PostgreSQL** über eine
|
||
**Node-API** (Express) – kein localStorage mehr für Mietdaten. Mehrere Nutzer
|
||
sehen denselben Datenstand in Echtzeit; Rechte werden serverseitig erzwungen
|
||
(Mieter nur eigene Wohnung, Verwaltung alles).
|
||
|
||
**Authentifizierung:** Login-Pflicht auf `/login` (Route-Guard in `App.tsx`).
|
||
Gruppen `verwaltung` und `mieter`; Mieter registrieren sich per einmaligem
|
||
**Registrierungscode** (`/registrieren`). Passwörter als SHA-256-Hash (im Browser
|
||
gerechnet, Vergleich serverseitig), Sitzung als HttpOnly-Cookie `mp_session`
|
||
(8 h, Tabelle `sessions`). Demo-Zugänge (Passwort `0815`): `j.hinterstein`
|
||
(Verwaltung), `a.berger` (Mieterin, Hauptstraße 12 Whg. 7), `m.wolf` (Mieter,
|
||
Ringweg 3 Whg. 4). Demo-Registrierungscode: `MP-2026-0815`.
|
||
|
||
**Die 6 Module:**
|
||
|
||
1. **Dashboard** (`/`) – Mieter: Begrüßung, nächste Zahlung, Mietdiagramm, Ankündigungen,
|
||
offene Meldungen. Verwaltung: Kennzahlen (Vermietungsquote, Miet-Soll, Rückstände),
|
||
Einnahmenverlauf + Belegung pro Haus (Recharts).
|
||
2. **Mietkonto & Zahlungen** (`/mietkonto`) – Buchungsverlauf 2026 mit Statusfilter,
|
||
Nebenkostenguthaben, Zahlungsdaten. Verwaltung: Zahlungsstatus aller Wohnungen,
|
||
Rückstandsliste, Haus-Filter.
|
||
3. **Schadensmeldungen** (`/schadensmeldungen`) – Tickets mit Kategorie, Dringlichkeit,
|
||
Status; Neuanlage per Dialog; Verwaltung pflegt Status per Select.
|
||
4. **Dokumente** (`/dokumente`) – Verträge/Abrechnungen/Hausordnung/Protokolle,
|
||
Suche + Kategorie-Chips. Mieter sieht eigene + Hausdokumente (unitId=null).
|
||
5. **Nachrichten** (`/nachrichten`) – Chat Mieter ↔ Verwaltung, Ungelesen-Markierung,
|
||
neue Unterhaltungen.
|
||
6. **Benutzerverwaltung** (`/benutzer`, nur Gruppe `verwaltung`) – Nutzerliste mit
|
||
Gruppen und Wohnungsbezug, Registrierungscodes erzeugen/deaktivieren,
|
||
Demo-Zugänge zurücksetzen.
|
||
|
||
## 2. Technischer Stack
|
||
|
||
| Teil | Technologie |
|
||
|---|---|
|
||
| Frontend | React 18 + TypeScript, Vite 7, Tailwind v3.4 + shadcn/ui (Radix), React Router 7 (BrowserRouter), Recharts |
|
||
| State | Store-Kontext im Browser, **alle Daten serverseitig** (kein localStorage mehr); Ladezustand `bereit`, Schreibzugriff async |
|
||
| API | `server/`: Node 20 + Express + pg, Session-Cookie, Rollenrechte, serverseitige Mail-Benachrichtigungen |
|
||
| Datenbank | PostgreSQL 16 (`mp-db`), Schema/Seed in `server/src/db.js` + `seed.js`, Volume `mp_pgdata` |
|
||
| Daten | `server/seed/generated.json` (50 Wohnungen + 552 Buchungen via `beispieldaten.py`) + `server/src/seed-data.js` (handschriftlich) |
|
||
| Auth | SHA-256-Hash (Browser) + HttpOnly-Cookie-Session (8 h), `src/lib/api.ts`, `src/lib/auth.ts` |
|
||
| Branding | Kunde: `src/assets/hinterstein/` (logo.png, joseph-hinterstein.jpg, kontakt.jpg); KI-generiert: `src/assets/logo.png`, `hero.jpg`, `modules.jpg` |
|
||
| Webserver | Docker-Container `mp-portal` (nginx), statisches `dist/`, proxiet `/api/` → `mp-api:3000` |
|
||
| Mail | `mail-server/`: Postfix-Relay (`mp-mail`) + Python-API (`mp-mail-api`) – **live**, `/opt/mp-mail`; Portal-nginx proxyt `POST /api/mail/send` mit Token aus `portal.env` |
|
||
| Server | Root-VPS hocks.eu (IONOS), Docker + Portainer, Subdomain via NPM (Nginx Proxy Manager, Let's Encrypt) |
|
||
|
||
## 3. Wichtige Dateien
|
||
|
||
Projektverzeichnis: `C:\Users\ah\Documents\Kimi\Workspaces\Mieterportal`
|
||
**Git:** `https://git.hocks.eu/ah-admin/Mieterportal` (Branch `main`). Secrets
|
||
ausgesperrt via `.gitignore` (`mail-server/mail.env` etc.).
|
||
|
||
| Datei | Inhalt |
|
||
|---|---|
|
||
| `src/store/store.tsx` | Store-Kontext: lädt `/api/bootstrap`, stellt Collections + `me`/`role`/`meineEinheit` bereit, async-Methoden für alle Schreibaktionen, `fmtEUR` |
|
||
| `src/lib/api.ts` | Fetch-Client (same-origin, JSON, `ApiFehler` mit deutscher Meldung) |
|
||
| `src/lib/auth.ts` | SHA-256 für Passwörter, `DEMO_PASSWORT` |
|
||
| `server/src/index.js` | Express-API: Auth (Login/Logout/me/Register), Bootstrap, Tickets, Threads/Messages, Benutzerverwaltung, Codes, `/api/healthz` |
|
||
| `server/src/db.js` | pg-Pool + idempotentes Schema (11 Tabellen, snake_case) |
|
||
| `server/src/seed.js` | Idempotenter Seed (läuft nur bei leerer users-Tabelle) |
|
||
| `server/src/seed-data.js` | Handschriftliche Demo-Daten (Tickets, Dokumente, Threads, Ankündigungen, Benutzer, Codes) |
|
||
| `server/src/mail.js` | Serverseitiger Versand über mp-mail-api (fire-and-forget) |
|
||
| `server/seed/generated.json` | 50 Wohnungen + 552 Buchungen, erzeugt von `beispieldaten.py` (Seed 20261002) |
|
||
| `beispieldaten.py` | Deterministischer Generator: `python beispieldaten.py [seed]` → `server/seed/generated.json` |
|
||
| `src/types/index.ts` | Alle Typen (Unit, Transaction, Ticket, DocumentItem, Thread, Announcement, Benutzer, RegistrierungsCode) |
|
||
| `src/components/AppLayout.tsx` | Sidebar (Desktop) + Mobile Top-Bar/Nav, Logout, Ungelesen-Badge, ThemeToggle |
|
||
| `src/components/ThemeToggle.tsx` | Dark-Mode-Umschalter (Sonne/Mond) |
|
||
| `src/lib/theme.ts` | `mp-theme` in localStorage (nur noch Theme!), Init vor erstem Render (main.tsx) |
|
||
| `src/pages/Login.tsx` | Login-Pflicht: Hero mit Joseph-Hinterstein-Portrait, Logo, Feature-Liste, Login + Registrierung + Demo-Einstiege |
|
||
| `src/pages/BenutzerVerwaltung.tsx` | Benutzerverwaltung (nur Verwaltung): Nutzer anlegen/pflegen, Registrierungscodes erzeugen |
|
||
| `docker/templates/default.conf.template` | nginx-Site als envsubst-Template: SPA, `/api/` → mp-api, `POST /api/mail/send` → mp-mail-api mit `${MAIL_API_TOKEN}` |
|
||
| `docker-compose.yml` | Stack: `mp-portal` (nginx), `mp-api` (Node, Build ./server), `mp-db` (PostgreSQL 16, Volume mp_pgdata), Netz npm_default |
|
||
| `deploy.sh` | Deploy-Workflow: Build, Paket (dist + compose + nginx + server), Upload, `docker compose up -d --build`, Health-Check |
|
||
| `mail-server/` | Postfix-Relay + schlanke Mail-API (Muster Schulportal, angepasst) – separater Stack unter /opt/mp-mail |
|
||
|
||
## 4. Deploy-Workflow (bewährt, aus Schulportal übernommen)
|
||
|
||
```bash
|
||
# 1. Bauen (im Projektverzeichnis)
|
||
npm run build
|
||
|
||
# 2. Paketieren + Hochladen (pscp braucht cwd = Projektverzeichnis)
|
||
tar -czf mp-dist.tar.gz -C dist .
|
||
"/c/Program Files/PuTTY/pscp.exe" -batch \
|
||
-hostkey "SHA256:4G3UxsugNkHH6BrPXScLXh+vX8P1sLgWKn8cZBKSRxw" \
|
||
-i "Z:\Devel\ssh\priv_ionos_hocks_vps.ppk" -P 38592 \
|
||
mp-dist.tar.gz root@hocks.eu:/tmp/
|
||
|
||
# 3. Entpacken + Container neu starten + Hash prüfen
|
||
"/c/Program Files/PuTTY/plink.exe" -batch -hostkey "SHA256:4G3UxsugNkHH6BrPXScLXh+vX8P1sLgWKn8cZBKSRxw" \
|
||
-i "Z:\Devel\ssh\priv_ionos_hocks_vps.ppk" -P 38592 root@hocks.eu \
|
||
"rm -rf /opt/mieterportal/dist && mkdir -p /opt/mieterportal/dist && \
|
||
tar -xzf /tmp/mp-dist.tar.gz -C /opt/mieterportal/dist && \
|
||
cd /opt/mieterportal && docker compose up -d && sleep 2 && \
|
||
curl -s https://mieterportal.hocks.eu/ | grep -o 'index-[^\"]*\.js'"
|
||
|
||
# Alternativ alles in einem Schritt:
|
||
./deploy.sh
|
||
```
|
||
|
||
- SSH: Port **38592**, Key `Z:\Devel\ssh\priv_ionos_hocks_vps.ppk` (Pageant), Hostkey-Flag zwingend.
|
||
- **`portal.env` auf dem Server** (gitignored, liegt nur dort): liefert
|
||
`MAIL_API_TOKEN` (für nginx-envsubst und mp-api) und `POSTGRES_PASSWORD`
|
||
(für mp-db und die DATABASE_URL des mp-api). Einmalig angelegt:
|
||
`grep '^MAIL_API_TOKEN=' /opt/mp-mail/mail.env > /opt/mieterportal/portal.env`
|
||
plus zufälliges `POSTGRES_PASSWORD=…`. Wichtig: compose interpoliert
|
||
`${VARIABLE}` nur aus einer `.env`-Datei im Projektverzeichnis (nicht aus
|
||
`env_file:`!) – daher auf dem Server `/opt/mieterportal/.env` als
|
||
**Symlink auf portal.env**:
|
||
`cd /opt/mieterportal && ln -sf portal.env .env`
|
||
- deploy.sh macht `docker compose up -d --build` (baut das mp-api-Image auf
|
||
dem Server, npm ci im Container) und prüft danach Container-Status,
|
||
Portal-Hash und `GET /api/healthz`.
|
||
- **Vorsicht Fail2Ban:** zu viele gescheiterte SSH-Versuche von einer IP führen zu einem
|
||
Ban (Timeout-Verbindungsbild). Ban aufheben auf dem Server: `fail2ban-client unban --all`.
|
||
- **Live (seit 02.10.2026):** Container `mp-portal` läuft auf hocks.eu, statisches `dist/`
|
||
unter `/opt/mieterportal/dist` (gemountet). NPM-Proxy-Host **id 7** (Datei
|
||
`/data/compose/1/data/nginx/proxy_host/7.conf`, DB-Tabelle proxy_host), Zertifikat
|
||
**npm-9** (Let's Encrypt, läuft bis 31.12.2026; via certbot/webroot erstellt, liegt im
|
||
npm-Container unter `/etc/letsencrypt/live/npm-9/`). HTTP→HTTPS-Redirect aktiv
|
||
(HSTS, Block-Exploits, HTTP/2). Änderungen am Proxy-Host im NPM-UI werden die
|
||
DB-Konfiguration konsistent halten; manuelle Conf-Edits werden beim nächsten
|
||
UI-Speichern überschrieben.
|
||
- HTML wird mit `no-cache` geliefert (nginx-Config), Assets hash-benannt → Reload genügt.
|
||
Der eingebettete Kimi-Browser cached hart → beim Testen Cache-Buster anhängen: `?v=X`.
|
||
|
||
## 5. Zugänge & Konfiguration
|
||
|
||
- **Demo-Zugänge (Passwort `0815`):** `j.hinterstein` (Verwaltung), `a.berger`
|
||
(Mieterin, Hauptstraße 12 Whg. 7), `m.wolf` (Mieter, Ringweg 3 Whg. 4).
|
||
Demo-Registrierungscode: `MP-2026-0815`. Passwörter liegen als SHA-256-Hash
|
||
in der users-Tabelle; Sitzung als HttpOnly-Cookie (Tabelle sessions).
|
||
Abmelden reicht zum „Reset" – nichts liegt mehr im Browser-localStorage
|
||
(außer dem Dark-Mode-Theme).
|
||
- **Verwaltungs-E-Mail:** `axel@hocks.eu` (echtes, zustellbares Postfach, damit
|
||
Benachrichtigungen ankommen) – im Portal änderbar.
|
||
- **Mail-API-Token:** bewusst nicht hier dokumentiert – liegt auf dem Server in
|
||
`/opt/mp-mail/mail.env` (Vorlage `mail.env.example` im Repo).
|
||
- **Mail-Stack live (seit 02.10.2026):** Container `mp-mail` + `mp-mail-api` laufen
|
||
auf hocks.eu unter `/opt/mp-mail` (Relay über `hocks-eu.mail.protection.outlook.com`).
|
||
Testmail und API-`/send` (mit `X-Api-Token`) verifiziert (status=sent); ohne Token 401.
|
||
- **Mail-Benachrichtigungen (seit v1.2 nginx-Proxy, seit v1.3 serverseitig,
|
||
seit v1.4 im HTML-Format wie im Schulportal):** Der Portal-nginx proxiet
|
||
`POST /api/mail/send` an `http://mp-mail-api:8080/send` mit Token aus
|
||
`portal.env` (403 für alles außer POST). **Versand aus der Node-API**
|
||
(`server/src/mail.js`, fire-and-forget): neue Schadensmeldung → alle aktiven
|
||
Verwaltungsbenutzer, neue Nachricht/Antwort → Gegenstelle. HTML im
|
||
Schulportal-Stil (`baueHtmlMail`: 600-px-Karte, Kopfzeile in Hinterstein-
|
||
Farben #22333b/Amber, Zeilen-Tabelle, Status-Box, Zitat-Box, Button „Zum
|
||
Mieterportal“, Footer) immer mit Plain-Text-Fallback (multipart/alternative).
|
||
E2E verifiziert: Zustellung an axel@hocks.eu, Postfix status=sent.
|
||
|
||
## 6. Konventionen & Besonderheiten
|
||
|
||
- **Dark Mode:** class-Strategie, Präferenz `mp-theme`, Init vor erstem Render.
|
||
Feste Farbklassen werden in `index.css` für `.dark` umrechnen
|
||
(`.dark .bg-white`/`.bg-slate-50`/`.bg-teal-50`); Sidebar ist eigenes Dunkel-Teal.
|
||
- **Barrierefreiheit:** aria-pressed/labels am ThemeToggle und Rollenwechsel, Fokus-Ringe.
|
||
- **Formatierung:** Beträge via `fmtEUR` (Intl de-DE), Datumsformate TT.MM.JJJJ.
|
||
- **Datenpflege:** Bestandsdaten ändern → `beispieldaten.py` anpassen und neu ausführen,
|
||
nicht generated.json von Hand editieren.
|
||
- **Sicherheit:** kein offenes Relay (Postfix mynetworks loopback + Docker-Netz,
|
||
sender_canonical zentral). Upload/Download-Funktionen sind Demo-Darstellungen.
|
||
- **Keine destruktiven Aktionen auf dem Server ohne ausdrückliche Bestätigung.**
|
||
|
||
## 7. Testing
|
||
|
||
- `npm run build` (`tsc -b`) ist die echte Typprüfung; `npx tsc --noEmit` prüft wegen
|
||
Solution-tsconfig ggf. nichts. Server-Syntax: `node --check server/src/*.js`.
|
||
- API-Livetests mit curl (Cookie-Jar): Login → `/api/auth/me` → `/api/bootstrap`
|
||
(Mieter-Sicht zählt kleiner!) → Ticket/Thread schreiben → Postfix-Log auf
|
||
Zustellung prüfen (`docker logs mp-mail | grep to=<empfaenger>`).
|
||
- Headless-Screenshots (Dark Mode = Systempräferenz, `--virtual-time-budget=10000`
|
||
wartet auf den Session-Check):
|
||
`"/c/Program Files/Google/Chrome/Application/chrome.exe" --headless --disable-gpu --screenshot=... --window-size=WxH "URL"`
|
||
- Screenshots ausgeliefert nach `preview/`.
|
||
|
||
## 8. Verlauf / offene Punkte
|
||
|
||
**Erledigt 04.10. (v1.36):** Mieterverwaltung. Mieter ist ein eigenes Objekt
|
||
1:1 zur Wohnung (Tabelle mieter: Name, Vorname, Telefon, E-Mail, Kalt-/Warmmiete,
|
||
Vertragsstatus aktiv|befristet|gekuendigt inkl. „gekündigt zum/von", Kaution
|
||
Höhe + Art bar|ueberwiesen|sonstiges (Freitext), Zahlung komplett|raten inkl.
|
||
Ratenanzahl, Mietzeitraum Start/Ende). Units schlank: Kaltmiete/Warmmiete/Mieter
|
||
aus API + Formularen entfernt (Spalten bleiben physisch), Status jetzt
|
||
vermietet|leerstand|unklar (Migration: frei→leerstand, units.tenant→mieter-Rows,
|
||
läuft bei Start automatisch). Server: Mieter-CRUD adminOnly (POST/PATCH/DELETE
|
||
/api/mieter, Anlage/Löschung setzt Unit-Status), Dokument-Upload pro Mieter +
|
||
Kategorie (POST /api/mieter/:id/dokumente?kategorie=, 1 Slot je Kategorie:
|
||
Personalausweis, SCHUFA-Auskunft, Bankauszug, Sonstiges; PDF+JPG/PNG; Upload
|
||
ersetzt, DELETE kaskadiert Dateien), Preview liefert Content-Type per Endung
|
||
(Bilder rendern in Lightbox/Thumbnails). Bootstrap: Mieter (Admin alle, Mieter
|
||
nur eigene Wohnung – PII-Schutz). Abrechnung + Mietvertrags-Aktivierung ziehen
|
||
Mietdaten aus mieter-Tabelle. Frontend: Seite /mieter (Listenansicht + Suche,
|
||
Vertrags-/Kaution-Badges), einspaltiger Dialog mit Dokumenten-Thumbnails
|
||
(Mini-iframe → PdfLightbox), Bestand/Wohnungsformular schlank, Dashboard/
|
||
Payments/BenutzerVerwaltung/Mietvertraege umgestellt. Deployt, E2E-getestet
|
||
(CRUD, Sonderfelder, Upload PDF+PNG, Kaskaden, Migration 44 Mieter live).
|
||
|
||
**Erledigt 04.10. (v1.37):** KI-Assistent (Verwaltung fragt, N8N + Kimi
|
||
antworten). Server (server/src/assist.js): POST /api/assist (auth+adminOnly,
|
||
leitet Frage+Verlauf an N8N_ASSIST_WEBHOOK weiter, x-assist-secret-Header,
|
||
90 s Timeout, erwartet JSON {antwort}) + 6 Tool-Endpunkte
|
||
/api/assist/tools/{leerstaende,mieteinnahmen,offene-schaeden,mieter-kontakt,
|
||
zaehler-anomalien,kosten} geschützt per Header x-assist-key=ASSIST_API_KEY,
|
||
kompakte JSON-Antworten direkt aus Postgres. Frontend: Seite /assistent
|
||
(Chat-UI, Verlauf nur Session, Beispielfragen, Admin-only). Env
|
||
(docker-compose + portal.env auf Server): N8N_ASSIST_WEBHOOK,
|
||
ASSIST_SHARED_SECRET, ASSIST_API_KEY (letztere zwei auf Server gesetzt).
|
||
N8N: importierbarer Workflow n8n/assist-workflow.json (Webhook → AI-Agent mit
|
||
Kimi OpenAI-kompatibel https://api.moonshot.ai/v1, Modell kimi-k2.6 → 6
|
||
HTTP-Tools → Respond {antwort}); Setup-Doku n8n/README-assistent.md.
|
||
Deployt, E2E-getestet (Tools 401/200, /api/assist sauberer 503 ohne Webhook).
|
||
|
||
**Erledigt 04.10. Abend (v1.37 fertig):** N8N-Workflow „Mieterportal – KI-Assistent
|
||
(Kimi)" serverseitig vollständig eingerichtet und aktiviert – direkt in die
|
||
n8n-SQLite-DB eingespielt (n8n 2.x API-Import unzuverlässig, 401). Finaler Aufbau
|
||
(Code-Node-Ansatz statt langchain, keine Credentials): Webhook POST mp-assist
|
||
(responseNode, wichtig: Property webhookId = Node-ID auf Root-Ebene, sonst 404
|
||
„not registered") → Code-Node prüft x-assist-secret, Kimi-Tool-Calling per
|
||
this.helpers.httpRequest (max. 6 Runden, kein temperature-Param – k2.6 erlaubt
|
||
nur 1, kein URLSearchParams in der Sandbox, 429 wird abgefangen) → Respond
|
||
{antwort}. PORTAL im Code = http://mp-api:3000 (internes npm_default-Netz).
|
||
Drei Geheimnisse als Literale im Code-Node (nicht im Repo, siehe
|
||
n8n/README-assistent.md). N8N_API_KEY aus der n8n-Compose entfernt (ungenutzt).
|
||
E2E live: „Wie hoch sind die monatlichen Mieteinnahmen?" → korrekte Antwort
|
||
(Warm 18.684 € / Kalt 17.686 €) via Portal-Login.
|
||
|
||
**Erledigt 04.10. Abend (Design):** Alle Formulare 2-spaltig auf shared Layout
|
||
umgebaut: Neue Komponente src/components/forms/FormSection.tsx (FormSection =
|
||
Karte mit Teal-Icon-Chip + Sora-Überschrift, Kind-Elemente in
|
||
grid md:grid-cols-2; Wide = md:col-span-2 für Checkboxen/Textareas/Uploads;
|
||
FormFehler). Umgebaut: Bestand (Haus+Wohnung), MieterVerwaltung,
|
||
Zaehler (Zähler+Zählerstand), Kosten, Tickets, BenutzerVerwaltung (Benutzer+
|
||
E-Mail), Mietvertraege (Vermieterdaten+Vertrag), Documents, Profil (Karten-
|
||
Raster 2-spaltig). Dialoge auf sm:max-w-2xl verbreitert. Fonts: Inter (Body) +
|
||
Sora (Headings, font-display) via Google Fonts in index.html + tailwind.config
|
||
fontFamily. Deployt (dist-Tausch auf Server, mp-portal-Neustart nötig wegen
|
||
Bind-Mount-Inode) und verifiziert (CSS/JS 200, Fonts im HTML, Login 200).
|
||
|
||
**Erledigt 04.10. Abend (v1.38):** KI-Assistent zusätzlich als einblendbares
|
||
Widget: AssistChat (Chat-Logik aus Assistent.tsx extrahiert, shared) +
|
||
AssistWidget (vertikale Teal-Lasche fixed rechts, mittig, oeffnet Slide-over-
|
||
Panel 430px mit Teal-Header; schließt per Escape und bei Seitenwechsel; nur
|
||
admin, auf /assistent ausgeblendet). Widget global in AppLayout eingebunden.
|
||
Deployt (direktes Kopieren ins bestehende dist-Verzeichnis – mv bricht den
|
||
Bind-Mount-Inode!) und verifiziert (CSS/JS/Login 200).
|
||
|
||
**Erledigt 04.10. (v1.35):** Mietvertragsmodul (Verwaltung, Navigation
|
||
„Mietverträge", Admin-only). Workflow: anlegen → Entwurf-PDF → Versand an
|
||
zukünftigen Mieter → Upload unterschriebenen Vertrag → manuell aktivieren
|
||
(Mieterkonto wird angelegt, Wohnung als vermietet geführt, Zugangsdaten
|
||
per E-Mail). DB (db.js): einstellungen (Schlüssel-Wert, Vermieterdaten:
|
||
vermieter_name/strasse/hausnummer/plz/ort/telefon/email/bankname/iban) +
|
||
mietvertraege (Status entwurf|versendet|unterschrieben|aktiv, Dokument-
|
||
und Benutzerbezüge als TEXT ohne harten FK, unit_id mit FK). Server:
|
||
server/src/vertrag.js (pdfkit-Mustervertrag: Parteien, §1 Mietobjekt aus
|
||
Bestand, §2 Mietzeit, §3 Miete/Nebenkosten/Zahlung, §4 Kaution ≤3 Nettokalt-
|
||
mieten, §5 Gebrauch/Instandhaltung/Schönheitsreparaturen/Rauchwarnmelder,
|
||
§6 Schlussbestimmungen, Unterschriftsfelder; erzeugeVertragsPdf legt/ersetzt
|
||
Dokument „Mietvertrag" der Wohnung). Endpunkte (alle adminOnly): CRUD
|
||
/api/mietvertraege[n], POST /:id/entwurf, /:id/versenden (PDF-Anhang via
|
||
mail.sendeMail + CC Verwaltung, Status→versendet), /:id/unterschrift
|
||
(multer single('file'), Dokument d-mvu-<id>, Status→unterschrieben),
|
||
/:id/aktivieren (Benutzername aus Vor-/Nachname [Umlaute→ae/oe/ue, unique
|
||
mit Zähler], Passwort sha256("0815"), users+einheit_id+telefon anlegen,
|
||
units.tenant/status setzen, Mail mit Zugangsdaten, Status→aktiv),
|
||
GET/PUT /api/einstellungen (Whitelist vermieter_*). Bootstrap liefert
|
||
mietvertraege + einstellungen (via vermieterDaten, kurze Keys).
|
||
DELETE /api/documents löst Verweise in mietvertraege; Wohnungs-Blockerliste
|
||
um mietvertraege erweitert. Frontend: types (Mietvertrag/MietvertragForm/
|
||
VermieterDaten), store (Bootstrap-Felder + 8 Aktionen, apiPut neu in
|
||
lib/api.ts), Seite src/pages/Mietvertraege.tsx (Listenansicht Standard +
|
||
Suche, Vermieterdaten-Dialog, Vertrags-Dialog mit Miet-Vorbefüllung aus
|
||
der Wohnung, Aktionen: Entwurf/PdfLightbox, Senden, Upload, Aktivieren
|
||
mit Confirm + Benutzername-Alert, Löschen; Warnbanner bei unvollständigen
|
||
Vermieterdaten), Anleitung neuer Abschnitt „Mietverträge".
|
||
E2E: Vertrag anlegen→Entwurf-PDF (Vorschau lädt)→versenden (CC-Verwaltung
|
||
in Mail-API-Log)→Unterschrift-Upload (Test-PDF)→aktivieren (Benutzer
|
||
vorhanden, Wohnung vermietet, Login mit 0815 möglich)→Aufräumen.
|
||
|
||
**Erledigt 04.10. (v1.34):** SparkReceipt-Zuordnung nutzt Mietobjekt-Adresse.
|
||
Problem: auf eingehenden Rechnungen steht als Adresse der Rechnungsempfänger
|
||
(Rott 250 = eigene Verwaltungsadresse, als Haus gepflegt) – die sekundäre
|
||
Adress-Zuordnung matchte damit immer das falsche Haus. Lösung: Gebäude
|
||
bekommen das Kennzeichen eigenobjekt (DB: buildings.eigenobjekt BOOLEAN,
|
||
Default FALSE); belegAutoZuordnung schließt solche Häuser aus
|
||
(WHERE eigenobjekt IS NOT TRUE) – primäre Stromzählernummer-Matchung
|
||
bleibt unverändert. POST/PATCH /api/buildings und mapBuilding liefern/
|
||
speichern eigenobjekt; PATCH merged via mapBuilding-Altbestand, bleibt
|
||
also erhalten wenn nicht mitsendet. Bestand.tsx: neuer Abschnitt
|
||
„Zuordnung Belege" mit Checkbox „Eigenobjekt / Rechnungsempfänger"
|
||
(Erklärung im Hint), Badge „Eigenobjekt" in Haus-Karte und Listenzeile.
|
||
Verwaltung muss Rott 250 einmalig als Eigenobjekt markieren; bereits
|
||
falsch zugeordnete Belege lassen sich einzeln per „Analysieren" neu
|
||
zuordnen (Schutz nur für 'manuell'). E2E: Webhook mit nur Empfängeradresse
|
||
→ keine Zuordnung statt Rott 250; mit Mietobjekt-Adresse im Text →
|
||
Zuordnung aufs Mietobjekt.
|
||
Fix in v1.33 mitnachgetragen: DELETE /api/tickets/:id löscht jetzt in
|
||
einer Transaktion zuerst messages, dann threads (threads.ticket_id hat
|
||
KEIN ON DELETE CASCADE), dann das Ticket – vorher 500 durch FK-Verletzung.
|
||
Blocker-Liste von DELETE /api/units/:id um transactions, tickets,
|
||
threads, registration_codes und belege erweitert (alle FKs auf units.id
|
||
ohne Cascade, vorher möglicher 500 statt 400er-Hinweis).
|
||
|
||
**Erledigt 04.10. (v1.33):** Einträge in allen Listen löschbar. Neue
|
||
adminOnly-DELETE-Endpunkte in server/src/index.js: /api/tickets/:id
|
||
(kaskadiert threads + messages), /api/meters/:id (kaskadiert meter_readings),
|
||
/api/belege/:id, /api/buildings/:name und /api/units/:id – letztere zwei
|
||
verweigern mit 400 + deutscher Blocker-Liste (Haus: Wohnungen/Zähler/Kosten;
|
||
Wohnung: Zähler/Dokumente/Kosten/Mieter), /api/users/:id (eigenes Konto und
|
||
letzten aktiven Verwalter geschützt, sessions vorher gelöscht). Store:
|
||
deleteTicket, deleteZaehler, deleteHaus, deleteWohnung, deleteBeleg,
|
||
loescheBenutzer (apiDelete + lokaler State-Filter, threads bei Ticket-Delete
|
||
mitgefiltert). Frontend: rote Trash2-Löschbuttons mit window.confirm +
|
||
alert(err) in Bestand (Haus Karte+Liste, Wohnungszeile), Zaehler (Karte+Liste),
|
||
Kosten BelegeListe, Tickets (admin, warnt vor Mitlöschung der Kommunikation),
|
||
BenutzerVerwaltung (eigenes Konto deaktiviert); Buttons nur bei role=admin,
|
||
in Mieter-sichtbaren Seiten (Zaehler, Tickets) zusätzlich gegated. E2E: 400-
|
||
Blocker beim Haus mit Wohnung/Zähler, Kaskaden-Löschung bis Haus weg,
|
||
Ticket anlegen+löschen, fremden Testbenutzer löschen, eigenes Konto → 400.
|
||
|
||
**Erledigt 04.10. (v1.32):** Deutsche Datumsformate in allen Modulen.
|
||
NEU src/lib/format.ts: fmtDatum (ISO YYYY-MM-DD → TT.MM.JJJJ, deutsche Werte
|
||
durchreichen) + fmtDatumZeit (mit Uhrzeit) – eingesetzt in Zaehler (Karte,
|
||
Liste, Verlauf), Dashboard (Ankündigungen, Tickets), Documents, Kosten
|
||
(Belegdatum), Messages, Tickets, BenutzerVerwaltung (letzterLogin), Profil,
|
||
PdfLightbox. Eingabe-Fix: StandEintragenDialog wandelt date-Feld jetzt
|
||
ISO→deutsch (vorher roh gesendet); Server normalisiert zusätzlich in
|
||
POST /api/meters und POST /api/meters/:id/readings via datumDeutsch()
|
||
(ISO → TT.MM.JJJJ vor Validierung/Speicherung). E2E: ISO-Eintrag
|
||
„2026-01-15" wird als „15.01.2026" gespeichert.
|
||
|
||
**Erledigt 04.10. (v1.31):** PDF-Lightbox für erzeugte/gespeicherte PDFs mit
|
||
Druck-, E-Mail- und Download-Option. Server: dokumentMitDatei-Helper
|
||
(Rechte/Dateiprüfung, basename gegen Pfad-Traversal) für download + NEU
|
||
GET /api/documents/:id/preview (inline, Content-Type application/pdf) und
|
||
POST /api/documents/:id/email (admin; Empfänger „an" oder automatisch Mieter
|
||
der Wohnung, immer CC Verwaltung, HTML-Karte + PDF als base64-Anhang via
|
||
mail.sendeMail). mail-server/api/server.py: Anhang-Support (attachment_b64/
|
||
attachment_filename/attachment_mime, Limit ~20 MB, Outbox-tauglich) – Datei
|
||
liegt BIND-MOUNTED in /opt/mp-mail/api/server.py, Deploy = pscp + docker
|
||
restart mp-mail-api (KEIN Image-Rebuild). Achtung: wget im Container auf
|
||
„localhost" → IPv6 refused, immer 127.0.0.1 verwenden; Port 8080 ist NICHT
|
||
auf dem Host veröffentlicht. Frontend: src/components/PdfLightbox.tsx
|
||
(iframe-Preview, iframe.contentWindow.print(), E-Mail-Formular), Store
|
||
sendeDokumentEmail; eingebunden in Kosten.tsx AbrechnungDialog (Vorschau-
|
||
Button, ergebnis speichert jetzt DocumentItem statt nur id) und Documents.tsx
|
||
(Vorschau-Button je Zeile mit hasFile).
|
||
|
||
**Erledigt 04.10. (v1.30):** Wohnungsformular einspaltig (Analogie Haus-Formular
|
||
v1.26): Abschnitte Zuordnung (Haus, Wohnungsnummer) / Ausstattung (Etage,
|
||
Wohnfläche, Heizungsart) / Miete (Kalt-/Warmmiete) / Mieter (Name, Status),
|
||
gemeinsame Helfer Abschnitt + Feld, Dialog max-h-90vh overflow-y-auto.
|
||
|
||
**Erledigt 04.10. (v1.29):** Heizungsart-Dropdown in Haus- und Wohnungsverwaltung;
|
||
Listenansicht ist jetzt Standard in allen Modulen. DB: buildings + units je
|
||
ALTER heizungsart TEXT DEFAULT ''; Server HEIZUNGSARTEN-Liste + heizungsartValid
|
||
(leer oder Listenwert), POST/PATCH /api/buildings und /api/units persistieren
|
||
das Feld, mapBuilding/mapUnit liefern es (Fallback ''). Frontend:
|
||
HEIZUNGSARTEN/Heizungsart in types (9 gängige Formen), Select „Nicht angegeben"
|
||
Sentinel '__keine' in beiden Dialogen; Wohnungs-Neuanlage übernimmt die
|
||
Heizungsart automatisch aus dem gewählten Haus (waehleHaus). Anzeige: Haus-Karte
|
||
(dl-Zeile mit Flame-Icon, nur wenn gesetzt) + Listen-Detailzeile.
|
||
Listenansicht-Default: Bestand.tsx + Zaehler.tsx ansicht-useState 'liste'.
|
||
Anleitung aktualisiert.
|
||
|
||
**Erledigt 04.10. (v1.28):** Zählerübersicht mit Suche + Listenansicht
|
||
(Analogie zur Hausübersicht v1.26): Suchfeld über Nummer/Art/Haus/Wohneinheit
|
||
(mit Trefferzähler), Toggle Karten/Liste; Liste = Tabelle (Art-Badge mit Icon,
|
||
Nummer, Haus, Wohneinheit, Stand rechts tabular-nums, Stand am, Anomalie-Icon,
|
||
Aktionen), responsive via hidden sm/md/lg. Bestehende Filter Haus/Art bleiben
|
||
und kombinieren mit der Suche.
|
||
|
||
**Erledigt 04.10. (v1.27):** Zähleranlage ohne Haus-Feld, mit Ablesedatum.
|
||
POST /api/meters akzeptiert standAm (Validierung via zaehlerVerlauf.parseDatum,
|
||
Default heute); stand_am + erster Verlaufseintrag (erstbefund) nutzen das Datum.
|
||
Frontend ZaehlerDialog: Haus-Textfeld entfernt – Haus folgt der Wohneinheit,
|
||
nur bei „Allgemeinzähler (ohne Wohnung)" erscheint eine Haus-Auswahl
|
||
(buildings-Select, Anzeige b.adresse). Ablesedatum (type=date, Default heute,
|
||
Pflicht) nur bei Neuanlage; Frontend wandelt YYYY-MM-DD → TT.MM.JJJJ. WICHTIG: parseDatum ist ein
|
||
lenient Parser (Überlauf 32.13. → Feb Folgejahr!) – für Benutzereingaben
|
||
zusätzlich zaehlerVerlauf.datumGueltig (Exaktheits-Roundtrip, nur
|
||
dd.mm.yyyy/yyyy-mm-dd) – eingesetzt in POST /api/meters und
|
||
POST /api/meters/:id/readings.
|
||
|
||
**Erledigt 04.10. (v1.26):** Haus-Formular ohne Namensfeld, Adresse in Einzelfelder.
|
||
buildings um strasse/hausnummer/plz/ort/lage/wohnflaeche/grundstuecksflaeche
|
||
erweitert (7 idempotente ALTERs, quadratmeter bleibt Alt-Spalte); name (PK) und
|
||
adresse (Anzeige) werden serverseitig aus Straße+Hausnummer bzw. komplett
|
||
abgeleitet (hausNameAus/hausAdresseAus, POST + PATCH). Start-Migration
|
||
haeuserFelderMigrieren zerlegt Alt-Adressen („Straße Nr., PLZ Ort") via
|
||
adresseZerlegen und zieht wohnflaeche aus quadratmeter nach (Log-Zeile
|
||
„Häuser: N Adresse(n) in Einzelfelder zerlegt"). pruefeHaus validiert PLZ
|
||
(5 Ziffern), Pflicht: Straße/Hausnummer/PLZ/Ort/Baujahr/Stockwerke/Etagenhöhe/
|
||
Anzahl Wohneinheiten/Wohnfläche; Lage + Grundstücksfläche optional.
|
||
Haus-Formular einspaltig in Abschnitten (Adresse/Gebäude/Flächen), Karte und
|
||
Liste zeigen h.adresse als Überschrift; NEU: Suche (Adresse/Ort/PLZ/Lage) und
|
||
Listenansicht (Tabelle, Zeilenklick öffnet Wohneinheiten, Kosten-Selects
|
||
zeigen b.adresse). Typ: Building (neu) + HausForm = Omit<name|adresse>.
|
||
Anleitung aktualisiert.
|
||
|
||
**Erledigt 04.10. (v1.25):** Zählerverlauf + Anomalitätserkennung. Neue Tabelle
|
||
meter_readings (seq BIGSERIAL als Sortierung – erstellt_am ist Text und sortiert
|
||
lexikografisch falsch!): jeder erfasste Stand bleibt als Eintrag bestehen
|
||
(quelle erstbefund/manuel/mieter, erfasst_von, anomalie + anomalie_grund);
|
||
meters.stand bleibt aktuellster Wert (Cache). Modul server/src/zaehlerVerlauf.js:
|
||
pruefeAnomalie (Ruecklauf | Datum in Zukunft | rueckdatiert | Stillstand ≥60 Tage
|
||
| Verbrauchsspitze >4× Median-Tagesrate, nur ≥2 Intervalle + Delta ≥10),
|
||
standEintragen (insert + Mail an Verwaltung bei Anomalie, HTML-Karte mit
|
||
Button), erstbefundFehlende (Start-Einmal-Backfill bestehender Zähler).
|
||
Endpunkte: GET/POST /api/meters/:id/readings (Mieter nur eigene Einheit,
|
||
meterFuer-Helper prüft), DELETE …/readings/:rid (admin, baut meters-Cache neu).
|
||
Bootstrap: letzte_anomalie via Subquery (ORDER BY seq DESC). Frontend
|
||
src/pages/Zaehler.tsx: VerlaufDialog (Liste neueste zuerst, Auffälligkeits-
|
||
Badge, Löschen für Verwaltung), StandEintragenDialog (Mieter + Verwaltung,
|
||
Warnbox bei Anomalie statt sofortigem Schließen), Anomalie-Badge auf der Karte.
|
||
Achtung Git-Bash: Pfad-Argumente an node werden von MSYS umgewandelt –
|
||
Pfade an node-Skripte OHNE führenden / übergeben. AUCH BEHOBEN: nginx
|
||
pufferte Container-IPs beim Start → nach mp-api-Recreate lieferte jede API-
|
||
Anfrage 502 (root cause mehrerer „schwarzer Bildschirm“-/Reload-Probleme).
|
||
Fix: resolver 127.0.0.11 + Variablen-proxy_pass in
|
||
docker/templates/default.conf.template; deploy.sh prüft Health jetzt inhaltlich
|
||
(curl -sf + grep datenbank:true).
|
||
|
||
**Erledigt 04.10. (v1.24):** Anleitungsseite /anleitung (Seite „Anleitung" im
|
||
Menü, sichtbar für Mieter und Verwaltung). Inhalte rollenbasiert in
|
||
src/data/anleitung.ts (Abschnitte je gruppe 'mieter'|'verwaltung'); Verwaltung
|
||
hat Reiter „Für die Verwaltung" / „Mieter-Ansicht". Stand-Badge (Datum + Build-
|
||
Hash) kommt aus src/data/anleitung-meta.ts, die von scripts/anleitung-meta.mjs
|
||
erzeugt wird – Aufruf vor jedem Build: als package.json „prebuild"-Hook (läuft
|
||
also bei JEDEM npm run build mit) plus eigener Schritt 1 in deploy.sh (1/5…5/5).
|
||
Meta-Datei ist gitignored (sonst Hash-Henne-Ei: Committen ändert den Hash).
|
||
Dunkelmodus-taugliche Klassen beachten.
|
||
|
||
**Erledigt 04.10. (v1.23):** Nebenkostenabrechnung als PDF direkt aus dem
|
||
Kosten- und Zählerstandsmodul. Neues Modul `server/src/abrechnung.js`
|
||
(pdfkit, neue Dependency – Achtung: Standardfont Helvetica/WinAnsi, kein
|
||
U+2212-Minus; ASCII '-' verwenden): lädt Wohnung, Hauseinheiten
|
||
(Flächenanteil = Wohnungsgröße/Hausgesamtfläche), umlegbare Kosten des Hauses
|
||
(jahr + (unit_id = Wohnung ODER NULL)), gruppiert nach Kostenart; Vorauszahlung
|
||
= (Warm- − Kaltmiete) × 12; Differenz → Nachzahlung (rot) oder Erstattung
|
||
(grün). PDF nach UPLOAD_DIR als `ab-<unit>-<jahr>.pdf`, idempotentes
|
||
documents-Row (Titel `Nebenkostenabrechnung <Jahr> – <Haus>, Whg. <Nr>`,
|
||
Kategorie „Nebenkostenabrechnung“, alte Datei wird ersetzt). Endpoint
|
||
`POST /api/abrechnungen` (auth, adminOnly) → `{dokument, zusammenfassung}`.
|
||
Frontend: `AbrechnungDialog` in src/pages/Kosten.tsx (Button im Kosten-Header
|
||
neben KostenDialog), Ergebnisbox mit Download-Link. E2E live: Testkosten
|
||
(hausweit + wohnungsbezogen) → Abrechnung we1/2026 → PDF geprüft (Umlaute,
|
||
€, ² ok) → Testdaten bereinigt. UAT-Liste: neue Spalte „Ziel-Version" (Spalte
|
||
K), COUNTIF-Bugfix (zählte fälschlich Spalte I statt J).
|
||
|
||
**Erledigt 04.10. (v1.22):** Direkt-E-Mail der Verwaltung an Mieter mit
|
||
automatischem CC an die Verwaltung. Mail-API (mail-server/api/server.py,
|
||
auf dem Server manuell ausgerollt, Container mp-mail-api per docker restart –
|
||
NICHT Teil von deploy.sh) um cc-Feld erweitert: Body cc kommagetrennt,
|
||
einzeln per EMAIL_RE validiert, versenden setzt msg["Cc"]. ACHTUNG Repo-
|
||
Stand vorher von Server frisch gezogen (from_address/from_name war nur
|
||
serverseitig). API: POST /api/users/:id/email (adminOnly) – Betreff/Text
|
||
Pflicht, Mieter muss E-Mail hinterlegt haben (sonst 400 mit Hinweis),
|
||
HTML-Karte baueHtmlMail, CC = mail.verwaltungEmails() (alle aktiven
|
||
Verwaltungs-Accounts mit E-Mail), Versand fire-and-forget via sendeMail(…, cc).
|
||
PATCH /api/users/:id akzeptiert jetzt auch email (Format- + Eindeutigkeits-
|
||
prüfung, LEERE E-Mail darf mehrfach vorkommen – Eindeutigkeit nur bei
|
||
nicht-leer, sonst Fehler beim Leeren). Frontend Benutzerverwaltung:
|
||
EmailDialog pro Mieter (Empfängeranzeige, Betreff, Nachricht, Hinweis auf
|
||
CC, Senden deaktiviert ohne Mieter-E-Mail), E-Mail-Adresse jetzt inline im
|
||
Benutzer-Eintrag editierbar (onBlur → setzeBenutzerEmail). Store:
|
||
sendeBenutzerEmail + setzeBenutzerEmail. E2E verifiziert: E-Mail setzen,
|
||
senden → Postfix eine queue-ID mit 3 Empfängern (Testadresse bounced
|
||
erwartet, joseph@hinterstein-immo.de + axel@hocks.eu status=sent),
|
||
Fehlerfall ohne E-Mail sauber, Testdaten bereinigt.
|
||
|
||
**Erledigt 04.10. (v1.21):** Live-Stammdaten aus SharePoint-Export-CSV
|
||
importiert (KEIN Dauersync – Nutzer-Entscheidung: einmaliger Import,
|
||
Pflege danach nur noch im Portal). CSV „Master Data V" (53 Zeilen,
|
||
37 Spalten, 12 Häuser in Krefeld, 46 Wohneinheiten inkl. Garagen/Zimmer,
|
||
43 Mietparteien) liegt NUR lokal im Workspace (PII, in .gitignore).
|
||
Import-Skript server/import/import-masterdata.py (stdin CSV → stdout SQL,
|
||
deterministische IDs slug(haus)-slug(we), deutsche Zahlenformate
|
||
'2.700'→2700 / '71,4'→71.4 / Baujahr '1.905'→1905, PLZ '47.829'→47829,
|
||
Etage aus 'Lage im Objekt' EG/1. OG/DG, WE01→'1'). buildings-NOT-NULL
|
||
(baujahr/stockwerke/etagenhoehe/wohneinheiten/quadratmeter) per ALTER
|
||
gelockert. Import ersetzt komplett: threads/tickets/transactions/costs/
|
||
belege/documents/meters/codes + Mieter-User + units + buildings;
|
||
Verwaltungs-User, Ankündigungen bleiben. Mieter bekommen Zufalls-Passwort-
|
||
Hash (unloggbar bis Passwort-Reset auf 0815 durch Verwaltung in /benutzer),
|
||
email='' (Mailversand überspringt leere Adressen, v1.20-Profil zum
|
||
Nachpflegen). Zähler NICHT importiert (CSV-Spalten nur '0').
|
||
Sync-Infrastruktur (v1.21-Vorentwicklung) komplett entfernt: mp-sync-cron
|
||
Container gelöscht, portal.env von SP_*/INTERN_SYNC_TOKEN bereinigt,
|
||
Code aus Arbeitsbaum entfernt (nie gepusht). WICHTIG seed.js-Fix:
|
||
Demo-Nachseeds (seedeZaehler/Haeuser/Kosten) prüften nur Tabellen-Leere
|
||
→ FK-Crashloop nach Import (hs-7 fehlt). Fix: demoBestandVorhanden()
|
||
gate auf units.id='hs-7'. Frontend: hartkodierte Demo-Zahlen entfernt –
|
||
AppLayout-Footer + Dashboard (Bestandszählung, Belegungsquote) jetzt
|
||
dynamisch aus buildings/units, Hauslisten in Payments/Zaehler/Benutzer-
|
||
Verwaltung dynamisch, Login-Demo-Zugang a.berger entfernt (existiert
|
||
nicht mehr), DEMO_UNIT_ID='' (Fallback = erste Einheit). E2E: Bootstrap
|
||
12 Häuser/46 Einheiten/43 Mieter, API healthy.
|
||
|
||
**Erledigt 04.10. (v1.20):** Stammdaten-Selbstpflege. users-Tabelle um
|
||
telefon/kontoinhaber/iban/bankname erweitert (ALTER ... ADD COLUMN IF NOT
|
||
EXISTS). PATCH /api/auth/profil (auth, eigene Daten): E-Mail (Pflicht +
|
||
Eindeutigkeitsprüfung case-insensitiv), Telefon, Kontoinhaber, IBAN
|
||
(Leerzeichen werden entfernt/großgeschrieben, Formatprüfung
|
||
^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$), Bankname. oeffentlicherBenutzer liefert die
|
||
neuen Felder mit (gelten damit auch für /api/auth/me, Bootstrap benutzer,
|
||
Benutzerverwaltung). Frontend: neue Seite /profil (Nav „Mein Profil", beide
|
||
Rollen): Ansichtskarte Konto & Wohnung (Name/Benutzername/Wohneit/ Mitglied
|
||
seit, readonly), editierbare Kontaktdaten (E-Mail, Telefon) und
|
||
Bankverbindung (nur Mieter, Hinweis auf Kaution). Store: aktualisiereProfil
|
||
→ PATCH, setMe mit Server-Antwort; Erfolgs-/Fehlermeldung inline. E2E
|
||
verifiziert: Speichern als a.berger (IBAN normalisiert), Auslesen via
|
||
/api/auth/me, Negativtests ungültige IBAN + fremde E-Mail werden sauber
|
||
abgelehnt. Demo-Daten der a.berger bleiben als Schaufenster-Datensatz.
|
||
|
||
**Erledigt 04.10. (v1.19):** Schadensmeldungen mit Kommunikation verknüpft.
|
||
threads-Tabelle um `ticket_id TEXT REFERENCES tickets(id)` erweitert (ALTER +
|
||
Index idx_threads_ticket). POST /api/tickets legt den Auto-Thread bereits mit
|
||
ticket_id an (Verknüpfung kam zur ursprünglichen Thread-Anlage nachträglich
|
||
dazu – bestehende Threads ohne ticket_id bleiben unverknüpft, nur NEUE
|
||
Meldungen). mapThread liefert ticketId mit. PATCH /api/tickets/:id schreibt
|
||
bei Statuswechsel eine Systemnachricht in den verknüpften Thread
|
||
(„Status … geändert: alt → neu", from_role 'admin', unread_by_tenant=TRUE)
|
||
und sendet eine HTML-Mail an den Mieter (Titel + alt/neu). Frontend:
|
||
Ticket-Karten zeigen einen „Kommunikation"-Bereich (Nachrichtenzahl, letzte
|
||
Nachricht, Link „Unterhaltung öffnen →" nach `/nachrichten?thread=<id>`);
|
||
Messages.tsx wertet den ?thread=-Parameter aus und selektiert die Unterhaltung.
|
||
E2E verifiziert: Ticket als a.berger → Thread mit ticketId + Erstnachricht +
|
||
KI-Antwort (N8N) im selben Thread; Statuswechsel als Admin → Systemnachricht
|
||
+ Mail-Versuch (Bounce nur wegen Demo-Adresse example.de, erwartet);
|
||
Testdaten bereinigt.
|
||
|
||
**Erledigt 04.10. (v1.17 + v1.18):** Auto-Zuordnung der Belege + eigener
|
||
OCR-Container. ZUORDNUNGSREGEL (Vorgabe des Nutzers): PRIMÄR Stromzählernummer,
|
||
SEKUNDÄR Adresse. belege-Tabelle um unit_id/gebaude/zuordnung/rohtext erweitert
|
||
(ALTER ... ADD COLUMN IF NOT EXISTS). `belegAutoZuordnung(text)`: Stromzähler
|
||
aus meters (längste Nummer zuerst) als Substring im Volltext, dann Hausname/
|
||
Adresse aus buildings; WICHTIG: Umlaut-Normalisierung (ß↔ss, Umlaute↔Ae/Oe/Ue)
|
||
auf beiden Seiten – sonst findet „Hauptstrasse" nicht „Hauptstraße".
|
||
Webhook prüft zuerst den Event-Volltext (eventVolltext = rekursiv alle
|
||
String-/Zahlenwerte); ohne Treffer + datei_url → belegAnalysieren() asynchron
|
||
nach dem Response (fire-and-forget). `zuordnung='manuell'` wird bei Updates/
|
||
OCR NIEMALS überschrieben (CASE-Expressions im ON CONFLICT und im UPDATE).
|
||
E2E verifiziert: Zählernummer→rw-4, Adresse→Hauptstraße 12, Testdaten weg.
|
||
OCR-CONTAINER mp-ocr (v1.18): server/ocr/ (FastAPI + Tesseract deu + PyMuPDF;
|
||
PDF→200dpi-PNG→pytesseract; Bilder direkt; /ocr {url} und /ocr/upload;
|
||
data:-URLs werden lokal dekodiert (httpx kann keine data:-URLs); 20 MB Limit).
|
||
Compose-Service ohne öffentlichen Port, nur im npm_default-Netz; deploy.sh
|
||
baut ihn serverseitig mit (`compose up -d --build`). mp-api: OCR_URL env
|
||
(default http://mp-ocr:8081), belegAnalysieren(id) (OCR-Text → Zuordnung →
|
||
rohtext ersetzen; Fallback rohtext), POST /api/belege/:id/analysieren
|
||
(adminOnly). Frontend: Belege-Tabelle mit Zuordnungs-Spalte (Ziel + „erkannt
|
||
per Zählernummer/Adresse/Manuell"), „Analysieren"-Button pro Beleg
|
||
(analysiereBeleg im Store), KostenDialog-Vorbefüllung um unitId/building
|
||
erweitert. E2E verifiziert: OCR über data-URL-PDF (reportlab-Test-PDF mit
|
||
„Zaehler-Nr: ST-0815-4711") → hs-7 zugeordnet; mp-ocr healthy.
|
||
|
||
**Erledigt 04.10. (v1.16):** SparkReceipt-Schnittstelle für gescannte Belege.
|
||
SparkReceipt bietet Webhooks (Doku: sparkreceipt.com/integrations/webhooks)
|
||
– POST auf eine URL bei document created/updated/deleted, Standard-Verzögerung
|
||
10 min, Event-Log mit „Send now". Setup beim Nutzer: SparkReceipt →
|
||
Integrationen → Webhooks → URL
|
||
`https://mieterportal.hocks.eu/api/belege/webhook?token=<SPARKRECEIPT_WEBHOOK_TOKEN>`
|
||
(Token liegt in /opt/mieterportal/portal.env, Stand 04.10.:
|
||
0e8c671ca1c4df3668bac6b2b57602f49ad0f4db8366cc42 – bei Rotation Eintrag in
|
||
portal.env ersetzen + `docker compose up -d`). NEUE TABELLE `belege`
|
||
(quelle, externe_id, haendler, betrag, waehrung, datum, jahr, kategorie,
|
||
notiz, datei_url, zugeordnet, erfasst_am; UNIQUE(quelle, externe_id)).
|
||
API: POST /api/belege/webhook (KEINE Session – Token per ?token= ODER
|
||
x-webhook-token-Header; ohne gesetztes Env → 503; toleranten Feld-Aliasen –
|
||
Payload ggf. unter body.document verschachtelt, Aliase merchant/vendor/total/
|
||
amount/date/category/file_url u. a.; delete-Events werden ignoriert;
|
||
INSERT ... ON CONFLICT (quelle, externe_id) DO UPDATE → idempotent).
|
||
PATCH /api/belege/:id (adminOnly) setzt zugeordnet. Bootstrap liefert belege
|
||
nur an die Verwaltung. Frontend: Tabs auf /kosten („Kostenpositionen" /
|
||
„Spark-Receipt-Belege", Admin-only) – Info-Box mit der Webhook-URL, Filter
|
||
Alle/Nicht zugeordnet, Tabelle mit Übernehmen-Button (öffnet KostenDialog
|
||
vorbelegt mit Betrag/Jahr/Notiz „SparkReceipt · Händler (#ID)";
|
||
onGespeichert markiert den Beleg zugeordnet) plus Beleg-Link (datei_url).
|
||
KostenDialog hat neue Props vorbefuellung/onGespeichert/triggerLabel.
|
||
E2E verifiziert (simulierte SR-Payloads): 401 ohne Token, Create, Update
|
||
selbe externe_id ohne Duplikat, Jahr-Ableitung aus Datum, Mieter 403, Admin
|
||
zugeordnet, Testbeleg per SQL entfernt. OFFEN: echtes Feld-Mapping gegen
|
||
ein echtes SparkReceipt-Event prüfen (Event-Log in SR zeigt das Payload –
|
||
ggf. Aliase in belegFelder() nachpflegen); Beleg-PDF-Download aus SR
|
||
nachrüsten, falls die datei_url ein öffentlicher Link ist.
|
||
|
||
**Erledigt 04.10. (v1.15):** Struktur „1 Haus → mehrere Wohneinheiten" in der
|
||
Verwaltung verankert + Bugfix Haus-Umbenennung. BUGFIX (wichtig): der FK
|
||
`costs_building_fkey` aus v1.14 hatte kein ON UPDATE CASCADE → jede
|
||
Haus-Umbenennung (PATCH /api/buildings/:name) schlug mit FK-Verletzung fehl
|
||
(„Interner Serverfehler"), sobald Kostenpositionen existieren. Fix in db.js:
|
||
Constraint per DO-Block idempotent auf ON UPDATE CASCADE neu gesetzt (DROP +
|
||
ADD; SCHEMA läuft als eine Simple-Query ohne Parameter, DO-Blöcke sind darin
|
||
erlaubt). E2E-Rundreise verifiziert: Umbenennen zieht units + meters +
|
||
costs mit, Rückbenennung sauber. UI (Bestand.tsx komplett überarbeitet):
|
||
Haus-Karten enthalten jetzt die 1:n-Struktur sichtbar – pro Haus
|
||
Belegungsfortschritt (Balken „X von Y Wohneinheiten angelegt", amber bei
|
||
Über-Soll, Hinweistext), eingebettete sortierte Wohnungsliste (Etage, dann
|
||
Nummer numerisch) mit Status/Mieter/Warmmiete/Bearbeiten, „+ Wohnung"
|
||
direkt pro Haus (festesHaus-Prop → Haus-Select vorbelegt und disabled,
|
||
Dialogtitel passt sich an). Globaler „Neue Wohnung"-Button oben bleibt mit
|
||
freier Hauswahl. Verwaiste Wohnungen (Haus existiert nicht) werden separat
|
||
mit Korrekturhinweis gelistet. Server: unverändert – POST/PATCH /api/units
|
||
prüft bereits, dass das Ziel-Haus in buildings existiert.
|
||
|
||
**Erledigt 04.10. (v1.14):** Steuermodul „Steuer & Kosten" (/kosten, Nav zwischen
|
||
Zählerstände und Dokumente, für beide Rollen sichtbar). Neue Tabelle `costs`
|
||
(building NOT NULL → buildings.name, unit_id NULL = hausweite Position, jahr,
|
||
kostenart, betrag, umlegbar BOOLEAN, notiz, erfasst_am), idempot vorbefüllt
|
||
mit 16 Demo-Positionen 2025/2026 (`seedeKosten()`). API (adminOnly):
|
||
POST/PATCH/DELETE /api/costs(/:id); Validierung: Kostenart aus KOSTEN_ARTEN
|
||
(15 Werte), jahr 2000..aktuell+1, betrag ≥ 0, Haus muss in buildings existieren
|
||
– bei gesetzter unitId folgt das Haus IMMER der Wohnung (eingereichtes Haus
|
||
wird überschrieben, vgl. meters). Bootstrap: Admin alle (jahr DESC), Mieter nur
|
||
eigene Wohnung. Frontend: Summenkarten (umlegbar/nicht umlegbar/Gesamt über die
|
||
Filterauswahl), Filterchips Umlegbarkeit + Selects Jahr/Haus (Admin), Tabelle
|
||
mit Kostenart/Notiz, Haus, Wohnung, Jahr, Badge, Betrag; Admin-Dialog
|
||
(Anlegen/Bearbeiten) mit Kostenart-, Umlegbarkeits-, Jahr-, Betrag-, Haus-
|
||
(disabled bei Wohnungswahl) und Wohnung-Select („Ganzes Haus" = Sentinel
|
||
'__haus'). Store: addKosten/updateKosten/deleteKosten. E2E verifiziert:
|
||
Hausableitung, Patch, Validierung 400 (Kostenart/Haus/Jahr/Betrag), Mieter
|
||
403, Löschen, Testdaten aufgeräumt.
|
||
|
||
**Erledigt 04.10. (v1.13):** Echter PDF-Upload für das Dokumentenmodul. Neue
|
||
Dep multer (^2.0.2, server/package-lock.json liegt jetzt im Repo, Docker
|
||
baut mit `npm ci`). DB: `documents` um `file_name`/`file_path` erweitert
|
||
(idempotent per `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` in db.js SCHEMA –
|
||
ACHTUNG: SCHEMA läuft als eine pool.query mit mehreren Statements,
|
||
funktioniert nur über das Simple-Query-Protokoll ohne Parameter). API:
|
||
POST /api/documents/:id/upload (adminOnly, multipart Feld „file", nur PDF,
|
||
25 MB, Datei heißt `<id>.pdf` im UPLOAD_DIR und ERSETZT die alte Datei;
|
||
`size` wird aus den echten Bytes gesetzt, `formatBytes` deutsche Anzeige);
|
||
GET /api/documents/:id/download (auth: Verwaltung immer, Mieter nur eigene
|
||
Wohnung oder Hausdokument; `path.basename` gegen Pfad-Traversal); DELETE
|
||
löscht die Datei best-effort mit. Ablage: UPLOAD_DIR env (default
|
||
./uploads), im Compose als named Volume `mp_uploads` an /app/uploads –
|
||
überlebt Container-Rekreation. nginx: `client_max_body_size 25m` für
|
||
location /api/ (war 256k!), proxy_read_timeout 60s. Frontend: Datei-Input
|
||
im Dokument-Dialog (Client-Validierung PDF/25 MB), Upload läuft NACH
|
||
Anlegen/Patchen über apiUpload (FormData, kein Content-Type setzen),
|
||
`syncDokument` ersetzt den State mit der Server-Fassung; PDF-Button ist
|
||
jetzt echter Download-Link, ohne Datei deaktiviert mit Hinweis.
|
||
E2E verifiziert: Upload/Download Byte-identisch (Admin + Mieter eigene
|
||
Wohnung), fremde Wohnung 403, Nicht-PDF 400, Upload ohne Doc 404,
|
||
Löschen entfernt Datei, Testdaten aufgeräumt.
|
||
|
||
**Erledigt 04.10. (v1.12):** Dokumente-CRUD für die Verwaltung (Schwerpunkt
|
||
Mietverträge). API (adminOnly): POST/PATCH/DELETE /api/documents(/:id) mit
|
||
Validierung (Titel nicht leer, Kategorie aus den 5 erlaubten Werten, unitId
|
||
null oder vorhandene Unit, Datum default `deutschesDatum()`, size default '–',
|
||
id `d-${Date.now()}`). Frontend: Admin-Dialog auf /dokumente (Titel, Kategorie,
|
||
Wohnung inkl. „Alle Häuser", Datum, Größe optional; Radix-Select braucht für
|
||
„Alle Häuser" den Sentinel '__alle' – leerer value ist unzulässig), pro Zeile
|
||
Bearbeiten/Löschen (window.confirm), Store-Aktionen addDokument/updateDokument/
|
||
deleteDokument + apiDelete. Mieter-Ansicht unverändert. E2E verifiziert:
|
||
Anlegen/Patchen/Löschen (Umlaute sauber), Validierung 400, Mieter 403,
|
||
Testdaten aufgeräumt. Bewusst NICHT gebaut: echter PDF-Upload (kein Storage
|
||
im Stack, Demo-Charakter – Download-Button bleibt dekorativ).
|
||
|
||
**Erledigt 04.10. (v1.11):** KI-Antworten auf Schadenmeldungen via n8n/Ollama.
|
||
Kette: Mieter-Ticket → mp-api `kiBenachrichtigen()` (POST an n8n, fire-and-
|
||
forget) → n8n-Workflow `mpKiAntwort0001` (Webhook POST `mp-schadensmeldung`,
|
||
responseMode onReceived; Knoten: Webhook → Code „KI-Antwort generieren
|
||
(Ollama)" → Code „Antwort ins Portal posten") → Ollama
|
||
`http://192.168.188.100:11434/api/generate` Modell `qwen2.5:7b` → POST
|
||
`https://mieterportal.hocks.eu/api/ki-antwort` (KI_API_TOKEN-Bearer) →
|
||
Nachricht als „Hinterstein Immobilien (KI-Assistent)" im Ticket-Thread + Mail
|
||
an Mieter. Konfig in /opt/mieterportal/portal.env: KI_API_TOKEN +
|
||
N8N_TICKET_WEBHOOK. n8n 2.41-Hürden beim direkten DB-Import: publizierte
|
||
Knoten in `workflow_history` (versionId = activeVersionId, authors='{}'
|
||
NOT NULL), `shared_workflow`-Zeile (projectId M7v7qD1yefYEQ9Dn, role
|
||
workflow:owner), Webhook-Knoten braucht node-level `webhookId`, fehlgeschlagener
|
||
`workflow_publication_outbox`-Eintrag blockiert Reaktivierung (Zeile löschen +
|
||
`docker restart n8n`). E2E verifiziert: KI-Antwort im Thread + Mail sauber.
|
||
|
||
**Erledigt 04.10. (v1.10):** Bestandsverwaltung. Neue Tabelle `buildings`
|
||
(PK name; Pflicht: Adresse, Baujahr, Stockwerke, Etagenhöhe, Wohneinheiten,
|
||
Quadratmeter), idempotent vorbefüllt mit den 3 Demo-Häusern (`seedeHaeuser()`).
|
||
Wohnungen bleiben über `units.building` (Text = Hausname) verknüpft; Haus-
|
||
Umbenennung zieht units + meters in einer Transaktion mit. API (adminOnly):
|
||
POST/PATCH /api/buildings, POST/PATCH /api/units mit Validierung. Frontend:
|
||
neue Admin-Seite /bestand („Häuser & Wohnungen", Nav hinter Zählerstände) mit
|
||
Haus-Karten (inkl. „angelegt: X von Y Wohneinheiten") und gefilterter
|
||
Wohnungsliste, je Anlegen/Bearbeiten-Dialog. Robustheit: uncaughtException/
|
||
unhandledRejection-Handler in index.js – defekte JSON-Bodies von außen haben
|
||
den mp-api-Prozess gekillt (502er), jetzt 500 + Dienst läuft weiter.
|
||
E2E verifiziert: anlegen/editieren/umbenennen (Kaskade), Validierung 400,
|
||
Mieter 403. Achtung Testfall: curl verbindet --data-binary + -d mit „&"
|
||
→ invalider JSON → 500.
|
||
|
||
**Erledigt 04.10. (v1.9):** Zähler-Modul + Registrierungs-Mail + getrennter Name.
|
||
Neue Tabelle `meters` (Zählernummer, Art Strom/Wasser/Gas/Wärme/Sonstiges,
|
||
Stand, Stand-Datum, Wohneinheit nullable, Haus, Pflege-Zeitstempel) mit
|
||
idempotentem Demo-Seed (`seedeZaehler()` läuft bei jedem Start, füllt nur bei
|
||
leerer Tabelle – auch nachträglich auf Live-DB). API: POST/PATCH /api/meters
|
||
(adminOnly), Bootstrap liefert meters (Admin: alle, Mieter: nur eigene Wohnung);
|
||
Haus wird aus der Wohneinheit abgeleitet, bei Allgemeinzählern direkt gepflegt.
|
||
Frontend: neue Seite /zaehler (Kartenraster, Filter nach Haus + Art, Admin-
|
||
Dialog zum Anlegen/Bearbeiten), Nav-Eintrag mit Gauge-Icon, Store-Aktionen
|
||
addZaehler/updateZaehler. Registrierung: neuer Mieter löst HTML-Mail an die
|
||
Verwaltung aus (Name/Benutzername/E-Mail/Wohnung, Button zur Benutzerverwaltung);
|
||
Formular jetzt mit getrennten Feldern Vorname/Nachname + Benutzername.
|
||
E2E verifiziert: Zähler-CRUD + Rechte (403 Mieter), Registrierung inkl.
|
||
Mail-Zustellung an beide Verwaltungspostfächer, Umlaute sauber.
|
||
|
||
**Erledigt 02.10. (v1.8):** Schadensmeldungen starten jetzt automatisch eine
|
||
Unterhaltung (Thread „Schadensmeldung: …" mit erster Nachricht inkl. Kategorie/
|
||
Priorität, ungelesen für die Gegenseite) – Antworten darauf lösen über
|
||
`/api/threads/:id/messages` wie gehabt Mails an Verwaltung + Mieter aus.
|
||
Zusätzlich Eingangsbestätigungs-Mail an den Mieter bei jeder Meldung.
|
||
Verwaltungs-Mails gehen jetzt an zwei echte Postfächer: joseph@hinterstein-immo.de
|
||
(Benutzer j.hinterstein, Seed + Live-DB geändert – .de ist laut Betreiber die
|
||
richtige Adresse) und axel@hocks.eu (neuer Verwaltungsbenutzer a.hocks, Passwort
|
||
0815, Seed + Live-DB). E2E verifiziert:
|
||
Meldung + Antwort → je Mail an beide Postfächer zugestellt (Postfix status=sent);
|
||
Mieter-Adressen der Demoseeds sind example.de und bouncen bewusst.
|
||
|
||
**Encoding-Wissen (02.10.):** Umlaute in Mails waren nie kaputt – die früheren
|
||
Test-Mails enthielten bereits � (U+FFFD), weil curl unter Git Bash Umlaute in
|
||
`-d '…'` über die ANSI-Codepage schickt (0xFC = invalid UTF-8 → ersetzt).
|
||
Kette Browser → API → DB → mail-api → Postfix ist UTF-8-sauber (per Hex-Check
|
||
c3bc in DB bestätigt). Bei API-Tests mit Umlauten: JSON als UTF-8-Datei schreiben
|
||
und `curl --data-binary @datei` verwenden.
|
||
|
||
**Erledigt 02.10. (v1.7):** Responsive Layout repariert. Sidebar skaliert jetzt in zwei
|
||
Stufen (`w-64` ab `lg`, `xl:w-72`) statt fixer 288 px. Mobile Ansicht (Top-Bar +
|
||
horizontal scrollende Nav) gilt jetzt bis 1024 px (`md:hidden` → `lg:hidden`) – vorher
|
||
stand zwischen 768–1024 px die volle Sidebar und quetschte Inhalt + Fußzeile.
|
||
Sidebar-Fußzeile bricht um (`flex-wrap`, `truncate`-sichere Buttons), `main`-Padding
|
||
folgt dem `lg`-Breakpoint. Tailwind war bereits das CSS-Framework (kein Wechsel nötig).
|
||
Screenshots: `preview/09-mobile-900px.png`, `preview/10-desktop-1280px.png`.
|
||
|
||
**Erledigt 02.10. (v1.6):** Login-Race behoben: `setMe` läuft jetzt erst nach
|
||
`ladeAlles()` (Start/Login/Registrierung) – vorher rendereten die Seiten gegen
|
||
noch leere Daten → Absturz → schwarzer Bildschirm, erst Reload half. Zusätzlich
|
||
`FehlerGrenze` (ErrorBoundary) in `App.tsx`: Render-Fehler zeigen Hinweis +
|
||
Reload-Button statt schwarzem Bildschirm.
|
||
|
||
**Erledigt 02.10. (v1.5):** Lokales Datenbank-Backup: `backup/vps-db-backup.cmd`
|
||
holt nächtlich (Aufgabenplanung „Mieterportal DB-Backup", täglich 03:17 Uhr,
|
||
Benutzer ah, interaktiv) per plink ein `pg_dump | gzip` vom Container mp-db und
|
||
legt es nach `D:\VPS_Backup\mieterportal\mieterportal-db-YYYY-MM-DD.sql.gz`
|
||
(14-tägige Aufbewahrung via forfiles, Log: `backup.log`). Probelauf OK
|
||
(11 KB, Dump-Format geprüft). Einschränkung: läuft nur, wenn der Rechner an
|
||
und Benutzer ah angemeldet ist (Z:-Laufwerk mit dem SSH-Key, Pageant).
|
||
|
||
**Erledigt 02.10. (v1.0):** Portal komplett neu aufgesetzt (5 Module, Mieter-/Verwaltungsansicht,
|
||
KI-Branding: Logo/Hero/Illustration), Docker-Setup (Dockerfile, nginx, compose),
|
||
Repo in git.hocks.eu angelegt, Deploy-Workflow + Dark Mode + beispieldaten.py aus dem
|
||
Schulportal übernommen, mail-server-Struktur angelegt (noch nicht live verkabelt).
|
||
|
||
**Erledigt 02.10. (v1.0 live):** Deployment auf hocks.eu durchgeführt: Container `mp-portal`
|
||
läuft (`/opt/mieterportal`, compose + gemountetes dist), NPM-Proxy-Host id 7 + LE-Zertifikat
|
||
npm-9 eingerichtet (per certbot/webroot, DB-Insert, 7.conf aus 5.conf abgeleitet, nginx reload).
|
||
Live geprüft: `https://mieterportal.hocks.eu/` → HTTP 200, Titel korrekt, SPA-Routing
|
||
(`/mietkonto`) 200, HTTP→HTTPS-Redirect 301, Zertifikat CN=mieterportal.hocks.eu gültig
|
||
bis 31.12.2026. Zertifikatsdateien liegen im npm-Container unter `/etc/letsencrypt/live/npm-9/`
|
||
(Backup der Zertifikate auf dem Host: `/root/le-mp/`). Fail2Ban-Ban nach frühen
|
||
Fehlversuchen ist nach ~40 min automatisch ausgelaufen.
|
||
|
||
**Erledigt 02.10. (v1.1 live):** Rebrand auf „Hinterstein Immobilien" (Kunden-Logo,
|
||
Joseph-Hinterstein-Portrait auf der Login-Seite, Kontakt-Banner), echtes
|
||
Authentifizierungssystem (Login-Pflicht, Gruppen Verwaltung/Mieter, SHA-256,
|
||
Session 8 h), Mieter-Registrierung per einmaligem Code, Benutzerverwaltung als
|
||
6. Modul (`/benutzer`), Demo-Zugänge mit Passwort `0815`. Mail-Stack auf dem Server
|
||
aktiviert (`/opt/mp-mail`, Testmail + API-/send verifiziert). Store-Key auf
|
||
`hinterstein-mieterportal-v2` gehoben. Live deployed und geprüft (HTTP 200, Titel
|
||
„Hinterstein Immobilien · Mieterportal").
|
||
|
||
**Erledigt 02.10. (v1.2 live):** E-Mail-Benachrichtigungen an das Portal
|
||
angeschlossen: nginx-Proxy `/api/mail/send` mit serverseitig injiziertem Token
|
||
(envsubst-Template + `portal.env` auf dem Server), `src/lib/mail.ts` sendet bei
|
||
neuen Schadensmeldungen (→ Verwaltung) und Nachrichten/Antworten (→ Gegenstelle).
|
||
E2E-Versand live verifiziert (versandt: true), GET auf den Endpunkt 403.
|
||
|
||
**Erledigt 02.10. (v1.3 live):** Serverseitige Datenbank: PostgreSQL 16 (`mp-db`,
|
||
Volume `mp_pgdata`) + Node-API (`mp-api`, Express + pg, eigenes Image via
|
||
deploy.sh `--build`). Schema 11 Tabellen (idempotent), Seed einmalig aus
|
||
`server/seed/generated.json` + `seed-data.js` (Verwaltungs-E-Mail auf echtes
|
||
Postfach axel@hocks.eu gesetzt). Frontend komplett auf API umgestellt:
|
||
`src/lib/api.ts`, Store lädt `/api/bootstrap`, kein localStorage mehr (außer
|
||
Theme), Ladebildschirm während Session-Check. Rollenrechte serverseitig
|
||
durchgesetzt und getestet (Mieterin sieht nur hs-7: 12 Buchungen/2 Tickets,
|
||
403 auf fremde Unterhaltung; Verwaltung sieht alles: 552 Buchungen/8 Tickets).
|
||
Registrierung, Code-Erzeugung, Ticket inkl. Mail-Benachrichtigung (status=sent
|
||
an axel@hocks.eu) E2E verifiziert; Testdaten danach bereinigt. deploy.sh baut
|
||
das API-Image auf dem Server; `.env`-Symlink auf portal.env für
|
||
Compose-Interpolation beachten (POSTGRES_PASSWORD!).
|
||
|
||
**Erledigt 02.10. (v1.4 live):** HTML-E-Mail-Benachrichtigungen im Schulportal-
|
||
Stil: `baueHtmlMail`/`buttonHtml` in `server/src/mail.js` (600-px-Karte,
|
||
Kopfzeile Hinterstein-Farben, Zeilen, Status-Box, Zitat, Aktions-Button, Footer),
|
||
immer mit Plain-Text-Fallback (multipart/alternative). Umgesetzt für
|
||
Schadensmeldung, neue Unterhaltung und Antwort. Vorschauen lokal renderbar
|
||
(`mail-vorschau*.html`, Screenshots in `preview/08-*.png`). Live getestet:
|
||
Zustellung an axel@hocks.eu status=sent, Test-Ticket bereinigt.
|
||
|
||
**Offen:**
|
||
- Zertifikatserneuerung prüfen (per 01.12.2026): NPM-Renewer sollte cert 9 über HTTP-Challenge
|
||
erneuern; sonst certbot/webroot + Kopie nach npm-9 wiederholen.
|
||
- Demo-Empfängeradressen der Mieter (example.de) durch echte ersetzen, sobald bekannt.
|
||
- Backup ggf. serverseitig ergänzen (zweite Sicherungslinie, falls lokaler Rechner
|
||
nachts aus ist), Backup-Wiederherstellung einmal testen.
|
||
- Sicherheit für Produktion: Passwort-Hash serverseitig salten (bcrypt/argon2), CSRF-Token,
|
||
Rate-Limiting auf /api/auth/login.
|
||
- Mögliche nächste Schritte: E-Mail-Verifizierung bei Registrierung, Dokumenten-Download
|
||
mit echten Dateien.
|
||
|
||
## 9. Sonstiges
|
||
|
||
- User kommuniziert auf Deutsch; alle UI-Texte und Antworten auf Deutsch.
|
||
- Dateien außerhalb des Workspaces immer erst in den Workspace kopieren.
|
||
- Vorsicht mit destruktiven Aktionen (Explizite Bestätigung einholen).
|
||
## 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.
|
||
|