From fb0baff3dfb2303849f950ec674cb85a54ea8ffa Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Thu, 1 Oct 2026 11:32:41 +0200 Subject: [PATCH] KPI "Hooks heute": nur die heute empfangenen Hooks zaehlen Co-Authored-By: Claude Sonnet 5.5 --- API.md | 8 ++--- api/docker/entrypoint.sh | 12 ------- api/src/Metrics/HookCounter.php | 40 ++++++++++++++-------- api/tests/Unit/Metrics/HookCounterTest.php | 24 +++++++++++++ docker-compose.yml | 4 +-- html/js/office-scene.js | 6 ++-- tests/frontend/office-scene.test.js | 2 +- 7 files changed, 59 insertions(+), 37 deletions(-) diff --git a/API.md b/API.md index e535b34..9f07378 100644 --- a/API.md +++ b/API.md @@ -646,11 +646,9 @@ Auth: keine (oeffentlich wie die anderen `GET`-Endpunkte). ``` `count` zaehlt jede angenommene Zustellung an `POST /api/v1/webhooks/codeberg` (auch -`ping`) **seit dem letzten Start des `api`-Containers** - nicht seit Projektbeginn: die -Zaehldatei wird bei jedem Start dieses Containers zurueckgesetzt (`docker/entrypoint.sh`, -`MONITOR_HOOKS_COUNTER_FILE`). Ein Neustart des `sync`-Containers alleine setzt den Zaehler -nicht zurueck, da nur der `api`-Container Webhooks empfaengt. Das Frontend zeigt den Wert als -eigene, immer sichtbare Kachel ganz rechts im KPI-Monitor (Issue #38). +`ping`) **am heutigen Tag** (Serverdatum); ab Mitternacht beginnt der Zaehler bei 0. Die +Zaehldatei (`MONITOR_HOOKS_COUNTER_FILE`) enthaelt Datum und Anzahl. Das Frontend zeigt den +Wert als eigene, immer sichtbare Kachel "Hooks heute" ganz rechts im KPI-Monitor (Issue #38). ## Fehlerformat diff --git a/api/docker/entrypoint.sh b/api/docker/entrypoint.sh index 1c1f9a1..27a6957 100755 --- a/api/docker/entrypoint.sh +++ b/api/docker/entrypoint.sh @@ -7,16 +7,4 @@ if [ ! -d /app/vendor ]; then composer install --no-interaction --no-progress --working-dir=/app fi -# Hooks-KPI (Issue #38): die Zaehldatei liegt im geteilten ./api-Volume und ueberlebt einen -# Container-Neustart daher von sich aus - "seit dem letzten Restart" muss also hier aktiv -# hergestellt werden. Nur beim Standard-Kommando des api-Containers (php -S ..., siehe -# Dockerfile CMD) zuruecksetzen: der sync-Container teilt sich zwar dasselbe Image/Entrypoint, -# startet aber mit einem anderen Kommando und empfaengt selbst keine Webhooks - ein eigener -# Neustart von ihm darf den Zaehler des api-Containers nicht faelschlich auf 0 setzen. -if [ "$1" = "php" ] && [ "$2" = "-S" ]; then - hooks_counter_file="${MONITOR_HOOKS_COUNTER_FILE:-/app/data/hooks-count}" - mkdir -p "$(dirname "$hooks_counter_file")" - echo 0 > "$hooks_counter_file" -fi - exec "$@" diff --git a/api/src/Metrics/HookCounter.php b/api/src/Metrics/HookCounter.php index b51ab69..6f7876c 100644 --- a/api/src/Metrics/HookCounter.php +++ b/api/src/Metrics/HookCounter.php @@ -5,22 +5,25 @@ declare(strict_types=1); namespace Monitor\Api\Metrics; /** - * Zaehlt eingehende Webhooks seit dem letzten Start des api-Containers (Issue #38, - * "Hooks"-KPI). Dateibasiert statt ein einfaches Objektfeld: der eingebaute PHP-Server - * (docker-compose.yml, PHP_CLI_SERVER_WORKERS) fuehrt jede Anfrage als eigene - * Skriptausfuehrung aus, ein Klassenzustand ueberlebt also keine zwei Anfragen - nur eine - * Datei tut das. "Seit dem letzten Restart" stellt nicht diese Klasse sicher, sondern - * docker/entrypoint.sh, das die Datei beim Start des api-Containers auf 0 zuruecksetzt (der - * sync-Container teilt sich zwar dasselbe Image, darf die Zaehldatei bei einem eigenen - * Neustart aber nicht anfassen, siehe Kommentar dort). + * Zaehlt die heute eingegangenen Webhooks (Issue #38, "Hooks heute"-KPI). Dateibasiert statt + * ein einfaches Objektfeld: der eingebaute PHP-Server (docker-compose.yml, + * PHP_CLI_SERVER_WORKERS) fuehrt jede Anfrage als eigene Skriptausfuehrung aus, ein + * Klassenzustand ueberlebt also keine zwei Anfragen - nur eine Datei tut das. Die Datei + * enthaelt " "; ein anderes Datum (neuer Tag, alter Dateiinhalt) zaehlt als 0, + * ein Reset beim Containerstart ist daher nicht noetig. */ final class HookCounter { - public function __construct(private readonly string $path) + /** @var \Closure(): string */ + private readonly \Closure $today; + + /** @param (callable(): string)|null $today liefert das aktuelle Datum (Y-m-d), fuer Tests ersetzbar */ + public function __construct(private readonly string $path, ?callable $today = null) { + $this->today = $today !== null ? \Closure::fromCallable($today) : static fn (): string => date('Y-m-d'); } - /** Erhoeht den Zaehler atomar (mehrere PHP_CLI_SERVER_WORKERS koennen gleichzeitig schreiben). */ + /** Erhoeht den heutigen Zaehler atomar (mehrere PHP_CLI_SERVER_WORKERS koennen gleichzeitig schreiben). */ public function increment(): int { $directory = dirname($this->path); @@ -34,11 +37,10 @@ final class HookCounter } flock($handle, LOCK_EX); - $current = (int) trim((string) stream_get_contents($handle)); - $next = $current + 1; + $next = $this->parse((string) stream_get_contents($handle)) + 1; ftruncate($handle, 0); rewind($handle); - fwrite($handle, (string) $next); + fwrite($handle, ($this->today)() . ' ' . $next); fflush($handle); flock($handle, LOCK_UN); fclose($handle); @@ -54,7 +56,7 @@ final class HookCounter $contents = @file_get_contents($this->path); - return $contents === false ? 0 : (int) trim($contents); + return $contents === false ? 0 : $this->parse($contents); } /** Fingerabdruck fuer den SSE-Endpoint (Realtime\ChangeFeed, F13). */ @@ -62,4 +64,14 @@ final class HookCounter { return (string) $this->count(); } + + /** Anzahl aus dem Dateiinhalt - 0, wenn sie nicht von heute stammt oder unlesbar ist. */ + private function parse(string $contents): int + { + if (preg_match('/^(\d{4}-\d{2}-\d{2}) (\d+)$/', trim($contents), $m) !== 1) { + return 0; + } + + return $m[1] === ($this->today)() ? (int) $m[2] : 0; + } } diff --git a/api/tests/Unit/Metrics/HookCounterTest.php b/api/tests/Unit/Metrics/HookCounterTest.php index afb6938..a578821 100644 --- a/api/tests/Unit/Metrics/HookCounterTest.php +++ b/api/tests/Unit/Metrics/HookCounterTest.php @@ -65,4 +65,28 @@ final class HookCounterTest extends TestCase $counter->increment(); self::assertSame('1', $counter->fingerprint()); } + + public function testCountResetsOnANewDay(): void + { + $day = '2026-10-01'; + $counter = new HookCounter($this->path, static function () use (&$day): string { + return $day; + }); + + $counter->increment(); + $counter->increment(); + self::assertSame(2, $counter->count()); + + $day = '2026-10-02'; + self::assertSame(0, $counter->count()); + self::assertSame(1, $counter->increment()); + } + + public function testLegacyFileContentCountsAsZero(): void + { + mkdir(dirname($this->path), 0o775, true); + file_put_contents($this->path, '12'); + + self::assertSame(0, (new HookCounter($this->path))->count()); + } } diff --git a/docker-compose.yml b/docker-compose.yml index 773b493..4ad79b0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,8 +40,8 @@ services: # jede Anfrage mit 401 ab - der Endpunkt ist aus dem Internet erreichbar und die # Signatur ist sein einziger Schutz. CODEBERG_WEBHOOK_SECRET: ${CODEBERG_WEBHOOK_SECRET:-} - # Zaehldatei fuer den Hooks-KPI (Issue #38, GET /api/v1/webhooks/stats): wird bei jedem - # Start dieses Containers von entrypoint.sh auf 0 zurueckgesetzt. + # Zaehldatei fuer den Hooks-KPI (Issue #38, GET /api/v1/webhooks/stats): speichert Datum + Anzahl der + # heute empfangenen Webhooks (ein neuer Tag beginnt bei 0). MONITOR_HOOKS_COUNTER_FILE: /app/data/hooks-count # Sprachaufnahmen (Issue #85): Ablageort der Rohaufnahmen aus record.html und # Obergrenze je Aufnahme. Die Groessengrenze muss zur Dockerfile-CMD passen diff --git a/html/js/office-scene.js b/html/js/office-scene.js index 5274f96..2d246a5 100644 --- a/html/js/office-scene.js +++ b/html/js/office-scene.js @@ -485,8 +485,8 @@ export function buildKpiTiles(kpis = []) { } /** - * KPI-foermiger Eintrag fuer den Hooks-KPI (Issue #38): Anzahl der seit dem letzten Start des - * api-Containers empfangenen Webhooks (GET /api/v1/webhooks/stats). Anders als + * KPI-foermiger Eintrag fuer den Hooks-KPI (Issue #38): Anzahl der heute + * empfangenen Webhooks (GET /api/v1/webhooks/stats). Anders als * buildAgentCountKpis() nicht aus bereits ohnehin geladenen Daten ableitbar, sondern ein * eigener serverseitiger Zaehler - daher eine eigene, injizierbare Funktion statt Teil davon. * Soll laut Issue #38 immer als letzte (rechte) Kachel im KPI-Monitor erscheinen, siehe @@ -497,7 +497,7 @@ export function buildKpiTiles(kpis = []) { * @returns {object} */ export function buildHooksKpi(count) { - return { key: "hooks_received", label: "Hooks", unit: "", current_value: count, history: [] }; + return { key: "hooks_received", label: "Hooks heute", unit: "", current_value: count, history: [] }; } /** diff --git a/tests/frontend/office-scene.test.js b/tests/frontend/office-scene.test.js index 8ee62c3..279768d 100644 --- a/tests/frontend/office-scene.test.js +++ b/tests/frontend/office-scene.test.js @@ -466,7 +466,7 @@ test("buildAgentCountKpis liefert 0/0 ohne Agenten", () => { test("buildHooksKpi liefert einen KPI-foermigen Eintrag mit der uebergebenen Anzahl (Issue #38)", () => { assert.deepEqual(buildHooksKpi(12), { key: "hooks_received", - label: "Hooks", + label: "Hooks heute", unit: "", current_value: 12, history: [],