Der Versandtakt nennt die Mails beim Namen
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 <noreply@anthropic.com>
feat/versandtakt
parent
c548d1c9a6
commit
714f19d4d4
|
|
@ -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<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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -30,10 +30,15 @@
|
|||
|
||||
<x-ui.panel class="mt-4">
|
||||
@foreach ($lanes as $lane)
|
||||
<x-ui.row :label="__('mail_pace.lane.'.$lane['key'])" :for="$lane['throttled'] ? $lane['countField'] : null">
|
||||
{{-- 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. --}}
|
||||
<x-ui.row :label="__('mail_pace.lane.'.$lane['key'])" :hint="$lane['key']"
|
||||
:for="$lane['throttled'] ? $lane['countField'] : null">
|
||||
<div class="flex flex-wrap items-end gap-3">
|
||||
<span class="font-mono text-xs text-muted">{{ $lane['key'] }}</span>
|
||||
|
||||
@if ($lane['throttled'])
|
||||
<div class="w-24">
|
||||
<x-ui.input :name="$lane['countField']" type="number" min="1"
|
||||
|
|
@ -72,17 +77,18 @@
|
|||
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('mail_pace.assignments_sub') }}</p>
|
||||
|
||||
<x-ui.panel class="mt-4">
|
||||
@foreach ($assignments as $class => $lane)
|
||||
<x-ui.row :label="class_basename($class)">
|
||||
@if (\App\Services\Mail\MailLane::isLocked($class))
|
||||
@foreach ($assignments as $eintrag)
|
||||
<x-ui.row :label="$eintrag['label']" wire:key="lane-{{ $eintrag['class'] }}">
|
||||
@if ($eintrag['locked'])
|
||||
<span class="inline-flex items-center gap-1.5 text-sm text-muted">
|
||||
<x-ui.icon name="lock" class="size-4" />{{ __('mail_pace.locked_hint') }}
|
||||
</span>
|
||||
@else
|
||||
<select wire:change="move('{{ $class }}', $event.target.value)"
|
||||
<select wire:change="move('{{ $eintrag['class'] }}', $event.target.value)"
|
||||
aria-label="{{ $eintrag['label'] }}"
|
||||
class="w-48 rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-body">
|
||||
@foreach ($laneOptions as $option)
|
||||
<option value="{{ $option }}" @selected($lane === $option)>{{ __('mail_pace.lane.'.$option) }}</option>
|
||||
<option value="{{ $option }}" @selected($eintrag['lane'] === $option)>{{ __('mail_pace.lane.'.$option) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue