presseportale/resources/views/livewire/admin/reports/index-status.blade.php
Kevin Adametz fc700677ff web: Abruf-Protokoll der RSS-Feeds — Reader-Dienst, gemeldete Abonnenten, Admin-Bericht, Panel (Roadmap 3.1)
- feed_fetches: jeder Feed-Abruf mit Art, Schlüssel, Portal, Sprache,
  erkanntem Reader (config/feeds.php, FeedReaderIdentifier), vom Dienst
  gemeldeter Abonnentenzahl und gekürztem User-Agent — keine IP. Schreiben
  per defer() nach der Antwort, auch bei Cache-Treffer.
- Aufbewahrung 395 Tage über press-releases:purge-events;
  FEED_FETCHES_RECORDED_SINCE.
- Admin ▸ Reports ▸ Indexierung & Abrufe: Block „Abrufe der RSS-Feeds"
  (Summen je Feed-Art, je Reader-Dienst, meistabgerufene Feeds;
  FeedFetchReport, Verdichtung in PHP für MySQL und SQLite).
- Panel „Auslieferung": Abrufe je Feed in 30 Tagen; RSS-Seite nennt die
  Zählung ohne IP.
- Tests: FeedFetchLogTest (5), IndexStatusCheckTest erweitert.
- dev/2026-09-01: Nachtrag Protokoll Teil 11, Roadmap 3.1; 0.7 gilt auch
  für Crawler- und Feed-Log.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 09:00:09 +00:00

474 lines
31 KiB
PHP

