208 lines
8.0 KiB
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();
|
|
}
|
|
}
|