images: nur freigegebene Bilder in Feed und PDF, versionierte Veröffentlichungssnapshots, Bestätigung an das gezeigte Bild gebunden, getrennte Rückhaltegründe (Umsetzungsaudit 29.09.: B-01 bis B-14, N09, OPT-3 bis OPT-5; 02.10.: O03)

- B-01/B-02: Startseiten-Feed, Card-Helfer und Nachweis-PDF nur mit
  isPubliclyDeliverable()
- B-03: Livewire-Temp-Upload 16 MiB
- N09: Snapshot je Bytes- und Metadaten-Hash inkl. OG-Variante, put()
  geprüft, alle Fassungen ins Archiv
- O03: published_ever_at in der Freigabetransaktion
  (markPublishedIfNowPublic) — Kundenbestätigung, Vorab-Sichtung, Korrektur
- B-04/B-14: Bestätigung an Bild-ID und SHA gebunden, unter Sperre
- B-05/B-06: Entfernen unter Sperre mit frischem Stand und Archivauftrag;
  Korrektur archiviert über den Auftrag, Nacharbeiten abgesichert
- B-07: precheck_withheld_at getrennt von withheld_at (Migration + Übernahme)
- B-08, B-09, B-11, B-12: published_ever_at nur bei Status published,
  Admin-Eigentümer mit Kundenregel, Sweep verwaister Originale,
  Verarbeitungsfehler als Formularmeldung
- K-02-Teil: nach Kundenbestätigung erneut routen
- OPT-3 bis OPT-5: pingImage für JPEG, Hash durchgereicht, erstes Frame

Admin-PM-Detailseite enthält zusätzlich K-06 (Autorisierung) und A-08
(Nachweis per POST), weil die Datei ungeteilt committet ist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-10-02 13:09:01 +00:00
parent 7cd84b2be3
commit 5ec252da12
23 changed files with 1675 additions and 149 deletions

View file

@ -0,0 +1,55 @@
<?php
namespace App\Console\Commands;
use App\Services\Archive\EvidenceArchive;
use App\Services\PressRelease\Images\PressReleaseImageRemover;
use Illuminate\Console\Command;
/**
* Umsetzungsaudit 29.09.2026, B-11: Beweisoriginale ohne Bildzeile (harter
* Abbruch nach dem Schreiben des Originals, z. B. memory_limit bei
* 8000 × 8000 px) nach einer Karenzzeit entfernen.
*
* Standard ist ein Trockenlauf. Dateien, auf die eine Bildzeile zeigt —
* auch weich gelöschte (Beweisarchiv ausstehend) —, bleiben immer liegen.
* Nach einem Datenbank-Restore fehlen Zeilen, deren Originale noch Beweis
* sind: Solange der Archiv-Sweep ausgesetzt ist (`archive:sweep-orphans
* --pause`, setzt auch `archive:reconcile-after-restore`), löscht auch dieser
* Befehl nichts.
*/
class SweepOrphanPressReleaseOriginals extends Command
{
protected $signature = 'press-release-images:sweep-orphan-originals
{--execute : Wirklich löschen (Standard ist ein Trockenlauf)}
{--hours=48 : Nur Dateien, die älter als X Stunden sind}';
protected $description = 'Entfernt Beweisoriginale ohne zugehörige Bildzeile (B-11)';
public function handle(PressReleaseImageRemover $remover, EvidenceArchive $archive): int
{
$execute = (bool) $this->option('execute');
if ($execute && $archive->isSweepPaused()) {
$this->warn('Orphan-Sweep ausgesetzt (Restore-Abgleich) — nichts gelöscht. Fortsetzen mit archive:sweep-orphans --resume.');
return self::SUCCESS;
}
$result = $remover->sweepOrphanOriginals(
$execute,
(int) $this->option('hours'),
fn (string $file) => $this->line(' verwaist: '.$file),
);
$this->info(sprintf(
'%s: %d Dateien geprüft, %d verwaist, %d entfernt.',
$execute ? 'Bereinigung' : 'Trockenlauf',
$result['scanned'],
$result['orphans'],
$result['removed'],
));
return $execute && $result['removed'] < $result['orphans'] ? self::FAILURE : self::SUCCESS;
}
}