press-releases/jobs: Scheduler respektiert manuelle Prüfung und kommt an Blockaden vorbei, Newsletter-Versand per Schalter gesperrt, kurze Scheduler-Sperren (Umsetzungsaudit 29.09.: K-01, K-02, K-05, K-06, K-09, E9, OPT-8, OPT-9; 02.10.: O06)
- K-01: publish-scheduled filtert manual_review_required_at; publish() prüft für ki/scheduler unter der Sperre (ManualReviewRequiredException) - K-02: Kontingent- und Bildsperre sind fachliche Ausgänge (Exit 0, einmaliger Verlaufseintrag); KI-Job ohne Retry bei Bildsperre - O06: Kandidaten per Cursor (scheduled_at, id), Limit zählt bearbeitete Meldungen, Blockaden nur ein Scan-Budget - K-05: KI-Job lädt vor dem Routing neu, überholter Status ohne Retry; backoff - K-06: Admin-Aktionen mit Permission-Prüfung und Fehlermeldung statt 500 - K-09: withoutOverlapping() mit kurzer Sperrdauer, purge-events 03:50, neuer Wochenlauf press-release-images:sweep-orphan-originals (So 04:45) - E9: NEWSLETTER_SENDING_ENABLED (Standard aus) sperrt Start, Resume und Job - OPT-9: ExpireGrandfathering prüft die Queue-Voraussetzung aus E18 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d6e49c11c3
commit
c82f279fef
20 changed files with 927 additions and 89 deletions
|
|
@ -5,6 +5,7 @@ use App\Mail\NewsletterCampaignMail;
|
|||
use App\Models\NewsletterCampaign;
|
||||
use App\Services\Newsletter\CampaignRecipients;
|
||||
use App\Services\Newsletter\CampaignSender;
|
||||
use App\Services\Newsletter\NewsletterSendingDisabledException;
|
||||
use Flux\Flux;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Livewire\Attributes\Layout;
|
||||
|
|
@ -104,7 +105,14 @@ new #[Layout('components.layouts.app'), Title('Newsletter-Kampagne')] class exte
|
|||
{
|
||||
$campaign = $this->persist();
|
||||
|
||||
$total = app(CampaignSender::class)->start($campaign);
|
||||
try {
|
||||
$total = app(CampaignSender::class)->start($campaign);
|
||||
} catch (NewsletterSendingDisabledException $e) {
|
||||
Flux::toast(heading: __('Versand gesperrt'), text: $e->getMessage(), variant: 'danger', duration: 8000);
|
||||
Flux::modal('confirm-campaign-send')->close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($total === 0) {
|
||||
Flux::toast(text: __('Keine Empfänger für die gewählten Filter — nichts versendet.'), variant: 'warning');
|
||||
|
|
@ -241,12 +249,23 @@ new #[Layout('components.layouts.app'), Title('Newsletter-Kampagne')] class exte
|
|||
{{ __('Test-Mail an mich') }}
|
||||
</flux:button>
|
||||
<flux:spacer />
|
||||
<flux:modal.trigger name="confirm-campaign-send">
|
||||
<flux:button variant="primary" icon="paper-airplane">
|
||||
@if (\App\Services\Newsletter\CampaignSender::sendingEnabled())
|
||||
<flux:modal.trigger name="confirm-campaign-send">
|
||||
<flux:button variant="primary" icon="paper-airplane">
|
||||
{{ __('Jetzt senden') }}
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
@else
|
||||
<flux:button variant="primary" icon="paper-airplane" disabled>
|
||||
{{ __('Jetzt senden') }}
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
@endif
|
||||
</div>
|
||||
@unless (\App\Services\Newsletter\CampaignSender::sendingEnabled())
|
||||
<flux:callout variant="warning" icon="exclamation-triangle" class="mt-3"
|
||||
:heading="__('Der Newsletter-Versand ist derzeit gesperrt')"
|
||||
:text="__('Kampagnen lassen sich als Entwurf vorbereiten und als Test-Mail prüfen. Der Versand wird erst mit NEWSLETTER_SENDING_ENABLED freigeschaltet.')" />
|
||||
@endunless
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use App\Models\PressReleaseSearchIndex;
|
|||
use App\Models\User;
|
||||
use App\Services\Admin\AdminPerformanceCache;
|
||||
use App\Services\PressRelease\BlacklistViolationException;
|
||||
use App\Services\PressRelease\ImageRightsConfirmationRequiredException;
|
||||
use App\Services\PressRelease\PressReleaseService;
|
||||
use App\Services\PressRelease\QuotaExceededException;
|
||||
use Flux\Flux;
|
||||
|
|
@ -184,11 +185,16 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
}
|
||||
|
||||
/**
|
||||
* Manuelle KI-Prüfung aus der Liste — bewusst ohne Routing: bewertet
|
||||
* und protokolliert, ändert aber nie den Status (Admin entscheidet).
|
||||
* Manuelle KI-Prüfung aus der Liste — ohne Routing: bewertet und
|
||||
* protokolliert, gibt aber nie frei. Ausnahme (Audit U03): Liefert der
|
||||
* Lauf Rot und wartet die Meldung noch in `review`, lehnt der Job sie ab
|
||||
* (inkl. Autorenmail). Deshalb dieselbe Berechtigung wie Freigeben und
|
||||
* Ablehnen (Audit K-06).
|
||||
*/
|
||||
public function runAiCheck(?int $id = null): void
|
||||
{
|
||||
$this->authorize('press-releases:publish');
|
||||
|
||||
$id ??= $this->aiCheckId;
|
||||
|
||||
if ($id === null) {
|
||||
|
|
@ -241,6 +247,10 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
} catch (QuotaExceededException $e) {
|
||||
Flux::toast(heading: __('Nicht veröffentlicht'), text: $e->getMessage(), variant: 'warning', duration: 8000);
|
||||
|
||||
return;
|
||||
} catch (ImageRightsConfirmationRequiredException $e) {
|
||||
Flux::toast(heading: __('Titelbild nicht bestätigt'), text: $e->getMessage(), variant: 'danger', duration: 8000);
|
||||
|
||||
return;
|
||||
} catch (\LogicException $e) {
|
||||
Flux::toast(text: $e->getMessage(), variant: 'danger');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue