diff --git a/app/Filament/Clusters/DailyOps/Pages/JoinTokenAnalyticsDashboard.php b/app/Filament/Clusters/DailyOps/Pages/JoinTokenAnalyticsDashboard.php
new file mode 100644
index 0000000..8bc26a4
--- /dev/null
+++ b/app/Filament/Clusters/DailyOps/Pages/JoinTokenAnalyticsDashboard.php
@@ -0,0 +1,127 @@
+components([
+ Section::make()
+ ->schema([
+ Select::make('range')
+ ->label(__('admin.join_token_analytics.filters.range'))
+ ->options(trans('admin.join_token_analytics.filters.range_options'))
+ ->default('24h')
+ ->native(false),
+ Select::make('event_id')
+ ->label(__('admin.join_token_analytics.filters.event'))
+ ->placeholder(__('admin.join_token_analytics.filters.event_placeholder'))
+ ->searchable()
+ ->getSearchResultsUsing(fn (string $search): array => $this->searchEvents($search))
+ ->getOptionLabelUsing(fn ($value): ?string => $this->resolveEventLabel($value))
+ ->native(false),
+ ])
+ ->columns(2),
+ ]);
+ }
+
+ private function searchEvents(string $search): array
+ {
+ return Event::query()
+ ->with('tenant')
+ ->when($search !== '', function ($query) use ($search) {
+ $query->where('slug', 'like', "%{$search}%")
+ ->orWhere('name->de', 'like', "%{$search}%")
+ ->orWhere('name->en', 'like', "%{$search}%");
+ })
+ ->orderByDesc('date')
+ ->limit(25)
+ ->get()
+ ->mapWithKeys(fn (Event $event) => [$event->id => $this->formatEventLabel($event)])
+ ->all();
+ }
+
+ private function resolveEventLabel(mixed $value): ?string
+ {
+ if (! is_numeric($value)) {
+ return null;
+ }
+
+ $event = Event::query()
+ ->with('tenant')
+ ->find((int) $value);
+
+ return $event ? $this->formatEventLabel($event) : null;
+ }
+
+ private function formatEventLabel(Event $event): string
+ {
+ $locale = app()->getLocale();
+ $name = $event->name[$locale] ?? $event->name['de'] ?? $event->name['en'] ?? $event->slug ?? __('admin.common.unnamed');
+ $tenant = $event->tenant?->name ?? __('admin.common.unnamed');
+ $date = $event->date?->format('Y-m-d');
+
+ return $date ? "{$name} ({$tenant}) {$date}" : "{$name} ({$tenant})";
+ }
+}
diff --git a/app/Filament/Widgets/JoinTokenOverviewWidget.php b/app/Filament/Widgets/JoinTokenOverviewWidget.php
new file mode 100644
index 0000000..949f12e
--- /dev/null
+++ b/app/Filament/Widgets/JoinTokenOverviewWidget.php
@@ -0,0 +1,147 @@
+resolveFilters();
+ $totals = $this->totalsByEventType($filters);
+
+ $success = $this->sumTotals($totals, self::SUCCESS_EVENTS);
+ $failures = $this->sumTotals($totals, self::FAILURE_EVENTS);
+ $rateLimited = $this->sumTotals($totals, self::RATE_LIMIT_EVENTS);
+ $uploads = $this->sumTotals($totals, self::UPLOAD_EVENTS);
+
+ $ratio = $success > 0 ? round(($failures / $success) * 100, 1) : null;
+ $ratioLabel = $ratio !== null ? "{$ratio}%" : __('admin.join_token_analytics.stats.no_data');
+
+ return [
+ Stat::make(__('admin.join_token_analytics.stats.success'), number_format($success))
+ ->color('success'),
+ Stat::make(__('admin.join_token_analytics.stats.failures'), number_format($failures))
+ ->color($failures > 0 ? 'danger' : 'success'),
+ Stat::make(__('admin.join_token_analytics.stats.rate_limited'), number_format($rateLimited))
+ ->color($rateLimited > 0 ? 'warning' : 'success'),
+ Stat::make(__('admin.join_token_analytics.stats.uploads'), number_format($uploads))
+ ->color('primary'),
+ Stat::make(__('admin.join_token_analytics.stats.failure_ratio'), $ratioLabel)
+ ->color($ratio !== null && $ratio >= 50 ? 'danger' : 'warning'),
+ ];
+ }
+
+ private function totalsByEventType(array $filters): array
+ {
+ return $this->baseQuery($filters)
+ ->selectRaw('event_type, COUNT(*) as total')
+ ->groupBy('event_type')
+ ->get()
+ ->mapWithKeys(fn (EventJoinTokenEvent $event) => [$event->event_type => (int) $event->total])
+ ->all();
+ }
+
+ private function sumTotals(array $totals, array $types): int
+ {
+ $sum = 0;
+
+ foreach ($types as $type) {
+ $sum += (int) ($totals[$type] ?? 0);
+ }
+
+ return $sum;
+ }
+
+ private function baseQuery(array $filters): Builder
+ {
+ $query = EventJoinTokenEvent::query()
+ ->whereBetween('occurred_at', [$filters['start'], $filters['end']]);
+
+ if ($filters['event_id']) {
+ $query->where('event_id', $filters['event_id']);
+ }
+
+ return $query;
+ }
+
+ private function resolveFilters(): array
+ {
+ $eventId = $this->pageFilters['event_id'] ?? null;
+ $eventId = is_numeric($eventId) ? (int) $eventId : null;
+ $range = is_string($this->pageFilters['range'] ?? null) ? $this->pageFilters['range'] : '24h';
+
+ [$start, $end] = $this->resolveWindow($range, $eventId);
+
+ return [
+ 'event_id' => $eventId,
+ 'range' => $range,
+ 'start' => $start,
+ 'end' => $end,
+ ];
+ }
+
+ private function resolveWindow(string $range, ?int $eventId): array
+ {
+ $now = now();
+ $start = match ($range) {
+ '2h' => $now->copy()->subHours(2),
+ '6h' => $now->copy()->subHours(6),
+ '12h' => $now->copy()->subHours(12),
+ '7d' => $now->copy()->subDays(7),
+ default => $now->copy()->subHours(24),
+ };
+ $end = $now;
+
+ if ($range === 'event_day' && $eventId) {
+ $eventDate = Event::query()->whereKey($eventId)->value('date');
+
+ if ($eventDate) {
+ $eventDay = Carbon::parse($eventDate);
+ $start = $eventDay->copy()->startOfDay();
+ $end = $eventDay->copy()->endOfDay();
+ }
+ }
+
+ return [$start, $end];
+ }
+}
diff --git a/app/Filament/Widgets/JoinTokenTopTokensWidget.php b/app/Filament/Widgets/JoinTokenTopTokensWidget.php
new file mode 100644
index 0000000..76d5060
--- /dev/null
+++ b/app/Filament/Widgets/JoinTokenTopTokensWidget.php
@@ -0,0 +1,191 @@
+resolveFilters();
+
+ return $table
+ ->query(fn (): Builder => $this->buildQuery($filters))
+ ->columns([
+ Tables\Columns\TextColumn::make('token_preview')
+ ->label(__('admin.join_token_analytics.table.token'))
+ ->copyable()
+ ->copyMessage(__('admin.events.messages.join_link_copied')),
+ Tables\Columns\TextColumn::make('event_label')
+ ->label(__('admin.join_token_analytics.table.event'))
+ ->getStateUsing(fn (EventJoinToken $record) => $this->formatEventLabel($record->event))
+ ->url(fn (EventJoinToken $record) => $record->event
+ ? EventResource::getUrl('view', ['record' => $record->event])
+ : null)
+ ->openUrlInNewTab(),
+ Tables\Columns\TextColumn::make('tenant_label')
+ ->label(__('admin.join_token_analytics.table.tenant'))
+ ->getStateUsing(fn (EventJoinToken $record) => $record->event?->tenant?->name ?? __('admin.common.unnamed')),
+ Tables\Columns\TextColumn::make('success_total')
+ ->label(__('admin.join_token_analytics.table.success'))
+ ->numeric(),
+ Tables\Columns\TextColumn::make('failure_total')
+ ->label(__('admin.join_token_analytics.table.failures'))
+ ->numeric(),
+ Tables\Columns\TextColumn::make('rate_limited_total')
+ ->label(__('admin.join_token_analytics.table.rate_limited'))
+ ->numeric(),
+ Tables\Columns\TextColumn::make('upload_total')
+ ->label(__('admin.join_token_analytics.table.uploads'))
+ ->numeric(),
+ Tables\Columns\TextColumn::make('last_seen_at')
+ ->label(__('admin.join_token_analytics.table.last_seen'))
+ ->since()
+ ->placeholder('—'),
+ ])
+ ->paginated(false);
+ }
+
+ private function buildQuery(array $filters): Builder
+ {
+ $query = EventJoinToken::query()
+ ->with(['event.tenant'])
+ ->when($filters['event_id'], fn (Builder $builder, int $eventId) => $builder->where('event_id', $eventId))
+ ->withCount([
+ 'analytics as success_total' => function (Builder $builder) use ($filters) {
+ $this->applyAnalyticsFilters($builder, $filters)
+ ->whereIn('event_type', self::SUCCESS_EVENTS);
+ },
+ 'analytics as failure_total' => function (Builder $builder) use ($filters) {
+ $this->applyAnalyticsFilters($builder, $filters)
+ ->whereIn('event_type', self::FAILURE_EVENTS);
+ },
+ 'analytics as rate_limited_total' => function (Builder $builder) use ($filters) {
+ $this->applyAnalyticsFilters($builder, $filters)
+ ->whereIn('event_type', self::RATE_LIMIT_EVENTS);
+ },
+ 'analytics as upload_total' => function (Builder $builder) use ($filters) {
+ $this->applyAnalyticsFilters($builder, $filters)
+ ->whereIn('event_type', self::UPLOAD_EVENTS);
+ },
+ ])
+ ->withMax([
+ 'analytics as last_seen_at' => function (Builder $builder) use ($filters) {
+ $this->applyAnalyticsFilters($builder, $filters);
+ },
+ ], 'occurred_at')
+ ->orderByDesc('failure_total')
+ ->orderByDesc('rate_limited_total')
+ ->orderByDesc('success_total')
+ ->limit(10);
+
+ return $query;
+ }
+
+ private function applyAnalyticsFilters(Builder $query, array $filters): Builder
+ {
+ return $query->whereBetween('occurred_at', [$filters['start'], $filters['end']]);
+ }
+
+ private function resolveFilters(): array
+ {
+ $eventId = $this->pageFilters['event_id'] ?? null;
+ $eventId = is_numeric($eventId) ? (int) $eventId : null;
+ $range = is_string($this->pageFilters['range'] ?? null) ? $this->pageFilters['range'] : '24h';
+
+ [$start, $end] = $this->resolveWindow($range, $eventId);
+
+ return [
+ 'event_id' => $eventId,
+ 'range' => $range,
+ 'start' => $start,
+ 'end' => $end,
+ ];
+ }
+
+ private function resolveWindow(string $range, ?int $eventId): array
+ {
+ $now = now();
+ $start = match ($range) {
+ '2h' => $now->copy()->subHours(2),
+ '6h' => $now->copy()->subHours(6),
+ '12h' => $now->copy()->subHours(12),
+ '7d' => $now->copy()->subDays(7),
+ default => $now->copy()->subHours(24),
+ };
+ $end = $now;
+
+ if ($range === 'event_day' && $eventId) {
+ $eventDate = Event::query()->whereKey($eventId)->value('date');
+
+ if ($eventDate) {
+ $eventDay = Carbon::parse($eventDate);
+ $start = $eventDay->copy()->startOfDay();
+ $end = $eventDay->copy()->endOfDay();
+ }
+ }
+
+ return [$start, $end];
+ }
+
+ private function formatEventLabel(?Event $event): string
+ {
+ if (! $event) {
+ return __('admin.common.unnamed');
+ }
+
+ $locale = app()->getLocale();
+ $name = $event->name[$locale] ?? $event->name['de'] ?? $event->name['en'] ?? $event->slug ?? __('admin.common.unnamed');
+ $tenant = $event->tenant?->name ?? __('admin.common.unnamed');
+ $date = $event->date?->format('Y-m-d');
+
+ return $date ? "{$name} ({$tenant}) {$date}" : "{$name} ({$tenant})";
+ }
+
+ private const SUCCESS_EVENTS = [
+ 'access_granted',
+ 'gallery_access_granted',
+ ];
+
+ private const RATE_LIMIT_EVENTS = [
+ 'token_rate_limited',
+ 'access_rate_limited',
+ 'download_rate_limited',
+ ];
+
+ private const FAILURE_EVENTS = [
+ 'invalid_token',
+ 'token_expired',
+ 'token_revoked',
+ 'event_not_public',
+ 'gallery_expired',
+ 'token_rate_limited',
+ 'access_rate_limited',
+ 'download_rate_limited',
+ ];
+
+ private const UPLOAD_EVENTS = [
+ 'upload_completed',
+ ];
+}
diff --git a/app/Filament/Widgets/JoinTokenTrendWidget.php b/app/Filament/Widgets/JoinTokenTrendWidget.php
new file mode 100644
index 0000000..f4f8337
--- /dev/null
+++ b/app/Filament/Widgets/JoinTokenTrendWidget.php
@@ -0,0 +1,178 @@
+resolveFilters();
+ $events = $this->baseQuery($filters)->get(['event_type', 'occurred_at']);
+ $hourly = $filters['start']->diffInHours($filters['end']) <= 48;
+ $bucketFormat = $hourly ? 'Y-m-d H:00' : 'Y-m-d';
+ $labelFormat = $hourly ? 'M d H:00' : 'M d';
+
+ $periodStart = $hourly ? $filters['start']->copy()->startOfHour() : $filters['start']->copy()->startOfDay();
+ $period = CarbonPeriod::create($periodStart, $hourly ? '1 hour' : '1 day', $filters['end']);
+
+ $grouped = $events->groupBy(fn (EventJoinTokenEvent $event) => $event->occurred_at?->format($bucketFormat));
+
+ $labels = [];
+ $success = [];
+ $failures = [];
+ $rateLimited = [];
+ $uploads = [];
+
+ foreach ($period as $point) {
+ $key = $point->format($bucketFormat);
+ $bucket = $grouped->get($key, collect());
+
+ $labels[] = $point->translatedFormat($labelFormat);
+ $success[] = $bucket->whereIn('event_type', self::SUCCESS_EVENTS)->count();
+ $failures[] = $bucket->whereIn('event_type', self::FAILURE_EVENTS)->count();
+ $rateLimited[] = $bucket->whereIn('event_type', self::RATE_LIMIT_EVENTS)->count();
+ $uploads[] = $bucket->whereIn('event_type', self::UPLOAD_EVENTS)->count();
+ }
+
+ return [
+ 'datasets' => [
+ [
+ 'label' => __('admin.join_token_analytics.trend.success'),
+ 'data' => $success,
+ 'borderColor' => '#16a34a',
+ 'backgroundColor' => 'rgba(22, 163, 74, 0.2)',
+ 'tension' => 0.35,
+ 'fill' => false,
+ ],
+ [
+ 'label' => __('admin.join_token_analytics.trend.failures'),
+ 'data' => $failures,
+ 'borderColor' => '#dc2626',
+ 'backgroundColor' => 'rgba(220, 38, 38, 0.2)',
+ 'tension' => 0.35,
+ 'fill' => false,
+ ],
+ [
+ 'label' => __('admin.join_token_analytics.trend.rate_limited'),
+ 'data' => $rateLimited,
+ 'borderColor' => '#f59e0b',
+ 'backgroundColor' => 'rgba(245, 158, 11, 0.2)',
+ 'tension' => 0.35,
+ 'fill' => false,
+ ],
+ [
+ 'label' => __('admin.join_token_analytics.trend.uploads'),
+ 'data' => $uploads,
+ 'borderColor' => '#2563eb',
+ 'backgroundColor' => 'rgba(37, 99, 235, 0.2)',
+ 'tension' => 0.35,
+ 'fill' => false,
+ ],
+ ],
+ 'labels' => $labels,
+ ];
+ }
+
+ protected function getType(): string
+ {
+ return 'line';
+ }
+
+ public function getHeading(): ?string
+ {
+ return __('admin.join_token_analytics.trend.heading');
+ }
+
+ private function baseQuery(array $filters): Builder
+ {
+ $query = EventJoinTokenEvent::query()
+ ->whereBetween('occurred_at', [$filters['start'], $filters['end']]);
+
+ if ($filters['event_id']) {
+ $query->where('event_id', $filters['event_id']);
+ }
+
+ return $query;
+ }
+
+ private function resolveFilters(): array
+ {
+ $eventId = $this->pageFilters['event_id'] ?? null;
+ $eventId = is_numeric($eventId) ? (int) $eventId : null;
+ $range = is_string($this->pageFilters['range'] ?? null) ? $this->pageFilters['range'] : '24h';
+
+ [$start, $end] = $this->resolveWindow($range, $eventId);
+
+ return [
+ 'event_id' => $eventId,
+ 'range' => $range,
+ 'start' => $start,
+ 'end' => $end,
+ ];
+ }
+
+ private function resolveWindow(string $range, ?int $eventId): array
+ {
+ $now = now();
+ $start = match ($range) {
+ '2h' => $now->copy()->subHours(2),
+ '6h' => $now->copy()->subHours(6),
+ '12h' => $now->copy()->subHours(12),
+ '7d' => $now->copy()->subDays(7),
+ default => $now->copy()->subHours(24),
+ };
+ $end = $now;
+
+ if ($range === 'event_day' && $eventId) {
+ $eventDate = Event::query()->whereKey($eventId)->value('date');
+
+ if ($eventDate) {
+ $eventDay = Carbon::parse($eventDate);
+ $start = $eventDay->copy()->startOfDay();
+ $end = $eventDay->copy()->endOfDay();
+ }
+ }
+
+ return [$start, $end];
+ }
+
+ private const SUCCESS_EVENTS = [
+ 'access_granted',
+ 'gallery_access_granted',
+ ];
+
+ private const RATE_LIMIT_EVENTS = [
+ 'token_rate_limited',
+ 'access_rate_limited',
+ 'download_rate_limited',
+ ];
+
+ private const FAILURE_EVENTS = [
+ 'invalid_token',
+ 'token_expired',
+ 'token_revoked',
+ 'event_not_public',
+ 'gallery_expired',
+ 'token_rate_limited',
+ 'access_rate_limited',
+ 'download_rate_limited',
+ ];
+
+ private const UPLOAD_EVENTS = [
+ 'upload_completed',
+ ];
+}
diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php
index 2b81e9f..dda0c44 100644
--- a/app/Http/Middleware/RedirectIfAuthenticated.php
+++ b/app/Http/Middleware/RedirectIfAuthenticated.php
@@ -7,13 +7,14 @@ use Closure;
use Illuminate\Auth\Middleware\RedirectIfAuthenticated as BaseMiddleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
+use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated extends BaseMiddleware
{
/**
* Handle an incoming request.
*/
- public function handle(Request $request, Closure $next, ...$guards)
+ public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = $guards === [] ? [null] : $guards;
diff --git a/app/Providers/Filament/SuperAdminPanelProvider.php b/app/Providers/Filament/SuperAdminPanelProvider.php
index 04f2f36..743f492 100644
--- a/app/Providers/Filament/SuperAdminPanelProvider.php
+++ b/app/Providers/Filament/SuperAdminPanelProvider.php
@@ -108,13 +108,6 @@ class SuperAdminPanelProvider extends PanelProvider
->authMiddleware([
Authenticate::class,
])
- ->pages([
- Pages\Dashboard::class,
- \App\Filament\SuperAdmin\Pages\WatermarkSettingsPage::class,
- \App\Filament\SuperAdmin\Pages\GuestPolicySettingsPage::class,
- \App\Filament\SuperAdmin\Pages\OpsHealthDashboard::class,
- \App\Filament\SuperAdmin\Pages\IntegrationsHealthDashboard::class,
- ])
->authGuard('super_admin');
// SuperAdmin-Zugriff durch custom Middleware, globale Sichtbarkeit ohne Tenant-Isolation
diff --git a/resources/lang/de/admin.php b/resources/lang/de/admin.php
index 8c60cc1..c244364 100644
--- a/resources/lang/de/admin.php
+++ b/resources/lang/de/admin.php
@@ -303,6 +303,52 @@ return [
'unavailable' => 'Nicht verfügbar',
],
],
+ 'join_token_analytics' => [
+ 'navigation' => [
+ 'label' => 'Join-Token-Analytics',
+ ],
+ 'heading' => 'Join-Token-Analytics',
+ 'subheading' => 'Überwache Gastzugriffe und Join-Token-Aktivität über Events hinweg.',
+ 'filters' => [
+ 'range' => 'Zeitraum',
+ 'event' => 'Event',
+ 'event_placeholder' => 'Alle Events',
+ 'range_options' => [
+ '2h' => 'Letzte 2 Stunden',
+ '6h' => 'Letzte 6 Stunden',
+ '12h' => 'Letzte 12 Stunden',
+ '24h' => 'Letzte 24 Stunden',
+ '7d' => 'Letzte 7 Tage',
+ 'event_day' => 'Event-Tag',
+ ],
+ ],
+ 'stats' => [
+ 'success' => 'Erfolge',
+ 'failures' => 'Fehlschläge',
+ 'rate_limited' => 'Rate-Limits',
+ 'uploads' => 'Uploads',
+ 'failure_ratio' => 'Fehlerquote',
+ 'no_data' => '—',
+ ],
+ 'trend' => [
+ 'heading' => 'Join-Token-Aktivität',
+ 'success' => 'Erfolge',
+ 'failures' => 'Fehlschläge',
+ 'rate_limited' => 'Rate-Limits',
+ 'uploads' => 'Uploads',
+ ],
+ 'table' => [
+ 'heading' => 'Top-Tokens',
+ 'token' => 'Token',
+ 'event' => 'Event',
+ 'tenant' => 'Mandant',
+ 'success' => 'Erfolge',
+ 'failures' => 'Fehlschläge',
+ 'rate_limited' => 'Rate-Limits',
+ 'uploads' => 'Uploads',
+ 'last_seen' => 'Zuletzt gesehen',
+ ],
+ ],
'events' => [
'fields' => [
diff --git a/resources/lang/en/admin.php b/resources/lang/en/admin.php
index 46434c1..9ac936c 100644
--- a/resources/lang/en/admin.php
+++ b/resources/lang/en/admin.php
@@ -303,6 +303,52 @@ return [
'unavailable' => 'Unavailable',
],
],
+ 'join_token_analytics' => [
+ 'navigation' => [
+ 'label' => 'Join token analytics',
+ ],
+ 'heading' => 'Join token analytics',
+ 'subheading' => 'Monitor guest access and join-token activity across events.',
+ 'filters' => [
+ 'range' => 'Time range',
+ 'event' => 'Event',
+ 'event_placeholder' => 'All events',
+ 'range_options' => [
+ '2h' => 'Last 2 hours',
+ '6h' => 'Last 6 hours',
+ '12h' => 'Last 12 hours',
+ '24h' => 'Last 24 hours',
+ '7d' => 'Last 7 days',
+ 'event_day' => 'Event day',
+ ],
+ ],
+ 'stats' => [
+ 'success' => 'Success',
+ 'failures' => 'Failures',
+ 'rate_limited' => 'Rate limited',
+ 'uploads' => 'Uploads',
+ 'failure_ratio' => 'Failure ratio',
+ 'no_data' => '—',
+ ],
+ 'trend' => [
+ 'heading' => 'Join-token activity',
+ 'success' => 'Success',
+ 'failures' => 'Failures',
+ 'rate_limited' => 'Rate limited',
+ 'uploads' => 'Uploads',
+ ],
+ 'table' => [
+ 'heading' => 'Top tokens',
+ 'token' => 'Token',
+ 'event' => 'Event',
+ 'tenant' => 'Tenant',
+ 'success' => 'Success',
+ 'failures' => 'Failures',
+ 'rate_limited' => 'Rate limited',
+ 'uploads' => 'Uploads',
+ 'last_seen' => 'Last seen',
+ ],
+ ],
'events' => [
'fields' => [
diff --git a/resources/views/filament/clusters/daily-ops/pages/join-token-analytics-dashboard.blade.php b/resources/views/filament/clusters/daily-ops/pages/join-token-analytics-dashboard.blade.php
new file mode 100644
index 0000000..a741733
--- /dev/null
+++ b/resources/views/filament/clusters/daily-ops/pages/join-token-analytics-dashboard.blade.php
@@ -0,0 +1,3 @@
+
+@php + use Filament\Support\Enums\GridDirection; + use Filament\Support\Icons\Heroicon; + use Illuminate\View\ComponentAttributeBag; + + $stacked = (new ComponentAttributeBag())->grid(['default' => 1], GridDirection::Column); + $tokensGrid = (new ComponentAttributeBag())->grid(['default' => 1], GridDirection::Column); + $linkGrid = (new ComponentAttributeBag())->grid(['default' => 1, 'md' => 2]); + $metricsGrid = (new ComponentAttributeBag())->grid(['default' => 1, 'sm' => 2, 'xl' => 4]); + $layoutsGrid = (new ComponentAttributeBag())->grid(['default' => 1, 'md' => 2]); +@endphp + +
- {{ $token['url'] }}
-
- {{ $compose['label'] }}
-{{ $compose['compose_id'] }}
-| When | -User | -Target | -Action | -Status | -
|---|---|---|---|---|
| {{ $log['created_at'] }} | -{{ $log['user'] }} | -{{ $log['service_id'] }} | -{{ ucfirst($log['action']) }} | -{{ $log['status_code'] ?? '—' }} | -
| No actions recorded yet. | -||||
Aktuelles Basis-Wasserzeichen
-{{ $compose['label'] }}
-{{ $compose['name'] ?? '–' }}
-{{ $compose['compose_id'] }}
-{{ $compose['error'] }}
+Services
+No services reported.
+- Last deploy: {{ $compose['last_deploy'] ? \Illuminate\Support\Carbon::parse($compose['last_deploy'])->diffForHumans() : '—' }} -
@endif -No Dokploy compose stacks configured.
+{{ $provider['label'] }}
-- {{ $provider['config_label'] }}: - - {{ $provider['is_configured'] ? __('admin.integrations_health.configured') : __('admin.integrations_health.unconfigured') }} - -
-- {{ __('admin.integrations_health.empty') }} -
+