Commit Graph

9 Commits (823eeaf413c44cbc8b259bee8dbcd8df52303d19)

Author SHA1 Message Date
nexxo 823eeaf413 Refuse a key that does not belong to the slot it sits in
put('stripe.secret', 'sk_live_REALMONEY') in test mode was accepted,
get() handed it out, billing.stripe_secret reported satisfied, and the
badge above it said "Testbetrieb". The strict rule closed the automatic
route into that state (no fallback); the typed one stayed open.

The prefix decides it without touching the network, and that rule now
lives in ONE place — OperatingMode::ofStripeKey() — called by the three
that were answering it separately: the slot migration (unchanged verdict,
`?? Live` for a value it cannot place), StripeCheck's `live` flag
(unchanged verdict, null stays false), and the readiness check, which
never asked at all. A key it cannot place is not reported as a
contradiction: this check only says what it can prove.

The two directions get their own `breaks` sentence, because the
consequences are opposite — real money moving while the console says
test, versus no money moving while the order looks paid.

The "Prüfen" button no longer contradicts the check either: the page
rendered only ok/reason, so a live key in the test slot answered
"Geprüft: in Ordnung". It now names the account the key belongs to.

Red first:

  ⨯ it refuses a live key sitting in the test slot
  ⨯ it refuses a test key sitting in the live slot
  ⨯ it says what the wrong key does, not that a field is empty

Guard tests (ConfirmInModal, ModalHeight, IconLayout, DisplayTimezone)
run with the blade change: green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:05:58 +02:00
nexxo 49d528dbea Fix round: point at the real tab, hide the button nobody can press, prove the quiet page load
Fixes four Important findings from the review of Task 12.

Corrected nine check `tab` values at the source (BillingChecks was already
correct; OnboardingChecks/ProvisioningChecks/DeliveryChecks all carried the
pre-redesign 'integrations' value, which is not a member of
Integrations::TABS): four onboarding checks now point at 'platform' or 'env'
depending on where their field actually is, five provisioning/delivery
checks point at 'services'. checkUrl()'s match-block resolver — praised as
correct for the six checks that point at a genuinely different admin page —
is unchanged; its `default` arm is now a pure safety net, not a route any
check actually relies on.

Wrapped the "Prüfen" button in @can('secrets.manage'): mount() admits
hosts.manage OR secrets.manage, but runCheck() requires secrets.manage alone,
so an Admin-role operator could see a button that 403s on press.

Fixed a docblock on HEARTBEAT_KEYS that asserted a staleness threshold was
kept in sync with OperationChecks::STALE_AFTER_MINUTES — that constant is a
key/settings-name map with no threshold of its own, and no such
synchronisation exists.

Three new tests, each confirmed red against a deliberately reintroduced
version of the bug it covers before being confirmed green: every check's tab
value against where its field actually lives, the run-check button hidden
from an operator who cannot press it, and Http::assertNothingSent() after a
plain page load.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 16:26:10 +02:00
nexxo e83b17ec37 Put every missing field on one page before a customer pays
Admin\Readiness renders App\Support\Readiness::byGroup() end to end: state,
label, breaks sentence and a fix link for every check, a button on the four
that have to ask a real service (DnsTokenCheck, WireguardEndpointCheck,
VmTemplateCheck, StripeCheck) and only ever run on demand, and one headline
line — ready for the operator's current mode, or how many blocking items are
still open. Wired into the nav and into Overview's own notice list beside the
same gate as Admin\Integrations, since both halves it configures come back
together on this page.

Fixed two check keys the guard test caught for real: onboarding.ssh_key and
delivery.inbound_password did not textually contain the vault key they check
(ssh.private_key, inbound_mail.password), which is exactly the kind of silent
gap this whole task exists to catch. Renamed to onboarding.ssh_private_key
and delivery.inbound_mail_password, with the two existing test files that
referenced the old keys updated to match.

