Fix #55: Verbindungsfehler-Fallback – kein Retry-Loop, spezifische Fehlermeldung

This commit is contained in:
FDS 2026-06-08 21:20:33 +02:00
parent 54f7d9bd38
commit 1a23b3e11d
4 changed files with 78 additions and 2 deletions

View file

@ -8,6 +8,8 @@
- **`LlmService`:** `probeOllama()` schreibt bei cURL-Fehlern jetzt einen Eintrag ins PHP-Fehlerlog (`docker compose logs web`) - **`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 - **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()` - **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 ## 2026-06-01 – Fix: EN16931-Validierungsfehler in ZUGFeRD-Rechnungen

View file

@ -378,8 +378,17 @@ class BelegController extends BaseController
$svc = new KontierungsvorschlagService(); $svc = new KontierungsvorschlagService();
$vorschlag = $svc->generateForBeleg($beleg); $vorschlag = $svc->generateForBeleg($beleg);
Beleg::saveKiVorschlag($id, $vorschlag); // [] = "verarbeitet, kein Ergebnis"; null würde endlose Auto-Retry-Schleife auslösen
echo json_encode($vorschlag ?? ['error' => 'Kein Vorschlag generiert']); 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 public function resetVorschlag(array $params): void

View file

@ -1101,6 +1101,16 @@ function _showKiBadgeError() {
'<button class="btn btn-secondary btn-sm" onclick="_startKiAnalyse()">Erneut versuchen</button></div>'; '<button class="btn btn-secondary btn-sm" onclick="_startKiAnalyse()">Erneut versuchen</button></div>';
} }
function _showKiBadgeConnectionError() {
const neuBtn = _kiAktiv
? ' <button class="btn btn-secondary btn-sm" onclick="_startKiAnalyse()">Erneut versuchen</button>'
: '';
document.getElementById('ki-badge').innerHTML =
'<div class="flash flash-warning">Ollama nicht erreichbar.' + neuBtn +
' <a href="/settings" style="color:inherit;text-decoration:underline">Einstellungen / Verbindung testen</a>' +
'</div>';
}
// Normalisiert _kiVorschlag zu einem Array (Objekt oder Array aus DB möglich). // Normalisiert _kiVorschlag zu einem Array (Objekt oder Array aus DB möglich).
// Gibt null zurück wenn kein Ergebnis vorhanden. // Gibt null zurück wenn kein Ergebnis vorhanden.
// Gibt leeres Array [] zurück wenn verarbeitet aber kein Ergebnis erzeugt. // Gibt leeres Array [] zurück wenn verarbeitet aber kein Ergebnis erzeugt.
@ -1119,6 +1129,7 @@ async function _startKiAnalyse() {
try { try {
const r = await fetch('/belege/' + _belegId + '/vorschlag/generate', {method: 'POST'}); const r = await fetch('/belege/' + _belegId + '/vorschlag/generate', {method: 'POST'});
const raw = await r.json(); const raw = await r.json();
if (raw && raw.connection_error) { _showKiBadgeConnectionError(); return; }
const arr = _normalizeKiArr(raw); const arr = _normalizeKiArr(raw);
if (arr && arr.length > 0) { _applyKiVorschlag(arr[0]); _showKiBadgeDone(arr); } if (arr && arr.length > 0) { _applyKiVorschlag(arr[0]); _showKiBadgeDone(arr); }
else _showKiBadgeNoResult(); else _showKiBadgeNoResult();

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
use Steuerkram\Database;
use Steuerkram\Models\Beleg;
class KiConnectionFallbackTest extends TestCase
{
protected string $testName = 'Test 43: KI-Verbindungsfehler-Fallback';
public function run(): void
{
$belegId = $this->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();
}
}