- 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>
474 lines
31 KiB
PHP
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>
|