<?php
use App\Enums\IndexState;
use App\Enums\Portal;
use App\Services\PressRelease\CrawlerHitReport;
use App\Services\PressRelease\FeedFetchReport;
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 und Abrufe (Roadmap 1c.0, 1c.4, 1c.9).
*
* 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.
* - Wie oft werden die RSS-Feeds geholt, von welchen Readern? Feed-Abruf-
* Protokoll (3.1) — Abrufe und gemeldete Abonnenten, keine eigene Zählung.
*
* Der Kunde sieht davon nur, was GSC_CUSTOMER_VISIBLE freigibt (E8).
*/
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, $checker): void {
$handle = fopen('php://output', 'wb');
fputcsv($handle, ['Geprüft', 'Portal', 'ID', 'Titel', 'URL', 'Befund', 'Google-Status', 'Zuletzt gecrawlt', 'Google führt', 'Kanonisch (Google)'], ';');
foreach ($rows as $check) {
fputcsv($handle, [
$check->checked_at->format('d.m.Y H:i'),
$check->portal->label(),
$check->press_release_id,
$check->pressRelease?->title ?? '',
$check->url,
$check->state->label(),
$check->coverage_state ?? '',
$check->last_crawl_time?->format('d.m.Y') ?? '',
$this->sourceLabel($checker->sourceVerdict($check)['kind']),
$check->google_canonical ?? '',
], ';');
}
fclose($handle);
}, $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);
$feedFetches = app(FeedFetchReport::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,
'feedTotals' => $feedFetches->totals($days),
'feedReaders' => $feedFetches->byReader($days),
'feedTop' => $feedFetches->topFeeds($days),
'feedsSince' => config('feeds.recorded_since') ? \Illuminate\Support\Carbon::parse((string) config('feeds.recorded_since')) : null,
'days' => $days,
];
}
}; ?>
<div class="space-y-8">
<x-portal.page-header
:badge="__('Admin Backend')"
:eyebrow="__('Administration · Reports')"
: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-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 — 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() }} · {{ __('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 · Rolle') }}</flux:table.column>
<flux:table.column>{{ __('Meldungen') }}</flux:table.column>
@foreach ($states as $state)
<flux:table.column>{{ ucfirst($state->label()) }}</flux:table.column>
@endforeach
<flux:table.column>{{ __('Ungeprüft') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@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-[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>
<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>
{{-- Feed-Abruf-Protokoll (3.1): Abrufe der RSS-Feeds, gemeldete Abonnenten. --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Abrufe der RSS-Feeds') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)] tabular-nums">
{{ __(':n Abrufe in :days Tagen', ['n' => number_format($feedTotals['total'], 0, ',', '.'), 'days' => $days]) }}
· {{ __('Portal :n', ['n' => number_format($feedTotals['portal'], 0, ',', '.')]) }}
· {{ __('Branchen :n', ['n' => number_format($feedTotals['category'], 0, ',', '.')]) }}
· {{ __('Newsrooms :n', ['n' => number_format($feedTotals['newsroom'], 0, ',', '.')]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Reader-Dienst') }}</flux:table.column>
<flux:table.column>{{ __('Abrufe :days Tage', ['days' => $days]) }}</flux:table.column>
<flux:table.column>{{ __('Feeds') }}</flux:table.column>
<flux:table.column>{{ __('Gemeldete Abonnenten') }}</flux:table.column>
<flux:table.column>{{ __('Letzter Abruf') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse ($feedReaders as $reader)
<flux:table.row :key="'feed-reader-'.($reader['reader'] ?? 'other')">
<flux:table.cell><span class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $reader['label'] }}</span></flux:table.cell>
<flux:table.cell><span class="text-[12.5px] tabular-nums text-[color:var(--color-ink-2)]">{{ number_format($reader['fetches'], 0, ',', '.') }}</span></flux:table.cell>
<flux:table.cell><span class="text-[12.5px] tabular-nums text-[color:var(--color-ink-2)]">{{ number_format($reader['feeds'], 0, ',', '.') }}</span></flux:table.cell>
<flux:table.cell><span class="text-[12.5px] tabular-nums text-[color:var(--color-ink-2)]">{{ $reader['subscribers'] === null ? '—' : number_format($reader['subscribers'], 0, ',', '.') }}</span></flux:table.cell>
<flux:table.cell><span class="text-[12px] tabular-nums text-[color:var(--color-ink-3)]">{{ $reader['last_at']->format('d.m.Y H:i') }}</span></flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="5">
<div class="px-4 py-10 text-center text-[12.5px] text-[color:var(--color-ink-3)]">
{{ __('Noch kein Feed-Abruf protokolliert.') }}
@if ($feedsSince)
{{ __('Erfasst seit :date.', ['date' => $feedsSince->format('d.m.Y')]) }}
@endif
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
@if ($feedTop !== [])
<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 abgerufene Feeds, :days Tage', ['days' => $days]) }}</div>
<ul class="px-5 pb-4 m-0 list-none space-y-1">
@foreach ($feedTop as $top)
<li class="flex flex-wrap items-baseline justify-between gap-x-3 text-[12.5px]">
<span class="text-[color:var(--color-ink)] truncate max-w-[70%]">{{ $top['label'] }}</span>
<span class="tabular-nums text-[color:var(--color-ink-3)]">{{ trans_choice(':n Abruf|:n Abrufe', $top['fetches'], ['n' => $top['fetches']]) }} · {{ trans_choice(':n Reader|:n Reader', $top['readers'], ['n' => $top['readers']]) }}@if ($top['subscribers'] !== null) · {{ __(':n Abonnenten gemeldet', ['n' => $top['subscribers']]) }}@endif</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)]">
{{ __('Gezählt werden Abrufe der Feed-Adressen; ein Reader-Dienst holt einen Feed einmal für alle seine Nutzer. „Gemeldete Abonnenten" ist die Zahl, die Dienste wie Feedly oder Inoreader selbst im User-Agent nennen — je Feed das Maximum, summiert. Erkennung über den User-Agent (config/feeds.php), keine IP-Adressen.') }}
@if ($feedsSince)
{{ __('Protokoll seit :date.', ['date' => $feedsSince->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>
@if ($latest->isNotEmpty())
<flux:button size="sm" variant="filled" icon="arrow-down-tray" wire:click="downloadLatest">{{ __('CSV') }}</flux:button>
@endif
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Geprüft') }}</flux:table.column>
<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') ?? '—']) }}@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>
</flux:table.cell>
<flux:table.cell>
<span @class(['badge dot', 'ok' => $check->state === IndexState::Indexed, 'warn' => in_array($check->state, [IndexState::CrawledNotIndexed, IndexState::NotCrawled], true), 'err' => $check->state === IndexState::Excluded, 'muted' => $check->state === IndexState::Unknown])>{{ $check->state->label() }}</span>
@if ($check->coverage_state)
<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="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>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
</article>
</div>