From 714f19d4d43f743499b36044141a29ae7cef6213 Mon Sep 17 00:00:00 2001 From: nexxo Date: Tue, 4 Aug 2026 00:52:04 +0200 Subject: [PATCH] Der Versandtakt nennt die Mails beim Namen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Zuordnung zeigte class_basename(): „DormantAccountWarningMail" stand dort, wo der Betreiber „Konto ohne Paket wird geloescht" sucht. Ein Klassenname ist eine Auskunft ueber den Bauplan, keine ueber die Mail. Die Namen liegen in lang/{de,en}/mail_pace.php und NICHT in MailCatalogue, obwohl der sehr aehnlich klingt: der Katalog zaehlt Mail*arten* — vier Mahnstufen, vier Eintraege, 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. Fehlt doch ein Name, steht der Klassenname da wie bisher statt einer leeren Zeile — und ein Test haelt fest, dass keiner fehlt, in beiden Sprachen. Der Warteschlangenname (mail-direkt und Geschwister) stand in der Wertspalte zwischen Beschriftung und Eingabefeldern und las sich wie ein weiterer Wert. Er steht jetzt als Unterzeile bei der Spur. Weg gehoert er nicht: es ist die Zeichenkette, die der Arbeiter in docker-compose.yml abhoert. Suite gruen, Codex ohne Befund. Der Namenstest faellt gegen die alte Fassung — nachgemessen, nicht angenommen. Co-Authored-By: Claude Opus 5 --- app/Livewire/Admin/MailPace.php | 41 +++++++++++++++- lang/de/mail_pace.php | 21 +++++++++ lang/en/mail_pace.php | 19 ++++++++ .../views/livewire/admin/mail-pace.blade.php | 22 +++++---- tests/Feature/Admin/MailPacePageTest.php | 47 ++++++++++++++++++- 5 files changed, 140 insertions(+), 10 deletions(-) diff --git a/app/Livewire/Admin/MailPace.php b/app/Livewire/Admin/MailPace.php index 9e7db3d..7b12deb 100644 --- a/app/Livewire/Admin/MailPace.php +++ b/app/Livewire/Admin/MailPace.php @@ -4,6 +4,7 @@ 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; @@ -162,7 +163,45 @@ class MailPace extends Component return view('livewire.admin.mail-pace', [ 'lanes' => $lanes, 'laneOptions' => [MailLane::DIRECT, MailLane::URGENT, MailLane::CALM], - 'assignments' => MailLane::all(), + '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 + */ + 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(); + } } diff --git a/lang/de/mail_pace.php b/lang/de/mail_pace.php index 1388034..bad489d 100644 --- a/lang/de/mail_pace.php +++ b/lang/de/mail_pace.php @@ -32,6 +32,27 @@ return [ 'assignments_title' => 'Zuordnung', 'assignments_sub' => 'Welche Mailklasse in welcher Spur fährt. Gesperrte Klassen fahren immer direkt — darauf wartet gerade jemand, drosseln würde dort nur schaden.', + 'class' => [ + // Die Namen, unter denen der Betreiber diese Mails kennt — nicht die + // Klassennamen, unter denen sie im Code stehen. Sie decken sich mit + // den Bezeichnungen in MailCatalogue, wo es dieselbe Sache ist; die + // Mahnungen sind eine Klasse fuer vier Mahnstufen und bekommen hier + // deshalb EINEN Namen fuer alle vier. + 'ResetPasswordMail' => 'Passwort zurücksetzen', + 'VerifyEmailMail' => 'E-Mail bestätigen (Registrierung)', + 'NewDeviceSignInMail' => 'Anmeldung von einem neuen Gerät', + 'SecurityBlockMail' => 'Adresse wegen Anmeldeversuchen gesperrt', + 'ContactRequestMail' => 'Kontaktanfrage von der Website', + 'OrderConfirmationMail' => 'Bestellbestätigung', + 'OperatorMessageMail' => 'Nachricht aus der Konsole', + 'MaintenanceAnnouncementMail' => 'Wartungsfenster angekündigt', + 'MaintenanceCancelledMail' => 'Wartungsfenster abgesagt', + 'CloudSuspendedMail' => 'Cloud abgeschaltet', + 'CloudResumedMail' => 'Cloud läuft wieder', + 'InvoiceMail' => 'Rechnung', + 'DunningNoticeMail' => 'Zahlungserinnerungen und Mahnungen', + 'DormantAccountWarningMail' => 'Konto ohne Paket wird gelöscht', + ], 'locked_hint' => 'Immer direkt (gesperrt)', 'locked_notice' => 'Diese Mail fährt immer direkt: darauf wartet gerade jemand.', 'moved' => 'Verschoben.', diff --git a/lang/en/mail_pace.php b/lang/en/mail_pace.php index 3d70756..d95dbbd 100644 --- a/lang/en/mail_pace.php +++ b/lang/en/mail_pace.php @@ -31,6 +31,25 @@ return [ 'assignments_title' => 'Assignment', 'assignments_sub' => 'Which mail class rides in which lane. Locked classes always ride direct — someone is waiting on them right now, and throttling would only hurt.', + 'class' => [ + // The names an operator knows these mails by — not the class names + // they carry in the code. The dunning mail is one class covering four + // levels, so it gets ONE name here for all four. + 'ResetPasswordMail' => 'Password reset', + 'VerifyEmailMail' => 'Confirm email address (sign-up)', + 'NewDeviceSignInMail' => 'Sign-in from a new device', + 'SecurityBlockMail' => 'Address blocked after failed sign-ins', + 'ContactRequestMail' => 'Contact request from the website', + 'OrderConfirmationMail' => 'Order confirmation', + 'OperatorMessageMail' => 'Message from the console', + 'MaintenanceAnnouncementMail' => 'Maintenance window announced', + 'MaintenanceCancelledMail' => 'Maintenance window cancelled', + 'CloudSuspendedMail' => 'Cloud suspended', + 'CloudResumedMail' => 'Cloud running again', + 'InvoiceMail' => 'Invoice', + 'DunningNoticeMail' => 'Payment reminders and dunning notices', + 'DormantAccountWarningMail' => 'Account without a plan will be deleted', + ], 'locked_hint' => 'Always direct (locked)', 'locked_notice' => 'This mail always rides direct: someone is waiting on it right now.', 'moved' => 'Moved.', diff --git a/resources/views/livewire/admin/mail-pace.blade.php b/resources/views/livewire/admin/mail-pace.blade.php index 17b208e..87367e1 100644 --- a/resources/views/livewire/admin/mail-pace.blade.php +++ b/resources/views/livewire/admin/mail-pace.blade.php @@ -30,10 +30,15 @@ @foreach ($lanes as $lane) - + {{-- Der Warteschlangenname steht als Unterzeile bei der Spur, + nicht in der Wertspalte: dort stand er zwischen + Beschriftung und Eingabefeldern und las sich wie ein + weiterer Wert. Er bleibt aber stehen — es ist der Name, + den der Arbeiter in docker-compose.yml abhört, und wer + dort nachsieht, braucht genau diese Zeichenkette. --}} +
- {{ $lane['key'] }} - @if ($lane['throttled'])
{{ __('mail_pace.assignments_sub') }}

- @foreach ($assignments as $class => $lane) - - @if (\App\Services\Mail\MailLane::isLocked($class)) + @foreach ($assignments as $eintrag) + + @if ($eintrag['locked']) {{ __('mail_pace.locked_hint') }} @else - @endif diff --git a/tests/Feature/Admin/MailPacePageTest.php b/tests/Feature/Admin/MailPacePageTest.php index 54e3abb..3474d24 100644 --- a/tests/Feature/Admin/MailPacePageTest.php +++ b/tests/Feature/Admin/MailPacePageTest.php @@ -5,6 +5,7 @@ use App\Mail\InvoiceMail; use App\Mail\ResetPasswordMail; use App\Services\Mail\MailLane; use App\Support\Settings; +use Illuminate\Support\Facades\Lang; use Livewire\Livewire; /** @@ -45,7 +46,7 @@ it('zeigt eine gesperrte Klasse als gesperrt statt mit einem Auswahlfeld', funct // wäre die Einladung zu einem Klick, der ohnehin nichts bewirkt. $html = Livewire::actingAs(operator('Owner'), 'operator') ->test(MailPace::class) - ->assertSee(class_basename(ResetPasswordMail::class)) + ->assertSee(__('mail_pace.class.ResetPasswordMail')) ->html(); expect($html)->not->toContain("move('".ResetPasswordMail::class."'"); @@ -105,3 +106,47 @@ it('speichert einen gueltigen Takt fuer beide gedrosselten Spuren', function () ->and(Settings::get('mail.pace.calm.count'))->toBe(7) ->and(Settings::get('mail.pace.calm.minutes'))->toBe(9); }); + +/** + * ── Namen statt Klassennamen ────────────────────────────────────────────── + * + * Die Seite zeigte `class_basename()`: „DormantAccountWarningMail" stand + * dort, wo der Betreiber „Konto ohne Paket wird gelöscht" sucht. Ein + * Klassenname ist eine Auskunft über den Bauplan, keine über die Mail. + */ +it('nennt jede Mailklasse bei ihrem Namen, nicht bei ihrer Klasse', function () { + $html = Livewire::actingAs(operator('Owner'), 'operator')->test(MailPace::class)->html(); + + foreach (array_keys(MailLane::all()) as $class) { + $kurz = class_basename($class); + + expect(Lang::has('mail_pace.class.'.$kurz)) + ->toBeTrue("Für {$kurz} fehlt ein Name in lang/de/mail_pace.php."); + + // Der Klassenname darf auch nicht mehr durchscheinen — er steht nur + // noch im wire:key und im Wert des Auswahlfelds, beides mit vollem + // Namensraum davor. + expect($html)->not->toContain('>'.$kurz.'<'); + } +}); + +it('führt für jede Mailklasse denselben Namen in beiden Sprachen', function () { + // Eine Sprache ohne den Eintrag zeigte still den Schlüssel selbst — + // „mail_pace.class.InvoiceMail" mitten in einer Tabelle. + $de = require lang_path('de/mail_pace.php'); + $en = require lang_path('en/mail_pace.php'); + + foreach (array_keys(MailLane::all()) as $class) { + expect($de['class'])->toHaveKey(class_basename($class)) + ->and($en['class'])->toHaveKey(class_basename($class)); + } +}); + +it('stellt den technischen Spurnamen unter die Spur, nicht zwischen die Felder', function () { + // Er bleibt sichtbar — der Arbeiter in docker-compose.yml hört genau + // diese Zeichenkette ab —, aber als Unterzeile der Beschriftung. + $html = Livewire::actingAs(operator('Owner'), 'operator')->test(MailPace::class)->html(); + + expect($html)->toContain(MailLane::CALM) + ->and($html)->not->toContain('font-mono text-xs text-muted">'.MailLane::CALM); +});