KI-Code & Mensch: Hand ♥ Hand Ein echtes Praxis-Beispiel

Was kann man in 8 bis 30 Minuten mit einem guten KI-System schaffen?

Auf der Seite Mensch + KI-Code Prozess findest Du eine ausführliche Beschreibung, wie Du ein System entwickelst, um mit Claude Code in relativ kurzer Zeit erwartungskonformen Code zu erstellen.

Aber wie funktioniert das tatsächlich in der Praxis?

Ich brauchte für mein internes Web-System zwei Ansichten aller Rechnungen aus zwei GmbH-Gesellschaften:

Tabelle inklusive aller Details.

Hätte ich das von Hand gemacht, hätte ich vielleicht einen funktionalen, aber irgendwie doch murksigen Code erstellt, denn: Ich kann vielleicht an 20 Dinge denken, aber nicht an 162 Faktoren (so viele Regeln prüft unser Gate-System) und schon gar nicht an tausende Faktorkombinationen, wenn es drauf ankommt, die Wechselwirkungen auch noch zu berücksichtigen!

Also greife ich zu Claude Code, denn das zugehörige Gate-System ist mittlerweile sehr weit entwickelt.

Viel Spaß beim Lesen und vielleicht auch mit- und nachbauen!

Ziel

Integration von Lexoffice-Rechnungen in die FREUND-GUI mit vollständiger HTMX-Tabelle, Sortierung, Filterung und Detailansicht.

Zeitraum

Spoiler: Im Kern gedauert hat das 8 Minuten. Mit allem Drumherum ca. 30 Minuten.

Was dieses Beispiel zeigt

  • Iterative Prompt-Entwicklung: Von einem naiven ersten Entwurf zu einem vollständig spezifizierten Prompt (v3.0)
  • Supervision durch KI: Automatische Analyse und Feedback zu Prompt-Lücken
  • Task-Dekomposition: Zerlegung in 11 atomare Sub-Tasks mit Master-Task
  • Gate-System in Aktion: 16 Contracts, 162 Regeln, 20 Hooks überwachen die Ausführung
  • Delegierte Ausführung: Externe KI implementiert autonom ohne menschliches Eingreifen
  • Akzeptanztests: Automatisierte Validierung aller Ergebnisse

Obacht, long read, aber es lohnt sich!

Prompt v1.0: Der erste Entwurf

Der erste Prompt-Entwurf beschreibt die Grundanforderung: Zwei Admin-Seiten für Lexoffice-Rechnungen. Er enthält die wesentlichen Informationen, lässt aber viele Details offen.

Eine Ermutigung vorweg

Wenn ein Kind laufen lernt, sieht das meist erstmal seltsam aus: dauernd fällt es aufs Gesicht. Aber wir helfen ihm immer wieder auf. Niemand käme da auf die Idee zu sagen: „Bleib liegen, das wird nix. Wir machen mit Deinem Bruder Jädd Tschippiti weiter."

Dieser Artikel soll genauso eine Ermutigung sein:

  • a) Umgefallene Kinder wieder aufzuheben
  • b) Eine durch ChatGPT vermurkste KI-ndheit zu überwinden

Let's go:

Prompt-Design-Prinzipien

  • Kontext zuerst: Datenquellen und Schema definieren
  • Explizite Ziele: Was soll am Ende funktionieren?
  • Referenz-Code: Bestehende Patterns als Vorlage nennen
  • Negative Definitionen: Was soll NICHT passieren?

prompt-v1.0.txt

Prompt v1.0 anzeigen

================================================================================
PROMPT: LEXOFFICE RECHNUNGEN INTEGRATION - FREUND GUI
================================================================================
Version: 1.0 (Erster Entwurf)
Projekt: schnuedeldue.oerg

================================================================================
1. AUFGABENBESCHREIBUNG
================================================================================

Implementiere zwei neue Admin-Seiten:

  /admin/rechnungen/karlscore        - Rechnungen der karlsCORE GmbH
  /admin/rechnungen/369wohlbefinden  - Rechnungen der 369 Wohlbefinden GmbH

Beide Seiten sind identisch aufgebaut, greifen aber auf unterschiedliche
Datenbanken zu.

================================================================================
2. DATENQUELLEN
================================================================================

Account                 | Datenbank                          | Anzahl
------------------------|------------------------------------|---------
karlsCORE GmbH          | freund_lexoffice_karlscore         | 2.555
369 Wohlbefinden GmbH   | freund_lexoffice_369wohlbefinden   | 808

================================================================================
3. UI-ANFORDERUNGEN
================================================================================

- Tabellenansicht mit Sortierung
- Filter nach Status und Datum
- Detailansicht per Klick
- Mobile-responsive

================================================================================
4. REFERENZ
================================================================================

Orientiere dich an:
- public/admin/emails/business.php (Listenansicht)
- public/htmx/emails/list.php (HTMX-Pattern)

Das verheerende Supervision-Feedback zu v1.0

Die Supervision analysierte den Prompt und identifizierte folgende Lücken:

supervision-feedback-v1.txt

Supervision-Feedback anzeigen

SUPERVISION ANALYSIS - Prompt v1.0
==================================

KRITISCHE LÜCKEN:

1. SCHEMA-DEFINITION FEHLT
   - Welche Spalten existieren in der DB?
   - Welche Datentypen?
   - Welche Felder sind NULL-fähig?

2. SPALTEN-AUSWAHL UNKLAR
   - Welche 6 Spalten in der Tabelle?
   - Welche Sortier-Optionen?
   - Format für Datum, Betrag?

3. STATUS-LOGIK UNDEFINIERT
   - Welche Status-Werte gibt es?
   - Wie werden sie dargestellt?
   - Was bedeutet "überfällig"?

4. FILTER-VERHALTEN UNSPEZIFIZIERT
   - Case-sensitive oder nicht?
   - Welche Felder durchsuchen?
   - Verhalten bei Filterwechsel?

5. PAGINATION FEHLT KOMPLETT
   - Wie viele Einträge pro Seite?
   - Wie sieht die Navigation aus?

6. DETAILANSICHT UNDEFINIERT
   - Welche Felder anzeigen?
   - Layout?
   - Rücknavigation?

EMPFEHLUNG: Prompt mit expliziten Definitionen erweitern.

Prompt v2.0: Mit Schema und UI-Details

Nachdem ich mich von der Supervisions-Schelle erholt hatte (meine Güte, wie kann man so viel vergessen?) habe ich auf Basis einiger Richtlinien und Constraints den Prompt erweitert:

Änderungen von v1.0 zu v2.0

  • Schema: Alle 35 Spalten mit Datentypen dokumentiert
  • Spaltenauswahl: Explizite 6-Spalten-Definition für Tabelle
  • Status-Badges: Farben und CSS-Klassen definiert
  • Overdue-Logik: Exakte Bedingungen für „überfällig"
  • Pagination: 25 pro Seite, Algorithmus für Seitenlinks
  • Filter: Case-insensitive, durchsuchte Felder, Verhalten

prompt-v2.0.txt

Prompt v2.0 anzeigen

================================================================================
PROMPT: LEXOFFICE RECHNUNGEN INTEGRATION - FREUND GUI
================================================================================
Version: 2.0 (Mit Schema und UI-Details)
Projekt: schnuedeldue.oerg

================================================================================
1. AUFGABENBESCHREIBUNG
================================================================================

Implementiere zwei neue Admin-Seiten:

  /admin/rechnungen/karlscore        - Rechnungen der karlsCORE GmbH
  /admin/rechnungen/369wohlbefinden  - Rechnungen der 369 Wohlbefinden GmbH

================================================================================
2. DATENQUELLEN UND SCHEMA
================================================================================

Account                 | Datenbank                          | Anzahl
------------------------|------------------------------------|---------
karlsCORE GmbH          | freund_lexoffice_karlscore         | 2.555
369 Wohlbefinden GmbH   | freund_lexoffice_369wohlbefinden   | 808

SCHEMA (verifiziert):
  id                   VARCHAR(36) PRIMARY KEY
  voucher_number       VARCHAR(50)
  voucher_date         DATE
  voucher_status       VARCHAR(20)  -- draft, open, paid, voided
  due_date             DATE         -- NULL bei draft
  address_name         VARCHAR(255)
  total_gross_amount   DECIMAL(12,2)
  line_items           LONGTEXT     -- JSON Array
  ...

================================================================================
3. UI-ANFORDERUNGEN
================================================================================

3.1 TABELLEN-SPALTEN (6 Spalten)
--------------------------------
  Nr.     | voucher_number     | String
  Datum   | voucher_date       | DD.MM.YYYY
  Kunde   | address_name       | max 40 Zeichen
  Betrag  | total_gross_amount | X.XXX,XX €
  Status  | voucher_status     | Badge mit Farbe
  Fällig  | due_date           | DD.MM.YYYY oder "-"

3.2 STATUS-BADGES
-----------------
  draft   = grau   (.status-draft)
  open    = blau   (.status-open)
  paid    = grün   (.status-paid)
  voided  = rot    (.status-voided)

3.3 OVERDUE-LOGIK
-----------------
  Überfällig wenn:
    status = 'open' AND due_date < CURDATE()

3.4 PAGINATION
--------------
  25 Einträge pro Seite
  Navigation: [<<] [<] [1] ... [5] [6] [7] ... [20] [>] [>>]

3.5 FILTER
----------
  - Textsuche in voucher_number, address_name, title
  - Case-insensitive
  - Status-Dropdown
  - Datumsbereich

================================================================================
4. REFERENZ-DATEIEN
================================================================================

- public/admin/emails/business.php
- public/htmx/emails/list.php
- src/Repositories/Email/NextcloudEmailRepository.php

Der Prompt v2.0 ging erneut durch die Supervision.

Supervision-Feedback zu v2.0

Die Supervision erkannte Verbesserungen, fand aber weitere offene Punkte:

supervision-feedback-v2.txt

Supervision-Feedback anzeigen

SUPERVISION ANALYSIS - Prompt v2.0
==================================

VERBESSERUNGEN ERKANNT:
✓ Schema dokumentiert
✓ Spalten definiert
✓ Status-Logik klar
✓ Pagination spezifiziert

VERBLEIBENDE LÜCKEN:

1. DETAILANSICHT FEHLT
   - Welche Felder anzeigen?
   - Positionen-Tabelle?
   - Summen-Darstellung?

2. ROUTING NICHT DEFINIERT
   - .htaccess Regeln?
   - Detail-URL-Pattern?

3. REPOSITORY-PATTERN UNKLAR
   - Container-Integration?
   - DI-Muster?

4. DATABASE CONFIG FEHLT
   - Wie werden die DBs eingebunden?
   - Readonly-Modus?

5. NEGATIVE DEFINITION FEHLT
   - Welche Felder NICHT anzeigen?
   - archived-Filter?

6. AKZEPTANZKRITERIEN FEHLEN
   - Wie wird Erfolg gemessen?
   - Testbare Kriterien?

EMPFEHLUNG: Vollständige technische Spezifikation ergänzen.

Prompt v3.0: Einmal Prompt mit alles!

Die finale Version enthält alles: Vollständiges Schema, Detailansicht, Routing, Repository-Code, CSS-Klassen und 22 messbare Akzeptanzkriterien.

Änderungen von v2.0 zu v3.0

  • Belegnachweis: SQL-Counts und Status-Verteilung dokumentiert
  • Detailansicht: Alle Felder mit Layout-Definition
  • Routing: Exakte .htaccess Regeln
  • Repository: Vollständiger PHP-Code als Referenz
  • HTMX-Endpoint: Komplette list.php Implementation
  • Negative Definition: Felder die NICHT angezeigt werden
  • Akzeptanzkriterien: 22 testbare Punkte

prompt-v3.0.txt (ca. 400 Zeilen)

Prompt v3.0 anzeigen

================================================================================
PROMPT: LEXOFFICE RECHNUNGEN INTEGRATION - FREUND GUI
================================================================================
Version: 3.0 (Alle Supervision-Punkte behoben)
Projekt: schnuedeldue.oerg
Datum: 2026-01-06

================================================================================
0. BELEGNACHWEIS (verifiziert via db_sql am 2026-01-06)
================================================================================

COUNTS:
  SELECT COUNT(*) FROM freund_lexoffice_karlscore.invoices;       -- 2555
  SELECT COUNT(*) FROM freund_lexoffice_369wohlbefinden.invoices; -- 808

STATUS-WERTE:
  karlscore:        draft=4, open=2354, paid=24, voided=173
  369wohlbefinden:  open=798, paid=3, voided=7

================================================================================
1. AUFGABENBESCHREIBUNG
================================================================================

Implementiere zwei neue Admin-Seiten:

  /admin/rechnungen/karlscore        - Rechnungen der karlsCORE GmbH
  /admin/rechnungen/369wohlbefinden  - Rechnungen der 369 Wohlbefinden GmbH

================================================================================
2. DATENQUELLEN UND SCHEMA
================================================================================

2.1 SCHEMA (35 Spalten, vollständig dokumentiert)
-------------------------------------------------
  id                   VARCHAR(36) PRIMARY KEY NOT NULL
  voucher_number       VARCHAR(50) NULL
  voucher_date         DATE NULL
  voucher_status       VARCHAR(20) NULL     -- draft, open, paid, voided
  due_date             DATE NULL            -- NULL bei draft
  address_name         VARCHAR(255) NULL
  address_supplement   VARCHAR(255) NULL
  address_street       VARCHAR(255) NULL
  address_zip          VARCHAR(20) NULL
  address_city         VARCHAR(100) NULL
  total_net_amount     DECIMAL(12,2) NULL
  total_gross_amount   DECIMAL(12,2) NULL
  total_tax_amount     DECIMAL(12,2) NULL
  line_items           LONGTEXT NULL        -- JSON Array
  payment_term_label   VARCHAR(500) NULL
  payment_term_duration INT NULL
  title                VARCHAR(255) NULL
  archived             TINYINT DEFAULT 0
  ...

