User Panel: Phase-8-Abschluss, Titelbild/Lizenzen/Zeitzonen und KI-Pruef-Pipeline
Phase 8 (Rest) + Umbauten vom 10./11.06.: - Ein Titelbild pro PM (Cover 1280x580), SVG-Platzhalter-Set + Picker, PressReleaseCoverImage-Resolver - Lizenz-/Rechteformular nach "Lizenztyp Bildupload" (7 Lizenztypen, Personen-/Sachrechte-Status, bedingte Pflichtfelder, Risikohinweise) - Veroeffentlichungs-Box vereinfacht (Embargo aus der Form-UI entfernt), geplante Termine in Europe/Berlin (Speicherung UTC, DISPLAY_TIMEZONE) - Quota-Stub (users.press_release_quota) + monatlicher Reset-Command - Einreichungs-Modal einheitlich in Show/Create/Edit; Ghost-Buttons auf filled; PM-Editor-Layout responsive entkoppelt (.pr-editor-layout) KI-Pruef-Pipeline (Phasen 1-5 des Entwicklungsplans): - API-Haertung: status nicht mehr per API setzbar, eigene Submit-Route durch denselben Funnel (Blacklist, Quota, Status-Log) - Klassifikation Rot/Gelb/Gruen asynchron (Queue classification, OpenAI-Treiber + deterministischer Fallback), ki_audits-Audit-Log - Routing: Rot -> rejected + Mail, Gelb -> Review-Queue, Gruen -> Auto-Publish; Scheduler publiziert nur gruene faellige PMs - Content-Score 0-100 -> Stufe (Standard/Geprueft/Hochwertig) inkl. Editor-Panel und Badges; Re-Klassifikation/-Score bei Aenderung - Admin: KI-Badge + Filter, On-Demand-Pruefung mit Anbieter-Override Suite: 442 passed, 4 skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0efabaf446
commit
a000238ca8
141 changed files with 5922 additions and 1001 deletions
51
app/Models/KiAudit.php
Normal file
51
app/Models/KiAudit.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\KiAuditFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* Audit-Log jeder KI-Entscheidung (Klassifikation / Content-Score).
|
||||
*
|
||||
* Schreibt für jede Bewertung einen unveränderlichen Eintrag inkl.
|
||||
* Anbieter, Modell, Ergebnis, Begründung und Roh-Antwort – für
|
||||
* Nachvollziehbarkeit und Nachweispflicht (DSGVO).
|
||||
*/
|
||||
class KiAudit extends Model
|
||||
{
|
||||
/** @use HasFactory<KiAuditFactory> */
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
public const TYPE_CLASSIFICATION = 'classification';
|
||||
|
||||
public const TYPE_CONTENT_SCORE = 'content_score';
|
||||
|
||||
protected $fillable = [
|
||||
'press_release_id',
|
||||
'type',
|
||||
'provider',
|
||||
'model',
|
||||
'result',
|
||||
'reason',
|
||||
'raw_response',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'raw_response' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function pressRelease(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PressRelease::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,9 @@
|
|||
namespace App\Models;
|
||||
|
||||
use App\Enums\Portal;
|
||||
use App\Enums\PressReleaseClassification;
|
||||
use App\Enums\PressReleaseContentTier;
|
||||
use App\Enums\PressReleasePlaceholder;
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Models\Concerns\HasUniqueSlug;
|
||||
use App\Scopes\PortalScope;
|
||||
|
|
@ -14,6 +17,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class PressRelease extends Model
|
||||
|
|
@ -21,6 +25,13 @@ class PressRelease extends Model
|
|||
/** @use HasFactory<PressReleaseFactory> */
|
||||
use HasFactory, HasUniqueSlug, SoftDeletes;
|
||||
|
||||
/**
|
||||
* Anzeige-Zeitzone für vom Nutzer erfasste Termine (scheduled_at,
|
||||
* embargo_at). In der Datenbank wird weiterhin UTC gespeichert
|
||||
* (config app.timezone).
|
||||
*/
|
||||
public const DISPLAY_TIMEZONE = 'Europe/Berlin';
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
|
|
@ -37,6 +48,13 @@ class PressRelease extends Model
|
|||
protected static function booted(): void
|
||||
{
|
||||
static::addGlobalScope(new PortalScope);
|
||||
|
||||
static::creating(function (self $pressRelease): void {
|
||||
if (blank($pressRelease->placeholder_variant)) {
|
||||
$seed = $pressRelease->uuid ?? $pressRelease->title ?? (string) now()->timestamp;
|
||||
$pressRelease->placeholder_variant = PressReleasePlaceholder::fromSeed($seed)->value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected $fillable = [
|
||||
|
|
@ -51,9 +69,15 @@ class PressRelease extends Model
|
|||
'slug',
|
||||
'text',
|
||||
'boilerplate_override',
|
||||
'placeholder_variant',
|
||||
'backlink_url',
|
||||
'keywords',
|
||||
'status',
|
||||
'classification',
|
||||
'classified_at',
|
||||
'content_score',
|
||||
'content_tier',
|
||||
'scored_at',
|
||||
'hits',
|
||||
'teaser_begin',
|
||||
'teaser_end',
|
||||
|
|
@ -69,7 +93,13 @@ class PressRelease extends Model
|
|||
{
|
||||
return [
|
||||
'portal' => Portal::class,
|
||||
'placeholder_variant' => PressReleasePlaceholder::class,
|
||||
'status' => PressReleaseStatus::class,
|
||||
'classification' => PressReleaseClassification::class,
|
||||
'classified_at' => 'datetime',
|
||||
'content_score' => 'integer',
|
||||
'content_tier' => PressReleaseContentTier::class,
|
||||
'scored_at' => 'datetime',
|
||||
'hits' => 'integer',
|
||||
'teaser_begin' => 'integer',
|
||||
'teaser_end' => 'integer',
|
||||
|
|
@ -81,6 +111,22 @@ class PressRelease extends Model
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Geplanter Veröffentlichungstermin in der Anzeige-Zeitzone (Europe/Berlin).
|
||||
*/
|
||||
public function scheduledAtLocal(): ?Carbon
|
||||
{
|
||||
return $this->scheduled_at?->copy()->setTimezone(self::DISPLAY_TIMEZONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sperrfrist (Embargo) in der Anzeige-Zeitzone (Europe/Berlin).
|
||||
*/
|
||||
public function embargoAtLocal(): ?Carbon
|
||||
{
|
||||
return $this->embargo_at?->copy()->setTimezone(self::DISPLAY_TIMEZONE);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
|
|
@ -116,6 +162,11 @@ class PressRelease extends Model
|
|||
return $this->hasMany(PressReleaseStatusLog::class)->orderByDesc('created_at');
|
||||
}
|
||||
|
||||
public function kiAudits(): HasMany
|
||||
{
|
||||
return $this->hasMany(KiAudit::class)->orderByDesc('created_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display-ready text. Returns sanitized HTML for Phase-7+ PMs and
|
||||
* <p>/<br>-wrapped legacy plain text for older imports.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\ImageLicenseType;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
|
@ -19,6 +20,16 @@ class PressReleaseImage extends Model
|
|||
'title',
|
||||
'description',
|
||||
'copyright',
|
||||
'author',
|
||||
'license_type',
|
||||
'license_detail',
|
||||
'license_url',
|
||||
'source_url',
|
||||
'persons_consent',
|
||||
'people_rights_status',
|
||||
'property_rights_status',
|
||||
'rights_notes',
|
||||
'rights_confirmed_at',
|
||||
'is_preview',
|
||||
'sort_order',
|
||||
'width',
|
||||
|
|
@ -32,6 +43,9 @@ class PressReleaseImage extends Model
|
|||
{
|
||||
return [
|
||||
'variants' => 'array',
|
||||
'license_type' => ImageLicenseType::class,
|
||||
'persons_consent' => 'boolean',
|
||||
'rights_confirmed_at' => 'datetime',
|
||||
'is_preview' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
'width' => 'integer',
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ class User extends Authenticatable
|
|||
'legacy_portal',
|
||||
'legacy_id',
|
||||
'password',
|
||||
'press_release_quota',
|
||||
'press_release_quota_used_this_month',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -73,9 +75,23 @@ class User extends Authenticatable
|
|||
'last_seen_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'press_release_quota' => 'integer',
|
||||
'press_release_quota_used_this_month' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verbleibendes PM-Kontingent in diesem Monat.
|
||||
*
|
||||
* Temporärer Stub bis zum echten Tarif-/Credit-Modul. Die Schnittstelle
|
||||
* (`pressReleaseQuotaRemaining()`) bleibt stabil, damit das
|
||||
* Veröffentlichungs-Modal nicht neu gebaut werden muss.
|
||||
*/
|
||||
public function pressReleaseQuotaRemaining(): int
|
||||
{
|
||||
return max(0, (int) $this->press_release_quota - (int) $this->press_release_quota_used_this_month);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's initials
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue