diff --git a/index.html b/index.html index ad0b398..66a1d94 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,12 @@ + + + Hinterstein Immobilien · Mieterportal diff --git a/kimi.md b/kimi.md index 612518d..233f720 100644 --- a/kimi.md +++ b/kimi.md @@ -223,8 +223,32 @@ 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). -**Offen:** Workflow in N8N importieren + Kimi-Credential + Header-Auth + -Webhook-URL in portal.env eintragen (Schritte in n8n/README-assistent.md). + +**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. (v1.35):** Mietvertragsmodul (Verwaltung, Navigation „Mietverträge", Admin-only). Workflow: anlegen → Entwurf-PDF → Versand an diff --git a/n8n/README-assistent.md b/n8n/README-assistent.md index f5b679f..c9f0325 100644 --- a/n8n/README-assistent.md +++ b/n8n/README-assistent.md @@ -2,51 +2,84 @@ Der Assistent läuft so: **Portal-Chat → N8N-Webhook → Kimi (Moonshot API) mit 6 Portal-Tools → Antwort zurück ins Portal.** -## 1. Voraussetzungen +> **Stand: Der Workflow ist auf dem Produktivsystem (hocks.eu) bereits serverseitig eingerichtet und aktiv.** +> Die Datei `assist-workflow.json` in diesem Ordner ist die dokumentierte Referenz-Version. +> Der produktive Workflow wurde direkt in die N8N-Datenbank eingespielt, weil die lokale +> N8N-API in Version 2.x keinen zuverlässigen programmatischen Import erlaubt (401-Problem). +> Bei einem Re-Import auf einem anderen System müssen die drei Platzhalter im Code-Node belegt werden. -- N8N läuft auf dem VPS (Container `n8n`). -- Kimi-API-Key von (Aufladung ab ~5 $; Verbrauch des Assistenten liegt typisch unter 5 €/Monat bei 20 Fragen/Tag, Modell `kimi-k2.6`: ~0,4 US-Cent pro Frage). +## 1. Architektur des Workflows -## 2. N8N-Workflow importieren +Ein einzelner Workflow **„Mieterportal – KI-Assistent (Kimi)"**, Webhook `POST mp-assist`, drei Nodes: -1. N8N öffnen → **Workflows → Import from File** → `assist-workflow.json` aus diesem Ordner. -2. Nach dem Import drei Platzhalter anpassen: +1. **Webhook Portal** (`n8n-nodes-base.webhook`, typeVersion 2, responseMode `responseNode`) + – produktive URL: `http://n8n:5678/webhook/mp-assist` (internes Docker-Netz `npm_default`). + – Wichtig: Der Node braucht die Property **`webhookId`** (gleich der Node-ID) auf Root-Ebene, + sonst registriert n8n den Webhook bei `responseMode: responseNode` nicht (404 „not registered"). +2. **Assistent (Kimi + Tools)** (`n8n-nodes-base.code`, typeVersion 2, runOnceForAllItems) + – Prüft den Header `x-assist-secret` selbst gegen das Shared Secret. + – Ruft `https://api.moonshot.ai/v1/chat/completions` (Modell `kimi-k2.6`) per + `this.helpers.httpRequest` auf, mit OpenAI-kompatiblem **Tool-Calling** für genau die + 6 Portal-Endpunkte unter `/api/assist/tools/…` (Schleife, max. 6 Runden). + – **Kein `temperature`-Parameter** senden – `kimi-k2.6` akzeptiert nur `temperature: 1`, + sonst 400 „invalid temperature". + – **Kein `URLSearchParams`** im Sandbox-Code verwenden – nicht verfügbar; Query-Strings + manuell mit `encodeURIComponent` bauen. + – 429 (Rate-Limit) wird abgefangen und als freundliche Meldung zurückgegeben. +3. **Antwort an Portal** (`n8n-nodes-base.respondToWebhook`, typeVersion 1.1) + – `respondWith: json`, Body `={{ { antwort: $json.antwort } }}`. -| Wo | Was | -|---|---| -| Webhook-Node → Credentials | **Header Auth** anlegen: Name `x-assist-secret`, Wert = `ASSIST_SHARED_SECRET` | -| Kimi-Node → Credentials | **OpenAI API**-Credential anlegen: API Key = Moonshot-Key, Base URL `https://api.moonshot.ai/v1` | -| Alle 6 Tool-Nodes | Im Header `x-assist-key` den Wert `ASSIST_API_KEY` durch das echte Geheimnis ersetzen | +Der langchain-basierte Entwurf (AI-Agent-Node) wurde verworfen: Die `@n8n/n8n-nodes-langchain`- +Nodes liefen auf diesem Server nicht zuverlässig. Der Code-Node-Ansatz hat keine externen +Abhängigkeiten und keine Credentials (alle Geheimnisse als Literale im Code-Node). -3. Workflow **aktivieren** (Toggle oben rechts). Die Webhook-URL steht dann im Webhook-Node, z. B. - `https://n8n.hocks.eu/webhook/mp-assist` – sie wird in Schritt 3 gebraucht. +## 2. Geheimnisse (Platzhalter im Workflow-JSON) -## 3. Portal-Env auf dem Server ergänzen +| Platzhalter im Code-Node | Bedeutung | Wo konfiguriert | +|---|---|---| +| `` | Header `x-assist-secret` zwischen Portal und n8n | `portal.env` (ASSIST_SHARED_SECRET) | +| `` | Header `x-assist-key` für die 6 Tool-Endpunkte | `portal.env` (ASSIST_API_KEY) | +| `` | Moonshot-API-Key (`sk-…`) | Code-Node, Konstante ganz oben | -In `/opt/mieterportal/portal.env` (Datei auf dem VPS): +## 3. Portal-Env auf dem Server + +In `/opt/mieterportal/portal.env` (nur auf dem VPS, gitignored): ```bash -N8N_ASSIST_WEBHOOK=https://n8n.hocks.eu/webhook/mp-assist +N8N_ASSIST_WEBHOOK=http://n8n:5678/webhook/mp-assist ASSIST_SHARED_SECRET= ASSIST_API_KEY= ``` -Danach: `cd /opt/mieterportal && docker compose up -d` (nimmt die neuen Env-Variablen mit). +Danach: `cd /opt/mieterportal && docker compose up -d`. -## 4. Test +## 4. Kosten -Im Portal als Verwaltung: **KI-Assistent** in der Navigation → „Welche Wohnungen stehen leer?“ -Der erste Aufruf dauert ggf. 10–30 s (Agent lädt Tools). +Kimi `kimi-k2.6`: ~0,4 US-Cent pro Frage (Systemprompt + Tool-Schleife). Bei 20 Fragen/Tag +typisch unter 5 €/Monat. Aufladung des Moonshot-Kontos ab ~5 $. + +## 5. Test + +Im Portal als Verwaltung: **KI-Assistent** in der Navigation → „Welche Wohnungen stehen leer?" +Der erste Aufruf dauert ggf. 10–60 s (Tool-Schleife). + +Serverseitig direkt testbar: + +```bash +docker exec mp-api node -e "fetch('http://n8n:5678/webhook/mp-assist',{method:'POST',headers:{'Content-Type':'application/json','x-assist-secret':''},body:JSON.stringify({frage:'Welche Wohnungen stehen leer?',verlauf:[],benutzer:'E2E'})}).then(async r=>console.log(r.status,await r.text()))" +``` ## Fehlerbilder -| Fehlermeldung im Chat | Ursache | +| Fehlermeldung | Ursache | |---|---| -| „noch nicht konfiguriert (N8N_ASSIST_WEBHOOK fehlt)" | Env-Variable fehlt / Container nicht neu gestartet | -| „Assistent antwortet nicht (N8N Status …)" | Webhook-URL falsch, Workflow nicht aktiv, oder Header-Secret passt nicht | -| „Assistent hat keine Antwort geliefert" | N8N liefert kein JSON `{ "antwort": "…" }` – Respond-Node prüfen | -| „braucht zu lange" | Kimi-Timeout > 90 s – Modell/Credential prüfen | +| „not registered" (404) | Workflow nicht aktiv, `webhookId` am Webhook-Node fehlt, oder n8n neu gestartet ohne Outbox-Eintrag | +| „invalid temperature" (400) | `temperature != 1` im Kimi-Payload | +| „Assistent antwortet nicht (N8N Status 500)" | Kimi 429 (Rate-Limit, kurz warten) oder Code-Fehler – Executions in n8n prüfen | +| Antwort „Tool … nicht erreichbar" | Portal-Tool-Endpunkte nicht erreichbar (Netz `npm_default`, `x-assist-key`) | ## Datenschutz-Hinweis -Das Tool `mieter_kontakt` liefert personenbezogene Daten an Kimi (Serverstandort Singapur, Training laut Anbieter-AGB möglich). Wer das vermeiden will, entfernt das Tool einfach aus dem AI-Agenten – alle anderen Tools liefern nur Bestands- und Zahlen-Daten. +Das Tool `mieter_kontakt` liefert personenbezogene Daten an Kimi (Serverstandort Singapur, +Training laut Anbieter-AGB möglich). Wer das vermeiden will, entfernt den Tool-Eintrag aus +dem `TOOLS`-Array im Code-Node – alle anderen Tools liefern nur Bestands- und Zahlen-Daten. diff --git a/n8n/assist-workflow.json b/n8n/assist-workflow.json index d19b901..73e36ac 100644 --- a/n8n/assist-workflow.json +++ b/n8n/assist-workflow.json @@ -1,275 +1,65 @@ { - "name": "Mieterportal – KI-Assistent (Kimi)", + "name": "Mieterportal - KI-Assistent (Kimi)", "nodes": [ { "parameters": { "httpMethod": "POST", "path": "mp-assist", - "authentication": "headerAuth", - "respondWhen": "lastNode", + "responseMode": "responseNode", "options": {} }, - "id": "a1b2c3d4-0001-4000-8000-000000000001", - "name": "Webhook (Portal)", + "id": "ea0e27df-a1f6-49bd-9771-5cb3ec263ba2", + "name": "Webhook Portal", "type": "n8n-nodes-base.webhook", "typeVersion": 2, - "position": [240, 520], - "credentials": { - "httpHeaderAuth": { - "id": "ASSIST_HEADER_AUTH", - "name": "mp-assist-secret" - } - } + "position": [ + 240, + 300 + ], + "webhookId": "ea0e27df-a1f6-49bd-9771-5cb3ec263ba2" }, { "parameters": { - "text": "={{ $json.frage }}", - "options": { - "systemMessage": "Du bist der KI-Assistent des Mieterportals „Hinterstein Immobilien\". Du antwortest ausschließlich der Verwaltung auf Deutsch, kurz und sachlich. Beantworte Fragen zum Wohnungsbestand, Mietern, Mieteinnahmen, Zählern, Kosten und Schadensmeldungen mit den bereitgestellten Tools. Rufe immer zuerst die passenden Tools ab, bevor du Zahlen nennst – halluziniere keine Daten. Wenn ein Tool keine Treffer liefert, sage das offen. Rechne mit den Tool-Ergebnissen weiter (Summen, Anteile), wenn es hilft. Personenbezogene Daten (Telefon, E-Mail) nur nennen, wenn explizit danach gefragt wird." - } + "mode": "runOnceForAllItems", + "jsCode": "\nconst body = $json.body || {};\nconst headers = $json.headers || {};\nconst frage = String(body.frage || '').trim();\nconst verlauf = Array.isArray(body.verlauf) ? body.verlauf : [];\n\n// Sicherung: Shared-Secret pruefen (nur internes Docker-Netz erreichbar)\nif (headers['x-assist-secret'] !== '') {\n throw new Error('Ungueltiges Shared Secret');\n}\nif (!frage) return [{ json: { antwort: 'Leere Frage.' } }];\n\nconst PORTAL = 'http://mp-api:3000';\nconst ASSIST_KEY = '';\nconst KIMI_URL = 'https://api.moonshot.ai/v1/chat/completions';\n\nconst TOOLS = [\n { type: 'function', function: { name: 'leerstaende',\n description: 'Liste der leerstehenden oder unklaren Wohneinheiten mit Haus, Nummer, Etage und Flaeche. Keine Parameter.',\n parameters: { type: 'object', properties: {}, additionalProperties: false } } },\n { type: 'function', function: { name: 'mieteinnahmen',\n description: 'Monatliches Miet-Soll (Warm- und Kaltmieten aller Mieter), gesamt und je Haus. Optional: haus (Teil des Hausnamens).',\n parameters: { type: 'object', properties: { haus: { type: 'string', description: 'optionaler Filter, Teil des Hausnamens' } }, additionalProperties: false } } },\n { type: 'function', function: { name: 'offene_schaeden',\n description: 'Offene Schadensmeldungen mit Titel, Kategorie, Prioritaet, Status, Mieter und Wohnung, nach Dringlichkeit sortiert. Keine Parameter.',\n parameters: { type: 'object', properties: {}, additionalProperties: false } } },\n { type: 'function', function: { name: 'mieter_kontakt',\n description: 'Kontaktdaten (Telefon, E-Mail) eines Mieters. Genau einen Parameter: name (Nach- oder Vorname) ODER wohnung (Hausadresse oder Wohnungsnummer).',\n parameters: { type: 'object', properties: {\n name: { type: 'string' },\n wohnung: { type: 'string' } }, additionalProperties: false } } },\n { type: 'function', function: { name: 'zaehler_anomalien',\n description: 'Zaehler mit Auffaelligkeiten im neuesten Verlaufseintrag (Ruecklauf, Stillstand, Verbrauchsspitze) mit Zaehlernummer, Art, Haus, Stand, Datum und Grund. Keine Parameter.',\n parameters: { type: 'object', properties: {}, additionalProperties: false } } },\n { type: 'function', function: { name: 'kosten',\n description: 'Kosten eines Jahres, gesamt und je Haus/Kostenart, aufgeteilt in umlegbar und nicht umlegbar. Pflicht: jahr (z. B. 2026), optional: haus.',\n parameters: { type: 'object', properties: {\n jahr: { type: 'integer', description: 'Pflicht, z. B. 2026' },\n haus: { type: 'string' } }, required: ['jahr'], additionalProperties: false } } },\n];\n\nasync function toolAusfuehren(name, args) {\n const parts = [];\n for (const k of Object.keys(args || {})) {\n const v = args[k];\n if (v !== undefined && v !== null && String(v).trim() !== '') parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(String(v)));\n }\n const qs = parts.join('&');\n return await this.helpers.httpRequest({\n method: 'GET',\n url: PORTAL + '/api/assist/tools/' + name + (qs ? '?' + qs : ''),\n headers: { 'x-assist-key': ASSIST_KEY },\n json: true,\n });\n}\n\nconst messages = [\n { role: 'system', content: 'Du bist der KI-Assistent des Mieterportals \"Hinterstein Immobilien\". Du antwortest ausschliesslich der Verwaltung auf Deutsch, kurz und sachlich. Beantworte Fragen zum Wohnungsbestand, Mietern, Mieteinnahmen, Zaehlern, Kosten und Schadensmeldungen mit den bereitgestellten Tools. Rufe immer zuerst die passenden Tools ab, bevor du Zahlen nennst - halluziniere keine Daten. Wenn ein Tool keine Treffer liefert, sage das offen. Personenbezogene Daten (Telefon, E-Mail) nur nennen, wenn explizit danach gefragt wird.' },\n ...verlauf.slice(-20).map((m) => ({ role: m.rolle === 'user' ? 'user' : 'assistant', content: String(m.text).slice(0, 4000) })),\n { role: 'user', content: frage },\n];\n\nlet antwort = null;\ntry {\nfor (let runde = 0; runde < 6 && !antwort; runde++) {\n const resp = await this.helpers.httpRequest({\n method: 'POST',\n url: KIMI_URL,\n headers: { Authorization: 'Bearer ' + '', 'Content-Type': 'application/json' },\n body: {\n model: 'kimi-k2.6',\n messages,\n tools: TOOLS,\n },\n json: true,\n timeout: 60000,\n });\n const msg = (resp.choices || [])[0]?.message;\n if (!msg) break;\n if (Array.isArray(msg.tool_calls) && msg.tool_calls.length) {\n messages.push(msg);\n for (const tc of msg.tool_calls) {\n let ergebnis;\n try {\n const args = JSON.parse(tc.function?.arguments || '{}');\n ergebnis = await toolAusfuehren(tc.function.name, args);\n } catch (e) {\n ergebnis = { fehler: String(e.message || e).slice(0, 300) };\n }\n messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(ergebnis).slice(0, 6000) });\n }\n } else {\n antwort = String(msg.content || '').trim() || 'Keine Antwort erhalten.';\n }\n}\n} catch (e) {\n const status = e && e.response && e.response.status;\n if (status === 429) return [{ json: { antwort: 'Der Assistent ist kurzzeitig ausgelastet (Rate-Limit der KI). Bitte in einer Minute erneut fragen.' } }];\n return [{ json: { antwort: 'Fehler beim Assistenten: ' + String(e.message || e).slice(0, 200) } }];\n}\nreturn [{ json: { antwort: antwort || 'Die Antwort konnte nicht erzeugt werden.' } }];\n" }, - "id": "a1b2c3d4-0002-4000-8000-000000000002", - "name": "AI-Agent", - "type": "@n8n/n8n-nodes-langchain.agent", - "typeVersion": 1.7, - "position": [460, 520] - }, - { - "parameters": { - "model": "kimi-k2.6", - "options": { - "baseURL": "https://api.moonshot.ai/v1" - } - }, - "id": "a1b2c3d4-0003-4000-8000-000000000003", - "name": "Kimi (OpenAI-kompatibel)", - "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", - "typeVersion": 1, - "position": [460, 720], - "credentials": { - "openAiApi": { - "id": "MOONSHOT_API_KEY", - "name": "Moonshot (Kimi) API" - } - } - }, - { - "parameters": { - "name": "leerstaende", - "description": "Liste der leerstehenden oder unklaren Wohneinheiten mit Haus, Nummer, Etage und Fläche. Keine Parameter nötig.", - "method": "GET", - "url": "https://mieterportal.hocks.eu/api/assist/tools/leerstaende", - "authentication": "none", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "x-assist-key", "value": "ASSIST_API_KEY" } - ] - }, - "sendQuery": false, - "options": {} - }, - "id": "a1b2c3d4-0004-4000-8000-000000000004", - "name": "Tool: Leerstände", - "type": "@n8n/n8n-nodes-langchain.toolHttpRequest", - "typeVersion": 4.2, - "position": [700, 160] - }, - { - "parameters": { - "name": "mieteinnahmen", - "description": "Monatliches Miet-Soll (Warm- und Kaltmieten aller Mieter), gesamt und je Haus. Optional Parameter: haus (Teil des Hausnamens, z. B. Straße).", - "method": "GET", - "url": "https://mieterportal.hocks.eu/api/assist/tools/mieteinnahmen", - "authentication": "none", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "x-assist-key", "value": "ASSIST_API_KEY" } - ] - }, - "sendQuery": true, - "queryParameters": { - "parameters": [ - { "name": "haus", "value": "" } - ] - }, - "options": {} - }, - "id": "a1b2c3d4-0005-4000-8000-000000000005", - "name": "Tool: Mieteinnahmen", - "type": "@n8n/n8n-nodes-langchain.toolHttpRequest", - "typeVersion": 4.2, - "position": [700, 280] - }, - { - "parameters": { - "name": "offene_schaeden", - "description": "Offene Schadensmeldungen mit Titel, Kategorie, Priorität, Status, Mieter und Wohnung, nach Dringlichkeit sortiert. Keine Parameter nötig.", - "method": "GET", - "url": "https://mieterportal.hocks.eu/api/assist/tools/offene-schaeden", - "authentication": "none", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "x-assist-key", "value": "ASSIST_API_KEY" } - ] - }, - "sendQuery": false, - "options": {} - }, - "id": "a1b2c3d4-0006-4000-8000-000000000006", - "name": "Tool: Schäden", - "type": "@n8n/n8n-nodes-langchain.toolHttpRequest", - "typeVersion": 4.2, - "position": [700, 400] - }, - { - "parameters": { - "name": "mieter_kontakt", - "description": "Kontaktdaten (Telefon, E-Mail) eines Mieters. Genau einen Parameter angeben: name (Nach- oder Vorname) oder wohnung (Hausadresse oder Wohnungsnummer).", - "method": "GET", - "url": "https://mieterportal.hocks.eu/api/assist/tools/mieter-kontakt", - "authentication": "none", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "x-assist-key", "value": "ASSIST_API_KEY" } - ] - }, - "sendQuery": true, - "queryParameters": { - "parameters": [ - { "name": "name", "value": "" }, - { "name": "wohnung", "value": "" } - ] - }, - "options": {} - }, - "id": "a1b2c3d4-0007-4000-8000-000000000007", - "name": "Tool: Mieterkontakt", - "type": "@n8n/n8n-nodes-langchain.toolHttpRequest", - "typeVersion": 4.2, - "position": [700, 520] - }, - { - "parameters": { - "name": "zaehler_anomalien", - "description": "Zähler mit Auffälligkeiten im neuesten Verlaufseintrag (Rücklauf, Stillstand, Verbrauchsspitze etc.) mit Zählernummer, Art, Haus, Stand, Datum und Grund. Keine Parameter nötig.", - "method": "GET", - "url": "https://mieterportal.hocks.eu/api/assist/tools/zaehler-anomalien", - "authentication": "none", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "x-assist-key", "value": "ASSIST_API_KEY" } - ] - }, - "sendQuery": false, - "options": {} - }, - "id": "a1b2c3d4-0008-4000-8000-000000000008", - "name": "Tool: Zähleranomalien", - "type": "@n8n/n8n-nodes-langchain.toolHttpRequest", - "typeVersion": 4.2, - "position": [700, 640] - }, - { - "parameters": { - "name": "kosten", - "description": "Kosten eines Jahres, gesamt und je Haus/Kostenart, aufgeteilt in umlegbar und nicht umlegbar. Parameter: jahr (Pflicht, z. B. 2026), optional haus (Teil des Hausnamens).", - "method": "GET", - "url": "https://mieterportal.hocks.eu/api/assist/tools/kosten", - "authentication": "none", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "x-assist-key", "value": "ASSIST_API_KEY" } - ] - }, - "sendQuery": true, - "queryParameters": { - "parameters": [ - { "name": "jahr", "value": "" }, - { "name": "haus", "value": "" } - ] - }, - "options": {} - }, - "id": "a1b2c3d4-0009-4000-8000-000000000009", - "name": "Tool: Kosten", - "type": "@n8n/n8n-nodes-langchain.toolHttpRequest", - "typeVersion": 4.2, - "position": [700, 760] + "id": "9f84d4b5-1dda-41e1-a2e7-587145582751", + "name": "Assistent (Kimi + Tools)", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 480, + 300 + ] }, { "parameters": { "respondWith": "json", - "responseBody": "={{ { antwort: $json.output } }}" + "responseBody": "={{ { antwort: $json.antwort } }}" }, - "id": "a1b2c3d4-0010-4000-8000-000000000010", + "id": "56f1d427-f049-48b2-bbc1-bb43750e778d", "name": "Antwort an Portal", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, - "position": [680, 900] + "position": [ + 720, + 300 + ] } ], "connections": { - "Webhook (Portal)": { + "Webhook Portal": { "main": [ [ { - "node": "AI-Agent", + "node": "Assistent (Kimi + Tools)", "type": "main", "index": 0 } ] ] }, - "AI-Agent": { - "ai_languageModel": [ - [ - { - "node": "Kimi (OpenAI-kompatibel)", - "type": "ai_languageModel", - "index": 0 - } - ] - ], - "ai_tool": [ - [ - { - "node": "Tool: Leerstände", - "type": "ai_tool", - "index": 0 - }, - { - "node": "Tool: Mieteinnahmen", - "type": "ai_tool", - "index": 0 - }, - { - "node": "Tool: Schäden", - "type": "ai_tool", - "index": 0 - }, - { - "node": "Tool: Mieterkontakt", - "type": "ai_tool", - "index": 0 - }, - { - "node": "Tool: Zähleranomalien", - "type": "ai_tool", - "index": 0 - }, - { - "node": "Tool: Kosten", - "type": "ai_tool", - "index": 0 - } - ] - ], + "Assistent (Kimi + Tools)": { "main": [ [ { @@ -284,4 +74,4 @@ "settings": { "executionOrder": "v1" } -} +} \ No newline at end of file diff --git a/src/components/forms/FormSection.tsx b/src/components/forms/FormSection.tsx new file mode 100644 index 0000000..3b0a985 --- /dev/null +++ b/src/components/forms/FormSection.tsx @@ -0,0 +1,62 @@ +import type { ReactNode } from 'react' +import type { LucideIcon } from 'lucide-react' +import { cn } from '@/lib/utils' + +/** + * Shared Form-Layout für das Mieterportal. + * Jede FormSection rendert ihre Felder in zwei gleich breiten Spalten + * (mobil: eine Spalte). Elemente, die über die volle Breite laufen sollen + * (Checkbox-Blöcke, Textareas, Uploads, Hinweistexte), in packen. + */ + +export function FormSection({ + titel, + icon: Icon, + children, + className, +}: { + titel: string + icon?: LucideIcon + children: ReactNode + className?: string +}) { + return ( +
+
+ {Icon && ( + + + + )} +

+ {titel} +

+
+
{children}
+
+ ) +} + +/** Volle Breite innerhalb einer 2-spaltigen FormSection. */ +export function Wide({ children, className }: { children: ReactNode; className?: string }) { + return
{children}
+} + +/** Fehlerzeile am Ende eines Formulars. */ +export function FormFehler({ fehler }: { fehler: string | null }) { + if (!fehler) return null + return ( +
+ {fehler} +
+ ) +} diff --git a/src/index.css b/src/index.css index 081e6e3..dbe9dd3 100644 --- a/src/index.css +++ b/src/index.css @@ -71,6 +71,11 @@ } body { @apply bg-background text-foreground; + font-family: 'Inter', ui-sans-serif, system-ui, sans-serif; + -webkit-font-smoothing: antialiased; + } + h1, h2, h3 { + font-family: 'Sora', 'Inter', ui-sans-serif, system-ui, sans-serif; } } diff --git a/src/pages/BenutzerVerwaltung.tsx b/src/pages/BenutzerVerwaltung.tsx index bd4f1ff..0017445 100644 --- a/src/pages/BenutzerVerwaltung.tsx +++ b/src/pages/BenutzerVerwaltung.tsx @@ -15,6 +15,7 @@ import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { useStore } from '../store/store' import { fmtDatumZeit } from '../lib/format' +import { FormSection, Wide, FormFehler } from '@/components/forms/FormSection' import type { Benutzer, BenutzerRolle } from '../types' function Chip({ label, value, tone = 'slate' }: { label: string; value: number | string; tone?: 'slate' | 'amber' | 'teal' | 'blue' }) { @@ -57,12 +58,12 @@ function NeuerBenutzerDialog() { Benutzer anlegen - + Neuen Benutzer anlegen
-
+
setName(e.target.value)} placeholder="Vor- und Nachname" /> @@ -71,8 +72,6 @@ function NeuerBenutzerDialog() { setBn(e.target.value)} placeholder="z. B. m.mueller" />
-
-
setEmail(e.target.value)} /> @@ -94,32 +93,30 @@ function NeuerBenutzerDialog() { ))}
-
- {rolle === 'mieter' && ( + {rolle === 'mieter' && ( +
+ + +
+ )}
- - + + setPw(e.target.value)} /> +

Der Benutzer sollte das Passwort nach dem ersten Login ändern.

- )} -
- - setPw(e.target.value)} /> -

Der Benutzer sollte das Passwort nach dem ersten Login ändern.

-
- {fehler && ( -
{fehler}
- )} + + @@ -172,45 +169,49 @@ function EmailDialog({ benutzer: b, einheitLabel }: { benutzer: Benutzer; einhei E-Mail - + E-Mail an {b.name}
-
- -
- {b.name} · {hatEmail ? b.email : 'keine E-Mail hinterlegt'} - {einheitLabel && {einheitLabel}} -
-

- Die Verwaltung erhält automatisch eine Kopie (CC). -

-
-
- - setBetreff(e.target.value)} - /> -
-
- -