images: Lebenszyklus — Veröffentlichungsgate, published_ever_at, Snapshot der veröffentlichten Fassung, Sofortlöschung, Legacy-Bereinigung, Beweiszugriff, Korrekturweg (Bildkennzeichnung Block E)

- publish()/changeStatusFromAdmin(): kein Bild aus Formular v2 ohne gültige Bestätigung (ImageRightsConfirmationRequiredException, Admin-Seite meldet es); published_ever_at an PM und Bildern; PublishedImageSnapshot (Cover + JSON mit Nachweis/Kennzeichnung)
- deleteFromAdmin() und press-releases:purge-drafts löschen Bilder nie veröffentlichter Meldungen samt Original und Bestätigung sofort (E4, O1)
- press-releases:backfill-published-ever (Status-Log), press-release-images:purge-inactive-legacy (E-B11, Trockenlauf Standard)
- Admin-Route für Beweisoriginal und veröffentlichte Fassung nur mit Zweck, ActivityType EvidenceAccess; Architekturtest: original_path nur in vier Dateien
- ImageCorrectionService + image_declaration_corrections (insert-only), withheld_at blendet das Bild öffentlich aus (publiclyVisible-Scope), Korrekturpanel auf der Admin-Seite
- Systemstatus: fehlgeschlagene Herkunftsprüfungen der letzten 7 Tage

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-22 09:53:49 +00:00
parent 2124ffd1f2
commit 46b89e1d23
24 changed files with 1202 additions and 12 deletions

View file

@ -0,0 +1,62 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Insert-only-Protokoll des Korrekturwegs (Bildkennzeichnung E.6): welcher
* Admin wann welches Feld eines Bildes mit welchem Grund geändert hat.
*/
class ImageDeclarationCorrection extends Model
{
public const UPDATED_AT = null;
public const FIELD_CREDIT = 'copyright';
public const FIELD_AI_DECLARATION = 'ai_declaration';
public const FIELD_WITHHELD = 'withheld';
protected $fillable = [
'press_release_image_id',
'press_release_id',
'admin_id',
'field',
'old_value',
'new_value',
'old_source',
'reason',
'created_at',
];
protected function casts(): array
{
return [
'created_at' => 'datetime',
];
}
protected static function booted(): void
{
static::updating(fn () => throw new \LogicException('image_declaration_corrections ist insert-only.'));
static::deleting(fn () => throw new \LogicException('image_declaration_corrections wird nicht gelöscht.'));
}
/**
* @return BelongsTo<PressReleaseImage, $this>
*/
public function image(): BelongsTo
{
return $this->belongsTo(PressReleaseImage::class, 'press_release_image_id');
}
/**
* @return BelongsTo<User, $this>
*/
public function admin(): BelongsTo
{
return $this->belongsTo(User::class, 'admin_id');
}
}

View file

@ -104,6 +104,7 @@ class PressRelease extends Model
'no_export',
'published_at',
'depublished_at',
'published_ever_at',
'gone_at',
'no_export_before_gone',
'scheduled_at',
@ -134,6 +135,7 @@ class PressRelease extends Model
'no_export' => 'boolean',
'published_at' => 'datetime',
'depublished_at' => 'datetime',
'published_ever_at' => 'datetime',
'gone_at' => 'datetime',
'no_export_before_gone' => 'boolean',
'sync_locked_at' => 'datetime',

View file

@ -8,6 +8,7 @@ use App\Enums\CreditSource;
use App\Enums\ImageLicenseType;
use App\Enums\ImageProvenanceStatus;
use Database\Factories\PressReleaseImageFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -60,6 +61,7 @@ use Illuminate\Support\Facades\URL;
* @property string|null $published_snapshot_sha256
* @property Carbon|null $published_ever_at
* @property Carbon|null $customer_confirmation_requested_at
* @property Carbon|null $withheld_at
* @property string|null $rules_version
* @property int|null $focus_x
* @property int|null $focus_y
@ -116,6 +118,7 @@ class PressReleaseImage extends Model
'published_snapshot_sha256',
'published_ever_at',
'customer_confirmation_requested_at',
'withheld_at',
'rules_version',
'focus_x',
'focus_y',
@ -147,6 +150,7 @@ class PressReleaseImage extends Model
'provenance_checked_at' => 'datetime',
'published_ever_at' => 'datetime',
'customer_confirmation_requested_at' => 'datetime',
'withheld_at' => 'datetime',
'focus_x' => 'integer',
'focus_y' => 'integer',
'no_crop' => 'boolean',
@ -220,6 +224,26 @@ class PressReleaseImage extends Model
return $this->published_ever_at !== null;
}
/**
* Vorläufig zurückgehalten (Korrekturweg E.6): öffentlich greift der
* Platzhalter, im Panel bleibt das Bild sichtbar.
*/
public function isWithheld(): bool
{
return $this->withheld_at !== null;
}
/**
* Öffentlich ausspielbare Bilder: nicht zurückgehalten.
*
* @param Builder<PressReleaseImage> $query
* @return Builder<PressReleaseImage>
*/
public function scopePubliclyVisible(Builder $query): Builder
{
return $query->whereNull('withheld_at');
}
public function url(bool $signed = false): ?string
{
if (blank($this->path)) {