From 0543a5a5421c5a7053925cabcd743e53c22721da Mon Sep 17 00:00:00 2001 From: nexxo Date: Fri, 31 Jul 2026 15:26:27 +0200 Subject: [PATCH] Let a failed measurement beat a green badge, and say which zone was missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three findings from the live readiness page, and the worst of them is the one that looks like nothing. Green "Erfüllt" sat directly above red "nicht in Ordnung", in the same row, twice — for the DNS token and for the VM template. The badge came from `satisfied` alone, the passive check that only establishes something IS configured, and the measurement was rendered beside it without being allowed to overrule it. Somebody scanning that list reads the badge, not the small print, and walks away with "all green" while a measurement said it does not work. R19 names this exact shape — a call that reads as an assurance and is not one — as worse than no check at all. The measurement wins now, for the badge, the icon and the reason line. zone_not_found was a dead end. It reads like "the token is wrong", so the operator replaces the token — but a wrong token never gets that far: it comes back as `rejected` from the 401 above. The token had just successfully listed the zones. What is missing is the ZONE. The check now returns the zones it did see, and the page puts them next to the one it wanted: looked for clupilot.cloud, this account holds clupilot.com. The question answers itself. An empty list says something else again, and gets its own sentence: the token belongs to a different Hetzner project. And two traffic tests were failing on main, unrelated to any of this, which is why they were checked against a clean checkout before being touched. They build "last month" as now()->subMonth()->format('Y-m'), and Carbon resolves that calendrically: on 31 July it lands on 1 July, so the row meant to be last period lands in the current one. Red on the 29th, 30th and 31st of every long month, and today is the 31st. The production code does not have the trap — currentPeriod() is now()->format('Y-m') with no arithmetic, and the two places that do compute months already guard it — so this is the tests, and only the tests. 2045 tests pass. Co-Authored-By: Claude Opus 5 --- app/Services/Dns/DnsTokenCheck.php | 18 +++++++- lang/de/readiness.php | 4 ++ lang/en/readiness.php | 4 ++ .../views/livewire/admin/readiness.blade.php | 45 ++++++++++++++++--- tests/Feature/Readiness/ActiveChecksTest.php | 42 +++++++++++++++++ tests/Feature/TrafficTest.php | 17 ++++++- 6 files changed, 121 insertions(+), 9 deletions(-) diff --git a/app/Services/Dns/DnsTokenCheck.php b/app/Services/Dns/DnsTokenCheck.php index 7d34bc6..834c084 100644 --- a/app/Services/Dns/DnsTokenCheck.php +++ b/app/Services/Dns/DnsTokenCheck.php @@ -47,10 +47,26 @@ final class DnsTokenCheck return ['ok' => false, 'reason' => 'rejected']; } + $verfuegbar = collect($zones->json('zones') ?? [])->pluck('name')->filter()->values(); $zoneId = collect($zones->json('zones') ?? [])->firstWhere('name', $zone)['id'] ?? null; if ($zoneId === null) { - return ['ok' => false, 'reason' => 'zone_not_found', 'zone' => $zone]; + // Die gefundenen Zonen gehören in die Antwort. + // + // `zone_not_found` allein ist eine Sackgasse: der Betreiber liest + // es als „der Token ist falsch" und tauscht ihn aus, obwohl der + // Token gerade eben erfolgreich die Zonenliste geholt hat — ein + // schlechter Token käme oben als `rejected` heraus. Was fehlt, ist + // die ZONE. Nebeneinander gestellt beantwortet sich die Frage von + // selbst: gesucht wurde `clupilot.cloud`, im Konto liegt + // `clupilot.com` — oder gar nichts, dann gehört die Zone dort + // erst angelegt. + return [ + 'ok' => false, + 'reason' => 'zone_not_found', + 'zone' => $zone, + 'available' => $verfuegbar->all(), + ]; } // The actual point of this whole check. A read-only token gets this diff --git a/lang/de/readiness.php b/lang/de/readiness.php index 07527ae..80cda00 100644 --- a/lang/de/readiness.php +++ b/lang/de/readiness.php @@ -114,4 +114,8 @@ return [ 'queue_provisioning' => 'Bereitstellungs-Worker läuft', 'queue_provisioning_breaks' => 'Der Zeitplaner kann laufen und Aufträge auf die provisioning-Warteschlange stellen, ohne dass sie je jemand abholt. Eine bezahlte Bestellung bleibt dann liegen — kein Fehler, keine Meldung, einfach nichts, bis jemand von Hand nachsieht.', ], + + 'zone_wanted' => 'Der Token ist gültig — er hat die Zonenliste geholt. Gesucht wurde die Zone :zone.', + 'zone_available' => 'In diesem Konto liegen: :zones. Entweder CLUPILOT_DNS_ZONE anpassen oder die Zone bei Hetzner anlegen.', + 'zone_none' => 'In diesem Konto liegt keine einzige Zone. Der Token gehört vermutlich zu einem anderen Hetzner-Projekt.', ]; diff --git a/lang/en/readiness.php b/lang/en/readiness.php index f909d11..573c428 100644 --- a/lang/en/readiness.php +++ b/lang/en/readiness.php @@ -109,4 +109,8 @@ return [ 'queue_provisioning' => 'Provisioning worker is running', 'queue_provisioning_breaks' => 'The scheduler can be running and putting jobs on the provisioning queue without anyone ever picking them up. A paid order then just sits there — no error, no notice, simply nothing, until someone happens to look.', ], + + 'zone_wanted' => 'The token is valid — it fetched the zone list. It looked for the zone :zone.', + 'zone_available' => 'This account holds: :zones. Either adjust CLUPILOT_DNS_ZONE or create the zone at Hetzner.', + 'zone_none' => 'This account holds no zones at all. The token probably belongs to a different Hetzner project.', ]; diff --git a/resources/views/livewire/admin/readiness.blade.php b/resources/views/livewire/admin/readiness.blade.php index 07382c1..3229d59 100644 --- a/resources/views/livewire/admin/readiness.blade.php +++ b/resources/views/livewire/admin/readiness.blade.php @@ -25,13 +25,30 @@