18 KiB
Lane L16 – Lotse-Chat für Monteure (lane/lotse-chat)
Stand: 2026-09-15 · Basis 6b8cdf5 (feature/craftvia-mvp, L1–L14 integriert) · Spec §15, §27, §31 · Brandbook §4.3, §9, §11.4, §12 · ARCHITEKTUR §4.5, §4.8
Ziel: In der mobilen Web-App schreibt oder spricht der Monteur wie in einem Chat („Auftrag fertig, Speicher installiert, 2 Std., 1 Filter verbaut“). Der Lotse ordnet den Auftrag zu, fragt fehlende Angaben nach und schlägt konkrete Aktionen vor. Er führt nichts selbst aus – jede Änderung braucht den Tipp des Monteurs auf „Bestätigen“.
1. Umfang / erfüllte Auftragspunkte
| Auftrag | Umsetzung |
|---|---|
| Datenmodell | LotseConversation (Mandant, Nutzer, optionaler Auftragskontext, lastMessageAt), LotseMessage (Rolle user/assistant/tool, Text, content JSON mit Chips, Karten-IDs, Sprungzielen, Modell-Fassung), LotseActionProposal (Art, Nutzlast, HMAC-payloadHash, Status proposed/confirmed/discarded/expired/failed, expiresAt = +30 min, Ergebnis/Fehlercode, confirmedAt/By, decidedAt). TenantSettings.lotseChatEnabled (Default an). Alle Tabellen mit tenant_id + RLS, in beiden TENANT_MODELS, Personenfelder in pii-fields.ts. Verlauf nur für den eigenen Nutzer (jede Abfrage filtert userId = ctx.userId). |
| Aufbewahrung | services/lotse/retention.ts (bestehender Job ai-retention): Gespräche mit lastMessageAt älter als AI_GENERATION_RETENTION_DAYS werden samt Nachrichten und Karten gelöscht (Chatverlauf = Protokolldaten eines Nutzers), offene abgelaufene Karten → expired; Audit lotse_chat_retention. |
| Provider | ai/lotse/chat-anthropic.ts: ein Schritt der Tool-Use-Schleife über das Anthropic SDK (beta.messages.create, Modell ANTHROPIC_MODEL), Opus 5 ohne Sampling-Parameter mit effort: "medium", serverseitiger Refusal-Fallback wie L9, Timeout 60 s. Denkblöcke des Modells werden innerhalb einer Schleife unverändert zurückgegeben; über Nachrichten hinweg nur Text. ai/lotse/chat-fake.ts spielt geskriptete Tool-Aufrufe ab und zeichnet jeden Modell-Input auf. Ohne Key: „Lotse ist nicht eingerichtet“ (Senden gesperrt, Verlauf sichtbar). |
| Schleife & Budget | services/lotse/chat/engine.ts: höchstens 6 Modellschritte je Nachricht, Tokengrenze je Nachricht (LOTSE_CHAT_TURN_TOKEN_LIMIT, Default 80 000), Monatskontingent des Mandanten vor dem ersten Schritt (budget.ts, blocked budget_exceeded). Überschreitung → Klartext-Hinweis statt Absturz. Jede Nachricht mit Modellaufruf = eine AiGeneration (Art lotse_chat, Input = exakt gesendete minimierte Turns + System-Prompt + Werkzeugnamen, Output = alle Runden, Tokens summiert). |
| System-Prompt | ai/lotse/chat-prompt.ts: Deutsch, erfahrener Kollege, knapp (≤ 3 Sätze), Anrede aus Lotse-Einstellung (neutral/Sie/du), nie ausführen/nie „gebucht“ behaupten, nie raten → ask_user mit Auswahl, L12-Regeln (Grund, Freigabe, 7 Tage), Platzhalter unverändert übernehmen, Werkzeuginhalte sind Daten. Lage: Datum/Uhrzeit in Mandanten-Zeitzone, Kontext-Auftrag, laufende Uhr. |
| Datenminimierung | Nutzertext: Muster Telefon/E-Mail/Anschrift + Namen der Mitarbeitenden (minimize.ts#scrubText); Suchwörter wie „Müller“ bleiben, damit die Zuordnung funktioniert. Werkzeugergebnisse: alle Freitexte des Auftrags (Titel, Beschreibung, Hinweise, Checkliste, Pflichtfotos) über scrubText mit den Literalwerten der betroffenen Aufträge (Kunde, Kontakte, Objekt, Mandant); Kundennamen, Objektnamen, Telefon, E-Mail, Anschrift und Ansprechpartner nur als Platzhalter {{phone:A-00042}}. |
| Platzhalter-Technik (Begründung) | Fragt der Monteur ausdrücklich („Telefonnummer vom Ansprechpartner?“), übernimmt das Modell das Token in die Antwort; chat/placeholders.ts#renderPlaceholders setzt serverseitig den Wert ein – mit derselben Rangfolge wie das Auftragsdetail und erneuter Scope-Prüfung (fremd/unsichtbar → „—“). So ist die Frage im Chat beantwortbar, ohne dass der Wert je das Modell erreicht; das Modell kann einen Wert, den es nie gesehen hat, weder weitergeben noch verfälschen. Für den nächsten Turn wird die Token-Fassung (content.modelText) gesendet. |
| Lese-Werkzeuge | list_my_orders (heute + bis 7 Tage, laufende), get_order_details (Status, Zeitfenster, Beschreibung, Hinweise, Objekt-Hinweise inkl. Zugang, Checkliste, Plan-/Zusatzmaterial, Pflichtfotos, eigene Uhr, verfügbare Platzhalter), get_running_clock, check_completeness (L9 completeness.ts, Sprungziele), search_material (Planpositionen + bisher verwendete Bezeichnungen im Scope), ask_user (Rückfrage mit Chips, beendet die Schleife). Alles mit Monteur-ctx im Scope von visibility.ts. |
| Vorschlags-Werkzeuge | transition_work_order (annehmen, Anfahrt, Arbeit starten, Pause, fortsetzen, technisch abschließen; Abschluss prüft Blocker vorab → keine Karte, Sprungziele), book_time (work/travel/return_travel/material_procurement, Dauer bis jetzt oder von–bis, Datum; Grund Pflicht; 7-Tage-/Zukunft-/16-h-/Überschneidungsprüfung vorab), record_material (Planposition per Name, Status aus Planmenge, Abweichung/Zusatzmaterial mit Grund; validateMaterialUsage von L4), add_note (Art + Text), suggest_report_fields (nur mit offenem Bericht). Fehlende Pflichtwerte → missing_values, keine Karte. |
| Auftragszuordnung | chat/orders.ts#resolveOrder: Suchbegriff (Nummer auch als Ziffern, Kunde, Objekt, Ort, Titel) > Kontext-Auftrag > Auftrag der laufenden Uhr > einziger heutiger Auftrag; mehrdeutig → order_ambiguous + Auswahl-Chips (Beschriftung mit Kunde nur für den Monteur, Wert „Auftrag A-00042“). |
| Bestätigen | chat/confirm.ts: Eigentümer (sonst not_found), Status proposed, Ablauf (→ expired), HMAC-Hash (→ failed tampered + Audit denied), Bearbeiten nur für freigegebene Felder (Auftrag/IDs fix, Hash neu). Ein atomares Row-Update beansprucht die Karte (Doppeltipp/parallel → idempotent), danach Ausführung über die bestehenden Services mit Monteur-ctx: transitionWorkOrder, startSession/pauseSession/resumeSession/endSession (inkl. L12-Auto-Wechsel, auf der Karte angekündigt), addManualTimeEntry (eigene Zeit → pending, Freigabepflicht), upsertMaterialUsage, createNote, Berichtsvorschlag in content.lotse (L9-Mechanismus: im Bericht übernehmen/verwerfen, Prüfbestätigung beim Absenden). Service-Fehler → Karte failed mit Klartext-Code + Lotse-Hinweis mit Sprungzielen (Pflichtfoto, Checkliste, Unterschrift, Meine Zeiten, Material, Bericht). Audit lotse_action_proposal before/after mit source: "lotse_chat". „Alle bestätigen“: feste Reihenfolge Zeit → Material → Notiz → Bericht → Status, Stopp beim ersten Fehler mit Hinweis „N Karten noch offen“. |
| Keine äußere Transaktion (Entscheidung) | Die Services öffnen eigene Transaktionen und senden Benachrichtigungen (Mail). Eine umschließende Transaktion hätte die DB-Transaktion über den Mailversand offen gehalten (5-s-Timeout, im Gate beobachtet). Deshalb: Karte atomar beanspruchen, Abschluss-Blocker vor jeder Änderung prüfen, dann Services einzeln. |
| UI mobil | /m/lotse (Verlauf, Eingabe, Senden, „Neuer Chat“, Mikrofon-Taste: Aufnahme ≤ 2 min → POST /api/v1/lotse/transcribe → Transkript im Eingabefeld editierbar; ohne Transkriptionsanbieter deaktiviert mit Hinweis). Aktionskarten mit klaren Werten (z. B. „Arbeitszeit · Auftrag A-00042 · Di., 15.09. · 13:05–15:05 Uhr · 2:00 Std. · Grund … · Zählt nach Freigabe“), Bestätigen/Bearbeiten/Verwerfen, Status als Text + Icon, „Gültig bis“. Chips nur an der letzten Antwort, Sprungziele als Liste. Offline: Hinweis, Eingabe bleibt (auch localStorage), nichts wird gesendet/ausgeführt. Einstieg: Bottom-Navigation „Lotse“ (nur wenn nutzbar) und „Lotse fragen“ im Auftragsdetail (Kontext). Touch ≥ 48 px, Farben nur Tokens, Lotse-Mark in Signalorange, Signalorange sonst nur an Primärknöpfen/offenen Karten. |
| Einstellungen | /settings/lotse: Schalter „Lotse-Chat für Monteure“ (wirkt nur bei eingeschaltetem Lotse), Datenfluss um Chat und Spracheingabe ergänzt, KI-Protokoll zeigt Art „Lotse-Chat“ (Transkriptionen der Spracheingabe: Art transcription, entityType lotse_chat, ohne Inhalt). |
2. Routen / Screens
| Route | Rolle | Inhalt |
|---|---|---|
/m/lotse |
Monteur, Teamleiter (lotse:use + field:execute) |
Chat ohne Auftragsbezug; ausgeschaltet → Hinweisseite |
/m/lotse?order=<id> |
dito | Chat mit Auftragskontext; fremder/unsichtbarer Auftrag → 404 |
/m/orders/[id] |
dito | Button „Lotse fragen“ unter der Vollständigkeits-Karte |
| Bottom-Navigation | dito | Eintrag „Lotse“ (6 Einträge, nur wenn Chat nutzbar) |
/settings/lotse |
Mandantenadministrator | Schalter + Datenfluss |
POST /api/v1/lotse/transcribe |
dito | multipart file → { text } (Audio wird nicht gespeichert), OpenAPI ergänzt |
Server Actions (actions/lotse/chat.ts, moduleGuard("lotse") + lotse:use, field:execute): senden, bestätigen (optional mit Änderungen), verwerfen, alle bestätigen, neuer Chat – jeweils mit frischer Chat-Ansicht als Ergebnis.
3. Dateien
Neu (Ownership L16 / Lotse-Pfade)
prisma/migrations/20260916200000_lotse_chat/migration.sqlsrc/lib/lotse/chat.ts(client-safe: Kartenarten, Zod-Schemas, Ansichtstypen)src/server/ai/lotse/{chat-types,chat-prompt,chat-fake,chat-anthropic}.tssrc/server/services/lotse/chat/{access,orders,placeholders,proposals,tools,engine,conversations,confirm,transcribe}.tssrc/server/actions/lotse/{chat,_chat-state}.ts,src/app/api/v1/lotse/transcribe/route.tssrc/app/(field)/m/(core)/lotse/{layout,page}.tsxsrc/components/lotse/chat/{lotse-chat,proposal-card,mic-button,ask-button}.tsxscripts/test-lotse-chat-engine.ts,scripts/test-lotse-chat-confirm.ts,scripts/test-lotse-chat-live.ts,scripts/lib/lotse-chat-fixture.ts
Geändert in Lotse-Pfaden (erlaubt laut Auftrag)
src/server/services/lotse/{settings,retention}.ts,src/server/actions/lotse-settings.ts,src/app/(app)/settings/lotse/page.tsx,messages/{de,en}/lotse.json
Fremdeingriffe (je klein, markiert „L16“)
prisma/schema.prisma(3 Modelle, 2 Enums, 1 Spalte) ·src/server/db.ts+src/server/backup/topology.ts(TENANT_MODELS) ·src/server/dsgvo/pii-fields.ts(3 Felder)src/components/field/bottom-nav.tsx(Eintrag + Proplotse),src/app/(field)/m/layout.tsx(FlaglotseChat),messages/{de,en}/field.json(nav.lotse)src/app/(field)/m/(core)/orders/[id]/page.tsx(Import + 1 Zeile Button)src/lib/api/openapi.ts(Pfad/lotse/transcribe–test-betrieb-apiverlangt jede Route im Dokument)src/components/audit-trail.tsx(Entity-Labels)scripts/test-e2e-tenant-isolation.ts(je eine Zeile für die 3 neuen Tenant-Modelle – der Test verlangt vollständige Abdeckung aller TENANT_MODELS)scripts/smoke-auth.ts(Monteur-/Admin-Prüfungen)
Keine Änderung an rbac.ts (bestehende Rechte lotse:use + field:execute + field:record_own_time/report:write genügen), keine neuen npm-Abhängigkeiten, keine Stubs.
4. Schema / Migration
20260916200000_lotse_chat (additiv): Enums LotseMessageRole, LotseProposalStatus; Tabellen lotse_conversations, lotse_messages, lotse_action_proposals (Kaskade von Gespräch zu Nachrichten/Karten), tenant_settings.lotse_chat_enabled BOOLEAN DEFAULT true; SELECT enable_tenant_rls(...) für alle drei Tabellen. Begründung: Chatverlauf und bestätigungspflichtige Vorschläge brauchen persistente, nutzerbezogene Datensätze mit Ablauf, Ergebnis und Audit-Bezug – weder AiGeneration (Protokoll, wird pseudonymisiert) noch Report-content passen. workOrderId ohne Fremdschlüssel (weiche Referenz, Auftrag kann soft-gelöscht werden; Sichtbarkeit wird bei jedem Lesen neu geprüft). Deploy: prisma migrate deploy (keine Rechte-Synchronisation nötig).
5. Tests
| Skript | Prüfungen | Ergebnis |
|---|---|---|
test-lotse-chat-engine.ts |
75: System-Prompt, Suchwörter; Zuordnung Kontext > laufende Uhr, Suchbegriff Kunde/Objekt/Nummer, Mehrdeutigkeit → Chips (Beschriftung mit Kunde, Wert ohne Namen), ask_user beendet Schleife; vier Karten aus einer Nachricht ohne jede Änderung an Zeiten/Material/Notizen/Status; fehlende Pflichtwerte (Grund, Dauer, Zusatzmaterial-/Mindermengen-Grund) → keine Karte; Überschneidung mit laufender Uhr, Pflichtfoto-Blocker mit Sprungziel, kein offener Bericht → keine Karte; Datenminimierung: Modell-Input ohne Telefon/E-Mail/Anschrift/PLZ/Kunden-, Kontakt-, Firmen-, Mitarbeiternamen/Mandantenkontakt, Platzhalter-Token vorhanden, fachliche Hinweise erhalten, Wert serverseitig eingesetzt, AiGeneration minimiert; Scope (Monteur ohne Zuweisung → not_found / Auftrag nicht gefunden), fremder Verlauf im selben Mandanten und aus Mandant B → not_found, B findet A-Aufträge nicht, ohne lotse:use → forbidden; Chat-Schalter aus / Modul aus → blocked, kein Modellaufruf, Audit; ohne Anbieter → not_configured; Monatskontingent → budget_exceeded; Rundengrenze; Tokengrenze; Anbieterfehler → Hinweis; Protokoll zeigt lotse_chat; Aufbewahrung löscht Verläufe nur des Mandanten |
grün |
test-lotse-chat-confirm.ts |
52: Annehmen/Arbeit starten über Services (Statuswechsel, Session, Akteur), Doppeltipp idempotent, Audit mit Quelle; Zeitnachtrag pending mit Grund, Bearbeiten (nur erlaubte Felder, Hash aktualisiert, Audit edited), Überschneidung → failed overlap; Material, Notiz (paralleler Doppeltipp → genau eine Notiz); Abschluss mit fehlendem Pflichtfoto → failed blocked, Auftrag + Session unverändert, Sprungziel; erneutes Bestätigen → already_decided; Ablauf nach 30 min; fremder Monteur/Admin/Mandant B → not_found; manipulierte Nutzlast → tampered; Verwerfen (idempotent, danach nicht bestätigbar); Monteur ohne Zuweisung → Service verweigert; „Alle bestätigen“ Reihenfolge + Stopp + zweiter Lauf; Berichtsvorschlag in content.lotse und Übernahme per L9; Chat aus / Modul aus → blocked |
grün |
test-lotse-chat-live.ts |
optional gegen Claude, nur mit ANTHROPIC_API_KEY |
übersprungen (kein Key) |
Angepasst: test-e2e-tenant-isolation.ts (Fixture um die 3 Tenant-Modelle ergänzt) – grün.
Gate: npm run gate grün – prisma generate, tsc, lint (0 Fehler, 3 bestehende Warnungen außerhalb L16), build inkl. Modul-Guard-Check (37 Action-Dateien), 79/79 Testskripte. Im ersten Lauf schlugen test-e2e-tenant-isolation (fehlende Fixture-Zeilen, behoben) und test-einsatz-field fehl (Transaktions-Timeout 5 s beim Mailversand innerhalb einer L12-Session-Transaktion unter Last paralleler Lanes; einzeln grün, im zweiten Gate grün – bestehendes Verhalten, siehe L12-Bericht §5.5).
RLS-Lauf RLS_ENFORCED=true (Lane-DB craftvia_lotsechat, Rolle craftvia_app): test-lotse-chat-engine, test-lotse-chat-confirm, test-e2e-tenant-isolation, test-tenant-isolation, test-rls-enforcement – 5/5 grün.
HTTP-Smoke (npx next start -p 3116, scripts/smoke-auth.ts, Demo-Seed): Monteur 37/37 grün (neu: /m/lotse mit „Lotse-Chat“, „Neuer Chat“, „Sprechen“; /m/lotse?order=<DEMO-07> mit Auftragsnummer; Auftragsdetail mit „Lotse fragen“; /m/lotse?order=<fremder Auftrag DEMO-08> → 404), Admin 12/12 grün (/settings/lotse mit „Lotse-Chat für Monteure“, Protokoll). Server danach beendet.
Visuelle Browserprüfung nicht durchgeführt (Anmeldung hätte Passwort- oder Token-Eingabe durch den Agenten erfordert).
6. Stubs & Abhängigkeiten
Keine Stubs. Genutzt: L2 transitionWorkOrder/computeCompletionBlockers/workOrderScope, L4 requireFieldOrder/createNote/upsertMaterialUsage/validateMaterialUsage/sniffMime, L12 startSession/pauseSession/resumeSession/endSession/getMyActiveSession/addManualTimeEntry/earliestStart, L5 requireVisibleReport/contentOf, L9 checkCompleteness/scrubText/lotseVoice/isLotseEnabled/Transkriptionsanbieter, L10b assertTokenBudget/requireApiContext/Aufbewahrungsjob.
7. Bekannte Lücken / Hinweise an den Architekten
- Live-Verhalten mit Claude ungeprüft (kein Key): Tool-Definitionen, Prompt und Denkblock-Rückgabe sind typgeprüft und gegen den Fake getestet;
scripts/test-lotse-chat-live.tsläuft, sobald ein Key gesetzt ist. Prompt-Feinschliff (Tonalität, Rückfragen) nach ersten echten Gesprächen empfohlen. - Kein
/api/v1für Senden/Bestätigen – die mobile UI nutzt Server Actions (wie L9); nur die Transkription ist eine API-Route (Upload > Server-Action-Limit). Für native Clients ggf. nachziehen. - Doppeltipp während der Ausführung: Der zweite Tipp erhält sofort
confirmed (idempotent), auch wenn die Ausführung danach noch scheitert; die Karte zeigt nach dem Neuladenfailed. - Abschluss nicht vollständig atomar: Blocker werden vorab geprüft; scheitert der Statuswechsel danach aus anderem Grund (z. B. parallele Änderung), bleibt die eigene Uhr beendet – wie beim bestehenden Zwei-Schritt-Flow der Primäraktion.
- Audit der Fachentitäten ohne Quelle: Die Services schreiben ihre Audits unverändert; die Verbindung zur Chat-Karte steht im Audit
lotse_action_proposal(Ergebnis-IDs,source: "lotse_chat"). Einsource-Feld im Fundament-Audit wäre sauberer. - Gespeicherte Antworten enthalten eingesetzte Werte (z. B. Telefonnummer), sichtbar nur für den Nutzer, gelöscht mit der Aufbewahrung. Das Modell sieht sie nie.
- Namen im Freitext des Monteurs werden (außer Mitarbeitenden) nicht ersetzt, damit die Suche „Objekt Müller“ funktioniert; Telefon/E-Mail/Anschrift schon.
- Zeit „2 Std.“ über Mitternacht → Rückfrage nach der Uhrzeit (kein Raten des Vortags).
- Verlauf ans Modell: letzte 20 Nachrichten als Text + Kartenstatus; Werkzeugergebnisse früherer Nachrichten werden nicht erneut gesendet (Kosten, Datenminimierung).
- Bottom-Navigation mit 6 Einträgen (~62 px je Eintrag bei 375 px) – bei weiteren Einträgen Profil/Sync zusammenlegen.
- Offline gibt es bewusst keine Warteschlange für Chat-Nachrichten (Eingabe bleibt lokal erhalten).