2.2 line_items JSON-SCHEMA
--------------------------
{
  "name": "Produktname",
  "description": "Beschreibung",
  "quantity": 12,
  "unitName": "Monate",
  "unitPrice": { "netAmount": 3.00 },
  "discountPercentage": 0,
  "lineItemAmount": 36.00
}

2.3 FELDER DIE NICHT ANGEZEIGT WERDEN (Negativ-Definition)
----------------------------------------------------------
  organization_id, version, language, tax_amounts, tax_type,
  shipping_date, shipping_type, shipping_end_date, introduction,
  remark, related_vouchers, closing_invoice, synced_at, raw_json

================================================================================
3. UI-ANFORDERUNGEN
================================================================================

3.1 LAYOUT [CRITICAL]
---------------------
  - KEINE Cards - Direkte Tabellenansicht
  - Mobile-First - Responsive ab 320px
  - HTMX - Alle Interaktionen ohne Page Reload

3.3 TABELLEN-SPALTEN (6)
------------------------
  Nr.     | voucher_number     | String              | Sortierbar
  Datum   | voucher_date       | DD.MM.YYYY          | Sortierbar (Default DESC)
  Kunde   | address_name       | max 40 Zeichen      | Sortierbar
  Betrag  | total_gross_amount | X.XXX,XX €          | Sortierbar
  Status  | voucher_status     | Badge               | Sortierbar
  Fällig  | due_date           | DD.MM.YYYY oder "-" | Sortierbar

3.5 OVERDUE-LOGIK
-----------------
  Überfällig wenn ALLE erfüllt:
    1. voucher_status = 'open'
    2. due_date IS NOT NULL
    3. due_date < CURDATE()

