diff --git a/Changes.md b/Changes.md index 59dc7a0..c1bd8ca 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,13 @@ --- +## 2026-09-20 – Überwachung des Nextcloud-Imports (#208) + +- **`migrations/096_nextcloud_watchdog.sql`** (neu): `company_settings.nextcloud_last_run` (Zeitstempel des letzten Laufs) und `nextcloud_alarm_stunden` (Schwelle, Default 24, 0 = aus). +- **`src/Services/NextcloudWatchdog.php`** (neu): Totmannschalter. `NextcloudImportService::import()` setzt nach erfolgreichem PROPFIND den Zeitstempel – auch bei stillen Läufen ohne Ereignis. Ist er älter als die Schwelle (oder nie gesetzt, bei konfigurierter Nextcloud), erscheint eine Warnung oben auf `/ereignisse` und ein ⚠ am Nav-Eintrag „Ereignisse". +- **Einstellungen:** Feld „Import-Überwachung" im Nextcloud-Abschnitt. +- **`tests/101_NextcloudWatchdogTest.php`** (neu): Schwellenlogik gegen künstlich alte Zeitstempel. + ## 2026-09-20 – Gezahlte Vorsteuer als Betriebsausgabe in der EÜR (EÜR-Zeile 57 / Kz 185) - **`migrations/095_vorsteuerkonten_betriebsausgabe.sql`** (neu): Die Konten 1571/1574/1576 (abziehbare Vorsteuer) lagen als `Zahlungskonto` im Kontenrahmen und wurden vom `EuerCalculator` daher ignoriert – die auf ihnen gebuchte Vorsteuer tauchte in der EÜR nirgends auf. Da die vereinnahmte USt seit Migration 076 als Betriebseinnahme (Zeile 17) und die ans Finanzamt gezahlte USt seit Migration 077 als Betriebsausgabe (Zeile 58) geführt werden, war die Bruttomethode unvollständig und der ausgewiesene Gewinn um die gezahlte Vorsteuer zu hoch. Alle drei Konten sind jetzt `Betriebsausgabe` mit `euer_zeile = '57'`. Auch 1574 (Vorsteuer aus ig. Erwerb) muss mit: sein Gegenkonto 1774 ist seit Migration 076 Betriebseinnahme, bei Reverse-Charge zählte also nur die Erwerbsteuer als Einnahme – die Buchung war entgegen der Absicht nicht ergebnisneutral. diff --git a/docs/funktionen.md b/docs/funktionen.md index 323ccc7..6b0e39f 100644 --- a/docs/funktionen.md +++ b/docs/funktionen.md @@ -14,7 +14,7 @@ Vollständige Kontenliste: [kontenrahmen.md](kontenrahmen.md) - SHA-256 Integritätsprüfung je Datei - Automatischer Weißrand-Zuschnitt bei JPEG/PNG via GD - KI-gestützter Kontierungsvorschlag (optional, siehe [konfiguration.md](konfiguration.md)) -- Nextcloud-Import via WebDAV (optional) +- Nextcloud-Import via WebDAV (optional); Überwachung: Warnung in Ereignisse/Nav, wenn kein Lauf mehr stattfand (Schwelle einstellbar, Standard 24 Std.) - Belege können nicht gelöscht werden, solange Buchungen darauf verweisen (GoBD) --- diff --git a/migrations/096_nextcloud_watchdog.sql b/migrations/096_nextcloud_watchdog.sql new file mode 100644 index 0000000..d6b5f44 --- /dev/null +++ b/migrations/096_nextcloud_watchdog.sql @@ -0,0 +1,6 @@ +-- Totmannschalter für den Nextcloud-Import (Issue #208). +-- nextcloud_last_run: Zeitstempel des letzten Importlaufs (auch ohne Dateiaktivität) +-- nextcloud_alarm_stunden: Schwelle in Stunden, 0 = Überwachung aus +ALTER TABLE company_settings + ADD COLUMN IF NOT EXISTS nextcloud_last_run DATETIME NULL, + ADD COLUMN IF NOT EXISTS nextcloud_alarm_stunden SMALLINT UNSIGNED NOT NULL DEFAULT 24; diff --git a/src/Controllers/EventController.php b/src/Controllers/EventController.php index 08eadbc..385368f 100644 --- a/src/Controllers/EventController.php +++ b/src/Controllers/EventController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Steuerkram\Controllers; use Steuerkram\Models\EventLog; +use Steuerkram\Services\NextcloudWatchdog; use Steuerkram\View; class EventController extends BaseController @@ -23,6 +24,7 @@ class EventController extends BaseController 'events' => $events, 'filter' => $filter, 'channels' => EventLog::channels(), + 'watchdog' => NextcloudWatchdog::warning(), 'eventsHtml' => $this->renderView('ereignisse/_body', [ 'events' => $events, ]), diff --git a/src/Models/Setting.php b/src/Models/Setting.php index 1d24851..30e6cde 100644 --- a/src/Models/Setting.php +++ b/src/Models/Setting.php @@ -42,7 +42,7 @@ class Setting invoice_nr_prefix=?, beleg_nr_prefix=?, anlage_nr_prefix=?, customer_nr_prefix=?, payment_days_default=?, test_mode=?, ki_vorschlag_aktiv=?, nextcloud_url=?, nextcloud_user=?, nextcloud_pass=?, - nextcloud_folder=?, nextcloud_done=?, + nextcloud_folder=?, nextcloud_done=?, nextcloud_alarm_stunden=?, mail_smtp_host=?, mail_smtp_port=?, mail_smtp_encryption=?, mail_smtp_user=?, mail_smtp_pass=?, mail_from_name=?, mail_from_email=?, @@ -80,6 +80,7 @@ class Setting !empty($data['nextcloud_pass']) ? Crypto::encrypt($data['nextcloud_pass']) : null, ($data['nextcloud_folder'] ?? '') ?: '/Belege/Import', ($data['nextcloud_done'] ?? '') ?: null, + max(0, min(8760, (int)($data['nextcloud_alarm_stunden'] ?? 24))), ($data['mail_smtp_host'] ?? '') ?: '', (int)($data['mail_smtp_port'] ?? 587), in_array($data['mail_smtp_encryption'] ?? '', ['tls', 'ssl', 'none']) ? $data['mail_smtp_encryption'] : 'tls', diff --git a/src/Services/NextcloudImportService.php b/src/Services/NextcloudImportService.php index 68166ac..02cc8b1 100644 --- a/src/Services/NextcloudImportService.php +++ b/src/Services/NextcloudImportService.php @@ -115,6 +115,8 @@ class NextcloudImportService return $result; } + NextcloudWatchdog::markRun(); + $result['details'][] = count($files) . ' Datei(en) im Import-Ordner gefunden.'; foreach ($files as $file) { diff --git a/src/Services/NextcloudWatchdog.php b/src/Services/NextcloudWatchdog.php new file mode 100644 index 0000000..597622d --- /dev/null +++ b/src/Services/NextcloudWatchdog.php @@ -0,0 +1,72 @@ + + +
docker compose exec web php scripts/container/nextcloud-import.php
diff --git a/tests/101_NextcloudWatchdogTest.php b/tests/101_NextcloudWatchdogTest.php
new file mode 100644
index 0000000..323ea9b
--- /dev/null
+++ b/tests/101_NextcloudWatchdogTest.php
@@ -0,0 +1,51 @@
+assert(
+ NextcloudWatchdog::evaluate($url, '2026-09-20 11:55:00', 24, $now) === null,
+ 'Frischer Lauf: keine Warnung'
+ );
+ $this->assert(
+ NextcloudWatchdog::evaluate($url, '2026-09-19 12:00:00', 24, $now) === null,
+ 'Genau an der Schwelle: keine Warnung'
+ );
+ $alt = NextcloudWatchdog::evaluate($url, '2026-08-30 12:00:00', 24, $now);
+ $this->assertNotEmpty($alt, 'Drei Wochen alter Lauf: Warnung');
+ $this->assert(str_contains((string)$alt, '30.08.2026'), 'Warnung nennt den letzten Lauf');
+ $this->assert(
+ NextcloudWatchdog::evaluate($url, '2026-08-30 12:00:00', 0, $now) === null,
+ 'Schwelle 0 schaltet die Überwachung ab'
+ );
+ $this->assert(
+ NextcloudWatchdog::evaluate('', null, 24, $now) === null,
+ 'Ohne konfigurierte Nextcloud keine Warnung'
+ );
+ $this->assertNotEmpty(
+ NextcloudWatchdog::evaluate($url, null, 24, $now),
+ 'Konfiguriert, aber nie gelaufen: Warnung'
+ );
+
+ // Zeitstempel in der Datenbank
+ \Steuerkram\Database::execute("UPDATE company_settings SET nextcloud_url = ?, nextcloud_alarm_stunden = 24, nextcloud_last_run = NULL WHERE id = 1", [$url]);
+ $this->assertNotEmpty(NextcloudWatchdog::warning(), 'DB: ohne Zeitstempel Warnung');
+ NextcloudWatchdog::markRun();
+ $this->assert(NextcloudWatchdog::warning() === null, 'DB: nach markRun() keine Warnung');
+ \Steuerkram\Database::execute("UPDATE company_settings SET nextcloud_last_run = NOW() - INTERVAL 3 DAY WHERE id = 1");
+ $this->assertNotEmpty(NextcloudWatchdog::warning(), 'DB: 3 Tage alter Zeitstempel löst Warnung aus');
+ }
+}