CluPilotCloud/app/Livewire/Admin/MailPace.php

169 lines
6.4 KiB
PHP

<?php
namespace App\Livewire\Admin;
use App\Services\Mail\MailLane;
use App\Support\Settings;
use Illuminate\Support\Facades\Queue;
use Livewire\Attributes\Layout;
use Livewire\Component;
use RuntimeException;
/**
* Der Notschalter und die Sicht auf die drei Versandspuren.
*
* Eigene Seite statt eines fünften Abschnitts auf admin/mail (App\Livewire\
* Admin\Mail): dort liegen Server, Postfächer, Zwecke und Wegwahl bereits
* „alles in einer Wurst" — eine weitere Scheibe wäre keine Verbesserung. Es
* ist auch inhaltlich eine andere Art Seite: admin/mail RICHTET EIN (einmal,
* und es gilt), diese hier BEOBACHTET UND GREIFT EIN (laufend, im Ernstfall
* mit einem Klick) — deshalb steht sie in der Navigation unter „Betrieb", wo
* schon Provisioning, Wartungen und Störungen stehen, nicht unter „System".
*
* Dieselbe Fähigkeit wie admin/mail (`mail.manage`): wer die Postfächer und
* den Server sehen und ändern darf, darf auch die Spuren sehen und die
* Drossel abstellen.
*/
#[Layout('layouts.admin')]
class MailPace extends Component
{
public bool $enabled = true;
public int $urgentCount = 30;
public int $urgentMinutes = 5;
public int $calmCount = 20;
public int $calmMinutes = 10;
public function mount(): void
{
$this->authorize('mail.manage');
$this->enabled = Settings::bool('mail.pace.enabled', true);
$this->urgentCount = (int) Settings::get('mail.pace.urgent.count', 30);
$this->urgentMinutes = (int) Settings::get('mail.pace.urgent.minutes', 5);
$this->calmCount = (int) Settings::get('mail.pace.calm.count', 20);
$this->calmMinutes = (int) Settings::get('mail.pace.calm.minutes', 10);
}
/**
* Der Notschalter: nach dem Muster von Admin\Plans::toggleSales() — eine
* Methode, eine Rückmeldung, kein Bestätigungsmodal. Er ist umkehrbar,
* derselbe Klick legt ihn zurück.
*/
public function togglePace(): void
{
$this->authorize('mail.manage');
$this->enabled = ! $this->enabled;
Settings::set('mail.pace.enabled', $this->enabled);
$this->dispatch('notify', message: __($this->enabled ? 'mail_pace.enabled_on_notice' : 'mail_pace.enabled_off_notice'));
}
/**
* Die beiden Kontingente speichern.
*
* `min:1` auf allen vier Feldern ist die zweite Hälfte der Untergrenze aus
* MailPaceServiceProvider::mindestensEins() — die liest nur ab, was hier
* gar nicht erst gespeichert werden darf. Ohne diese Prüfung käme eine
* eingetippte 0 ungeprüft bis in die Einstellung durch und flöge erst beim
* nächsten Auftrag an der Lesestelle auf, nachdem der Betreiber schon
* „gespeichert" gesehen hat — ein Kontingent von 0 heißt dort: jede Mail
* wird endlos zurückgelegt, ein Fenster von 0 Minuten schaltet die Drossel
* still ab.
*/
public function savePace(): void
{
$this->authorize('mail.manage');
$data = $this->validate([
'urgentCount' => ['required', 'integer', 'min:1'],
'urgentMinutes' => ['required', 'integer', 'min:1'],
'calmCount' => ['required', 'integer', 'min:1'],
'calmMinutes' => ['required', 'integer', 'min:1'],
]);
Settings::set('mail.pace.urgent.count', $data['urgentCount']);
Settings::set('mail.pace.urgent.minutes', $data['urgentMinutes']);
Settings::set('mail.pace.calm.count', $data['calmCount']);
Settings::set('mail.pace.calm.minutes', $data['calmMinutes']);
$this->dispatch('notify', message: __('mail_pace.saved'));
}
/**
* Eine Mailklasse in eine andere Spur verschieben.
*
* Ein Ziel außerhalb der drei bekannten Spuren kommt nur über einen
* manuellen Aufruf zustande — kein <select> im DOM bietet eines an — und
* wird verworfen statt in die Einstellungen zu wandern, wo `MailLane::
* for()` es beim nächsten Lesen ohnehin still auf „ruhig" zurückfallen
* ließe, ohne dass der Betreiber sähe, dass sein Klick nichts bewirkt hat.
*/
public function move(string $mailableClass, string $lane): void
{
$this->authorize('mail.manage');
if (! in_array($lane, [MailLane::DIRECT, MailLane::URGENT, MailLane::CALM], true)) {
return;
}
try {
MailLane::assign($mailableClass, $lane);
} catch (RuntimeException) {
// Gesperrt: der Server lehnt so oder so ab (Task 1), das Schloss in
// der Ansicht ist nur die Höflichkeit davor. Wer trotzdem hier
// ankommt — kein <select> bietet einer gesperrten Klasse eines an,
// also nur ein manueller Aufruf — bekommt eine Meldung statt eines
// Serverfehlers.
$this->dispatch('notify', message: __('mail_pace.locked_notice'));
return;
}
$this->dispatch('notify', message: __('mail_pace.moved'));
}
public function render()
{
// Je Spur: die Warteschlange, die App\Mail\Concerns\RidesALane
// tatsächlich befüllt (Task 2) — derselbe Name, den auch der Arbeiter
// in docker-compose.yml abhört (Task 4). Im echten Betrieb (Redis)
// eine ehrliche Zahl aus der tatsächlichen Liste; unter der
// Sync-Warteschlange, die die Testsuite erzwingt, immer 0, weil dort
// nichts je wartet statt verschickt zu werden — dort ist die Zahl
// ebenso ehrlich, nur bedeutungslos für einen Testfall, der einen
// Rückstand nachstellen wollte.
$lanes = [
[
'key' => MailLane::DIRECT,
'throttled' => false,
'waiting' => Queue::size(MailLane::DIRECT),
],
[
'key' => MailLane::URGENT,
'throttled' => true,
'waiting' => Queue::size(MailLane::URGENT),
'countField' => 'urgentCount',
'minutesField' => 'urgentMinutes',
],
[
'key' => MailLane::CALM,
'throttled' => true,
'waiting' => Queue::size(MailLane::CALM),
'countField' => 'calmCount',
'minutesField' => 'calmMinutes',
],
];
return view('livewire.admin.mail-pace', [
'lanes' => $lanes,
'laneOptions' => [MailLane::DIRECT, MailLane::URGENT, MailLane::CALM],
'assignments' => MailLane::all(),
]);
}
}