3.6 STATUS-BADGE-FARBEN
-----------------------
  draft   = grau   (#6c757d)
  open    = blau   (#0d6efd)
  paid    = grün   (#198754)
  voided  = rot    (#dc3545)

3.7 DETAILANSICHT
-----------------
Route: /admin/rechnungen/{account}/{id}

KOPFDATEN: voucher_number, voucher_date, voucher_status, due_date, title
KUNDE: address_name, address_supplement, address_street, address_zip, address_city
POSITIONEN: Tabelle aus line_items (name, description, quantity, unitPrice, discount, amount)
SUMMEN: total_net_amount, total_tax_amount, total_gross_amount
ZAHLUNGSBEDINGUNGEN: payment_term_label, payment_term_duration

3.8 PAGINATION
--------------
  25 Einträge pro Seite (fix)
  Bei <= 7 Seiten: [1] [2] [3] [4] [5] [6] [7]
  Bei > 7 Seiten:  [1] ... [4] [5] [6] ... [20]

3.9 META-ANZEIGE (PFLICHT)
--------------------------
  "{von}-{bis} von {gesamt} Rechnungen"

================================================================================
4. TECHNISCHE IMPLEMENTIERUNG
================================================================================

4.1 DATEI-STRUKTUR
------------------
public/admin/rechnungen/karlscore.php
public/admin/rechnungen/369wohlbefinden.php
public/admin/rechnungen/detail.php
public/htmx/invoices/list.php
src/Repositories/Lexoffice/InvoiceRepository.php

4.2 ROUTING (.htaccess)
-----------------------
RewriteRule ^admin/rechnungen/karlscore/?$ /admin/rechnungen/karlscore.php [L,QSA]
RewriteRule ^admin/rechnungen/369wohlbefinden/?$ /admin/rechnungen/369wohlbefinden.php [L,QSA]
RewriteRule ^admin/rechnungen/(karlscore|369wohlbefinden)/([a-f0-9-]{36})$ /admin/rechnungen/detail.php?account=$1&id=$2 [L,QSA]
RewriteRule ^htmx/invoices/list$ /htmx/invoices/list.php [L,QSA]

4.3 DATABASE CONFIG
-------------------
"lexoffice_karlscore": { "database": "freund_lexoffice_karlscore", "readonly": true }
"lexoffice_369wohlbefinden": { "database": "freund_lexoffice_369wohlbefinden", "readonly": true }

4.5 REPOSITORY (vollständiger Code)
-----------------------------------
[~150 Zeilen PHP-Code mit list(), count(), findById()]

4.6 HTMX ENDPOINT (vollständiger Code)
--------------------------------------
[~180 Zeilen PHP-Code mit Tabellen-Rendering, Pagination, Filter]

================================================================================
8. AKZEPTANZKRITERIEN (22 Punkte)
================================================================================

ROUTING:
  [ ] /admin/rechnungen/karlscore erreichbar
  [ ] /admin/rechnungen/369wohlbefinden erreichbar
  [ ] Detail-URLs funktionieren

TABELLE:
  [ ] 6 Spalten korrekt
  [ ] Alle Spalten sortierbar
  [ ] Default-Sortierung: Datum DESC
  [ ] Überfällige Rechnungen rot markiert

FILTER:
  [ ] Textsuche funktioniert
  [ ] Status-Filter funktioniert
  [ ] Datumsbereich funktioniert
  [ ] Archived immer ausgeschlossen

PAGINATION:
  [ ] 25 pro Seite
  [ ] Seitennavigation korrekt
  [ ] Meta-Anzeige vorhanden

DETAIL:
  [ ] Alle Kopfdaten angezeigt
  [ ] Kundenadresse vollständig
  [ ] Positionen-Tabelle korrekt
  [ ] Summen korrekt formatiert

RESPONSIVE:
  [ ] 320px funktioniert
  [ ] 768px funktioniert
  [ ] 1200px funktioniert

HURRA! Der Supervision sind die Worte ausgegangen!

Das bedeutet: Alle Lücken im definierten Rahmen sind geschlossen und der Prompt ist bereit zur Ausführung!

supervision-approved.txt

Supervision-Bestätigung anzeigen

SUPERVISION ANALYSIS - Prompt v3.0
==================================

STATUS: ✓ APPROVED

PRÜFPUNKTE:
✓ Schema vollständig dokumentiert
✓ Belegnachweis mit echten Daten
✓ Spaltenauswahl explizit
✓ Status-Logik eindeutig
✓ Overdue-Bedingungen exakt
✓ Detailansicht vollständig definiert
✓ Routing-Regeln spezifiziert
✓ Repository-Code als Referenz
✓ HTMX-Endpoint als Referenz
✓ Negative Definition vorhanden
✓ Akzeptanzkriterien messbar

EMPFEHLUNG: Prompt zur Ausführung freigeben.

Task-Erstellung und Delegation

Der Prompt wurde in 11 atomare Sub-Tasks zerlegt und einem Master-Task zugeordnet. Diese Struktur ermöglicht parallele Bearbeitung und klare Fortschrittsverfolgung.

Sub-Tasks (1836-1846)

ID Beschreibung Priorität
1836 databases.json erweitern 1
1837 .htaccess Routing 2
1838 Container.php erweitern 3
1839 InvoiceRepository erstellen 4
1840 Admin karlscore.php 5
1841 Admin 369wohlbefinden.php 5
1842 Detail-Ansicht detail.php 6
1843 HTMX list.php Fragment 7
1844 Navigation erweitern 8
1845 CSS-Styles 8
1846 Akzeptanztest 9

Master-Task 1847

  • Typ: master_task
  • Sub-Tasks: 1836-1846 (11 Tasks)
  • Delegation: Handoff an externe KI
  • Status-Tracking: ki_protokoll.ki_tasks

Handoff-Mechanismus

Der Master-Task wurde mit Status pending und allen Kontext-Informationen an eine externe KI-Session übergeben. Die supervisierende KI (claude-code) beobachtet nur, greift nicht ein.

Task 1836: databases.json erweitern

Der erste Sub-Task legt die Grundlage: Die Datenbank-Verbindungen für beide Lexoffice-Accounts müssen in der zentralen Konfiguration registriert werden.

Aufgabe

Die Datei config/databases.json enthält alle verfügbaren Datenbankverbindungen. Für die Lexoffice-Integration müssen zwei neue readonly-Connections hinzugefügt werden.

task-1836-databases.txt

Vollständiger Task-Prompt anzeigen

LEXOFFICE-1: databases.json erweitern

DATEI: config/databases.json

ÄNDERUNG: Unter "connections" hinzufügen:

"lexoffice_karlscore": {
    "driver": "mysql",
    "host": "localhost",
    "database": "freund_lexoffice_karlscore",
    "charset": "utf8mb4",
    "collation": "utf8mb4_unicode_ci",
    "readonly": true,
    "user": "lexoffice_sync",
    "pass": "Lex0ff1ce_Sync_2026!"
},
"lexoffice_369wohlbefinden": {
    "driver": "mysql",
    "host": "localhost",
    "database": "freund_lexoffice_369wohlbefinden",
    "charset": "utf8mb4",
    "collation": "utf8mb4_unicode_ci",
    "readonly": true,
    "user": "lexoffice_sync",
    "pass": "Lex0ff1ce_Sync_2026!"
}

WICHTIG:
- readonly: true (nur Lesezugriff)
- Credentials aus bestehendem Lexoffice-Sync übernehmen
- JSON-Syntax validieren nach Änderung

Ergebnis

Nach Abschluss stehen die Verbindungen lexoffice_karlscore und lexoffice_369wohlbefinden im Container zur Verfügung.

↑ Zurück zur Task-Übersicht

Task 1837: .htaccess erweitern

Die Routing-Regeln bestimmen, welche URLs auf welche PHP-Dateien gemappt werden. Ohne diese Regeln wären die neuen Admin-Seiten nicht erreichbar.

Aufgabe

Vier neue RewriteRules für die Rechnungs-URLs: Zwei für die Listenansichten, eine für die Detailansicht mit UUID-Validierung, eine für den HTMX-Endpoint.

task-1837-htaccess.txt

Vollständiger Task-Prompt anzeigen

LEXOFFICE-2: .htaccess erweitern

DATEI: public/.htaccess

EINFÜGEN VOR der Catch-All-Regel:

# Admin Rechnungen routes
RewriteRule ^admin/rechnungen/karlscore/?$ /admin/rechnungen/karlscore.php [L,QSA]
RewriteRule ^admin/rechnungen/369wohlbefinden/?$ /admin/rechnungen/369wohlbefinden.php [L,QSA]
RewriteRule ^admin/rechnungen/(karlscore|369wohlbefinden)/([a-f0-9-]{36})$ /admin/rechnungen/detail.php?account=$1&id=$2 [L,QSA]

# HTMX Invoices
RewriteRule ^htmx/invoices/list$ /htmx/invoices/list.php [L,QSA]

DETAILS:
- [L,QSA] = Last rule, Query String Append
- UUID-Pattern: [a-f0-9-]{36} für Lexoffice-IDs
- account wird als GET-Parameter übergeben
- Optional trailing slash mit /?$

Ergebnis

URLs wie /admin/rechnungen/karlscore/abc-123-def werden korrekt auf detail.php?account=karlscore&id=abc-123-def geroutet.

↑ Zurück zur Task-Übersicht

Task 1838: Container.php erweitern

Der DI-Container ist das Herzstück der Dependency Injection. Die neue Factory-Methode ermöglicht typsicheren Zugriff auf das InvoiceRepository.

Aufgabe

Eine neue Methode getLexofficeInvoiceRepository(string $account) hinzufügen, die das Repository mit der richtigen Datenbankverbindung instantiiert.

task-1838-container.txt

Vollständiger Task-Prompt anzeigen

LEXOFFICE-3: Container.php erweitern

DATEI: src/Container.php

IMPORT HINZUFÜGEN:
use Freund\Repositories\Lexoffice\InvoiceRepository;

METHODE HINZUFÜGEN:

/**
 * Get Lexoffice Invoice Repository for specific account
 *
 * @param string $account 'karlscore' or '369wohlbefinden'
 * @return InvoiceRepository
 */
public function getLexofficeInvoiceRepository(string $account): InvoiceRepository
{
    $key = "lexoffice_invoice_repo_{$account}";
    return $this->resolve($key, function () use ($account) {
        $connName = InvoiceRepository::getConnectionName($account);
        $pdo = $this->createConnection($connName, true); // readonly
        return new InvoiceRepository($pdo, $account);
    });
}

DETAILS:
- Lazy Loading via resolve()
- Separate Instanz pro Account (karlscore vs 369)
- Readonly-Connection erzwingen
- Type Hints für IDE-Unterstützung

Ergebnis

Aufruf: $container->getLexofficeInvoiceRepository('karlscore') liefert ein konfiguriertes Repository.

↑ Zurück zur Task-Übersicht

Task 1839: InvoiceRepository erstellen

Das Repository kapselt alle Datenbankzugriffe. Es implementiert die Methoden für Listenabfrage, Zählung und Einzelabruf: jeweils mit Filterung und Sortierung.

Aufgabe

Neue Klasse InvoiceRepository mit drei Hauptmethoden: list(), count(), findById(). Inklusive Whitelist-Validierung für Sortierung und Account-Mapping.

task-1839-repository.txt

Vollständiger Task-Prompt anzeigen

LEXOFFICE-4: InvoiceRepository.php erstellen

DATEI: src/Repositories/Lexoffice/InvoiceRepository.php

STRUKTUR:
- Namespace: Freund\Repositories\Lexoffice
- Constructor: PDO $pdo, string $account
- Static: getConnectionName(string $account): string
- Static: getAllowedAccounts(): array

METHODEN:

list(array $filters, string $sort, string $order, int $limit, int $offset): array
  - Filter: q (Textsuche), status, date_from, date_to
  - Textsuche: LOWER() für case-insensitive
  - Whitelist: ALLOWED_SORTS für SQL-Injection-Schutz
  - Immer: WHERE archived = 0

count(array $filters): int
  - Gleiche Filter wie list()
  - Für Pagination-Berechnung

findById(string $id): ?array
  - UUID-Validierung: preg_match('/^[a-f0-9-]{36}$/', $id)
  - Return null wenn nicht gefunden

SICHERHEIT:
- Prepared Statements überall
- bindValue mit expliziten Types
- Whitelist für ORDER BY

Ergebnis

Ein vollständiges Repository mit ~150 Zeilen, das alle CRUD-Read-Operationen für Rechnungen abdeckt.

↑ Zurück zur Task-Übersicht

Task 1840: karlscore.php erstellen

Die Admin-Seite für karlsCORE GmbH. Sie enthält das HTML-Gerüst mit Filtern und einem leeren Container, der per HTMX die Tabelle lädt.

Aufgabe

Vollständige Admin-Seite mit Header, Navigation, Filter-Formular und HTMX-Target für die Rechnungstabelle.

task-1840-karlscore.txt

Vollständiger Task-Prompt anzeigen

LEXOFFICE-5: karlscore.php erstellen

DATEI: public/admin/rechnungen/karlscore.php

STRUKTUR:
1. Auth-Check (mit isLocalAccess() Bypass)
2. Layout-Header einbinden
3. Filter-Formular:
   - Textsuche (input type="search")
   - Status-Dropdown (Alle, Entwurf, Offen, Bezahlt, Storniert)
   - Datum von/bis (type="date")
4. HTMX-Container:
   - id="invoices-table"
   - hx-get="/htmx/invoices/list?account=karlscore"
   - hx-trigger="load"
5. Layout-Footer einbinden

FILTER-FORMULAR:
<form class="invoice-filters">
  <input type="search" name="q" placeholder="Suche..."
         hx-get="/htmx/invoices/list?account=karlscore"
         hx-trigger="keyup changed delay:300ms"
         hx-target="#invoices-table"
         hx-include=".invoice-filters">

  <select name="status" hx-get="..." hx-trigger="change">
    <option value="">Alle Status</option>
    <option value="draft">Entwurf</option>
    <option value="open">Offen</option>
    <option value="paid">Bezahlt</option>
    <option value="voided">Storniert</option>
  </select>

  <input type="date" name="date_from" hx-get="..." hx-trigger="change">
  <input type="date" name="date_to" hx-get="..." hx-trigger="change">
</form>

HTMX-TARGET:
<div id="invoices-table"
     hx-get="/htmx/invoices/list?account=karlscore"
     hx-trigger="load">
  Lade Rechnungen...
</div>

Ergebnis

Vollständige Admin-Seite unter /admin/rechnungen/karlscore, die beim Laden automatisch die Tabelle via HTMX abruft.

↑ Zurück zur Task-Übersicht

Task 1841: 369wohlbefinden.php erstellen

Die Admin-Seite für 369 Wohlbefinden GmbH. Strukturell identisch zu karlscore.php, nur mit anderem Account-Parameter.

Aufgabe

Kopie von karlscore.php mit angepasstem Account-Parameter. Der HTMX-Endpoint unterscheidet anhand des account-Parameters, welche Datenbank abgefragt wird.

task-1841-369wohlbefinden.txt

Vollständiger Task-Prompt anzeigen

LEXOFFICE-6: 369wohlbefinden.php erstellen

DATEI: public/admin/rechnungen/369wohlbefinden.php

ÄNDERUNGEN gegenüber karlscore.php:
- $account = '369wohlbefinden'
- Alle hx-get URLs: account=369wohlbefinden
- Seiten-Titel: "Rechnungen 369 Wohlbefinden"

REST IDENTISCH:
- Auth-Check
- Filter-Formular
- HTMX-Container
- Layout-Includes

BEISPIEL hx-get:
  hx-get="/htmx/invoices/list?account=369wohlbefinden"

Ergebnis

Zweite Admin-Seite unter /admin/rechnungen/369wohlbefinden mit Zugriff auf die 369-Datenbank.

↑ Zurück zur Task-Übersicht

Task 1842: detail.php erstellen

Die Detailansicht zeigt alle Informationen einer einzelnen Rechnung: Kopfdaten, Kunde, Positionen, Summen und Zahlungsbedingungen.

Aufgabe

Eine shared Detailseite, die beide Accounts bedient. Der Account wird via GET-Parameter übergeben, die UUID validiert und die Rechnung aus dem Repository geladen.

task-1842-detail.txt

Vollständiger Task-Prompt anzeigen

LEXOFFICE-7: detail.php erstellen

DATEI: public/admin/rechnungen/detail.php

PARAMETER:
- $_GET['account'] - 'karlscore' oder '369wohlbefinden'
- $_GET['id'] - UUID der Rechnung

VALIDIERUNG:
- Account gegen Whitelist prüfen
- UUID-Format: preg_match('/^[a-f0-9-]{36}$/', $id)
- 404 wenn Rechnung nicht gefunden

ANZEIGE-BEREICHE:

1. BREADCRUMB:
   Rechnungen karlsCORE > RE-2024-0001

2. KOPFDATEN:
   - Rechnungsnummer (h1)
   - Status-Badge
   - Rechnungsdatum
   - Fälligkeit
   - Titel (falls vorhanden)

3. KUNDE:
   - address_name
   - address_supplement (falls vorhanden)
   - address_street
   - address_zip + address_city
   - address_country_code (nur wenn nicht 'DE')

4. POSITIONEN (Tabelle):
   | Pos | Bezeichnung | Menge | Einheit | Einzelpreis | Rabatt | Gesamt |
   - line_items JSON parsen
   - description unter name (klein, grau)
   - Rabatt nur anzeigen wenn > 0

5. SUMMEN:
   - Netto: total_net_amount
   - MwSt: total_tax_amount
   - Brutto: total_gross_amount (fett)

6. ZAHLUNGSBEDINGUNGEN:
   - payment_term_label
   - payment_term_duration (Tage)

7. AKTIONEN:
   - Button "PDF herunterladen" (disabled, title="Funktion folgt")
   - Link "Zurück zur Liste"

Ergebnis

Vollständige Detailansicht mit allen Rechnungsinformationen und Breadcrumb-Navigation zurück zur Liste.

↑ Zurück zur Task-Übersicht

Task 1843: HTMX list.php erstellen

Der HTMX-Endpoint ist das Herzstück der Interaktivität. Er rendert die Tabelle als HTML-Fragment und wird bei jeder Filter-, Sortier- oder Paginierungsaktion aufgerufen.

Aufgabe

Ein reines HTML-Fragment (kein vollständiges Dokument) mit Tabelle, Pagination und Meta-Anzeige. Reagiert auf GET-Parameter für Filter, Sortierung und Seite.

task-1843-htmx-list.txt

Vollständiger Task-Prompt anzeigen

LEXOFFICE-8: list.php (HTMX) erstellen

DATEI: public/htmx/invoices/list.php

HEADER:
header('Content-Type: text/html; charset=utf-8');

PARAMETER:
- account (required, whitelist-validiert)
- q (optional, Textsuche)
- status (optional, draft|open|paid|voided)
- date_from, date_to (optional, DATE)
- sort (optional, default: voucher_date)
- order (optional, default: desc)
- page (optional, default: 1)

ABLAUF:
1. Auth-Check (mit isLocalAccess() Bypass)
2. Account validieren
3. Repository via Container holen
4. Filter/Sort/Page aus GET
5. list() + count() aufrufen
6. HTML rendern

TABELLE:
- Sortierbare Header mit hx-get Links
- Zeilen mit Overdue-Klasse
- Status-Badges
- Detail-Links

PAGINATION:
- Nur wenn totalPages > 1
- Algorithmus: Erste, aktuelle±1, Letzte
- hx-get mit hx-include für Filter

META:
echo '<p class="table-meta">' . $from . '-' . $to . ' von ' . $total . ' Rechnungen</p>';

FUNKTIONEN:
- renderInvoiceRow(array $item, string $account): void
- renderPagination(...): void
- renderPageLink(...): void

Ergebnis

~180 Zeilen PHP, die bei jedem HTMX-Request ein aktualisiertes Tabellen-Fragment liefern.

↑ Zurück zur Task-Übersicht

Task 1844: Navigation erweitern

Die Haupt-Navigation muss um zwei neue Einträge ergänzt werden, damit Benutzer die Rechnungsseiten erreichen können.

Aufgabe

Im $navItems-Array zwei neue Einträge für die Rechnungs-Admin-Seiten hinzufügen.

task-1844-nav.txt

Vollständiger Task-Prompt anzeigen

LEXOFFICE-9: nav.php erweitern

DATEI: templates/layout/nav.php

IM $navItems ARRAY HINZUFÜGEN:

$navItems = [
    'dashboard' => 'Übersicht',
    'emails' => 'E-Mails',
    'wissen' => 'Wissen',
    'aufgaben' => 'Aufgaben',
    'regeln' => 'Regeln',
    'autorenprofil' => 'Profil',
    'dokumente' => 'Dokumente',
    'suche' => 'Suche',
    'contacts' => 'Kontakte',
    // NEU: Admin-Rechnungen
    'admin/rechnungen/karlscore' => 'RE karlsCORE',
    'admin/rechnungen/369wohlbefinden' => 'RE 369',
];

KURZFORM:
- "RE" steht für "Rechnungen"
- Kompakte Labels für Navigation
- Sortierung: Nach bestehenden Einträgen

Ergebnis

Die Navigation zeigt jetzt "RE karlsCORE" und "RE 369" als klickbare Links.

↑ Zurück zur Task-Übersicht

Task 1845: CSS-Styles erweitern

Neue CSS-Klassen für Status-Badges, Überfällig-Markierung und Pagination. Die Styles nutzen CSS Custom Properties für konsistente Farben.

Aufgabe

In app.css die Styles für .status-*, .row-overdue, .cell-due und Pagination-Elemente hinzufügen.

task-1845-css.txt

Vollständiger Task-Prompt anzeigen

LEXOFFICE-10: app.css erweitern

DATEI: public/assets/css/app.css

STATUS-BADGES:
.status-draft {
    background: #6c757d;
    color: white;
    padding: 0.2rem 0.5rem;
    border-radius: 3px;
    font-size: 0.85em;
}
.status-open { background: #0d6efd; color: white; ... }
.status-paid { background: #198754; color: white; ... }
.status-voided { background: #dc3545; color: white; ... }

OVERDUE-STYLES:
.row-overdue {
    background-color: rgba(220, 53, 69, 0.1);
}
.row-overdue .cell-due {
    color: #dc3545;
    font-weight: 600;
}

PAGINATION:
.pagination ul {
    list-style: none;
    display: flex;
    gap: 0.25rem;
    padding: 0;
    margin: 1rem 0;
}
.pagination li a {
    display: block;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--border-color);
    border-radius: 3px;
    text-decoration: none;
}
.pagination li.active a {
    background: var(--primary-color);
    color: white;
    border-color: var(--primary-color);
}
.pagination li.disabled span {
    color: var(--text-muted);
    cursor: not-allowed;
}

TABLE-META:
.table-meta {
    color: var(--text-muted);
    font-size: 0.875rem;
    margin-top: 0.5rem;
}

Ergebnis

~85 neue Zeilen CSS für konsistentes Styling der Rechnungstabelle.

↑ Zurück zur Task-Übersicht

Task 1846: Akzeptanztest durchführen

Der finale Task: Alle 22 Akzeptanzkriterien aus dem Prompt systematisch prüfen und dokumentieren.

Aufgabe

HTTP-Tests für alle Endpoints, manuelle Prüfung der UI-Funktionen, Responsive-Tests auf verschiedenen Viewports.

task-1846-akzeptanztest.txt

Vollständiger Task-Prompt anzeigen

LEXOFFICE-11: Akzeptanztest

ROUTING-TESTS:
curl -s -o /dev/null -w "%{http_code}" https://schnuedeldue.oerg/admin/rechnungen/karlscore
# Erwartung: 200

curl -s -o /dev/null -w "%{http_code}" https://schnuedeldue.oerg/admin/rechnungen/369wohlbefinden
# Erwartung: 200

curl -s -o /dev/null -w "%{http_code}" https://schnuedeldue.oerg/htmx/invoices/list?account=karlscore
# Erwartung: 200

curl -s -o /dev/null -w "%{http_code}" https://schnuedeldue.oerg/htmx/invoices/list?account=invalid
# Erwartung: 400

TABELLEN-TESTS:
[ ] 6 Spalten: Nr., Datum, Kunde, Betrag, Status, Fällig
[ ] Sortierung: Klick auf Header ändert Sortierung
[ ] Default: Nach Datum absteigend
[ ] Overdue: Rote Markierung bei überfälligen Rechnungen

FILTER-TESTS:
[ ] Textsuche: Eingabe filtert Tabelle
[ ] Status: Dropdown ändert Ergebnisse
[ ] Datum: Von/Bis grenzt Zeitraum ein
[ ] Reset: Filter-Änderung setzt Seite auf 1

PAGINATION-TESTS:
[ ] 25 Einträge pro Seite
[ ] Navigation: Erste/Letzte Seite
[ ] Meta: "1-25 von 2555 Rechnungen"

DETAIL-TESTS:
[ ] Klick öffnet Detailansicht
[ ] Alle Kopfdaten vorhanden
[ ] Kundenadresse vollständig
[ ] Positionen-Tabelle korrekt
[ ] Summen formatiert
[ ] Zurück-Link funktioniert

RESPONSIVE-TESTS (Chromium Screenshot):
[ ] 320px: Mobil-Layout
[ ] 768px: Tablet-Layout
[ ] 1200px: Desktop-Layout

Ergebnis

Alle 22 Kriterien bestanden. Dokumentation der Testergebnisse im Task-Log.

↑ Zurück zur Task-Übersicht

Gate-System: Architektur-Überblick

Das Gate-System ist das Qualitätssicherungs-Framework, das jeden KI-generierten Code überwacht. Es besteht aus vier Hauptkomponenten, die ineinandergreifen.

Die vier Säulen

Komponente Anzahl Funktion
Contracts 16 Regelsammlungen für spezifische Domänen
Rules 162 Einzelne Prüfregeln mit Severity-Level
Hooks 20 Event-Handler für Tool-Aufrufe
Transitions Zustandsübergänge und Gates

gate-system --architecture

Architektur-Diagramm anzeigen

┌─────────────────────────────────────────────────────────────────┐
│                     GATE SYSTEM                               │
└─────────────────────────────────────────────────────────────────┘

                         ┌─────────────┐
                         │   Claude    │
                         │   Code     │
                         └──────┬──────┘
                                │
                    Tool-Aufruf (Edit, Write, Bash...)
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│                    HOOK DISPATCHER                            │
│  bootstrap.py → Analysiert Event-Typ und Tool-Namen           │
└─────────────────────────────────────────────────────────────────┘
                                │
          ┌─────────────────────┼─────────────────────┐
          │                     │                     │
          ▼                     ▼                     ▼
┌─────────────────┐   ┌─────────────────┐   ┌─────────────────┐
│ PreToolUse      │   │ PostToolUse     │   │ SessionStart   │
│ - Validierung   │   │ - Nachbereitung │   │ - Initialisierung│
│ - Blockierung   │   │ - Analyse       │   │ - Task-Loader  │
│ - Guidance      │   │ - Permissions   │   │ - Context      │
└─────────────────┘   └─────────────────┘   └─────────────────┘
          │                     │
          ▼                     ▼
┌─────────────────────────────────────────────────────────────────┐
│                    CONTRACT VALIDATOR                         │
│  Lädt relevante Contracts → Prüft gegen 162 Rules             │
└─────────────────────────────────────────────────────────────────┘
                                │
                    ┌───────────┼───────────┐
                    │           │           │
                    ▼           ▼           ▼
              CRITICAL     MAJOR      MINOR
              → BLOCK      → WARN      → INFO

Datenspeicherung

  • Contracts & Rules: masterdev.contracts, masterdev.contract_rules
  • Hook-Logs: ki_protokoll.hook_log
  • Validierungen: ki_protokoll.contract_validation_log
  • Code-Entities: masterdev.code_entities + Qdrant

Ablauf bei jedem Tool-Aufruf

  1. PreToolUse: Validierung gegen Contracts, ggf. Blockierung
  2. Tool-Ausführung: Nur wenn PreToolUse passiert
  3. PostToolUse: Permissions, Code-Analyse, Logging
  4. Feedback: Zurück an Claude Code als System-Reminder

Contracts: Die 16 Regelwerke

Ein Contract ist eine Sammlung thematisch zusammengehöriger Regeln. Jeder Contract definiert, was in seinem Geltungsbereich erlaubt, verboten oder empfohlen ist.

Aktive Contracts (Stand: 2026-01-06)

Contract Rules Critical Major Minor
API Response Contract 5 3 2 0
Architecture Gate Contract 6 5 1 0
Code Quality Standards 10 4 5 1
CSS Contract 6 2 4 0
DB Access Security Protocol 8 8 0 0
HTML Tables Contract 13 7 5 1
HTMX Patterns Contract 12 8 3 1
JS Browser Architecture 12 9 3 0
Karl Kratz Content Style 6 0 4 2
Karl Kratz Glossar-Eintrag 13 2 9 2
Karl Kratz Technischer Beitrag 26 3 13 10
Layered Architecture Validation 9 9 0 0
Python Pipeline Contract 12 7 3 2
Task Prompt Standards 6 4 2 0
Text Quality Standards 10 5 5 0
View Structure Contract 8 7 1 0
Gesamt 162 83 60 19

contract_text_validator list

Contract-Kategorien anzeigen

CODE-QUALITÄT (4 Contracts, 41 Rules)
├── code-quality-standards     Controller-Patterns, DI, Error-Handling
├── architecture-gate-contract Layered Architecture, Dependency Rules
├── layered-architecture       Verzeichnisstruktur, Namespace-Checks
└── view-structure-contract    CRUD-Views, Template-Patterns

FRONTEND (4 Contracts, 43 Rules)
├── css-contract               Custom Properties, keine Inline-Styles
├── html-tables-contract       Semantic HTML, Accessibility
├── htmx-patterns-contract     hx-Attribute, Endpoint-Patterns
└── js-browser-architecture    ES6 Modules, Event-Handling

API & DATEN (3 Contracts, 25 Rules)
├── api-response-contract      JSON-Struktur, HTTP-Codes
├── db-access-security         Prepared Statements, keine Raw-SQL
└── python-pipeline-contract   Pipeline-Patterns, Error-Handling

CONTENT & TEXT (3 Contracts, 45 Rules)
├── karl-kratz-content         Autorenprofil, Relativierung
├── karl-kratz-glossar         150-300 Wörter, Definition + Beispiele
└── karl-kratz-technischer     Didaktik, Code-Beispiele, Boxen

PROZESS (2 Contracts, 16 Rules)
├── task-prompt-standards      Prompt-Struktur, Akzeptanzkriterien
└── text-quality-standards     Rechtschreibung, Stil, Formatierung

Relevante Contracts für diese Session

Bei der Lexoffice-Integration wurden folgende Contracts aktiv geprüft:

  • Code Quality Standards: 10 Rules für PHP-Code
  • HTMX Patterns Contract: 12 Rules für HTMX-Endpoints
  • CSS Contract: 6 Rules für Styles
  • DB Access Security: 8 Rules für Repository-Pattern

Contract-Struktur (YAML)

Jeder Contract ist in YAML definiert und enthält Metadaten, Schwellwerte und Regelreferenzen:

code-quality-standards.yaml (Auszug)

Beispiel-Contract anzeigen

contract_key: code-quality-standards
name: Code Quality Standards
version: 2.1
scope: global

thresholds:
  pass_critical: 0    # Keine Critical-Violations erlaubt
  pass_major: 2       # Max 2 Major-Violations
  pass_minor: 5       # Max 5 Minor-Violations

rules:
  - id: CQS-001
    severity: critical
    type: forbidden
    pattern: "\\$_GET|\\$_POST|\\$_REQUEST"
    message: "Direkter Superglobal-Zugriff verboten"

  - id: CQS-002
    severity: major
    type: forbidden
    pattern: "echo.*<"
    message: "Controller dürfen kein HTML ausgeben"

↑ Zurück zur Architektur

Rules: Die 162 Einzelregeln

Jede Rule hat eine eindeutige ID, ein Severity-Level und einen Pattern-Matcher. Bei Verletzung wird je nach Severity blockiert, gewarnt oder informiert.

Severity-Level

Level Anzahl Aktion Beispiel
CRITICAL 83 Blockiert Operation SQL-Injection, direkter DB-Zugriff
MAJOR 60 Warnung + Guidance Hardcoded URLs, fehlende Typen
MINOR 19 Info-Hinweis Code-Style, Formatierung

contract_text_validator rules -c code-quality-standards

Beispiel: Code Quality Standards (10 Rules)

CRITICAL (4 Rules) → Operation wird BLOCKIERT

CQ-SQL-001  Controller dürfen keine direkten SQL-Queries enthalten
            Pattern: SELECT|INSERT|UPDATE|DELETE.*FROM in Controller

CQ-DI-001   Controller müssen DI nutzen, keine Static Facades
            Pattern: ::getInstance()|new.*Repository()

PHP-ERR-001 Catch-Block mit nur return ohne Logging ist verboten
            Pattern: catch.*\{.*return.*\} ohne Logger

PHP-ERR-002 @ Error Suppression ist verboten
            Pattern: @\w+\(

───────────────────────────────────────────────────────────

MAJOR (5 Rules) → Warnung + Guidance

CQ-HTML-001 Controller dürfen kein HTML direkt ausgeben
            Pattern: echo.*<|print.*<

CQ-HTTP-001 Controller müssen $this->notFound() nutzen
            Pattern: http_response_code\(404\)

PHP-CFG-001 Hardcoded URLs sind verboten
            Pattern: https?://[^"'\s]+(?!config)

PHP-CFG-002 Hardcoded localhost URLs sind verboten
            Pattern: localhost:\d+|127\.0\.0\.1

PHP-KISS-001 Mehr als 5 Parameter → Builder-Pattern erwägen
            Pattern: function.*\(.*,.*,.*,.*,.*,

───────────────────────────────────────────────────────────

MINOR (1 Rule) → Info-Hinweis

CQ-TYPE-001 Properties sollten PHP 8+ Typen haben
            Pattern: private \$\w+ ohne Typ-Deklaration

Rule-Anatomie

Jede Rule besteht aus diesen Komponenten:

rule-anatomy.yaml

Rule-Struktur anzeigen

rule_id: CQ-SQL-001           # Eindeutige ID
rule_type: forbidden           # forbidden | required | warning
severity: critical             # critical | major | minor

pattern: "SELECT|INSERT|UPDATE|DELETE.*FROM"
pattern_type: regex            # regex | contains | exact

applies_to:
  extensions: [".php"]
  paths: ["src/Controllers/*", "public/*"]
  excludes: ["src/Repositories/*"]

message: "Controller dürfen keine direkten SQL-Queries enthalten"

fix_suggestion: |
  Verschiebe die Query in ein Repository:
  1. Erstelle src/Repositories/{Entity}Repository.php
  2. Implementiere die Query als Methode
  3. Injiziere das Repository via Container

guideline: |
  Repositories kapseln Datenbankzugriffe und ermöglichen:
  - Testbarkeit durch Mocking
  - Wiederverwendbarkeit
  - Zentrale Änderungen

Validierung in dieser Session

Bei der Lexoffice-Integration wurden alle 6 PHP-Dateien gegen relevante Rules geprüft:

  • InvoiceRepository.php: 0 Violations (Repository-Pattern korrekt)
  • list.php: 0 Violations (HTMX-Endpoint sauber)
  • karlscore.php: 0 Violations (Admin-View konform)
  • 369wohlbefinden.php: 0 Violations
  • detail.php: 0 Violations
  • Container.php: 0 Violations (DI-Pattern befolgt)

Ergebnis: 0 Critical, 0 Major, 0 Minor: Alle Gates passiert.

↑ Zurück zu Contracts

Hooks: Die 20 Event-Handler

Hooks sind Python-Module, die bei bestimmten Events ausgeführt werden. Sie validieren, transformieren oder protokollieren: je nach Typ vor oder nach der Tool-Ausführung.

Hook-Typen

Event Zeitpunkt Typische Hooks
SessionStart Sitzungsbeginn task_loader, state_context, project_registry
PreToolUse Vor Tool-Ausführung contract_validator, permission_guard, state_write_guard
PostToolUse Nach Tool-Ausführung fix_permissions, code_analyzer, log_to_db
UserPromptSubmit Nutzereingabe text_contract_pre, phase_guidance
Stop Antwort fertig text_contract_post, task_reminder

ls /var/www/tools/ki-protokoll/claude-hook/modules/

Alle 20 Hook-Module anzeigen

VALIDIERUNG & BLOCKIERUNG (PreToolUse)
├── contract_validator.py    Prüft Code gegen Contracts
├── permission_guard.py      Prüft Dateizugriffsrechte
├── state_write_guard.py     Verhindert Schreiben in geschützte Pfade
├── block_direct_db.py       Blockiert direkten MySQL-Zugriff
├── block_direct_qdrant.py   Blockiert direkten Qdrant-Zugriff
├── docs_guardrail.py        Verhindert /docs/*.md Erstellung
└── quality_gate.py          Prüft Qualitäts-Schwellwerte

NACHBEREITUNG (PostToolUse)
├── fix_permissions.py       Setzt www-data:www-data nach Edit/Write
├── code_analyzer.py         Extrahiert Entities, queued Embeddings
├── file_backup.py           Erstellt Backup vor Änderungen
├── log_to_db.py             Protokolliert alle Tool-Aufrufe
└── quality_validator.py     Validiert Ergebnis nach Ausführung

KONTEXT & GUIDANCE (SessionStart/UserPrompt)
├── task_loader.py           Lädt offene Tasks aus ki_tasks
├── state_context.py         Speichert Session-Zustand
├── project_registry.py      Erkennt Projekt aus Pfad
├── contract_loader.py       Lädt relevante Contracts
├── phase_guidance.py        Gibt Phase-spezifische Hinweise
└── doku_awareness.py        Zeigt zugehörige Dokumentation

CONTENT & TEXT (Stop/UserPrompt)
├── text_contract_pre.py     Text-Contracts vor Generierung
├── text_contract_post.py    Validiert generierten Text
├── doku_reminder.py         Erinnert an Doku-Updates
├── task_reminder.py         Erinnert an offene Tasks
└── gate_status.py           Zeigt aktuellen Gate-Status

Hook-Ausführung in dieser Session

Während der Lexoffice-Implementation wurden folgende Hooks aktiv:

hook_log --session lexoffice-integration

Session-Hooks anzeigen

19:52:12 PostToolUse fix_permissions
          InvoiceRepository.php → www-data:www-data ✓

19:52:13 PostToolUse code_analyzer
          InvoiceRepository.php → 12 Entities extrahiert
          Queued: 3 Methods, 2 Constants, 1 Class ✓

19:53:28 PreToolUse  contract_validator
          Container.php → code-quality-standards PASS

19:53:28 PostToolUse fix_permissions
          Container.php → www-data:www-data ✓

19:54:15 PreToolUse  permission_guard
          .htaccess → Schreibzugriff erlaubt ✓

19:55:03 PreToolUse  contract_validator
          list.php → htmx-patterns-contract PASS

19:55:04 PostToolUse code_analyzer
          list.php → 3 Functions extrahiert ✓

19:58:47 PreToolUse  contract_validator
          app.css → css-contract PASS

───────────────────────────────────────────────────────────

ZUSAMMENFASSUNG:
  PreToolUse:  4 Validierungen, 0 Blockierungen
  PostToolUse: 5 Permissions-Fixes, 2 Code-Analysen
  Gesamt:      9 Hook-Ausführungen, alle erfolgreich

Hook-Anatomie (Python)

Jeder Hook ist ein Python-Modul mit einer run()-Funktion:

fix_permissions.py (vereinfacht)

Hook-Code anzeigen

"""PostToolUse Hook: Setzt Dateiberechtigungen nach Edit/Write"""

import os
import subprocess

EVENTS = ["PostToolUse"]
TOOLS  = ["Edit", "Write"]

def run(context: dict) -> dict:
    """Wird nach jedem Edit/Write aufgerufen"""

    file_path = context.get("file_path")
    if not file_path or not os.path.exists(file_path):
        return {"success": True}

    # Setze www-data:www-data
    subprocess.run([
        "chown", "www-data:www-data", file_path
    ], check=True)

    return {
        "success": True,
        "message": f"{file_path} → www-data:www-data"
    }

↑ Zurück zu Rules

Transitions: Zustandsübergänge und Gates

Transitions beschreiben die Zustandsübergänge während einer KI-Session. Jeder Übergang muss ein Gate passieren: eine Validierungsschwelle basierend auf den Contract-Ergebnissen.

Session-Phasen

Phase Beschreibung Gate-Bedingung
INIT Session-Start, Kontext laden project_registry erfolgreich
PLANNING Task analysieren, Approach planen task_prompt_standards erfüllt
IMPLEMENTING Code schreiben, Dateien ändern Alle PreToolUse-Validierungen bestanden
VALIDATING Tests, Code-Review, Contracts 0 Critical, ≤2 Major Violations
COMPLETE Task abgeschlossen Alle Akzeptanzkriterien erfüllt

gate-system --transitions lexoffice-session

Session-Flow anzeigen

LEXOFFICE-INTEGRATION SESSION FLOW
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

19:50:00  ● INIT
           │  task_loader: Task 1847 geladen
           │  project_registry: schnuedeldue.oerg
           │  contract_loader: 4 Contracts aktiviert
           │
           ▼  GATE PASSED (Kontext vollständig)

19:50:30  ● PLANNING
           │  Prompt v3.0 analysiert
           │  11 Sub-Tasks identifiziert (1836-1846)
           │  Abhängigkeiten gemappt
           │
           ▼  GATE PASSED (task_prompt_standards: 0 Violations)

19:52:00  ● IMPLEMENTING
           │
           │  19:52:12 Edit InvoiceRepository.php
           │           └─ ✓ code-quality-standards
           │           └─ ✓ db-access-security
           │
           │  19:53:28 Edit Container.php
           │           └─ ✓ architecture-gate-contract
           │
           │  19:54:15 Edit .htaccess
           │           └─ ✓ permission_guard
           │
           │  19:55:03 Write list.php
           │           └─ ✓ htmx-patterns-contract
           │
           │  19:56:21 Write karlscore.php
           │  19:56:45 Write 369wohlbefinden.php
           │  19:57:32 Write detail.php
           │           └─ ✓ view-structure-contract
           │
           │  19:58:14 Edit nav.php
           │  19:58:47 Edit app.css
           │           └─ ✓ css-contract
           │
           ▼  GATE PASSED (9 Edits, 0 Blockierungen)

19:59:00  ● VALIDATING
           │  code_analyzer: 6 Dateien analysiert
           │  quality_validator: 833 LoC, 98.5% Score
           │  contract_validator: 0 Critical, 0 Major, 0 Minor
           │
           ▼  GATE PASSED (Alle Schwellwerte unterschritten)

20:00:00  ● COMPLETE
           │  22/22 Akzeptanzkriterien erfüllt
           │  Task 1847 → Status: completed
           │
           └─ SESSION ERFOLGREICH

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ZUSAMMENFASSUNG:
  Phasen durchlaufen:  5/5
  Gates passiert:      5/5 ✓
  Blockierungen:       0
  Gesamtdauer:         ~10 Minuten

Gate-Logik

Jedes Gate prüft die Ergebnisse der Contract-Validierung gegen definierte Schwellwerte:

gate-logic.py (Pseudocode)

Gate-Entscheidungslogik anzeigen

def check_gate(validation_result, contract):
    """Prüft ob Gate passiert werden kann"""

    critical = validation_result.count_critical()
    major = validation_result.count_major()
    minor = validation_result.count_minor()

    # Schwellwerte aus Contract laden
    thresholds = contract.thresholds

    if critical > thresholds.pass_critical:  # Default: 0
        return GateResult.BLOCKED

    if major > thresholds.pass_major:  # Default: 2
        return GateResult.WARNING

    if minor > thresholds.pass_minor:  # Default: 5
        return GateResult.INFO

    return GateResult.PASSED


# Ergebnis-Behandlung
match gate_result:
    case GateResult.BLOCKED:
        # Operation abbrechen, Fehler zurückgeben
        raise GateBlockedException(violations)

    case GateResult.WARNING:
        # Warnung in system-reminder, aber fortfahren
        emit_warning(violations)

    case GateResult.PASSED:
        # Alles gut, keine Aktion nötig
        pass

Was passiert bei Blockierung?

Wenn ein Gate eine Operation blockiert:

  1. Die Tool-Ausführung wird abgebrochen
  2. Claude Code erhält einen system-reminder mit:
    • Welcher Contract verletzt wurde
    • Welche spezifischen Rules
    • Fix-Suggestions für jede Violation
  3. Claude Code muss den Code korrigieren und erneut versuchen

In dieser Session gab es keine Blockierungen: alle Gates wurden beim ersten Versuch passiert.

↑ Zurück zu Hooks

Gate-System-Interventionen

Das Gate-System überwacht alle Dateioperationen und greift bei Bedarf korrigierend ein. Während dieser Session wurden mehrere Hook-Aktivierungen protokolliert.

Aktive Hooks

Hook Typ Funktion
fix_permissions PostToolUse Setzt www-data:www-data nach Edit/Write
state_write_guard PreToolUse Verhindert Schreiben in geschützte Pfade
code_analyzer PostToolUse Analysiert PHP-Dateien, aktualisiert Knowledge Base

Protokollierte Eingriffe

Zeit Hook Aktion
19:52:12 fix_permissions InvoiceRepository.php → www-data:www-data
19:52:13 code_analyzer InvoiceRepository analysiert, 12 Entities extrahiert
19:55:03 fix_permissions list.php → www-data:www-data
19:55:04 code_analyzer list.php analysiert, 3 Functions extrahiert
19:58:47 fix_permissions app.css → www-data:www-data

Keine Blockierungen

Während dieser Session wurden keine Operationen blockiert. Alle Schreibvorgänge erfolgten in erlaubten Pfaden und entsprachen den definierten Constraints.

Implementation-Monitoring

Die externe KI implementierte Task 1847 autonom. Die Beobachtung erfolgte über Datei-Monitoring und Log-Analyse ohne Eingriff.

Erstellte Dateien (Reihenfolge)

Zeit Datei Größe
19:52:12 src/Repositories/Lexoffice/InvoiceRepository.php 4.611 B
19:53:28 src/Container.php (modifiziert) +20 Zeilen
19:54:15 public/.htaccess (modifiziert) +3 Rules
19:55:03 public/htmx/invoices/list.php 7.649 B
19:56:21 public/admin/rechnungen/karlscore.php 2.841 B
19:56:45 public/admin/rechnungen/369wohlbefinden.php 2.856 B
19:57:32 public/admin/rechnungen/detail.php 5.123 B
19:58:14 templates/layout/nav.php (modifiziert) +12 Zeilen
19:58:47 public/assets/css/app.css (modifiziert) +85 Zeilen

Beobachteter Fehler

In list.php wurde zunächst ein falscher Pfad verwendet:

// Fehlerhaft (19:55:03)
dirname(__DIR__, 2) . '/src/bootstrap.php'

// Korrekt (19:59:48 - autonome Korrektur)
dirname(__DIR__, 3) . '/src/bootstrap.php'

Die externe KI erkannte den Fehler beim ersten HTTP-Test und korrigierte ihn selbstständig.

Supervisor-Disziplin: Strikte Rollentrennung

Die beobachtende KI (claude-code) versuchte einmalig, den Pfad-Fehler zu korrigieren. Der menschliche Supervisor intervenierte mit einer klaren Rollendefinition:

„Die Supervisor-Instanz ist ausschließlich zur Beobachtung, Dokumentation und Erläuterung autorisiert. Aktive Eingriffe in den Implementierungsprozess sind untersagt."

Die unerlaubte Änderung wurde unmittelbar revertiert. Die externe KI korrigierte den Fehler 60 Sekunden später vollständig autonom, was die Validität des Supervision-Konzepts bestätigte.

Akzeptanztests

Nach Abschluss der Implementation wurden automatisierte HTTP-Tests durchgeführt, um alle Akzeptanzkriterien zu validieren.

Test-Suite

Endpoint Erwartung Ergebnis
/admin/rechnungen/karlscore HTTP 200 PASS
/admin/rechnungen/369wohlbefinden HTTP 200 PASS
/htmx/invoices/list?account=karlscore HTTP 200, HTML-Table PASS
/htmx/invoices/list?account=369wohlbefinden HTTP 200, HTML-Table PASS
/admin/rechnungen/karlscore/{uuid} HTTP 200, Detail-View PASS
/htmx/invoices/list?account=invalid HTTP 400 PASS

Funktionale Tests

  • Sortierung: Alle 6 Spalten sortierbar (ASC/DESC)
  • Filter: Freitext, Status, Datumsbereich funktional
  • Pagination: Korrekt bei 25+ Einträgen
  • Detail-Link: Navigation zur Detailansicht
  • Breadcrumb: Zurück-Navigation vorhanden

Responsive-Test

Viewport-Tests bei 320px, 768px, 1200px bestanden. Tabelle scrollt horizontal auf kleinen Screens, Filter stacken vertikal.

Ergebnis und Screenshots

Die fertige Implementation zeigt eine vollständige Rechnungsverwaltung mit Listenansicht und Detailseiten.

Rechnungsliste

FREUND Rechnungsliste

Listenansicht mit Sortierung, Filter und Pagination

Rechnungsdetail

FREUND Rechnungsdetail

Detailansicht mit Positionen, Summen und Kundenadresse

Code-Metriken

Metrik Wert
Neue Dateien 6
Modifizierte Dateien 3
Zeilen Code (neu) ~450
Zeilen CSS (neu) ~85
Implementationszeit ~8 Minuten

Babbeln kann jeder, hier die Live-Demo

Das hier ist fast 1:1 die Tabelle aus dem echten System. Der geilen Optik halber wollte ich das hier noch in das niedliche Terminalfenster pressen. Dafür habe ich auf HTMX verzichtet und stattdessen reines JavaScript verwendet.

Aber dennoch: Auch diese Demo ist nach dem hier beschriebenen Verfahren innerhalb weniger Minuten sehr präzise und ohne jede weitere Korrekturschleife entstanden. Das spart eine Menge Zeit, Nerven und Tokens.

Datenquelle

Die Daten stammen aus einem Demo-Datensatz des MariaDB Datenbankservers (karlkratz_de.demo_invoices).

php // Alle Daten laden (einmalig beim Seitenaufruf) $invoices = []; $statusCounts = ['draft' = 0, 'open' => 0, 'paid' => 0, 'voided' => 0]; try { // Verwende DatabaseManager statt direktem tenant.json Zugriff $pdo = \App\Services\DatabaseManager::getConnection('mariadb'); $stmt = $pdo->query(" SELECT id, voucher_number, voucher_date, voucher_status, due_date, address_name, total_gross_amount, total_net_amount, total_tax_amount, address_street, address_zip, address_city, line_items FROM demo_invoices WHERE archived = 0 ORDER BY voucher_date DESC "); $invoices = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($invoices as $inv) { $status = $inv['voucher_status'] ?? 'open'; if (isset($statusCounts[$status])) $statusCounts[$status]++; } } catch (Exception $e) { echo '

Datenbankfehler: ' . htmlspecialchars($e->getMessage()) . '

'; } $statusLabels = ['draft' => 'Entwurf', 'open' => 'Offen', 'paid' => 'Bezahlt', 'voided' => 'Storniert']; ?>

SELECT * FROM demo_invoicesEntwurf: = $statusCounts['draft'] ? Offen: = $statusCounts['open'] ? Bezahlt: = $statusCounts['paid'] ? Storniert: = $statusCounts['voided'] ?

Alle Entwurf Offen Bezahlt Storniert

Nr. Datum Kunde Betrag Status Fällig

0 Rechnungen← Zurück

Datum:

Fällig:

Technische Details

  • Tabelle: karlkratz_de.demo_invoices (= count($invoices) ? Einträge)
  • Features: Client-side Filter, Sortierung, Inline-Details (kein Page-Reload)

Original-Code: InvoiceRepository.php

Das InvoiceRepository ist die zentrale Datenzugriffsschicht fuer Lexoffice-Rechnungen. Es implementiert das Repository-Pattern mit Multi-Account-Support.

Architektur-Entscheidungen

  • Multi-Account: ACCOUNT_MAP verwaltet zwei Lexoffice-Konten
  • Whitelist-Sortierung: ALLOWED_SORTS verhindert SQL-Injection bei ORDER BY
  • Parameter-Binding: Alle Filter werden als Prepared Statements gebunden
  • UUID-Validierung: findById() validiert Format vor Datenbankzugriff

src/Repositories/Lexoffice/InvoiceRepository.php (157 Zeilen)

<?php

declare(strict_types: 1);

namespace Schnuedeldue\Repositories\Lexoffice;

use InvalidArgumentException;
use PDO;

/**
 * InvoiceRepository for Lexoffice invoice data
 *
 * Handles data access for Lexoffice invoices from synced databases.
 * Supports multiple accounts (karlscore, 369wohlbefinden).
 */
class InvoiceRepository
{
    private PDO $pdo;
    private string $account;

    private const ACCOUNT_MAP = [
        'karlscore' => 'lexoffice_karlscore',
        '369wohlbefinden' => 'lexoffice_369wohlbefinden',
    ];

    private const ALLOWED_SORTS = [
        'voucher_number',
        'voucher_date',
        'address_name',
        'total_gross_amount',
        'voucher_status',
        'due_date',
    ];

    public function __construct(PDO $pdo, string $account)
    {
        if (!isset(self::ACCOUNT_MAP[$account])) {
            throw new InvalidArgumentException(
                "Unknown account: {$account}. Allowed: " . implode(', ', array_keys(self::ACCOUNT_MAP))
            );
        }
        $this->pdo = $pdo;
        $this->account = $account;
    }

    public static function getConnectionName(string $account): string
    {
        if (!isset(self::ACCOUNT_MAP[$account])) {
            throw new InvalidArgumentException("Unknown account: {$account}");
        }
        return self::ACCOUNT_MAP[$account];
    }

    public static function getAllowedAccounts(): array
    {
        return array_keys(self::ACCOUNT_MAP);
    }

    public function list(array $filters, string $sort, string $order, int $limit, int $offset): array
    {
        $sql = "SELECT id, voucher_number, voucher_date, voucher_status,
                       due_date, address_name, total_gross_amount
                FROM invoices
                WHERE archived = 0";
        $params = [];

        if (!empty($filters['q'])) {
            $sql .= " AND (LOWER(voucher_number) LIKE LOWER(?)
                       OR LOWER(address_name) LIKE LOWER(?)
                       OR LOWER(title) LIKE LOWER(?))";
            $q = '%' . $filters['q'] . '%';
            $params[] = $q;
            $params[] = $q;
            $params[] = $q;
        }

        if (!empty($filters['status'])) {
            $sql .= " AND voucher_status = ?";
            $params[] = $filters['status'];
        }

        if (!empty($filters['date_from'])) {
            $sql .= " AND voucher_date >= ?";
            $params[] = $filters['date_from'];
        }
        if (!empty($filters['date_to'])) {
            $sql .= " AND voucher_date <= ?";
            $params[] = $filters['date_to'];
        }

        $sort = in_array($sort, self::ALLOWED_SORTS, true) ? $sort : 'voucher_date';
        $order = strtoupper($order) === 'ASC' ? 'ASC' : 'DESC';

        $sql .= " ORDER BY {$sort} {$order} LIMIT ? OFFSET ?";

        $stmt = $this->pdo->prepare($sql);

        $i = 1;
        foreach ($params as $param) {
            $stmt->bindValue($i++, $param, PDO::PARAM_STR);
        }
        $stmt->bindValue($i++, $limit, PDO::PARAM_INT);
        $stmt->bindValue($i, $offset, PDO::PARAM_INT);

        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    public function count(array $filters): int
    {
        $sql = "SELECT COUNT(*) FROM invoices WHERE archived = 0";
        $params = [];

        // ... identische Filterlogik wie list() ...

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return (int) $stmt->fetchColumn();
    }

    public function findById(string $id): ?array
    {
        if (!preg_match('/^[a-f0-9-]{36}$/', $id)) {
            return null;
        }

        $stmt = $this->pdo->prepare(
            "SELECT * FROM invoices WHERE id = ? AND archived = 0"
        );
        $stmt->execute([$id]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        return $result ?: null;
    }
}

Sicherheitsaspekte

  • SQL-Injection Prevention: Alle Filter nutzen Prepared Statements mit PDO::PARAM_*
  • ORDER BY Whitelist: Sortierfelder werden gegen ALLOWED_SORTS validiert
  • UUID-Regex: findById() prueft Format /^[a-f0-9-]{36}$/ vor Query
  • Readonly-Connection: Container bindet Repository an READ ONLY Session

Original-Code: list.php (HTMX-Endpoint)

Der HTMX-Endpoint liefert HTML-Fragmente fuer die Rechnungstabelle. Er wird per AJAX geladen und ermoeglicht Sortierung, Filterung und Pagination ohne Seitenreload.

HTMX-Konzept

  • HTML-over-Wire: Server liefert fertige HTML-Fragmente, kein JSON
  • hx-get: Triggert GET-Request bei Klick/Input/Change
  • hx-target: Ersetzt Inhalt des Ziel-Elements (#invoices-table)
  • hx-include: Sendet alle Filter-Inputs mit jedem Request

public/htmx/invoices/list.php (199 Zeilen)

<?php

declare(strict_types: 1);

require_once dirname(__DIR__, 3) . '/src/bootstrap.php';

use Schnuedeldue\Container;
use Schnuedeldue\Repositories\Lexoffice\InvoiceRepository;

header('Content-Type: text/html; charset=utf-8');

if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
    http_response_code(405);
    echo '<p class="message message-error">Method not allowed</p>';
    exit;
}

$container = Container::getInstance();
$user = isLocalAccess()
    ? ['id' => 0, 'email' => 'local@bypass', 'name' => 'Local']
    : $container->getSessionService()->getCurrentUser();

if (!$user) {
    http_response_code(401);
    echo '<p class="message message-error">Nicht autorisiert</p>';
    exit;
}

// Account-Validierung
$allowedAccounts = InvoiceRepository::getAllowedAccounts();
$account = $_GET['account'] ?? '';
if (!in_array($account, $allowedAccounts, true)) {
    http_response_code(400);
    echo '<p class="message message-error">Ungueltiger Account</p>';
    exit;
}

try {
    $repository = $container->getLexofficeInvoiceRepository($account);

    // Filter extrahieren
    $filters = [];
    if (!empty($_GET['q'])) {
        $filters['q'] = trim($_GET['q']);
    }
    if (!empty($_GET['status'])) {
        $filters['status'] = $_GET['status'];
    }
    if (!empty($_GET['date_from'])) {
        $filters['date_from'] = $_GET['date_from'];
    }
    if (!empty($_GET['date_to'])) {
        $filters['date_to'] = $_GET['date_to'];
    }

    $sort = $_GET['sort'] ?? 'voucher_date';
    $order = $_GET['order'] ?? 'desc';

    $page = max(1, (int) ($_GET['page'] ?? 1));
    $perPage = 25;
    $offset = ($page - 1) * $perPage;

    $items = $repository->list($filters, $sort, strtoupper($order), $perPage, $offset);
    $total = $repository->count($filters);
    $totalPages = (int) ceil($total / $perPage);

    $hxInclude = '.invoice-filters input, .invoice-filters select';
    $baseUrl = "/htmx/invoices/list?account=" . htmlspecialchars($account, ENT_QUOTES, 'UTF-8');

    // Tabellen-Header mit sortierbaren Spalten
    echo '<div class="table-responsive">';
    echo '<table class="data-table">';
    echo '<thead><tr>';

    $columns = [
        'voucher_number' => 'Nr.',
        'voucher_date' => 'Datum',
        'address_name' => 'Kunde',
        'total_gross_amount' => 'Betrag',
        'voucher_status' => 'Status',
        'due_date' => 'Faellig',
    ];

    foreach ($columns as $col => $label) {
        $isSorted = ($sort === $col);
        $nextOrder = ($isSorted && $order === 'asc') ? 'desc' : 'asc';
        $sortedClass = $isSorted ? " sorted-{$order}" : '';

        echo '<th class="col-' . $col . ' sortable' . $sortedClass . '">';
        echo '<a href="#" hx-get="' . $baseUrl . '&sort=' . $col . '&order=' . $nextOrder . '" ';
        echo 'hx-target="#invoices-table" hx-include="' . $hxInclude . '">' . $label . '</a>';
        echo '</th>';
    }
    echo '</tr></thead>';

    // Tabelleninhalt
    if (empty($items)) {
        echo '<tbody><tr><td colspan="6" class="empty-state">Keine Rechnungen gefunden</td></tr></tbody>';
    } else {
        echo '<tbody>';
        foreach ($items as $item) {
            renderInvoiceRow($item, $account);
        }
        echo '</tbody>';
    }
    echo '</table></div>';

    // Pagination
    if ($totalPages > 1) {
        renderPagination($page, $totalPages, $baseUrl, $sort, $order, $hxInclude);
    }

    // Meta-Info
    $from = $total > 0 ? ($page - 1) * $perPage + 1 : 0;
    $to = min($page * $perPage, $total);
    echo '<p class="table-meta">' . $from . '-' . $to . ' von ' . $total . ' Rechnungen</p>';

} catch (Throwable $e) {
    error_log('[InvoicesListHTMX] Error: ' . $e->getMessage());
    echo '<p class="message message-error">Fehler beim Laden</p>';
}

// === HELPER FUNCTIONS ===

function renderInvoiceRow(array $item, string $account): void
{
    $id = htmlspecialchars($item['id'], ENT_QUOTES, 'UTF-8');
    $number = htmlspecialchars($item['voucher_number'] ?? '-', ENT_QUOTES, 'UTF-8');
    $date = $item['voucher_date'] ? date('d.m.Y', strtotime($item['voucher_date'])) : '-';
    $customer = htmlspecialchars(mb_substr($item['address_name'] ?? '-', 0, 40), ENT_QUOTES, 'UTF-8');
    $amount = number_format((float) ($item['total_gross_amount'] ?? 0), 2, ',', '.') . ' EUR';
    $status = $item['voucher_status'] ?? 'open';
    $dueDate = $item['due_date'] ? date('d.m.Y', strtotime($item['due_date'])) : '-';

    // Ueberfaelligkeits-Check
    $isOverdue = $status === 'open'
        && $item['due_date'] !== null
        && strtotime($item['due_date']) < strtotime('today');

    $rowClass = $isOverdue ? ' class="row-overdue"' : '';

    $statusLabels = [
        'draft' => 'Entwurf',
        'open' => 'Offen',
        'paid' => 'Bezahlt',
        'voided' => 'Storniert',
    ];
    $statusLabel = $statusLabels[$status] ?? $status;

    $detailUrl = "/admin/rechnungen/{$account}/{$id}";

    echo '<tr' . $rowClass . '>';
    echo '<td data-label="Nr."><a href="' . $detailUrl . '">' . $number . '</a></td>';
    echo '<td data-label="Datum">' . $date . '</td>';
    echo '<td data-label="Kunde">' . $customer . '</td>';
    echo '<td data-label="Betrag" class="text-right">' . $amount . '</td>';
    echo '<td data-label="Status"><span class="status status-' . $status . '">' . $statusLabel . '</span></td>';
    echo '<td data-label="Faellig" class="cell-due">' . $dueDate . '</td>';
    echo '</tr>';
}

function renderPagination(int $page, int $totalPages, string $baseUrl, string $sort, string $order, string $hxInclude): void
{
    // Intelligente Pagination mit Ellipsis bei >7 Seiten
    // ...
}

Request-Response-Flow

  1. Admin-Seite laedt mit hx-trigger="load"
  2. HTMX sendet GET an /htmx/invoices/list?account=karlscore
  3. PHP rendert HTML-Tabelle mit Daten aus Repository
  4. HTMX ersetzt #invoices-table mit Response
  5. Bei Sort/Filter: erneuter Request mit hx-include-Parametern

Original-Code: karlscore.php

Die Admin-Seite fuer karlsCORE GmbH ist ein minimalistisches Template, das die Rechnungstabelle per HTMX laedt. Die gesamte Komplexitaet liegt in list.php.

Aufbau

  • Auth-Check: Einbindung von auth.php fuer Session-Validierung
  • Filter-UI: Input/Select-Elemente mit HTMX-Attributen
  • Lazy Loading: hx-trigger="load" laedt Tabelle beim Seitenaufruf
  • Account-Isolation: Konstante $account = 'karlscore'

public/admin/rechnungen/karlscore.php (68 Zeilen)

<?php

declare(strict_types: 1);

require_once dirname(__DIR__, 3) . '/templates/layout/auth.php';

$pageTitle = 'Rechnungen karlsCORE - SCHNUEDELDUE';
$currentPage = 'admin/rechnungen/karlscore';
$account = 'karlscore';

require dirname(__DIR__, 3) . '/templates/layout/header.php';
require dirname(__DIR__, 3) . '/templates/layout/nav.php';
?>
    <main class="app-main">
        <h1>Rechnungen karlsCORE GmbH</h1>

        <section class="invoices-section">
            <div class="invoice-filters">
                <!-- Suchfeld -->
                <input type="text" name="q" id="filter-q"
                    class="filter-input"
                    placeholder="Suchen..."
                    hx-get="/htmx/invoices/list?account=<?= $account ?>"
                    hx-target="#invoices-table"
                    hx-trigger="input changed delay:300ms, search"
                    hx-include=".invoice-filters input, .invoice-filters select"
                    hx-indicator="#table-spinner">

                <!-- Status-Filter -->
                <select name="status" id="filter-status"
                    class="filter-select"
                    hx-get="/htmx/invoices/list?account=<?= $account ?>"
                    hx-target="#invoices-table"
                    hx-trigger="change"
                    hx-include=".invoice-filters input, .invoice-filters select"
                    hx-indicator="#table-spinner">
                    <option value="">Status: Alle</option>
                    <option value="draft">Entwurf</option>
                    <option value="open">Offen</option>
                    <option value="paid">Bezahlt</option>
                    <option value="voided">Storniert</option>
                </select>

                <!-- Datumsfilter -->
                <input type="date" name="date_from" id="filter-date-from"
                    class="filter-input filter-date"
                    hx-get="/htmx/invoices/list?account=<?= $account ?>"
                    hx-target="#invoices-table"
                    hx-trigger="change"
                    hx-include=".invoice-filters input, .invoice-filters select"
                    hx-indicator="#table-spinner"
                    placeholder="Von">

                <input type="date" name="date_to" id="filter-date-to"
                    class="filter-input filter-date"
                    hx-get="/htmx/invoices/list?account=<?= $account ?>"
                    hx-target="#invoices-table"
                    hx-trigger="change"
                    hx-include=".invoice-filters input, .invoice-filters select"
                    hx-indicator="#table-spinner"
                    placeholder="Bis">

                <span id="table-spinner" class="htmx-indicator spinner"></span>
            </div>

            <!-- Tabellen-Container: wird per HTMX befuellt -->
            <div id="invoices-table"
                hx-get="/htmx/invoices/list?account=<?= $account ?>"
                hx-trigger="load"
                hx-include=".invoice-filters input, .invoice-filters select"
                hx-indicator="#table-spinner">
                <p class="text-muted">Lade Rechnungen...</p>
            </div>
        </section>
    </main>
<?php require dirname(__DIR__, 3) . '/templates/layout/footer.php'; ?>

HTMX-Attribute

Attribut Funktion
hx-get AJAX GET-Request an URL
hx-target Element, das mit Response ersetzt wird
hx-trigger Event, das Request ausloest (load, change, input)
hx-include Selector fuer zusaetzliche Inputs im Request
hx-indicator Element, das waehrend Request sichtbar wird

Original-Code: 369wohlbefinden.php

Die Admin-Seite fuer 369 Wohlbefinden GmbH ist strukturell identisch mit karlscore.php - nur mit anderem Account-Parameter.

DRY-Analyse

Die beiden Admin-Seiten zeigen Code-Duplikation - ein typisches Beispiel, wo ein generisches Template mit Account-Parameter eleganter waere. Der KI-generierte Code hat hier das KISS-Prinzip ueber DRY gestellt: Zwei einfache Dateien statt einer komplexeren Routing-Logik.

public/admin/rechnungen/369wohlbefinden.php (68 Zeilen)

<?php

declare(strict_types: 1);

require_once dirname(__DIR__, 3) . '/templates/layout/auth.php';

$pageTitle = 'Rechnungen 369 Wohlbefinden - SCHNUEDELDUE';
$currentPage = 'admin/rechnungen/369wohlbefinden';
$account = '369wohlbefinden';  // <-- Einziger Unterschied

require dirname(__DIR__, 3) . '/templates/layout/header.php';
require dirname(__DIR__, 3) . '/templates/layout/nav.php';
?>
    <main class="app-main">
        <h1>Rechnungen 369 Wohlbefinden GmbH</h1>

        <section class="invoices-section">
            <div class="invoice-filters">
                <!-- Identisches Filter-UI wie karlscore.php -->
                <input type="text" name="q" id="filter-q"
                    class="filter-input"
                    placeholder="Suchen..."
                    hx-get="/htmx/invoices/list?account=<?= $account ?>"
                    hx-target="#invoices-table"
                    hx-trigger="input changed delay:300ms, search"
                    hx-include=".invoice-filters input, .invoice-filters select"
                    hx-indicator="#table-spinner">

                <select name="status" id="filter-status"
                    class="filter-select"
                    hx-get="/htmx/invoices/list?account=<?= $account ?>"
                    hx-target="#invoices-table"
                    hx-trigger="change"
                    hx-include=".invoice-filters input, .invoice-filters select"
                    hx-indicator="#table-spinner">
                    <option value="">Status: Alle</option>
                    <option value="draft">Entwurf</option>
                    <option value="open">Offen</option>
                    <option value="paid">Bezahlt</option>
                    <option value="voided">Storniert</option>
                </select>

                <input type="date" name="date_from"
                    hx-get="/htmx/invoices/list?account=<?= $account ?>"
                    ...>

                <input type="date" name="date_to"
                    ...>

                <span id="table-spinner" class="htmx-indicator spinner"></span>
            </div>

            <div id="invoices-table"
                hx-get="/htmx/invoices/list?account=<?= $account ?>"
                hx-trigger="load"
                hx-include=".invoice-filters input, .invoice-filters select"
                hx-indicator="#table-spinner">
                <p class="text-muted">Lade Rechnungen...</p>
            </div>
        </section>
    </main>
<?php require dirname(__DIR__, 3) . '/templates/layout/footer.php'; ?>

Trade-off: DRY vs. KISS

Alternative Loesung (DRY): Eine generische rechnungen.php mit $account = $_GET['account']

Gewaehlt (KISS): Zwei separate Dateien mit festen Account-Werten

Vorteil: Klare URL-Struktur (/admin/rechnungen/karlscore), einfachere Navigation, keine Validierungslogik in der View

Original-Code: detail.php

Die Rechnungs-Detailansicht zeigt alle Informationen einer Rechnung: Kundendaten, Positionen, Summen und Zahlungsbedingungen.

Komponenten

  • Breadcrumb: Navigation zurueck zur Liste
  • Header: Rechnungsnummer + Status
  • Meta: Datum, Faelligkeit, Titel
  • Kunde: Vollstaendige Adresse
  • Positionen: Tabelle mit Menge, Preis, Rabatt
  • Summen: Netto, MwSt, Brutto

public/admin/rechnungen/detail.php (182 Zeilen)

<?php

declare(strict_types: 1);

require_once dirname(__DIR__, 3) . '/templates/layout/auth.php';

use Schnuedeldue\Container;
use Schnuedeldue\Repositories\Lexoffice\InvoiceRepository;

// Parameter-Validierung
$allowedAccounts = InvoiceRepository::getAllowedAccounts();
$account = $_GET['account'] ?? '';
$id = $_GET['id'] ?? '';

if (!in_array($account, $allowedAccounts, true)) {
    http_response_code(400);
    exit('Ungueltiger Account');
}

if (!preg_match('/^[a-f0-9-]{36}$/', $id)) {
    http_response_code(400);
    exit('Ungueltige ID');
}

// Rechnung laden
$container = Container::getInstance();
$repository = $container->getLexofficeInvoiceRepository($account);
$invoice = $repository->findById($id);

if (!$invoice) {
    http_response_code(404);
    exit('Rechnung nicht gefunden');
}

// Labels vorbereiten
$accountLabels = [
    'karlscore' => 'karlsCORE GmbH',
    '369wohlbefinden' => '369 Wohlbefinden GmbH',
];
$accountLabel = $accountLabels[$account] ?? $account;

$statusLabels = [
    'draft' => 'Entwurf',
    'open' => 'Offen',
    'paid' => 'Bezahlt',
    'voided' => 'Storniert',
];
$status = $invoice['voucher_status'] ?? 'open';
$statusLabel = $statusLabels[$status] ?? $status;

// Line Items aus JSON parsen
$lineItems = [];
if (!empty($invoice['line_items'])) {
    $lineItems = json_decode($invoice['line_items'], true) ?? [];
}

$pageTitle = 'Rechnung ' . htmlspecialchars($invoice['voucher_number'] ?? '-') . ' - SCHNUEDELDUE';
$currentPage = "admin/rechnungen/{$account}";

require dirname(__DIR__, 3) . '/templates/layout/header.php';
require dirname(__DIR__, 3) . '/templates/layout/nav.php';
?>
    <main class="app-main">
        <!-- Breadcrumb-Navigation -->
        <nav class="breadcrumb">
            <a href="/admin/rechnungen/<?= htmlspecialchars($account) ?>">
                Rechnungen <?= htmlspecialchars($accountLabel) ?>
            </a>
            <span>&rsaquo;</span>
            <span><?= htmlspecialchars($invoice['voucher_number'] ?? '-') ?></span>
        </nav>

        <article class="invoice-detail">
            <!-- Header mit Nummer und Status -->
            <header class="invoice-header">
                <h1><?= htmlspecialchars($invoice['voucher_number'] ?? '-') ?></h1>
                <span class="status status-<?= htmlspecialchars($status) ?>">
                    <?= htmlspecialchars($statusLabel) ?>
                </span>
            </header>

            <!-- Meta-Informationen -->
            <section class="invoice-meta">
                <dl>
                    <dt>Rechnungsdatum</dt>
                    <dd><?= $invoice['voucher_date'] ? date('d.m.Y', strtotime($invoice['voucher_date'])) : '-' ?></dd>
                    <dt>Faelligkeit</dt>
                    <dd><?= $invoice['due_date'] ? date('d.m.Y', strtotime($invoice['due_date'])) : '-' ?></dd>
                </dl>
            </section>

            <!-- Kundenadresse -->
            <section class="invoice-customer">
                <h2>Kunde</h2>
                <address>
                    <?= htmlspecialchars($invoice['address_name'] ?? '-') ?><br>
                    <?= htmlspecialchars($invoice['address_street'] ?? '') ?><br>
                    <?= htmlspecialchars($invoice['address_zip'] ?? '') ?>
                    <?= htmlspecialchars($invoice['address_city'] ?? '') ?>
                </address>
            </section>

            <!-- Positionen-Tabelle -->
            <?php if (!empty($lineItems)): ?>
            <section class="invoice-items">
                <h2>Positionen</h2>
                <table class="data-table">
                    <thead>
                        <tr>
                            <th>Pos</th>
                            <th>Bezeichnung</th>
                            <th class="text-right">Menge</th>
                            <th>Einheit</th>
                            <th class="text-right">Einzelpreis</th>
                            <th class="text-right">Rabatt</th>
                            <th class="text-right">Gesamt</th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach ($lineItems as $i => $item): ?>
                        <tr>
                            <td><?= $i + 1 ?></td>
                            <td><?= htmlspecialchars($item['name'] ?? '-') ?></td>
                            <td class="text-right"><?= number_format((float)($item['quantity'] ?? 0), 2, ',', '.') ?></td>
                            <td><?= htmlspecialchars($item['unitName'] ?? '') ?></td>
                            <td class="text-right"><?= number_format((float)($item['unitPrice']['netAmount'] ?? 0), 2, ',', '.') ?> EUR</td>
                            <td class="text-right">
                                <?= ($item['discountPercentage'] ?? 0) > 0 ? number_format((float)$item['discountPercentage'], 1, ',', '.') . '%' : '-' ?>
                            </td>
                            <td class="text-right"><?= number_format((float)($item['lineItemAmount'] ?? 0), 2, ',', '.') ?> EUR</td>
                        </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            </section>
            <?php endif; ?>

            <!-- Summen-Block -->
            <section class="invoice-totals">
                <h2>Summen</h2>
                <dl class="totals-list">
                    <dt>Netto</dt>
                    <dd><?= number_format((float)($invoice['total_net_amount'] ?? 0), 2, ',', '.') ?> EUR</dd>
                    <dt>MwSt</dt>
                    <dd><?= number_format((float)($invoice['total_tax_amount'] ?? 0), 2, ',', '.') ?> EUR</dd>
                    <dt class="total-gross">Brutto</dt>
                    <dd class="total-gross"><?= number_format((float)($invoice['total_gross_amount'] ?? 0), 2, ',', '.') ?> EUR</dd>
                </dl>
            </section>

            <!-- Actions -->
            <section class="invoice-actions">
                <button type="button" class="btn btn-secondary" disabled>PDF herunterladen</button>
                <a href="/admin/rechnungen/<?= htmlspecialchars($account) ?>" class="btn btn-outline">
                    Zurueck zur Liste
                </a>
            </section>
        </article>
    </main>
<?php require dirname(__DIR__, 3) . '/templates/layout/footer.php'; ?>

Lexoffice-Datenstruktur

Die line_items werden als JSON-Array in der DB gespeichert (Lexoffice API-Format):

{
  "name": "Beratung KI-Strategie",
  "quantity": 8.0,
  "unitName": "Stunden",
  "unitPrice": { "netAmount": 250.00 },
  "discountPercentage": 0,
  "lineItemAmount": 2000.00
}

Original-Code: Container.php (DI-Erweiterung)

Der Dependency Injection Container wurde um die Factory-Methode getLexofficeInvoiceRepository() erweitert. Sie erstellt das Repository mit der korrekten Datenbankverbindung fuer den jeweiligen Account.

Container-Prinzip

  • Singleton: Eine Container-Instanz pro Request
  • Lazy Loading: Services werden erst bei Bedarf erstellt
  • Caching: Einmal erstellte Instanzen werden wiederverwendet
  • Account-Isolation: Jeder Lexoffice-Account erhaelt eigene Repository-Instanz

src/Container.php (Auszug: Zeilen 141-149)

<?php

declare(strict_types: 1);

namespace Schnuedeldue;

use PDO;
use Schnuedeldue\Container\RepositoryFactoryTrait;
use Schnuedeldue\Container\ServiceFactoryTrait;
use Schnuedeldue\Repositories\Lexoffice\InvoiceRepository;

/**
 * Simple DI Container
 *
 * Manages service instantiation and dependency injection.
 * Implements lazy loading with singleton pattern.
 */
class Container
{
    use RepositoryFactoryTrait;
    use ServiceFactoryTrait;

    private static ?Container $instance = null;
    private array $services = [];

    private function __construct() {}

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    // ... andere Methoden (getPdo, createConnection, resolve) ...

    /**
     * Get Lexoffice Invoice Repository for specific account
     *
     * @param string $account 'karlscore' or '369wohlbefinden'
     * @return InvoiceRepository
     */
    public function getLexofficeInvoiceRepository(string $account): InvoiceRepository
    {
        // Cache-Key pro Account, damit beide Repos koexistieren koennen
        $key = "lexoffice_invoice_repo_{$account}";

        return $this->resolve($key, function () use ($account) {
            // Connection-Name aus Repository holen (Kapselt Account-Mapping)
            $connName = InvoiceRepository::getConnectionName($account);

            // Readonly-Connection erstellen (Sicherheit)
            $pdo = $this->createConnection($connName, true);

            // Repository mit PDO und Account instanziieren
            return new InvoiceRepository($pdo, $account);
        });
    }
}

Schluesselkonzepte

Konzept Implementierung
Account-spezifischer Cache-Key lexoffice_invoice_repo_{$account}
Lazy Closure Factory wird erst bei erstem Zugriff ausgefuehrt
Connection-Delegation Repository kennt Account→DB-Mapping
Readonly-Flag createConnection($name, true) setzt READ ONLY

databases.json

Die Verbindungsdaten sind in config/databases.json definiert:

{
  "connections": {
    "lexoffice_karlscore": {
      "host": "localhost",
      "database": "schnuedeldue_lexoffice_karlscore",
      "charset": "utf8mb4"
    },
    "lexoffice_369wohlbefinden": {
      "host": "localhost",
      "database": "schnuedeldue_lexoffice_369wohlbefinden",
      "charset": "utf8mb4"
    }
  }
}

Das Ergebnis: Messbare Code-Qualität

Was passiert, wenn der KI-Code-Mensch-Prozess konsequent angewendet wird? Der Code, der in ~8 Minuten autonom erstellt wurde, durchläuft dieselben Qualitätsprüfungen wie jeder andere Code im System.

code-quality-check --full-analysis /freund/lexoffice-integration

● Code Quality Analyse: Lexoffice Rechnungen Integration

  Projekt-Übersicht

  ┌────────────────────────────┬─────────────────┐
  │ Metrik                     │ Wert            │
  ├────────────────────────────┼─────────────────┤
  │ Codezeilen gesamt          │ 833             │
  │ Durchschnittliche Qualität │ 98.5%           │
  │ Security Score             │ 100%            │
  │ Implementationszeit        │ ~8 min          │
  └────────────────────────────┴─────────────────┘

  ┌────────────────────────────────────────┬────────┬────────┬───────────┐
  │ Datei                                  │ LoC    │ Score  │ Security  │
  ├────────────────────────────────────────┼────────┼────────┼───────────┤
  │ InvoiceRepository.php                  │ 157    │ 92.2%  │ 100%      │
  │ list.php (HTMX)                        │ 199    │ 99.4%  │ 100%      │
  │ karlscore.php                          │ 68     │ 100%   │ 100%      │
  │ 369wohlbefinden.php                    │ 68     │ 100%   │ 100%      │
  │ detail.php                             │ 182    │ 99.1%  │ 100%      │
  │ Container.php (erweitert)              │ 159    │ 100%   │ 100%      │
  └────────────────────────────────────────┴────────┴────────┴───────────┘

  ═══════════════════════════════════════════════════════════════════════

● PHP Analysis (833 LoC)

  Contract-Verstöße

  CRITICAL (0)

    ✓ CQ-SQL-001   Prepared Statements überall
    ✓ CQ-DI-001    Repository via Container (DI)
    ✓ PHP-SEC-001  Keine SQL-Injection möglich
    ✓ PHP-SEC-002  XSS-Escaping via htmlspecialchars()

  MAJOR (0)

    ✓ CQ-AUTH-001  Auth-Check in allen Endpunkten
    ✓ CQ-VAL-001   Input-Validierung (Account-Whitelist, UUID-Pattern)
    ✓ PHP-ERR-001  Error-Logging im Catch-Block

  MINOR (3)

  ┌─────────────┬─────────────────────────────┬──────────────────────────────┐
  │ Rule        │ Datei                       │ Problem                      │
  ├─────────────┼─────────────────────────────┼──────────────────────────────┤
  │ PHP-KISS-01 │ list.php:153                │ 6 Parameter (max: 5)         │
  │ PHP-KISS-01 │ list.php:194                │ 6 Parameter (max: 5)         │
  │ PHP-DRY-001 │ InvoiceRepository.php:65    │ Filter-Logik dupliziert      │
  └─────────────┴─────────────────────────────┴──────────────────────────────┘

  Code Quality Principles

  ┌────────────┬────────┬─────────────────────────────────────────────────┐
  │ Prinzip    │ Score  │ Details                                         │
  ├────────────┼────────┼─────────────────────────────────────────────────┤
  │ DRY        │ 93.5%  │ Filter-Logik in list() und count() ähnlich      │
  │ KISS       │ 100%   │ Avg Complexity: 4.3, Max: 6 (list)              │
  │ SRP        │ 100%   │ Repository = Data, Views = Presentation         │
  │ OOP        │ 100%   │ InvoiceRepository kapselt DB-Zugriff            │
  └────────────┴────────┴─────────────────────────────────────────────────┘

  PSR Compliance

  ┌──────────┬─────────┬───────────────────────────────────────────────────┐
  │ Standard │ Score   │ Beschreibung                                      │
  ├──────────┼─────────┼───────────────────────────────────────────────────┤
  │ PSR-1    │ 100%    │ Basic Coding Standard                             │
  │ PSR-4    │ 100%    │ Autoloading (Namespace = Pfad)                    │
  │ PSR-12   │ 96.2%   │ 15 Zeilen > 120 Zeichen (HTMX-URLs)               │
  └──────────┴─────────┴───────────────────────────────────────────────────┘

  ═══════════════════════════════════════════════════════════════════════

● Security Analysis

  ┌──────────────────────┬─────────┬────────────────────────────────────────┐
  │ Check                │ Status  │ Details                                │
  ├──────────────────────┼─────────┼────────────────────────────────────────┤
  │ SQL Injection        │ PASS    │ Prepared Statements, bindValue()       │
  │ XSS                  │ PASS    │ htmlspecialchars(ENT_QUOTES, UTF-8)    │
  │ Auth Bypass          │ PASS    │ Session-Check + Local-IP-Bypass        │
  │ Input Validation     │ PASS    │ Account-Whitelist, UUID-Regex          │
  │ Path Traversal       │ PASS    │ Keine dynamischen Includes             │
  │ Sensitive Data       │ PASS    │ Credentials in config/databases.json   │
  └──────────────────────┴─────────┴────────────────────────────────────────┘

  Overall Security Level: 100% ✓

  ═══════════════════════════════════════════════════════════════════════

● Architecture & Design Patterns

  ┌────────────────────────┬─────────┬──────────────────────────────────────┐
  │ Pattern/Regel          │ Status  │ Details                              │
  ├────────────────────────┼─────────┼──────────────────────────────────────┤
  │ Dependency Injection   │ PASS    │ Container->getLexofficeInvoiceRepo() │
  │ Repository Pattern     │ PASS    │ InvoiceRepository kapselt DB-Zugriff │
  │ Whitelist Validation   │ PASS    │ ALLOWED_SORTS, getAllowedAccounts()  │
  │ Mobile-First CSS       │ PASS    │ Responsive Tabelle, data-label       │
  │ HTMX Fragments         │ PASS    │ list.php rendert nur HTML-Fragment   │
  │ No Page Reload         │ PASS    │ Sort/Filter/Pagination via HTMX      │
  └────────────────────────┴─────────┴──────────────────────────────────────┘

  ═══════════════════════════════════════════════════════════════════════

● Analyse abgeschlossen

  Gesamtergebnis:         98.5% Code-Qualität
  Kritische Issues:       0
  Major Issues:           0
  Minor Issues:           3 (Parameter-Count, DRY)

  Analysedauer:           0.8s für 833 LoC (6 Dateien)

  ✓ Alle Gates passiert
  ✓ Bereit für Deployment

Was das bedeutet

833 Zeilen funktionsfähiger, sicherer Code: autonom generiert in ~8 Minuten. Kein SQL-Injection-Risiko, kein XSS, saubere Architektur mit Repository-Pattern und Dependency Injection. Die 3 Minor Issues (zu viele Parameter in Hilfsfunktionen, duplizierte Filter-Logik) sind dokumentiert und könnten in einem Refactoring-Pass behoben werden.

Was können wir alles Tolles daraus lernen?

Diese Session demonstriert mehrere Aspekte der KI-Mensch-Zusammenarbeit im Software-Engineering. Schauen wir uns die wichtigsten Learnings an:

Prompt-Engineering macht den Unterschied

Was ist das? Prompt-Engineering ist die Kunst, einer KI so präzise zu sagen, was man will, dass sie es auch wirklich versteht. Klingt trivial, ist es aber nicht.

Warum ist das wichtig? Ein vager Prompt führt zu vagen Ergebnissen. Die KI „rät" dann, was Du gemeint haben könntest und das geht meistens schief. Ein präziser Prompt hingegen liefert präzise Ergebnisse.

Was lernen wir daraus?

  • Strukturierte Prompts mit expliziten Referenzen führen zu besseren Ergebnissen: Wir haben gesehen, wie aus einem 20-Zeilen-Prompt (v1.0) ein 400-Zeilen-Prompt (v3.0) wurde. Jede zusätzliche Zeile war eine potenzielle Fehlerquelle weniger. Konkrete Beispiele, Schema-Definitionen und Referenz-Code eliminieren Interpretationsspielraum.
  • Akzeptanzkriterien ermöglichen objektive Validierung: Statt „mach eine schöne Tabelle" haben wir 22 messbare Kriterien definiert. Das ist wie ein Vertrag: Beide Seiten wissen genau, was „fertig" bedeutet. Keine Diskussionen, keine Nachbesserungen.
  • Kontext aus existierendem Code reduziert Halluzinationen: Wenn die KI echten Code aus dem Projekt als Referenz bekommt, erfindet sie keine Fantasy-APIs. Sie orientiert sich an dem, was funktioniert. Das ist wie ein Praktikant, der erstmal zuschaut, bevor er loslegt.

Autonome Fehlerkorrektur funktioniert

Was ist das? Die Fähigkeit einer KI, eigene Fehler zu erkennen und zu korrigieren ohne dass ein Mensch eingreifen muss.

Warum ist das wichtig? Wenn Du bei jedem Fehler manuell eingreifen musst, bist Du der Flaschenhals. Die KI kann nur so schnell arbeiten, wie Du korrigierst. Autonome Fehlerkorrektur entfesselt das volle Potenzial.

Was lernen wir daraus?

  • Die externe KI erkannte und korrigierte Fehler selbstständig: Als ein Pfad nicht stimmte, hat die KI das Problem analysiert, die Ursache gefunden und den Fix implementiert. Alles ohne menschliches Zutun.
  • Keine menschliche Intervention nötig für technische Korrekturen: Das spart nicht nur Zeit, sondern auch Nerven. Du musst nicht jeden Fehler verstehen, um ihn beheben zu lassen.
  • Supervisor-Rolle: Beobachten, nicht eingreifen: Der Mensch wechselt von „Ich mache alles selbst" zu „Ich beobachte und greife nur ein, wenn nötig". Das ist ein fundamentaler Paradigmenwechsel.

Das Gate-System als Qualitätsnetz

Was ist das? Ein System aus Contracts, Regeln und Hooks, das automatisch die Code-Qualität überwacht und bei Bedarf eingreift.

Warum ist das wichtig? Menschen machen Fehler. KIs machen Fehler. Ein automatisches Qualitätsnetz fängt diese Fehler ab, bevor sie Schaden anrichten.

Was lernen wir daraus?

  • Automatische Permission-Fixes verhindern Deployment-Probleme: Wie oft ist ein Deploy gescheitert, weil jemand die Dateiberechtigungen vergessen hat? Mit dem Gate-System: nie mehr.
  • Code-Analyse baut kontinuierlich Knowledge Base auf: Jede neue Datei wird analysiert und in die Wissensbasis aufgenommen. Das System wird mit jeder Zeile Code schlauer.
  • Kein Hook blockierte: Das ist das schönste Ergebnis. Die Implementation entsprach von Anfang an allen Constraints. Das Supervision-Feedback hatte seinen Job getan.

Effizienz: 30 Minuten für ein komplettes Feature

Was bedeutet das? In der Zeit, in der man sonst vielleicht die Requirements diskutiert hätte, war das Feature fertig, getestet und dokumentiert.

Phase Dauer Was passierte
Prompt-Entwicklung ~10 min 3 Iterationen bis zur vollständigen Spezifikation
Task-Erstellung ~2 min 11 Sub-Tasks automatisch generiert
Implementation (extern) ~8 min 6 PHP-Dateien, 1 CSS-Datei, Routing
Akzeptanztests ~3 min 22 Kriterien automatisch validiert
Dokumentation ~7 min Diese Seite hier
Gesamt ~30 min Produktionsreifer Code

Und jetzt Du!

Wenn Du bis hierhin gelesen hast: Respekt! Das war ein langer Artikel mit viel technischem Detail. Aber genau das macht den Unterschied zwischen „KI-Spielerei" und „KI-Produktivität".

Die KI-Gemeinschaft

Dieses Wissen ist Teil dessen, was wir in der KI-Gemeinschaft vermitteln und gemeinsam weiterentwickeln.

Was erwartet Dich dort?

  • Monatliche Live-Seminare zu KI-Themen
  • Zugang zu allen Materialien und Systemen
  • Direkte Begleitung bei Deinen KI-Projekten
  • Eine Community von Gleichgesinnten

Die KI-Gemeinschaft ist für Menschen, die KI nicht nur nutzen, sondern verstehen und meistern wollen.

Mehr zur KI-Gemeinschaft erfahren

Schlusswort

KI-Systeme wie Claude Code sind keine Magie. Sie sind Werkzeuge. Mächtige Werkzeuge, ja, aber eben Werkzeuge. Der Unterschied zwischen „Das funktioniert irgendwie" und „Das funktioniert präzise und reproduzierbar" liegt in der Methodik.

Diese Methodik kann man lernen. Diesen Artikel zu lesen war ein erster Schritt. Der nächste Schritt: Selber machen. Und wenn Du dabei Unterstützung möchtest: Du weißt ja jetzt, wo Du sie findest.

Liebe Grüße, Karl