admin: Bericht „Indexierung & Abrufe" — Rolle Erstquelle/Kopie, Quellenbefund, Crawler-Abrufe; Schlüsselpfad ab Projektwurzel
Der Bericht meldete auf Live „nicht konfiguriert", während der Lauf längst Daten holte: der relative Pfad der Schlüsseldatei war im Cron (Projektwurzel) lesbar, im Web-Request (public/) nicht. Relative Pfade werden jetzt ab base_path() aufgelöst. Die Verteilung trennt zusätzlich nach Rolle in der kanonischen Zuordnung (1c.8): bei einer Kopie mit Verweis ist „ausgeschlossen" der erwartete Befund. Neu: Quellenbefund über Erstquellen (welche Adresse Google führt, fremde Hosts), Abrufe durch Such- und KI-Dienste je Dienst, Tag und Meldung mit CSV. Herkunftsmarker als Join statt korreliertem EXISTS, Aggregate 15 Minuten gecacht. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1c421c0377
commit
c26bdff681
8 changed files with 609 additions and 87 deletions
|
|
@ -232,7 +232,7 @@
|
|||
</flux:navlist.item>
|
||||
<flux:navlist.item icon="magnifying-glass-circle" :href="route('admin.reports.index-status')"
|
||||
:current="request()->routeIs('admin.reports.index-status')" wire:navigate>
|
||||
{{ __('Indexierung') }}
|
||||
{{ __('Indexierung & Abrufe') }}
|
||||
</flux:navlist.item>
|
||||
@can('settings:manage')
|
||||
<flux:navlist.item icon="heart" :href="route('admin.reports.system-health')"
|
||||
|
|
|
|||
|
|
@ -2,31 +2,43 @@
|
|||
|
||||
use App\Enums\IndexState;
|
||||
use App\Enums\Portal;
|
||||
use App\Services\PressRelease\CrawlerHitReport;
|
||||
use App\Services\SearchConsole\IndexStatusChecker;
|
||||
use App\Services\SearchConsole\IndexStatusReport;
|
||||
use App\Services\SearchConsole\SearchConsoleClient;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Volt\Component;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
* Admin ▸ Reports ▸ Indexierung (Roadmap 1c.4).
|
||||
* Admin ▸ Reports ▸ Indexierung und Abrufe (Roadmap 1c.0, 1c.4, 1c.9).
|
||||
*
|
||||
* Verteilung der jüngsten Search-Console-Befunde je Portal, getrennt nach
|
||||
* Direkteinreichung und Syndikation über die API. Diese Verteilung ist die
|
||||
* Grundlage für 1c.5 (Kundenansicht ja/nein, E8) — der Kunde sieht bis
|
||||
* dahin nichts davon.
|
||||
* Drei Fragen, drei Quellen:
|
||||
* - Ist die Meldung im Google-Index? Search Console, jüngster Befund je
|
||||
* Meldung, getrennt nach Herkunft (direkt/API) und Rolle (Erstquelle/
|
||||
* Kopie mit rel=canonical) — die Verteilung ist die Grundlage für 1c.5.
|
||||
* - Welche Adresse führt Google als Quelle? Quellenbefund über
|
||||
* googleCanonical, nur für Erstquellen.
|
||||
* - Welche Such- und KI-Dienste holen die Seiten? Crawler-Abruf-Protokoll.
|
||||
*
|
||||
* Der Kunde sieht davon nur, was GSC_CUSTOMER_VISIBLE freigibt (E8).
|
||||
*/
|
||||
new #[Layout('components.layouts.app'), Title('Indexierung')] class extends Component
|
||||
new #[Layout('components.layouts.app'), Title('Indexierung und Abrufe')] class extends Component
|
||||
{
|
||||
#[Url]
|
||||
public int $days = 30;
|
||||
|
||||
public function downloadLatest(): StreamedResponse
|
||||
{
|
||||
$rows = app(IndexStatusReport::class)->latest(2000);
|
||||
$checker = app(IndexStatusChecker::class);
|
||||
$name = sprintf('indexierung-%s.csv', now()->format('Y-m-d'));
|
||||
|
||||
return response()->streamDownload(function () use ($rows): void {
|
||||
return response()->streamDownload(function () use ($rows, $checker): void {
|
||||
$handle = fopen('php://output', 'wb');
|
||||
fputcsv($handle, ['Geprüft', 'Portal', 'ID', 'Titel', 'URL', 'Befund', 'Google-Status', 'Zuletzt gecrawlt', 'Kanonisch'], ';');
|
||||
fputcsv($handle, ['Geprüft', 'Portal', 'ID', 'Titel', 'URL', 'Befund', 'Google-Status', 'Zuletzt gecrawlt', 'Google führt', 'Kanonisch (Google)'], ';');
|
||||
|
||||
foreach ($rows as $check) {
|
||||
fputcsv($handle, [
|
||||
|
|
@ -38,6 +50,7 @@ new #[Layout('components.layouts.app'), Title('Indexierung')] class extends Comp
|
|||
$check->state->label(),
|
||||
$check->coverage_state ?? '',
|
||||
$check->last_crawl_time?->format('d.m.Y') ?? '',
|
||||
$this->sourceLabel($checker->sourceVerdict($check)['kind']),
|
||||
$check->google_canonical ?? '',
|
||||
], ';');
|
||||
}
|
||||
|
|
@ -46,18 +59,56 @@ new #[Layout('components.layouts.app'), Title('Indexierung')] class extends Comp
|
|||
}, $name, ['Content-Type' => 'text/csv; charset=UTF-8']);
|
||||
}
|
||||
|
||||
public function downloadCrawlers(): StreamedResponse
|
||||
{
|
||||
$rows = app(CrawlerHitReport::class)->byService($this->days);
|
||||
$name = sprintf('crawler-abrufe-%s.csv', now()->format('Y-m-d'));
|
||||
|
||||
return response()->streamDownload(function () use ($rows): void {
|
||||
$handle = fopen('php://output', 'wb');
|
||||
fputcsv($handle, ['Dienst', 'Art', sprintf('Abrufe %d Tage', $this->days), sprintf('Meldungen %d Tage', $this->days), 'Abrufe gesamt', 'Letzter Abruf'], ';');
|
||||
|
||||
foreach ($rows as $row) {
|
||||
fputcsv($handle, [$row['label'], $row['kind'] === 'search' ? 'Suche' : 'KI', $row['fetches'], $row['releases'], $row['fetches_total'], $row['last_at']->format('d.m.Y H:i')], ';');
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
}, $name, ['Content-Type' => 'text/csv; charset=UTF-8']);
|
||||
}
|
||||
|
||||
public function sourceLabel(string $kind): string
|
||||
{
|
||||
return match ($kind) {
|
||||
'self' => __('diese Adresse'),
|
||||
'own_other' => __('eigene Fassung, anderes Portal'),
|
||||
'foreign' => __('fremdes Portal'),
|
||||
default => __('keine Angabe'),
|
||||
};
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
$report = app(IndexStatusReport::class);
|
||||
$checker = app(IndexStatusChecker::class);
|
||||
$crawlers = app(CrawlerHitReport::class);
|
||||
$days = in_array($this->days, [7, 30, 90], true) ? $this->days : 30;
|
||||
|
||||
return [
|
||||
'configured' => app(SearchConsoleClient::class)->isConfigured(),
|
||||
'customerVisible' => (bool) config('search_console.customer_visible'),
|
||||
'canonicalActive' => (bool) config('canonical.enabled'),
|
||||
'distribution' => $report->distribution(),
|
||||
'sources' => $report->sourceSummary($checker),
|
||||
'latest' => $report->latest(100),
|
||||
'lastCheckedAt' => $report->lastCheckedAt(),
|
||||
'states' => IndexState::cases(),
|
||||
'portals' => [Portal::Presseecho, Portal::Businessportal24],
|
||||
'checker' => $checker,
|
||||
'crawlerServices' => $crawlers->byService($days),
|
||||
'crawlerDaily' => $crawlers->daily(14),
|
||||
'crawlerTop' => $crawlers->topReleases($days),
|
||||
'crawlersSince' => config('crawlers.recorded_since') ? \Illuminate\Support\Carbon::parse((string) config('crawlers.recorded_since')) : null,
|
||||
'days' => $days,
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
|
@ -66,31 +117,34 @@ new #[Layout('components.layouts.app'), Title('Indexierung')] class extends Comp
|
|||
<x-portal.page-header
|
||||
:badge="__('Admin Backend')"
|
||||
:eyebrow="__('Administration · Reports')"
|
||||
:title="__('Indexierung')"
|
||||
:description="__('Stand der jüngsten Search-Console-Prüfung je Meldung, getrennt nach Direkteinreichung und Syndikation über die API. „Noch nicht gecrawlt“ und „gecrawlt, nicht indexiert“ sind verschiedene Befunde. Die Herkunft ist nur für Meldungen aus dem neuen System belegbar — der Altbestand ohne Status-Log zählt als direkt. Der Kunde sieht diese Daten erst, wenn die Anzeige freigegeben ist.')"
|
||||
:title="__('Indexierung und Abrufe')"
|
||||
:description="__('Stand der jüngsten Search-Console-Prüfung je Meldung, getrennt nach Herkunft (direkt oder API) und Rolle (Erstquelle oder Kopie mit Verweis). „Noch nicht gecrawlt“ und „gecrawlt, nicht indexiert“ sind verschiedene Befunde; bei einer Kopie ist „ausgeschlossen“ der erwartete Befund. Die Herkunft ist nur für Meldungen aus dem neuen System belegbar — der Altbestand ohne Status-Log zählt als direkt. Darunter: welche Adresse Google als Quelle führt und welche Such- und KI-Dienste die Seiten holen.')"
|
||||
/>
|
||||
|
||||
<section class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<section class="grid grid-cols-1 gap-4 sm:grid-cols-4">
|
||||
<x-portal.stat-card :variant="$configured ? 'primary' : 'warn'" :label="__('Search Console')" :value="$configured ? __('verbunden') : __('nicht konfiguriert')">
|
||||
<x-slot:meta>{{ $configured ? __('täglicher Lauf 05:10 Uhr') : __('GSC_ENABLED und Schlüsseldatei setzen') }}</x-slot:meta>
|
||||
</x-portal.stat-card>
|
||||
<x-portal.stat-card variant="muted" :label="__('Letzte Prüfung')" :value="$lastCheckedAt?->format('d.m.Y H:i') ?? '—'">
|
||||
<x-slot:meta>{{ __('Momentaufnahmen, jede Antwort eine Zeile') }}</x-slot:meta>
|
||||
</x-portal.stat-card>
|
||||
<x-portal.stat-card :variant="$canonicalActive ? 'primary' : 'muted'" :label="__('Kanonische Zuordnung')" :value="$canonicalActive ? __('aktiv') : __('aus')">
|
||||
<x-slot:meta>{{ __('Kopien verweisen auf die Erstquelle (1c.8, E9)') }}</x-slot:meta>
|
||||
</x-portal.stat-card>
|
||||
<x-portal.stat-card :variant="$customerVisible ? 'warn' : 'muted'" :label="__('Kundenansicht')" :value="$customerVisible ? __('offen') : __('geschlossen')">
|
||||
<x-slot:meta>{{ __('Entscheidung E8 — config search_console.customer_visible') }}</x-slot:meta>
|
||||
<x-slot:meta>{{ __('Entscheidung E8 — GSC_CUSTOMER_VISIBLE') }}</x-slot:meta>
|
||||
</x-portal.stat-card>
|
||||
</section>
|
||||
|
||||
@foreach ($portals as $portal)
|
||||
<article class="panel overflow-hidden">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ $portal->label() }}</span>
|
||||
<span class="section-eyebrow">{{ $portal->label() }} · {{ __('Google-Index') }}</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ __('im Archiv sichtbare Meldungen, Stand der jeweils jüngsten Prüfung') }}</span>
|
||||
</div>
|
||||
<flux:table>
|
||||
<flux:table.columns>
|
||||
<flux:table.column>{{ __('Herkunft') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Herkunft · Rolle') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Meldungen') }}</flux:table.column>
|
||||
@foreach ($states as $state)
|
||||
<flux:table.column>{{ ucfirst($state->label()) }}</flux:table.column>
|
||||
|
|
@ -98,37 +152,193 @@ new #[Layout('components.layouts.app'), Title('Indexierung')] class extends Comp
|
|||
<flux:table.column>{{ __('Ungeprüft') }}</flux:table.column>
|
||||
</flux:table.columns>
|
||||
<flux:table.rows>
|
||||
@foreach (['direct' => __('Direkt eingereicht'), 'api' => __('Syndikation (API)')] as $origin => $label)
|
||||
@php
|
||||
$row = $distribution[$portal->value][$origin] ?? [];
|
||||
$checkedTotal = max(0, ($row['total'] ?? 0) - ($row['unchecked'] ?? 0));
|
||||
@endphp
|
||||
<flux:table.row :key="$portal->value.'-'.$origin">
|
||||
<flux:table.cell>
|
||||
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $label }}</span>
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
<span class="text-[12.5px] tabular-nums text-[color:var(--color-ink-2)]">{{ number_format($row['total'] ?? 0, 0, ',', '.') }}</span>
|
||||
</flux:table.cell>
|
||||
@foreach ($states as $state)
|
||||
@php $count = (int) ($row[$state->value] ?? 0); @endphp
|
||||
@foreach (['direct' => __('Direkt eingereicht'), 'api' => __('Syndikation (API)')] as $origin => $originLabel)
|
||||
@foreach (['source' => __('Erstquelle'), 'copy' => __('Kopie mit Verweis')] as $role => $roleLabel)
|
||||
@php
|
||||
$row = $distribution[$portal->value][$origin][$role] ?? [];
|
||||
$checkedTotal = max(0, ($row['total'] ?? 0) - ($row['unchecked'] ?? 0));
|
||||
@endphp
|
||||
@continue(($row['total'] ?? 0) === 0 && $role === 'copy')
|
||||
<flux:table.row :key="$portal->value.'-'.$origin.'-'.$role">
|
||||
<flux:table.cell>
|
||||
<span class="text-[12.5px] tabular-nums text-[color:var(--color-ink-2)]">{{ number_format($count, 0, ',', '.') }}</span>
|
||||
@if ($checkedTotal > 0)
|
||||
<span class="block text-[11px] tabular-nums text-[color:var(--color-ink-4)]">{{ number_format($count / $checkedTotal * 100, 1, ',', '.') }} %</span>
|
||||
@endif
|
||||
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $originLabel }}</span>
|
||||
<span class="block text-[11px] text-[color:var(--color-ink-4)]">{{ $roleLabel }}</span>
|
||||
</flux:table.cell>
|
||||
@endforeach
|
||||
<flux:table.cell>
|
||||
<span class="text-[12.5px] tabular-nums text-[color:var(--color-ink-3)]">{{ number_format($row['unchecked'] ?? 0, 0, ',', '.') }}</span>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
<flux:table.cell>
|
||||
<span class="text-[12.5px] tabular-nums text-[color:var(--color-ink-2)]">{{ number_format($row['total'] ?? 0, 0, ',', '.') }}</span>
|
||||
</flux:table.cell>
|
||||
@foreach ($states as $state)
|
||||
@php
|
||||
$count = (int) ($row[$state->value] ?? 0);
|
||||
$expected = $role === 'copy' ? $state === IndexState::Excluded : $state === IndexState::Indexed;
|
||||
@endphp
|
||||
<flux:table.cell>
|
||||
<span @class(['text-[12.5px] tabular-nums', 'font-semibold text-[color:var(--color-ink)]' => $expected && $count > 0, 'text-[color:var(--color-ink-2)]' => ! ($expected && $count > 0)])>{{ number_format($count, 0, ',', '.') }}</span>
|
||||
@if ($checkedTotal > 0)
|
||||
<span class="block text-[11px] tabular-nums text-[color:var(--color-ink-4)]">{{ number_format($count / $checkedTotal * 100, 1, ',', '.') }} %</span>
|
||||
@endif
|
||||
</flux:table.cell>
|
||||
@endforeach
|
||||
<flux:table.cell>
|
||||
<span class="text-[12.5px] tabular-nums text-[color:var(--color-ink-3)]">{{ number_format($row['unchecked'] ?? 0, 0, ',', '.') }}</span>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</flux:table.rows>
|
||||
</flux:table>
|
||||
<div class="px-5 py-3 border-t border-[color:var(--color-bg-rule)] text-[11.5px] text-[color:var(--color-ink-4)]">
|
||||
{{ __('Prozentwerte beziehen sich auf die geprüften Meldungen der Zeile. Fett: der Befund, der für die Rolle erwartet wird — Erstquelle indexiert, Kopie ausgeschlossen zugunsten der Erstquelle.') }}
|
||||
</div>
|
||||
</article>
|
||||
@endforeach
|
||||
|
||||
{{-- Quellenbefund (1c.9): welche Adresse Google als maßgeblich führt.
|
||||
Nur Erstquellen — bei Kopien ist die Abweichung gewollt. --}}
|
||||
<article class="panel overflow-hidden">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Quellenbefund') }}</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ __('Erstquellen mit Prüfung: welche Adresse Google als maßgeblich führt') }}</span>
|
||||
</div>
|
||||
<flux:table>
|
||||
<flux:table.columns>
|
||||
<flux:table.column>{{ __('Portal') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Geprüft') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Diese Adresse') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Eigene Fassung, anderes Portal') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Fremdes Portal') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Keine Angabe') }}</flux:table.column>
|
||||
</flux:table.columns>
|
||||
<flux:table.rows>
|
||||
@foreach ($portals as $portal)
|
||||
@php $row = $sources[$portal->value]; @endphp
|
||||
<flux:table.row :key="'src-'.$portal->value">
|
||||
<flux:table.cell><span class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $portal->label() }}</span></flux:table.cell>
|
||||
<flux:table.cell><span class="text-[12.5px] tabular-nums text-[color:var(--color-ink-2)]">{{ number_format($row['total'], 0, ',', '.') }}</span></flux:table.cell>
|
||||
@foreach (['self', 'own_other', 'foreign', 'unknown'] as $kind)
|
||||
<flux:table.cell>
|
||||
<span class="text-[12.5px] tabular-nums text-[color:var(--color-ink-2)]">{{ number_format($row[$kind], 0, ',', '.') }}</span>
|
||||
@if ($row['total'] > 0)
|
||||
<span class="block text-[11px] tabular-nums text-[color:var(--color-ink-4)]">{{ number_format($row[$kind] / $row['total'] * 100, 1, ',', '.') }} %</span>
|
||||
@endif
|
||||
@if ($kind === 'foreign' && $row['foreign_hosts'] !== [])
|
||||
<span class="block text-[11px] text-[color:var(--color-ink-4)] mt-1">{{ collect($row['foreign_hosts'])->map(fn ($n, $host) => $host.' '.$n)->implode(' · ') }}</span>
|
||||
@endif
|
||||
</flux:table.cell>
|
||||
@endforeach
|
||||
</flux:table.row>
|
||||
@endforeach
|
||||
</flux:table.rows>
|
||||
</flux:table>
|
||||
<div class="px-5 py-3 border-t border-[color:var(--color-bg-rule)] text-[11.5px] text-[color:var(--color-ink-4)]">
|
||||
{{ __('„Keine Angabe“ heißt: Google nennt keine kanonische Adresse — meist, weil die Seite noch nicht gecrawlt oder nicht indexiert ist. Warum Google eine bestimmte Fassung wählt, ist nicht bekannt.') }}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{{-- Crawler-Abruf-Protokoll (1c.0): Abrufe, keine Zitate. --}}
|
||||
<article class="panel overflow-hidden">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Abrufe durch Such- und KI-Dienste') }}</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-1">
|
||||
@foreach ([7, 30, 90] as $range)
|
||||
<button type="button" wire:click="$set('days', {{ $range }})"
|
||||
@class([
|
||||
'px-2 py-0.5 rounded-[4px] text-[11px] font-semibold transition-colors',
|
||||
'bg-[color:var(--color-hub)] text-white' => $days === $range,
|
||||
'text-[color:var(--color-ink-3)] hover:bg-[color:var(--color-bg-elev)]' => $days !== $range,
|
||||
])>{{ __(':days Tage', ['days' => $range]) }}</button>
|
||||
@endforeach
|
||||
</div>
|
||||
@if ($crawlerServices !== [])
|
||||
<flux:button size="sm" variant="filled" icon="arrow-down-tray" wire:click="downloadCrawlers">{{ __('CSV') }}</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$dailyPeak = max(1, (int) collect($crawlerDaily)->max(fn ($d) => $d['search'] + $d['ai']));
|
||||
@endphp
|
||||
<div class="px-5 pt-4 pb-2">
|
||||
<div class="flex items-baseline justify-between mb-2">
|
||||
<span class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Abrufe je Tag, letzte 14 Tage') }}</span>
|
||||
<span class="text-[11px] text-[color:var(--color-ink-4)]">
|
||||
<span class="inline-block size-2 rounded-[2px] bg-[color:var(--color-hub)] align-middle"></span> {{ __('KI') }}
|
||||
<span class="inline-block size-2 rounded-[2px] bg-[color:var(--color-ink-4)] align-middle ml-2"></span> {{ __('Suche') }}
|
||||
· {{ __('Spitze :n', ['n' => number_format($dailyPeak, 0, ',', '.')]) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-end gap-1 h-16">
|
||||
@foreach ($crawlerDaily as $day)
|
||||
@php $total = $day['search'] + $day['ai']; @endphp
|
||||
<div class="flex-1 flex flex-col justify-end h-full" title="{{ $day['date']->format('d.m.Y') }}: {{ $day['ai'] }} KI · {{ $day['search'] }} Suche">
|
||||
<div class="bg-[color:var(--color-hub)] rounded-t-[2px]" style="height: {{ $total > 0 ? max(2, round($day['ai'] / $dailyPeak * 100)) : 0 }}%"></div>
|
||||
<div class="bg-[color:var(--color-ink-4)]" style="height: {{ $total > 0 ? max($day['search'] > 0 ? 2 : 0, round($day['search'] / $dailyPeak * 100)) : 2 }}%"></div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="flex justify-between mt-1 text-[10.5px] tabular-nums text-[color:var(--color-ink-4)]">
|
||||
<span>{{ $crawlerDaily[0]['date']->format('d.m.') }}</span>
|
||||
<span>{{ __('heute') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:table>
|
||||
<flux:table.columns>
|
||||
<flux:table.column>{{ __('Dienst') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Art') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Abrufe :days Tage', ['days' => $days]) }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Meldungen :days Tage', ['days' => $days]) }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Abrufe gesamt') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Letzter Abruf') }}</flux:table.column>
|
||||
</flux:table.columns>
|
||||
<flux:table.rows>
|
||||
@forelse ($crawlerServices as $service)
|
||||
<flux:table.row :key="'crawler-'.$service['service']">
|
||||
<flux:table.cell><span class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $service['label'] }}</span></flux:table.cell>
|
||||
<flux:table.cell><span class="badge {{ $service['kind'] === 'search' ? 'muted' : 'hub' }}">{{ $service['kind'] === 'search' ? __('Suche') : __('KI') }}</span></flux:table.cell>
|
||||
<flux:table.cell><span class="text-[12.5px] tabular-nums text-[color:var(--color-ink-2)]">{{ number_format($service['fetches'], 0, ',', '.') }}</span></flux:table.cell>
|
||||
<flux:table.cell><span class="text-[12.5px] tabular-nums text-[color:var(--color-ink-2)]">{{ number_format($service['releases'], 0, ',', '.') }}</span></flux:table.cell>
|
||||
<flux:table.cell><span class="text-[12.5px] tabular-nums text-[color:var(--color-ink-3)]">{{ number_format($service['fetches_total'], 0, ',', '.') }}</span></flux:table.cell>
|
||||
<flux:table.cell><span class="text-[12px] tabular-nums text-[color:var(--color-ink-3)]">{{ $service['last_at']->format('d.m.Y H:i') }}</span></flux:table.cell>
|
||||
</flux:table.row>
|
||||
@empty
|
||||
<flux:table.row>
|
||||
<flux:table.cell colspan="6">
|
||||
<div class="px-4 py-10 text-center text-[12.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('Noch kein Abruf protokolliert.') }}
|
||||
@if ($crawlersSince)
|
||||
{{ __('Erfasst seit :date.', ['date' => $crawlersSince->format('d.m.Y')]) }}
|
||||
@endif
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@endforelse
|
||||
</flux:table.rows>
|
||||
</flux:table>
|
||||
|
||||
@if ($crawlerTop !== [])
|
||||
<div class="border-t border-[color:var(--color-bg-rule)]">
|
||||
<div class="px-5 pt-4 pb-2 text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Am häufigsten abgerufen, :days Tage', ['days' => $days]) }}</div>
|
||||
<ul class="px-5 pb-4 m-0 list-none space-y-1">
|
||||
@foreach ($crawlerTop as $top)
|
||||
<li class="flex flex-wrap items-baseline justify-between gap-x-3 text-[12.5px]">
|
||||
<a href="{{ route('admin.press-releases.show', $top['press_release_id']) }}" class="text-[color:var(--color-ink)] hover:underline truncate max-w-[70%]">{{ $top['title'] }}</a>
|
||||
<span class="tabular-nums text-[color:var(--color-ink-3)]">{{ trans_choice(':n Abruf|:n Abrufe', $top['fetches'], ['n' => $top['fetches']]) }} · {{ trans_choice(':n Dienst|:n Dienste', $top['services'], ['n' => $top['services']]) }}</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="px-5 py-3 border-t border-[color:var(--color-bg-rule)] text-[11.5px] text-[color:var(--color-ink-4)]">
|
||||
{{ __('Ein Abruf belegt, dass der Crawler des Dienstes die Seite geholt hat — nicht, dass sie dort angezeigt oder zitiert wird. Erkennung über den User-Agent (config/crawlers.php), keine IP-Adressen.') }}
|
||||
@if ($crawlersSince)
|
||||
{{ __('Protokoll seit :date.', ['date' => $crawlersSince->format('d.m.Y')]) }}
|
||||
@endif
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel overflow-hidden">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Jüngste Prüfungen') }}</span>
|
||||
|
|
@ -142,17 +352,19 @@ new #[Layout('components.layouts.app'), Title('Indexierung')] class extends Comp
|
|||
<flux:table.column>{{ __('Meldung') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Portal') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Befund') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Google führt') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Zuletzt gecrawlt') }}</flux:table.column>
|
||||
</flux:table.columns>
|
||||
<flux:table.rows>
|
||||
@forelse ($latest as $check)
|
||||
@php $source = $checker->sourceVerdict($check); @endphp
|
||||
<flux:table.row :key="$check->id">
|
||||
<flux:table.cell>
|
||||
<span class="text-[12px] tabular-nums text-[color:var(--color-ink-3)]">{{ $check->checked_at->format('d.m.Y H:i') }}</span>
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
<a href="{{ route('admin.press-releases.show', $check->press_release_id) }}" class="text-[13px] font-semibold text-[color:var(--color-ink)] hover:underline">{{ $check->pressRelease?->title ?? '#'.$check->press_release_id }}</a>
|
||||
<span class="block text-[11px] text-[color:var(--color-ink-4)]">{{ __('veröffentlicht :date', ['date' => $check->pressRelease?->published_at?->format('d.m.Y') ?? '—']) }}</span>
|
||||
<span class="block text-[11px] text-[color:var(--color-ink-4)]">{{ __('veröffentlicht :date', ['date' => $check->pressRelease?->published_at?->format('d.m.Y') ?? '—']) }}@if ($check->pressRelease?->canonical_press_release_id) · {{ __('Kopie mit Verweis') }}@endif</span>
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
<span class="text-[12.5px] text-[color:var(--color-ink-2)]">{{ $check->portal->label() }}</span>
|
||||
|
|
@ -163,13 +375,19 @@ new #[Layout('components.layouts.app'), Title('Indexierung')] class extends Comp
|
|||
<span class="block text-[11px] text-[color:var(--color-ink-4)] mt-1">{{ $check->coverage_state }}</span>
|
||||
@endif
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
<span class="text-[12px] text-[color:var(--color-ink-2)]">{{ $this->sourceLabel($source['kind']) }}</span>
|
||||
@if (in_array($source['kind'], ['own_other', 'foreign'], true))
|
||||
<span class="block text-[11px] text-[color:var(--color-ink-4)] break-all">{{ $source['host'] }}</span>
|
||||
@endif
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
<span class="text-[12px] text-[color:var(--color-ink-3)]">{{ $check->last_crawl_time?->format('d.m.Y') ?? '—' }}</span>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@empty
|
||||
<flux:table.row>
|
||||
<flux:table.cell colspan="5">
|
||||
<flux:table.cell colspan="6">
|
||||
<div class="px-4 py-10 text-center text-[12.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('Noch keine Prüfung gespeichert. Erster Lauf: php artisan press-releases:check-index-status') }}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue