Say on the front page that the site is switched off
tests / pest (push) Failing after 8m54s Details
tests / assets (push) Successful in 20s Details
tests / release (push) Has been skipped Details

"Ich komme auf dem lokalen Dev-Server nicht auf www.dev." Diagnosed rather
than guessed: Laravel answers 200 on that host, and the request never
reaches it — site.public is false, so PublicSiteGate returns the placeholder
with a 503 to every visitor who is not on the management VPN and not signed
in as an operator. Which is precisely what a broken deployment looks like
from outside, and the only place that said so was the switch itself, on
another page.

So the console says it now, on the page an operator opens when something
looks wrong, and the notice links to the switch.

The second half of the same report — being sent to app.… — is the missing
SITE_HOST. Without it the marketing site is registered without a hostname:
it answers on the PORTAL's host as well, and every route() it generates
points at APP_URL, so a link on the site lands on the portal's domain. Also
said, also with the host named.

Only where the installation is host-separated at all, though. A fresh
checkout binds nothing to a hostname by design — RestrictAdminHost:
"upgrading must not lock anyone out of a system that was working" — and a
warning there is furniture. My first version fired on every clean
installation and two existing tests said so immediately; they were right.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feature/betriebsmodus^2 v1.3.47
nexxo 2026-07-30 13:36:42 +02:00
parent f9993a1f48
commit 70fa136e22
5 changed files with 85 additions and 1 deletions

View File

@ -1 +1 @@
1.3.46
1.3.47

View File

@ -6,6 +6,8 @@ use App\Livewire\Concerns\BuildsRunSteps;
use App\Models\Customer;
use App\Models\Host;
use App\Services\Provisioning\HostCapacity;
use App\Support\AdminArea;
use App\Support\Settings;
use App\Models\Instance;
use App\Models\MonitoringTarget;
use App\Models\ProvisioningRun;
@ -367,6 +369,40 @@ class Overview extends Component
])];
}
// The site being switched off. It is a deliberate state, not a fault —
// but it answers 503 to every visitor who is not on the VPN, and the
// only place that says so is the switch itself, three clicks away on
// another page. Asked as "ich komme nicht auf www.dev", which is exactly
// what it looks like from outside.
if (! Settings::bool('site.public', true)) {
$notices[] = [
'level' => 'warning',
'text' => __('admin.notice.site_hidden'),
'route' => 'admin.settings',
];
}
// No SITE_HOST. The marketing site is then registered without a
// hostname, so it answers on the PORTAL's host as well — and every
// route() it generates points at APP_URL, which is why a link on the
// site lands on the portal's domain.
//
// Only where the installation is host-separated at all. On a fresh
// checkout nothing is bound to a hostname by design (see
// RestrictAdminHost: "upgrading must not lock anyone out of a system
// that was working"), and a warning there would be furniture — the same
// mistake the capacity notice made before it learned to ask whether
// there is a host at all.
if (AdminArea::isHostBound() && config('admin_access.site_hosts') === []) {
$notices[] = [
'level' => 'warning',
'text' => __('admin.notice.no_site_host', [
'app' => (string) parse_url((string) config('app.url'), PHP_URL_HOST),
]),
'route' => 'admin.integrations',
];
}
return $notices;
}

View File

@ -88,6 +88,8 @@ return [
],
'notice' => [
'site_hidden' => 'Website und Kundenbereich sind ausgeblendet — Besucher außerhalb des VPN bekommen die Platzhalterseite (503). Umschalten unter Einstellungen → Installation.',
'no_site_host' => 'SITE_HOST ist nicht gesetzt. Die Website antwortet dadurch auch auf :app, und Links auf ihr zeigen dorthin statt auf den eigenen Namen.',
'no_capacity' => 'Kein Host hat noch Platz für „:plan“ — das Paket braucht :needs GB, der freieste Host hat :largest GB. Eine bezahlte Bestellung dafür würde bei der Reservierung scheitern.',
'failed_runs' => ':n fehlgeschlagene Bereitstellung(en).',
'host_error' => 'Host :host meldet einen Fehler.',

View File

@ -88,6 +88,8 @@ return [
],
'notice' => [
'site_hidden' => 'The website and the customer portal are hidden — a visitor outside the VPN gets the placeholder page (503). The switch is under Settings → Installation.',
'no_site_host' => 'SITE_HOST is not set. The website therefore also answers on :app, and links on it point there instead of at its own name.',
'no_capacity' => 'No host has room for “:plan” any more — the package needs :needs GB and the roomiest host has :largest GB. A paid order for it would fail at reservation.',
'failed_runs' => ':n failed provisioning run(s).',
'host_error' => 'Host :host reports an error.',

View File

@ -256,3 +256,47 @@ it('names the status in words rather than printing the key at it', function () {
->assertDontSee('customers.status.')
->assertDontSee('support.status.');
});
// ---- Two things the console owed the operator ----
it('says on the front page that the site is switched off', function () {
// Asked as "ich komme nicht auf www.dev". The site answers 503 to every
// visitor who is not on the VPN, which is exactly what a broken deployment
// looks like from outside — and the only place that said so was the switch
// itself, on another page.
App\Support\Settings::set('site.public', false);
Livewire::actingAs(operator('Owner'), 'operator')
->test(Overview::class)
->assertSee(__('admin.notice.site_hidden'))
// And it leads to the switch.
->assertSee(route('admin.settings'), false);
App\Support\Settings::set('site.public', true);
Livewire::actingAs(operator('Owner'), 'operator')
->test(Overview::class)
->assertDontSee(__('admin.notice.site_hidden'));
});
it('says when the website has no hostname of its own', function () {
// Without SITE_HOST the marketing site is registered without a hostname, so
// it answers on the PORTAL's host too — and every route() it generates
// points at APP_URL. That is why a link on the site lands on app.…, which
// was the other half of the same report.
// Only fires where the installation is host-separated at all: on a fresh
// checkout nothing is bound, and a warning there would be furniture.
config()->set('admin_access.hosts', ['admin.dev.example.test']);
config()->set('admin_access.site_hosts', []);
config()->set('app.url', 'https://app.dev.example.test');
Livewire::actingAs(operator('Owner'), 'operator')
->test(Overview::class)
->assertSee('app.dev.example.test');
config()->set('admin_access.site_hosts', ['www.dev.example.test']);
Livewire::actingAs(operator('Owner'), 'operator')
->test(Overview::class)
->assertDontSee(__('admin.notice.no_site_host', ['app' => 'app.dev.example.test']));
});