Also fixed the guard test's own assertion: Pest's toContain() is variadic, so
a second string argument is a second required substring, not a failure
message (the same trap tests/Feature/ModalHeightTest.php already documents
about itself) — it failed on every entry, including ones already covered.
Replaced with str_contains()+toBeTrue(), which keeps the check and the
message both.

Two pre-existing Overview tests assumed a bare install has nothing to report;
it now correctly always starts with a readiness gap, so both were narrowed to
what they actually test — no invented data, no invented incident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 15:59:34 +02:00
nexxo 494a3a817e Fix round: guard the parse, bound the future, name the real tab
Covers the queue assignment the second heartbeat exists to prove, stops an
unreadable heartbeat value from taking the whole readiness page down with it,
rejects a heartbeat dated into the future, and points both checks at the tab
that actually restarts the scheduler and provisioning worker.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 14:49:06 +02:00
nexxo 6ca3e6ef4a Notice when nobody is picking up the queue
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 14:32:09 +02:00
nexxo 0f4226c488 Fix round: read the mailer against NON_DELIVERING, not against 'log' alone
delivery.mailer_not_log now reads MailboxTransport::NON_DELIVERING (made
public) instead of a narrower `!== 'log'`, so 'array' — this suite's own
MAIL_MAILER — and an unset default no longer pass as ready when nothing
would actually be delivered. provisioning.monitoring_token's severity now
follows provisioning.monitoring.required the same way RegisterMonitoring
itself does, instead of asserting a fixed 'warning' the pipeline does not
always honour. Two breaks sentences (dns_zone, mail_templates) now name a
concrete failure mechanism instead of a correct-but-vague description.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:27:14 +02:00
nexxo 325cee50ca Check the machines, the DNS and the post as well
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:05:18 +02:00
nexxo b1e5bf8023 Fix round: match the actual selection IssueInvoice and sync use
Two of the five billing checks measured something close to, but not
the same as, what actually gates the flow they report on:

- billing.invoice_series counted distinct kinds without filtering on
  `active`, but IssueInvoice draws its series with
  where('kind', ...)->where('active', true) (IssueInvoice.php:373,
  :444) — a deactivated row is invisible to it exactly like a missing
  one. An installation with a switched-off 'invoice' series was
  reported ready while no invoice could draw a number: false-green.

- billing.catalogue_synced counted every PlanPrice with a null
  stripe_price_id, but stripe:sync-catalogue only ever mirrors
  PUBLISHED versions (SyncStripeCatalogue's own header comment). A
  draft's unsynced price is an ordinary state, not a gap; the old
  query kept this check permanently unsatisfied on any installation
  that had ever drafted a next version — an alarm always on and
  therefore never read.

Both queries now mirror the condition the real gate uses instead of a
similar-looking one of their own. Two tests added that were red before
the fix: an inactive required series that must not read as ready, and
an unpublished draft's unsynced price that must not raise an alarm.

Also strengthens the weakest breaks sentence: invoice_series_breaks
named the mechanism ("a document cannot draw a number") rather than
the consequence, unlike its four siblings. It now says what the
customer and operator actually experience — a running cloud with no
invoice, the failure only logged — matching company_details_breaks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:42:09 +02:00
nexxo d9c0ad0e4a Say what breaks, not just which field is empty
Task 7: the readiness collector (App\Support\Readiness) and its first
group, billing (App\Support\Readiness\BillingChecks). Reports missing
prerequisites — the active mode's Stripe key, its webhook signing
secret, complete company details, an invoice series per document kind,
a fully Stripe-synced catalogue — without enforcing anything itself;
the real locks (IssueInvoice, the checkout's Stripe-key guard) stay
where they are.

The webhook check uses App\Support\StripeWebhookSecret::current()
rather than querying config() by mode a second time, and the company
check calls CompanyProfile::missingForInvoicing() rather than
duplicating its list — two sources for one question is how they drift
apart.

Added lang/de/readiness.php and lang/en/readiness.php with every
`readiness.*` key this group uses, each `_breaks` sentence naming the
actual downstream failure rather than repeating the field name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:25:27 +02:00