CluPilotCloud/app/Livewire/Admin/MailPace.php

208 lines
8.0 KiB
PHP

<?php
namespace App\Livewire\Admin;
use App\Services\Mail\MailLane;
use App\Support\Settings;
use Illuminate\Support\Facades\Lang;
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' => $this->assignmentRows(),
]);
}
/**
* Die Zuordnung mit lesbaren Namen statt Klassennamen.
*
* Die Seite zeigte `class_basename()` — „DormantAccountWarningMail" steht
* dort, wo der Betreiber „Konto ohne Paket wird gelöscht" sucht. Ein
* Klassenname ist eine Auskunft über den Bauplan, keine über die Mail.
*
* Die Namen liegen in der Sprachdatei und NICHT in MailCatalogue, obwohl
* der sehr ähnlich klingt: der Katalog zählt Mail*arten* (vier
* Mahnstufen, vier Einträge, vier Absenderwege), diese Seite verteilt
* Mail*klassen* auf Spuren — und alle vier Mahnstufen sind EINE Klasse,
* die als eine Zeile eine Spur bekommt. Zwei Listen, weil es zwei
* verschiedene Dinge sind; MailPaceLabelsTest hält fest, dass keine
* Klasse ohne Namen bleibt.
*
* Fehlt einer doch, steht der Klassenname da wie bisher — unschön, aber
* lesbar, statt einer leeren Zeile.
*
* @return array<int, array{class: class-string, lane: string, label: string, locked: bool}>
*/
private function assignmentRows(): array
{
return collect(MailLane::all())
->map(function (string $lane, string $class) {
$kurz = class_basename($class);
$schluessel = 'mail_pace.class.'.$kurz;
return [
'class' => $class,
'lane' => $lane,
'label' => Lang::has($schluessel) ? __($schluessel) : $kurz,
'locked' => MailLane::isLocked($class),
];
})
->values()
->all();
}
}