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>
This commit is contained in:
parent
ff75159b8f
commit
fc700677ff
18 changed files with 794 additions and 13 deletions
|
|
@ -3,6 +3,7 @@
|
|||
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;
|
||||
|
|
@ -22,6 +23,8 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
|
|||
* - 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).
|
||||
*/
|
||||
|
|
@ -91,6 +94,7 @@ new #[Layout('components.layouts.app'), Title('Indexierung und Abrufe')] class e
|
|||
$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 [
|
||||
|
|
@ -108,6 +112,10 @@ new #[Layout('components.layouts.app'), Title('Indexierung und Abrufe')] class e
|
|||
'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,
|
||||
];
|
||||
}
|
||||
|
|
@ -339,6 +347,72 @@ new #[Layout('components.layouts.app'), Title('Indexierung und Abrufe')] class e
|
|||
</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>
|
||||
|
|
|
|||
|
|
@ -1604,10 +1604,13 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
@if (($visibility['feeds'] ?? []) !== [])
|
||||
<ul class="mt-1 m-0 p-0 list-none space-y-0.5 text-[12.5px]">
|
||||
@foreach ($visibility['feeds'] as $feed)
|
||||
<li><a href="{{ $feed['url'] }}" target="_blank" rel="noopener" class="text-[color:var(--color-ink-2)] underline underline-offset-2 hover:text-[color:var(--color-ink)]">{{ $feed['label'] }}</a></li>
|
||||
<li class="flex flex-wrap items-baseline justify-between gap-x-3">
|
||||
<a href="{{ $feed['url'] }}" target="_blank" rel="noopener" class="text-[color:var(--color-ink-2)] underline underline-offset-2 hover:text-[color:var(--color-ink)]">{{ $feed['label'] }}</a>
|
||||
<span class="tabular-nums text-[11.5px] text-[color:var(--color-ink-3)]">{{ trans_choice(':n Abruf|:n Abrufe', $feed['fetches_30d'] ?? 0, ['n' => number_format($feed['fetches_30d'] ?? 0, 0, ',', '.')]) }} · {{ __('30 Tage') }}</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<p class="mt-2 mb-0 text-[11px] text-[color:var(--color-ink-4)]">{{ __('Die Feeds führen die jüngsten :n Meldungen — wer sie abonniert hat, ist uns nicht bekannt.', ['n' => \App\Http\Controllers\Web\FeedController::MAX_ITEMS]) }}</p>
|
||||
<p class="mt-2 mb-0 text-[11px] text-[color:var(--color-ink-4)]">{{ __('Die Feeds führen die jüngsten :n Meldungen. Gezählt werden Abrufe der Feeds (ein Reader-Dienst holt einmal für alle seine Nutzer) — wer sie abonniert hat, ist uns nicht bekannt.', ['n' => \App\Http\Controllers\Web\FeedController::MAX_ITEMS]) }}</p>
|
||||
@else
|
||||
<p class="mt-1 mb-0 text-[12.5px] text-[color:var(--color-ink-3)]">{{ __('Derzeit in keinem Feed — die Meldung ist nicht öffentlich.') }}</p>
|
||||
@endif
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue