diff --git a/Changes.md b/Changes.md
index 7de518d..9b34645 100644
--- a/Changes.md
+++ b/Changes.md
@@ -8,6 +8,8 @@
- **`LlmService`:** `probeOllama()` schreibt bei cURL-Fehlern jetzt einen Eintrag ins PHP-Fehlerlog (`docker compose logs web`)
- **Einstellungsseite:** Neuer Button „Verbindung testen" im KI-Abschnitt; zeigt bei Erfolg verfügbare Modelle, bei Fehler cURL-Fehler, URL und Firewall-Hinweis an
- **Route:** `POST /settings/llm-test` → `SettingsController::llmTest()`
+- **`BelegController`:** Bei Verbindungsfehler wird jetzt `[]` statt `null` gespeichert (verhindert endlose Auto-Retry-Schleife); Fehlermeldung differenziert zwischen „Ollama nicht erreichbar" und „kein Vorschlag generiert"
+- **Beleg-Detailseite:** Zeigt bei Verbindungsfehler spezifische Meldung „Ollama nicht erreichbar" mit Link zu den Einstellungen statt der generischen Warnung
## 2026-06-01 – Fix: EN16931-Validierungsfehler in ZUGFeRD-Rechnungen
diff --git a/src/Controllers/BelegController.php b/src/Controllers/BelegController.php
index b89cf76..4d145f9 100644
--- a/src/Controllers/BelegController.php
+++ b/src/Controllers/BelegController.php
@@ -378,8 +378,17 @@ class BelegController extends BaseController
$svc = new KontierungsvorschlagService();
$vorschlag = $svc->generateForBeleg($beleg);
- Beleg::saveKiVorschlag($id, $vorschlag);
- echo json_encode($vorschlag ?? ['error' => 'Kein Vorschlag generiert']);
+ // [] = "verarbeitet, kein Ergebnis"; null würde endlose Auto-Retry-Schleife auslösen
+ Beleg::saveKiVorschlag($id, $vorschlag ?? []);
+ if ($vorschlag === null) {
+ $llm = new \Steuerkram\Services\LlmService();
+ echo json_encode($llm->isReachable()
+ ? ['error' => 'Kein Vorschlag generiert']
+ : ['connection_error' => true]
+ );
+ return;
+ }
+ echo json_encode($vorschlag);
}
public function resetVorschlag(array $params): void
diff --git a/src/Views/belege/detail.php b/src/Views/belege/detail.php
index d05ea6c..ef5bb6e 100644
--- a/src/Views/belege/detail.php
+++ b/src/Views/belege/detail.php
@@ -1101,6 +1101,16 @@ function _showKiBadgeError() {
'';
}
+function _showKiBadgeConnectionError() {
+ const neuBtn = _kiAktiv
+ ? ' '
+ : '';
+ document.getElementById('ki-badge').innerHTML =
+ '
';
+}
+
// Normalisiert _kiVorschlag zu einem Array (Objekt oder Array aus DB möglich).
// Gibt null zurück wenn kein Ergebnis vorhanden.
// Gibt leeres Array [] zurück wenn verarbeitet aber kein Ergebnis erzeugt.
@@ -1119,6 +1129,7 @@ async function _startKiAnalyse() {
try {
const r = await fetch('/belege/' + _belegId + '/vorschlag/generate', {method: 'POST'});
const raw = await r.json();
+ if (raw && raw.connection_error) { _showKiBadgeConnectionError(); return; }
const arr = _normalizeKiArr(raw);
if (arr && arr.length > 0) { _applyKiVorschlag(arr[0]); _showKiBadgeDone(arr); }
else _showKiBadgeNoResult();
diff --git a/tests/43_KiConnectionFallbackTest.php b/tests/43_KiConnectionFallbackTest.php
new file mode 100644
index 0000000..b240532
--- /dev/null
+++ b/tests/43_KiConnectionFallbackTest.php
@@ -0,0 +1,54 @@
+createTestBeleg();
+
+ // [] = "verarbeitet, kein Ergebnis" – verhindert endlose Auto-Retry-Schleife
+ Beleg::saveKiVorschlag($belegId, []);
+ $beleg = Beleg::find($belegId);
+ $this->assertNotEmpty($beleg['ki_vorschlag'], 'Leeres Array muss als JSON gespeichert werden, nicht als SQL NULL');
+
+ $decoded = json_decode($beleg['ki_vorschlag'], true);
+ $this->assert(is_array($decoded) && count($decoded) === 0, 'Gespeicherter Wert muss leeres Array sein');
+
+ // null = "noch nicht verarbeitet" – löst Auto-Retry auf der Beleg-Seite aus
+ Beleg::saveKiVorschlag($belegId, null);
+ $beleg = Beleg::find($belegId);
+ $this->assert($beleg['ki_vorschlag'] === null, 'saveKiVorschlag(null) muss SQL NULL ergeben');
+
+ // Nicht-leeres Array wird korrekt gespeichert und zurückgelesen
+ $vorschlag = [[
+ 'buchungstyp' => 'Ausgabe',
+ 'kategorie_konto_id' => 5,
+ 'zahlungskonto_id' => 2,
+ 'ust_satz' => 19,
+ 'betrag_netto' => 99.00,
+ 'buchungstext' => 'Fallback-Test',
+ ]];
+ Beleg::saveKiVorschlag($belegId, $vorschlag);
+ $beleg = Beleg::find($belegId);
+ $decoded = json_decode($beleg['ki_vorschlag'], true);
+ $this->assert(is_array($decoded) && count($decoded) === 1, 'Vorschlag-Array korrekt gespeichert');
+ $this->assertEquals('Ausgabe', $decoded[0]['buchungstyp'], 'buchungstyp korrekt');
+ }
+
+ private function createTestBeleg(): int
+ {
+ Database::execute(
+ "INSERT INTO belege (beleg_nr, filename, storage_path, mime_type, file_hash, belegtyp)
+ VALUES ('TEST-KI-FALLBACK-001', 'test.pdf', 'static/uploads/test/test.pdf', 'application/pdf',
+ SHA2('fallback-test', 256), 'Eingangsrechnung')"
+ );
+ return (int)Database::lastInsertId();
+ }
+}