diff --git a/README.md b/README.md
index ef39789..2661d90 100644
--- a/README.md
+++ b/README.md
@@ -88,7 +88,9 @@ make dev-scale-e2e
`make dev-e2e` uses the real HTTP APIs, PostgreSQL, queues, PowerDNS, DNSdist,
mTLS edge control, pool migration, Pebble DNS-01 issuance, cache/purge delivery,
and OpenResty HTTP/HTTPS runtime. Browser acceptance is deliberately manual and is specified in
-[manual browser qualification](docs/manual-browser-qualification.md).
+[manual browser qualification](docs/manual-browser-qualification.md). The
+[frontend route coverage audit](docs/frontend-route-coverage.md) maps every
+application route family to its human, automation-only, or agent-only surface.
Production uses immutable commit-SHA GHCR images through
[`compose.prod.yml`](compose.prod.yml); it has no `latest` tags and no production
diff --git a/core/app/Filament/Admin/Pages/Telemetry.php b/core/app/Filament/Admin/Pages/Telemetry.php
index b0d1d32..9c6eed7 100644
--- a/core/app/Filament/Admin/Pages/Telemetry.php
+++ b/core/app/Filament/Admin/Pages/Telemetry.php
@@ -2,9 +2,15 @@
namespace App\Filament\Admin\Pages;
+use App\Http\Controllers\Admin\UsageController;
+use App\Models\Domain;
use App\Models\UsageRollup;
use App\Support\AnalyticsStore;
use Carbon\CarbonImmutable;
+use Filament\Actions\Action;
+use Filament\Forms\Components\DateTimePicker;
+use Filament\Forms\Components\Select;
+use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Support\Facades\Http;
use Throwable;
@@ -21,6 +27,39 @@ class Telemetry extends Page
protected string $view = 'filament.admin.pages.telemetry';
+ protected function getHeaderActions(): array
+ {
+ return [
+ Action::make('rebuildUsage')
+ ->label('Rebuild usage')
+ ->icon('heroicon-o-arrow-path')
+ ->color('warning')
+ ->schema([
+ Select::make('domain_id')->label('Domain (optional)')
+ ->options(fn (): array => Domain::query()->orderBy('name')->limit(500)->pluck('name', 'id')->all())
+ ->searchable(),
+ DateTimePicker::make('from')->label('From (UTC)')->timezone('UTC')->seconds(false)->required(),
+ DateTimePicker::make('to')->label('To (UTC)')->timezone('UTC')->seconds(false)->required()->after('from'),
+ ])
+ ->fillForm(fn (): array => [
+ 'from' => CarbonImmutable::now('UTC')->subHour()->startOfHour(),
+ 'to' => CarbonImmutable::now('UTC')->startOfHour(),
+ ])
+ ->requiresConfirmation()
+ ->action(function (array $data): void {
+ request()->merge([
+ 'domain_id' => $data['domain_id'] ?? null,
+ 'from' => CarbonImmutable::parse($data['from'], 'UTC')->startOfHour()->toIso8601String(),
+ 'to' => CarbonImmutable::parse($data['to'], 'UTC')->startOfHour()->toIso8601String(),
+ ]);
+ $response = app(UsageController::class)->rebuild(request());
+ $operation = $response->getData(true)['data'];
+ Notification::make()->info()->title('Usage rebuild queued')
+ ->body("Operation {$operation['operation_id']} will rebuild complete UTC hours without changing serving behavior.")->send();
+ }),
+ ];
+ }
+
public function getStateProperty(): array
{
$store = app(AnalyticsStore::class);
diff --git a/core/app/Filament/Admin/Resources/DnsClusters/Pages/ListDnsClusters.php b/core/app/Filament/Admin/Resources/DnsClusters/Pages/ListDnsClusters.php
index e1d1c24..c030dbe 100644
--- a/core/app/Filament/Admin/Resources/DnsClusters/Pages/ListDnsClusters.php
+++ b/core/app/Filament/Admin/Resources/DnsClusters/Pages/ListDnsClusters.php
@@ -3,7 +3,10 @@
namespace App\Filament\Admin\Resources\DnsClusters\Pages;
use App\Filament\Admin\Resources\DnsClusters\DnsClusterResource;
+use App\Http\Controllers\Admin\DnsOperationController;
+use Filament\Actions\Action;
use Filament\Actions\CreateAction;
+use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
class ListDnsClusters extends ListRecords
@@ -12,6 +15,19 @@ class ListDnsClusters extends ListRecords
protected function getHeaderActions(): array
{
- return [CreateAction::make()];
+ return [
+ Action::make('reconcileAllZones')
+ ->label('Reconcile all zones')
+ ->icon('heroicon-o-arrow-path')
+ ->color('warning')
+ ->requiresConfirmation()
+ ->action(function (): void {
+ $response = app(DnsOperationController::class)->reconcile(request());
+ $operation = $response->getData(true)['data'];
+ Notification::make()->info()->title('Global DNS reconciliation queued')
+ ->body("Operation {$operation['id']} will process active zones in bounded chunks.")->send();
+ }),
+ CreateAction::make(),
+ ];
}
}
diff --git a/core/app/Filament/Domain/Resources/Domains/Pages/ViewDomain.php b/core/app/Filament/Domain/Resources/Domains/Pages/ViewDomain.php
index f019194..1bf1bf9 100644
--- a/core/app/Filament/Domain/Resources/Domains/Pages/ViewDomain.php
+++ b/core/app/Filament/Domain/Resources/Domains/Pages/ViewDomain.php
@@ -6,6 +6,8 @@
use App\Enums\DomainLifecycleState;
use App\Filament\Domain\Resources\Domains\DomainResource;
use App\Http\Controllers\CacheController;
+use App\Http\Controllers\DnsDeploymentController;
+use App\Http\Controllers\ProxyController;
use App\Jobs\EnsureManagedCertificates;
use App\Jobs\ImportDnsZone;
use App\Jobs\ReconcileDnsZone;
@@ -57,6 +59,22 @@ public function getSubheading(): ?string
protected function getHeaderActions(): array
{
$actions = [
+ Action::make('reconcileDns')->label('Reconcile authoritative DNS')->icon('heroicon-o-arrow-path')
+ ->visible(fn (): bool => $this->record->lifecycle_state === DomainLifecycleState::Active)
+ ->action(function (): void {
+ $response = app(DnsDeploymentController::class)->reconcile(request(), $this->record);
+ $operation = $response->getData(true)['data'];
+ Notification::make()->info()->title('DNS reconciliation queued')
+ ->body("Operation {$operation['id']} will preserve the previous valid zone until activation succeeds.")->send();
+ }),
+ Action::make('deployEdge')->label('Reconcile edge delivery')->icon('heroicon-o-cloud-arrow-up')
+ ->visible(fn (): bool => $this->record->dnsRecords()->where('mode', 'proxied')->exists())
+ ->action(function (): void {
+ $response = app(ProxyController::class)->deploy(request(), $this->record);
+ $operation = $response->getData(true)['data'];
+ Notification::make()->info()->title('Edge reconciliation queued')
+ ->body("Operation {$operation['operation_id']} will deploy the latest desired revision.")->send();
+ }),
Action::make('tlsMode')->label('TLS mode')->icon('heroicon-o-lock-closed')->schema([
Select::make('mode')->options(['managed' => 'Managed', 'custom' => 'Custom', 'disabled' => 'Disabled'])->required(),
])->fillForm(fn (): array => ['mode' => $this->record->tls_mode])
@@ -399,12 +417,12 @@ static function () use ($zone): void {
->all();
return [
- ActionGroup::make($group(['verifyNameservers', 'forceVerifyNameservers', 'activate', 'disable', 'importZone', 'exportZone']))
+ ActionGroup::make($group(['verifyNameservers', 'forceVerifyNameservers', 'reconcileDns', 'activate', 'disable', 'importZone', 'exportZone']))
->label('Domain actions')
->icon('heroicon-o-globe-alt')
->color('gray')
->button(),
- ActionGroup::make($group(['proxyDefaults', 'rollbackProxy', 'moveEdgePool']))
+ ActionGroup::make($group(['deployEdge', 'proxyDefaults', 'rollbackProxy', 'moveEdgePool']))
->label('Delivery')
->icon('heroicon-o-cloud')
->button(),
diff --git a/core/app/Filament/Shared/Pages/ApiTokens.php b/core/app/Filament/Shared/Pages/ApiTokens.php
index eeee959..9815a17 100644
--- a/core/app/Filament/Shared/Pages/ApiTokens.php
+++ b/core/app/Filament/Shared/Pages/ApiTokens.php
@@ -14,6 +14,8 @@ class ApiTokens extends Page
protected static ?string $navigationLabel = 'API tokens';
+ protected static string|\UnitEnum|null $navigationGroup = 'Account';
+
protected static ?string $slug = 'tokens';
protected string $view = 'filament.shared.pages.api-tokens';
diff --git a/core/app/Providers/Filament/AdminPanelProvider.php b/core/app/Providers/Filament/AdminPanelProvider.php
index 3fb159b..2297aa1 100644
--- a/core/app/Providers/Filament/AdminPanelProvider.php
+++ b/core/app/Providers/Filament/AdminPanelProvider.php
@@ -36,7 +36,7 @@ public function panel(Panel $panel): Panel
->colors(['primary' => Color::Blue])
->sidebarCollapsibleOnDesktop()
->databaseNotifications()
- ->navigationGroups(['Control plane', 'Customers', 'Edge network', 'Operations'])
+ ->navigationGroups(['Control plane', 'Customers', 'Edge network', 'Operations', 'Observe', 'Account'])
->discoverResources(in: app_path('Filament/Admin/Resources'), for: 'App\\Filament\\Admin\\Resources')
->resources([DomainResource::class])
->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages')
diff --git a/core/resources/css/app.css b/core/resources/css/app.css
index 62b9686..dceab20 100644
--- a/core/resources/css/app.css
+++ b/core/resources/css/app.css
@@ -7,3 +7,47 @@
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}
+
+@layer base {
+ body { margin: 0; }
+}
+
+@layer components {
+ .cdn-landing-body { min-height: 100vh; background: #f8fafc; color: #0f172a; font-family: var(--font-sans); }
+ .cdn-landing-shell { width: min(72rem, calc(100% - 2rem)); margin-inline: auto; }
+ .cdn-landing-header { display: flex; min-height: 5rem; align-items: center; justify-content: space-between; gap: 1rem; border-bottom: 1px solid #e2e8f0; }
+ .cdn-landing-brand { display: inline-flex; align-items: center; gap: .65rem; color: inherit; font-weight: 750; letter-spacing: -.02em; text-decoration: none; }
+ .cdn-landing-mark { display: grid; width: 2rem; height: 2rem; place-items: center; border-radius: .65rem; background: #2563eb; color: white; font-size: .875rem; }
+ .cdn-landing-link { color: #475569; font-size: .875rem; font-weight: 600; text-decoration: none; }
+ .cdn-landing-link:hover { color: #1d4ed8; }
+ .cdn-landing-hero { display: grid; align-items: center; gap: 3rem; padding-block: clamp(4rem, 10vw, 8rem); }
+ .cdn-landing-eyebrow { margin: 0 0 1rem; color: #2563eb; font-size: .75rem; font-weight: 750; letter-spacing: .1em; text-transform: uppercase; }
+ .cdn-landing-hero h1 { max-width: 48rem; margin: 0; font-size: clamp(2.4rem, 7vw, 4.5rem); font-weight: 750; letter-spacing: -.055em; line-height: 1.02; }
+ .cdn-landing-copy { max-width: 42rem; margin: 1.5rem 0 0; color: #475569; font-size: 1.05rem; line-height: 1.75; }
+ .cdn-landing-actions { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 2rem; }
+ .cdn-landing-button { display: inline-flex; min-height: 2.75rem; align-items: center; justify-content: center; border: 1px solid #2563eb; border-radius: .7rem; background: #2563eb; color: white; padding: .65rem 1rem; font-size: .875rem; font-weight: 700; text-decoration: none; }
+ .cdn-landing-button:hover { background: #1d4ed8; }
+ .cdn-landing-button-secondary { border-color: #cbd5e1; background: white; color: #1e293b; }
+ .cdn-landing-button-secondary:hover { border-color: #94a3b8; background: #f8fafc; }
+ .cdn-landing-panel { border: 1px solid #dbeafe; border-radius: 1.25rem; background: white; padding: 1.5rem; box-shadow: 0 24px 60px rgb(15 23 42 / .08); }
+ .cdn-landing-panel-label { color: #64748b; font-size: .75rem; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; }
+ .cdn-landing-panel ul { display: grid; gap: .25rem; margin: 1rem 0 0; padding: 0; list-style: none; }
+ .cdn-landing-panel li { display: flex; gap: 1rem; align-items: center; border-radius: .75rem; padding: .9rem; }
+ .cdn-landing-panel li:nth-child(odd) { background: #f8fafc; }
+ .cdn-landing-panel li > span { color: #2563eb; font-size: .75rem; font-weight: 750; }
+ .cdn-landing-panel strong, .cdn-landing-panel small { display: block; }
+ .cdn-landing-panel small { margin-top: .2rem; color: #64748b; }
+}
+
+@media (min-width: 56rem) {
+ .cdn-landing-hero { grid-template-columns: minmax(0, 1.45fr) minmax(18rem, .55fr); }
+}
+
+@media (prefers-color-scheme: dark) {
+ .cdn-landing-body { background: #090e1a; color: #f8fafc; }
+ .cdn-landing-header { border-color: #1e293b; }
+ .cdn-landing-link, .cdn-landing-copy { color: #94a3b8; }
+ .cdn-landing-button-secondary, .cdn-landing-panel { border-color: #334155; background: #111827; color: #f8fafc; }
+ .cdn-landing-button-secondary:hover, .cdn-landing-panel li:nth-child(odd) { background: #1e293b; }
+ .cdn-landing-panel small, .cdn-landing-panel-label { color: #94a3b8; }
+}
diff --git a/core/resources/views/components/ui/data-table.blade.php b/core/resources/views/components/ui/data-table.blade.php
new file mode 100644
index 0000000..92a93b6
--- /dev/null
+++ b/core/resources/views/components/ui/data-table.blade.php
@@ -0,0 +1,13 @@
+@props(['caption' => null])
+
+
class('cdn-table-wrap') }}>
+
+ @if (filled($caption))
+ {{ $caption }}
+ @endif
+ @if (isset($header))
+ {{ $header }}
+ @endif
+ {{ $slot }}
+
+
diff --git a/core/resources/views/components/ui/empty-state.blade.php b/core/resources/views/components/ui/empty-state.blade.php
new file mode 100644
index 0000000..ba2d68c
--- /dev/null
+++ b/core/resources/views/components/ui/empty-state.blade.php
@@ -0,0 +1,14 @@
+@props([
+ 'title' => null,
+ 'description' => null,
+])
+
+class('cdn-empty-state') }} role="status">
+ @if (filled($title))
+
{{ $title }}
+ @endif
+
{{ $description ?? $slot }}
+ @if (isset($actions))
+
{{ $actions }}
+ @endif
+
diff --git a/core/resources/views/components/ui/list-row.blade.php b/core/resources/views/components/ui/list-row.blade.php
new file mode 100644
index 0000000..9498054
--- /dev/null
+++ b/core/resources/views/components/ui/list-row.blade.php
@@ -0,0 +1,20 @@
+@props([
+ 'title',
+ 'meta' => null,
+ 'href' => null,
+])
+
+@php($tag = $href ? 'a' : 'div')
+
+<{{ $tag }} {{ $attributes->class('cdn-list-row') }} @if ($href) href="{{ $href }}" @endif>
+
+
{{ $title }}
+ @if (filled($meta))
+
{{ $meta }}
+ @endif
+ {{ $slot }}
+
+ @if (isset($aside))
+ {{ $aside }}
+ @endif
+{{ $tag }}>
diff --git a/core/resources/views/components/ui/stat-card.blade.php b/core/resources/views/components/ui/stat-card.blade.php
new file mode 100644
index 0000000..ba0e35f
--- /dev/null
+++ b/core/resources/views/components/ui/stat-card.blade.php
@@ -0,0 +1,21 @@
+@props([
+ 'label',
+ 'value',
+ 'description' => null,
+ 'tone' => 'neutral',
+ 'href' => null,
+])
+
+@php($tag = $href ? 'a' : 'div')
+
+<{{ $tag }}
+ {{ $attributes->class('cdn-stat-card') }}
+ data-tone="{{ $tone }}"
+ @if ($href) href="{{ $href }}" aria-label="Open {{ $label }}" @endif
+>
+ {{ $label }}
+ {{ $value }}
+ @if (filled($description))
+ {{ $description }}
+ @endif
+{{ $tag }}>
diff --git a/core/resources/views/components/ui/status-pill.blade.php b/core/resources/views/components/ui/status-pill.blade.php
new file mode 100644
index 0000000..d3bcf28
--- /dev/null
+++ b/core/resources/views/components/ui/status-pill.blade.php
@@ -0,0 +1,5 @@
+@props(['tone' => 'neutral'])
+
+class('cdn-status-pill') }} data-tone="{{ $tone }}">
+ {{ $slot }}
+
diff --git a/core/resources/views/filament/admin/pages/platform-settings.blade.php b/core/resources/views/filament/admin/pages/platform-settings.blade.php
index 70a4c8f..ff79d98 100644
--- a/core/resources/views/filament/admin/pages/platform-settings.blade.php
+++ b/core/resources/views/filament/admin/pages/platform-settings.blade.php
@@ -1,6 +1,6 @@
diff --git a/core/resources/views/filament/admin/pages/system-dns-identity.blade.php b/core/resources/views/filament/admin/pages/system-dns-identity.blade.php
index 7e97c4e..0fa6eef 100644
--- a/core/resources/views/filament/admin/pages/system-dns-identity.blade.php
+++ b/core/resources/views/filament/admin/pages/system-dns-identity.blade.php
@@ -1,13 +1,16 @@
@if ($preview)
Review the normalized high-risk DNS identity payload before applying it.
{{ json_encode($preview, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}
-
Confirm and queue update
+
+ Confirm and queue update
+ Queuing update…
+
@endif
diff --git a/core/resources/views/filament/admin/pages/telemetry.blade.php b/core/resources/views/filament/admin/pages/telemetry.blade.php
index 49e6e74..15282b6 100644
--- a/core/resources/views/filament/admin/pages/telemetry.blade.php
+++ b/core/resources/views/filament/admin/pages/telemetry.blade.php
@@ -14,12 +14,12 @@
- ClickHouse {{ $state['available'] ? 'available' : 'unavailable' }}
- Vector metrics {{ $state['buffer']['available'] ? 'available' : 'unavailable' }}
- {{ ($state['meta']['partial'] ?? true) ? 'Partial / provisional' : 'Finalized' }}
+ ClickHouse {{ $state['available'] ? 'available' : 'unavailable' }}
+ Vector metrics {{ $state['buffer']['available'] ? 'available' : 'unavailable' }}
+ {{ ($state['meta']['partial'] ?? true) ? 'Partial / provisional' : 'Finalized' }}
@if (!$state['available'])
- Analytics unavailable. Traffic serving is independent and remains active. Finalized PostgreSQL usage is still shown below.
+
@endif
@@ -33,43 +33,31 @@
['label' => 'Origin errors', 'value' => number_format((int) ($summary['origin_errors'] ?? 0)), 'description' => 'Origin failures requiring review', 'tone' => ((int) ($summary['origin_errors'] ?? 0)) > 0 ? 'warning' : 'success'],
['label' => 'Security blocks', 'value' => number_format((int) ($summary['security_blocks'] ?? 0)), 'description' => 'Bounded protection decisions', 'tone' => 'warning'],
] as $stat)
-
-
{{ $stat['label'] }}
-
{{ $stat['value'] }}
-
{{ $stat['description'] }}
-
+
@endforeach
-
-
- | UTC hour | Requests | Transfer |
-
+
+ | UTC hour | Requests | Transfer |
@forelse ($state['traffic'] as $row)
| {{ $row['bucket'] ?? 'Unknown' }} | {{ number_format((int) ($row['requests'] ?? 0)) }} | {{ $formatBytes(((int) ($row['bytes_in'] ?? 0)) + ((int) ($row['bytes_out'] ?? 0))) }} |
@empty
| No traffic was recorded in this range. |
@endforelse
-
-
-
+
-
-
- | Type | Response | Queries |
-
+
+ | Type | Response | Queries |
@forelse ($state['dns'] as $row)
| {{ $row['qtype'] ?? 'Unknown' }} | {{ $row['rcode'] ?? 'Unknown' }} | {{ number_format((int) ($row['queries'] ?? 0)) }} |
@empty
| No DNS activity was recorded in this range. |
@endforelse
-
-
-
+
@@ -88,7 +76,7 @@
@if (isset($row['status'])){{ $row['status'] }}@endif
@empty
- No {{ $stream }} events in the last hour.
+
@endforelse
@@ -109,7 +97,7 @@
{{ is_numeric($value) ? number_format((float) $value, 0) : $value }}
@empty
- Vector delivery metrics are unavailable.
+
@endforelse
@@ -118,18 +106,14 @@
Global usage CSV
-
-
- | Domain / interval | Requests | Transfer | DNS | State |
-
+
+ | Domain / interval | Requests | Transfer | DNS | State |
@forelse ($state['usage'] as $row)
{{ $row['domain'] }} {{ $row['interval'] }} | {{ number_format($row['requests']) }} | {{ $formatBytes($row['bytes']) }} | {{ number_format($row['dns_queries']) }} | {{ str($row['status'])->headline() }} |
@empty
| No finalized usage intervals are available yet. |
@endforelse
-
-
-
+
diff --git a/core/resources/views/filament/domain/pages/analytics.blade.php b/core/resources/views/filament/domain/pages/analytics.blade.php
index 49b272f..b17aa89 100644
--- a/core/resources/views/filament/domain/pages/analytics.blade.php
+++ b/core/resources/views/filament/domain/pages/analytics.blade.php
@@ -34,11 +34,11 @@
@if ($state['domain'])
- ClickHouse {{ $state['available'] ? 'available' : 'unavailable' }}
- {{ ($state['meta']['partial'] ?? true) ? 'Partial / provisional' : 'Finalized' }}
+ ClickHouse {{ $state['available'] ? 'available' : 'unavailable' }}
+ {{ ($state['meta']['partial'] ?? true) ? 'Partial / provisional' : 'Finalized' }}
@if (!$state['available'])
- Analytics unavailable. DNS and edge serving continue normally; finalized PostgreSQL usage remains available below.
+
@endif
@@ -55,11 +55,7 @@
['label' => 'Origin errors', 'value' => number_format((int) ($summary['origin_errors'] ?? 0)), 'description' => 'Origin failures', 'tone' => ((int) ($summary['origin_errors'] ?? 0)) > 0 ? 'warning' : 'success'],
['label' => 'Security blocks', 'value' => number_format((int) ($summary['security_blocks'] ?? 0)), 'description' => 'Protection decisions', 'tone' => 'warning'],
] as $stat)
-
-
{{ $stat['label'] }}
-
{{ $stat['value'] }}
-
{{ $stat['description'] }}
-
+
@endforeach
@@ -79,7 +75,7 @@
{{ $title }}
{{ $metrics ?: 'No activity' }}
@empty
- No data was recorded for this view.
+
@endforelse
@@ -101,7 +97,7 @@
@if (isset($row['status'])){{ $row['status'] }}@endif
@empty
- No {{ $stream }} events in the last hour.
+
@endforelse
@@ -114,18 +110,14 @@
Usage CSV export
-
-
- | UTC interval | Requests | Transfer | DNS | State |
-
+
+ | UTC interval | Requests | Transfer | DNS | State |
@forelse ($state['usage'] as $row)
| {{ $row['interval'] }} | {{ number_format($row['requests']) }} | {{ $formatBytes($row['bytes']) }} | {{ number_format($row['dns_queries']) }} | {{ str($row['status'])->headline() }} |
@empty
| No finalized usage intervals are available yet. |
@endforelse
-
-
-
+
@endif
diff --git a/core/resources/views/filament/domain/pages/dashboard.blade.php b/core/resources/views/filament/domain/pages/dashboard.blade.php
index 6f86449..7eafdf8 100644
--- a/core/resources/views/filament/domain/pages/dashboard.blade.php
+++ b/core/resources/views/filament/domain/pages/dashboard.blade.php
@@ -2,11 +2,7 @@
@@ -19,12 +15,12 @@
{{ $domain->display_name ?: $domain->name }}
{{ $domain->name }} · {{ $domain->dns_records_count }} records · {{ $domain->proxied_records_count }} proxied
-
+
{{ str($domain->lifecycle_state->value)->replace('_', ' ')->headline() }}
-
+
@empty
- No domains are assigned yet. Create your first domain to begin.
+
@endforelse
diff --git a/core/resources/views/welcome.blade.php b/core/resources/views/welcome.blade.php
index 22d6130..7a7516e 100644
--- a/core/resources/views/welcome.blade.php
+++ b/core/resources/views/welcome.blade.php
@@ -3,220 +3,41 @@
-
- {{ config('app.name', 'Laravel') }}
-
-
-
- @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
- @vite(['resources/css/app.css', 'resources/js/app.js'])
- @else
-
- @endif
+
+
+ CDNFoundry
+ @vite(['resources/css/app.css', 'resources/js/app.js'])
-
-
- @if (Route::has('login'))
-
- @endif
-
-
-
-
-
Let's get started
-
With so many options available to you,
we suggest you start with the following:
-
-
-
-
- v{{ app()->version() }}
-
- View changelog
-
-
-
+
+
+
+
+
+
+
Private CDN control plane
+
Operate DNS, edge delivery, TLS, cache, and security from one place.
+
Choose the workspace that matches your role. Runtime traffic and security decisions remain independent of this control plane.
+
-
- {{-- Laravel Logo --}}
-
-
- {{-- 13 --}}
-
-
-
-
-
-
- @if (Route::has('login'))
-
- @endif
+
+
+
diff --git a/core/tests/Feature/FilamentPanelAccessTest.php b/core/tests/Feature/FilamentPanelAccessTest.php
index b9928d8..d888274 100644
--- a/core/tests/Feature/FilamentPanelAccessTest.php
+++ b/core/tests/Feature/FilamentPanelAccessTest.php
@@ -77,7 +77,7 @@ public function test_administrator_operational_pages_render_and_domain_users_can
$admin = User::factory()->admin()->create();
$user = User::factory()->create();
- foreach (['/admin/users', '/admin/domains', '/admin/dns-clusters', '/admin/edges', '/admin/edge-pools', '/admin/operations', '/admin/audit-logs', '/admin/system-dns-identity', '/admin/platform-settings', '/admin/tokens', '/admin/profile'] as $path) {
+ foreach (['/admin/users', '/admin/domains', '/admin/dns-clusters', '/admin/edges', '/admin/edge-pools', '/admin/operations', '/admin/audit-logs', '/admin/system-dns-identity', '/admin/platform-settings', '/admin/telemetry', '/admin/tokens', '/admin/profile'] as $path) {
$this->actingAs($admin)->get($path)->assertOk();
$this->actingAs($user)->get($path)->assertForbidden();
}
diff --git a/core/tests/Feature/FilamentWorkflowTest.php b/core/tests/Feature/FilamentWorkflowTest.php
index 5077655..0a27ced 100644
--- a/core/tests/Feature/FilamentWorkflowTest.php
+++ b/core/tests/Feature/FilamentWorkflowTest.php
@@ -3,16 +3,24 @@
namespace Tests\Feature;
use App\Enums\DomainLifecycleState;
+use App\Filament\Admin\Pages\Telemetry;
+use App\Filament\Admin\Resources\DnsClusters\Pages\ListDnsClusters;
use App\Filament\Admin\Resources\Edges\Pages\EditEdge;
+use App\Filament\Admin\Resources\Edges\Pages\ListEdges;
use App\Filament\Admin\Resources\Edges\RelationManagers\CellsRelationManager;
use App\Filament\Domain\Resources\Domains\Pages\ViewDomain;
use App\Filament\Domain\Resources\Domains\RelationManagers\DnsRecordsRelationManager;
+use App\Jobs\BuildUsageRollups;
+use App\Jobs\ReconcileAllDnsZones;
+use App\Jobs\ReconcileAllEdgeDomains;
+use App\Jobs\ReconcileDnsZone;
use App\Jobs\ReconcileEdgeDomain;
use App\Models\Domain;
use App\Models\Edge;
use App\Models\EdgeArtifact;
use App\Models\EdgeCell;
use App\Models\EdgePool;
+use App\Models\Operation;
use App\Models\User;
use App\Support\DnsRecordData;
use Filament\Facades\Filament;
@@ -233,4 +241,68 @@ public function test_disabling_from_the_domain_panel_automatically_queues_edge_r
$this->assertSame(5, $domain->revision);
Queue::assertPushed(ReconcileEdgeDomain::class, fn (ReconcileEdgeDomain $job): bool => $job->domainId === $domain->id);
}
+
+ public function test_administrator_can_queue_global_reconciliation_and_bounded_usage_rebuilds(): void
+ {
+ Queue::fake();
+ $admin = User::factory()->admin()->create();
+ $domain = Domain::query()->create(['name' => 'usage-ui.example.test', 'display_name' => 'Usage UI']);
+ Filament::setCurrentPanel(Filament::getPanel('admin'));
+ $this->actingAs($admin);
+
+ Livewire::test(ListDnsClusters::class)->callAction('reconcileAllZones')->assertHasNoActionErrors();
+ Livewire::test(ListEdges::class)->callAction('reconcileAllDomains')->assertHasNoActionErrors();
+ Livewire::test(Telemetry::class)->callAction('rebuildUsage', data: [
+ 'domain_id' => $domain->id,
+ 'from' => '2026-07-20 08:00:00',
+ 'to' => '2026-07-20 10:00:00',
+ ])->assertHasNoActionErrors();
+
+ $this->assertSame(1, Operation::query()->where('type', 'dns.global_reconcile')->count());
+ $this->assertSame(1, Operation::query()->where('type', 'edge.global_reconcile')->count());
+ $this->assertDatabaseHas('operations', [
+ 'type' => 'usage.rebuild',
+ 'actor_id' => $admin->id,
+ ]);
+ Queue::assertPushed(ReconcileAllDnsZones::class);
+ Queue::assertPushed(ReconcileAllEdgeDomains::class);
+ Queue::assertPushed(BuildUsageRollups::class);
+ }
+
+ public function test_domain_reconcile_actions_reuse_the_policy_aware_deployment_endpoints(): void
+ {
+ Queue::fake();
+ $user = User::factory()->create();
+ $domain = Domain::query()->create([
+ 'name' => 'reconcile-ui.example.test',
+ 'display_name' => 'Reconcile UI',
+ 'lifecycle_state' => DomainLifecycleState::Active,
+ 'revision' => 3,
+ ]);
+ $domain->users()->attach($user);
+ $domain->dnsRecords()->create(DnsRecordData::validate([
+ 'type' => 'A',
+ 'name' => 'www',
+ 'content' => '192.0.2.20',
+ 'ttl' => 300,
+ 'mode' => 'proxied',
+ 'origin' => [
+ 'host' => '8.8.8.8', 'port' => 443, 'scheme' => 'https',
+ 'host_header' => 'www.reconcile-ui.example.test', 'sni' => 'www.reconcile-ui.example.test',
+ 'verify_tls' => true, 'connect_timeout_ms' => 2000, 'response_timeout_ms' => 30000,
+ 'retry_count' => 0, 'websocket' => false, 'health_check' => null,
+ ],
+ ], $domain->name));
+ Filament::setCurrentPanel(Filament::getPanel('app'));
+ $this->actingAs($user);
+
+ Livewire::test(ViewDomain::class, ['record' => $domain->id])
+ ->callAction('reconcileDns')->assertHasNoActionErrors()
+ ->callAction('deployEdge')->assertHasNoActionErrors();
+
+ $this->assertDatabaseHas('operations', ['type' => 'dns.zone_reconcile', 'actor_id' => $user->id]);
+ $this->assertDatabaseHas('operations', ['type' => 'edge.domain_reconcile', 'actor_id' => $user->id]);
+ Queue::assertPushed(ReconcileDnsZone::class, fn (ReconcileDnsZone $job): bool => $job->domainId === $domain->id);
+ Queue::assertPushed(ReconcileEdgeDomain::class, fn (ReconcileEdgeDomain $job): bool => $job->domainId === $domain->id);
+ }
}
diff --git a/docs/frontend-route-coverage.md b/docs/frontend-route-coverage.md
new file mode 100644
index 0000000..4c10444
--- /dev/null
+++ b/docs/frontend-route-coverage.md
@@ -0,0 +1,74 @@
+# Frontend route coverage
+
+This audit covers the application-owned routes in `core/routes/web.php` and
+`core/routes/api.php`. Framework routes for Livewire, Filament, Sanctum,
+Horizon internals, local storage, and Laravel's fallback health route are not
+product actions and are excluded.
+
+The browser UI is intentionally a policy-aware Filament surface, not an HTTP
+client pasted over the API. Browser actions call the same controllers,
+validators, policies, models, and queued jobs as their API equivalents. An API
+route remains API-only when it exists for automation, pagination, detailed
+machine diagnostics, or an edge agent rather than for a person to manage.
+
+## Public and account routes
+
+| Route family | Browser surface | Coverage |
+|---|---|---|
+| `GET /`, `/api/health`, `/api/ready`, `/api/nameservers` | Product landing page links to both panels and service health; nameservers appear in domain delegation | Complete |
+| `/api/auth/login`, `/api/auth/logout`, `/api/me` | Filament login/logout and shared Profile page | Complete |
+| `/api/me/tokens` | Shared API tokens page with one-time secret display and revoke confirmation | Complete |
+| `/api/operations/{operation}` | Domain actions show operation IDs; administrator Operations lists global detail and retry state | Complete |
+
+## Domain-user routes
+
+| API route family | Browser surface | Coverage |
+|---|---|---|
+| `/api/domains` and `/api/domains/{domain}` | Domains resource: assigned-domain list, create, status, display label, disable, and lifecycle state | Complete |
+| `/verify-nameservers`, `/activate`, `/status` | Domain actions and Domain status card | Complete |
+| `/dns/records`, `/dns/import`, `/dns/export` | DNS records relation with create/edit/delete/bulk delete, BIND import/export, type-aware validation, and permission-gated NS rows | Complete |
+| `/dns/records/{record}/geo` and `/geo/preview` | Geo-DNS fields and Preview record action | Complete |
+| `/dns/deployment` and `/dns/reconcile` | Authoritative DNS deployment card and **Reconcile authoritative DNS** action | Complete |
+| `/proxy`, `/origin`, `/origin/test`, `/origin/health` | Delivery settings, proxied-record origin form, test action, and visible health state | Complete |
+| `/deployment`, `/deploy`, `/rollback`, `/revisions` | Delivery status, validated revisions, and rollback action. Edge deploy remains deliberately automatic in the UI per the roadmap; the API trigger is for automation | Complete by contract |
+| `/cache`, `/cache/development-mode`, `/cache/purge`, `/cache/purges` | Cache menu: settings, bounded development mode, epoch/URL purge, and latest delivery state | Complete |
+| `/tls`, `/tls/status`, `/tls/reissue`, `/tls/renew`, `/tls/upload`, `/tls/custom-certificate` | TLS menu and TLS status card | Complete |
+| `/security`, `/security/rules`, `/security/rules/import` | Security profile action and ordered Security rules relation | Complete |
+| `/security/ddos`, `/security/ddos/status`, `/security/ddos/events`, `/security/events` | Security readiness/status card, bounded settings, state actions for admins, and recent reason codes | Complete |
+| `/analytics/*`, `/logs/*` | Assigned-domain Analytics and logs page with all aggregate views and bounded masked previews | Complete |
+| `/usage`, `/usage/export` | Finalized usage table and session-authenticated CSV export | Complete |
+
+Advanced custom origin ports remain API-only as required by the roadmap. The
+browser form exposes the standard HTTP/80 and HTTPS/443 pairs so the common
+path stays safe and understandable.
+
+## Administrator routes
+
+| API route family | Browser surface | Coverage |
+|---|---|---|
+| `/api/admin/users` and user domain assignment | Users resource plus the domain Users relation | Complete |
+| `/api/admin/audit-logs` | Read-only Audit logs resource | Complete |
+| `/api/admin/domains/{domain}/force-verify` | Permission-gated Force verify domain action | Complete |
+| `/api/admin/dns/clusters` | DNS clusters resource with secret-safe create/edit, test, enable, and disable | Complete |
+| `/api/admin/dns/deployments`, `/failed-deployments`, `/reconcile` | Domain deployment cards and cluster-level **Reconcile all zones** action; failures are searchable in Operations | Complete |
+| `/api/admin/system/status`, `/api/admin/operations` | Dashboard queue/health cards and Operations resource with guarded retry | Complete |
+| `/api/admin/system/settings*` | Platform settings and System DNS identity pages with preview/confirmation for high-risk identity changes | Complete |
+| `/api/admin/edges` | Edges resource with enrollment secret boundary, edit, rotate, enable/disable, drain/undrain, and emergency state | Complete |
+| `/api/admin/edge-pools` | Service pools resource with provisioning, enable/disable, withdrawal, and emergency state | Complete |
+| `/api/admin/edge-cells*` | Per-edge Cells relation with address edit, drain/undrain, restart, emergency state, and runtime diagnostics | Complete |
+| `/api/admin/edge-deployments`, `/edge-routing`, `/edge-deployments/reconcile` | Domain delivery cards, pool targets, edge diagnostics, and **Reconcile all domains** maintenance action | Complete |
+| `/api/admin/domains/{domain}/isolation`, `/move`, `/restrict`, `/quarantine`, `/release` | Permission-gated Delivery and Security domain actions | Complete |
+| `/api/admin/analytics/*`, `/logs/*`, `/usage`, `/usage/export`, `/usage/rebuild` | Telemetry and usage page, masked previews, global CSV, and bounded **Rebuild usage** action | Complete |
+
+## Machine-only routes
+
+`/edge/v1/register`, heartbeat, manifest/artifact/full configuration,
+apply/reject acknowledgements, task polling, and task results are an authenticated
+edge-agent protocol. Exposing them as human buttons would violate the runtime
+architecture and one-time enrollment boundary. Their human-observable state is
+available through Edges, Cells, Operations, Audit logs, and deployment cards.
+
+Cursor pagination and machine export variants stay API-only; their human
+equivalents use Filament pagination, bounded tables, and session-authenticated
+CSV downloads. Horizon remains the authenticated administrator UI for its own
+framework routes.