diff --git a/docs/STATUS.md b/docs/STATUS.md index aeac239..8173220 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,5 +1,15 @@ # Projektstatus +## Nach 1.35.0: E-Mail-Versand und Systemseite aufgeräumt, Protokoll leeren (13.10.2026) + +**Anlass:** Umut: E-Mail-Seite und „Inhalte übertragen“ sehen uneinheitlich aus, Unterpunkt „E-Mail-Versand“ steht immer offen, „Logs leeren“ fehlt. + +**Erledigt:** Fließtext in Karten hat jetzt durchgehend 13 px (vorher Browser-16 px neben 13 px), Felder bleiben 16 px. Neue gemeinsame Bausteine in `admin.css`: `.card-section` (Abschnitt mit Trennlinie), `.button-row`, `.form-foot`, `.option`/`.option-list` (Auswahl als Karten), `.pill-error`, Dateiauswahl im Knopfstil. E-Mail-Seite: Wege als Karten mit Erklärung, SMTP 2×2, Testmail-Abschnitt, „Speichern“ als Hauptknopf im Kartenfuß (war ungestylt). Protokoll mit Zustandsplakette, Hinweis vorn, Servermeldung zugeklappt (`admin/_mail_entry.html.twig`). „Protokoll leeren“ (`POST /admin/system/e-mail/protokoll`, löscht `mail.log` und `mail.log.1`). Unterpunkt nur im Bereich System sichtbar; in der waagrechten Leiste (761–1000 px) neben „System“. + +**Geprüft:** `SystemScreenTest` erweitert (Leeren nur mit Token, danach leer, Editor 403, Unterpunkt auf Einstellungen nicht sichtbar). PHPUnit vollständig 682 grün, php-cs-fixer sauber, `lint:twig`, `test:admin`, `test:editor` grün. Im Browser (Hüfner lokal) bei 1240, 900 und 375 px: E-Mail-Seite, Systemseite, Einstellungen; kein waagrechtes Scrollen am Handy. + +**Offen:** nicht veröffentlicht (kein Tag/Release). + ## Fassung 1.35.0: E-Mail-Versand als eigene Seite, HTTPS-Probe bei IONOS (12.10.2026) **Anlass:** huefner-gartenbau.de: mit `native://default` geht der Versand, Umut will die Einstellung samt SMTP in der Verwaltung, als Unterpunkt statt auf „System“. „HTTPS erzwingen“ meldete 403, obwohl HTTPS lief. diff --git a/public/assets/admin.css b/public/assets/admin.css index 5411341..abe1ee6 100644 --- a/public/assets/admin.css +++ b/public/assets/admin.css @@ -293,7 +293,24 @@ label { display: block; margin: 18px 0 7px; font-size: 13px; font-weight: 600 } } .card-head h2, .card-head h3 { margin: 0 } .card-head .muted { font-size: 12px } -.card-body { padding: 8px 22px 24px; min-width: 0 } +.card-body { padding: 8px 22px 24px; min-width: 0; font-size: 13px; line-height: 1.6 } +/* Fließtext in Karten ist 13 px wie Hinweise und Fakten. Felder bleiben bei 16 px - darunter + vergrößert iOS beim Antippen die ganze Seite. :where() hält die Regel schwächer als jede Klasse. */ +:where(.card-body) :where(input:not([type=checkbox], [type=radio]), select, textarea) { font-size: 16px } +.card-body h3 { font-size: 14px; margin: 26px 0 4px } +.card-body h3:first-child { margin-top: 14px } +/* Abschnitte einer Karte: Trennlinie, Überschrift, gleicher Abstand - als fieldset oder form. */ +.card-section { border: 0; border-top: 1px solid var(--line); margin: 22px 0 0; padding: 18px 0 0; min-width: 0 } +.card-section > legend { float: left; width: 100%; font-weight: 650; font-size: 14px; margin: 0 0 12px; padding: 0 } +.card-section > legend + * { clear: both } +.card-section > h3, .card-body .card-section > h3:first-child { font-size: 14px; font-weight: 650; margin: 0 0 12px } +.card-section .check-label { margin: 14px 0 0 } +.card-section > h3 + .check-label, .card-section > h3 + input + .check-label { margin-top: 0 } +.card-section label.field-first { margin-top: 0 } +/* Dateiauswahl im Stil der übrigen Knöpfe statt des grauen Browserknopfs. */ +input[type=file] { padding: 6px; cursor: pointer; max-width: 720px } +input[type=file]::file-selector-button { margin-right: 12px; padding: 7px 14px; border: 1px solid var(--line); border-radius: 6px; background: #f3f5f1; color: var(--ink); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer } +input[type=file]:hover::file-selector-button { background: #e8ede6 } .card-body code { overflow-wrap: anywhere } /* Das Kleingedruckte einer Karte: zugeklappt, damit auf einem Handy der Knopf oben bleibt. */ .hint { margin: 12px 0 0 } @@ -306,6 +323,22 @@ label { display: block; margin: 18px 0 7px; font-size: 13px; font-weight: 600 } .card-body > p:first-child { margin-top: 14px } .card-body .primary, .card-body .secondary { margin-top: 22px } .card-foot { padding: 16px 22px; border-top: 1px solid var(--line); background: #fafbf8; border-radius: 0 0 12px 12px } +/* Der Fuss eines Formulars in einer Karte: Hauptknopf links, Erklärung daneben. */ +.form-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; font-size: 12px } +.form-foot .primary { margin: 0 } +/* Knöpfe nebeneinander, gleicher Abstand, ohne den Abstand nach oben der Einzelknöpfe. */ +.button-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px } +.card-body .button-row .primary, .card-body .button-row .secondary { margin: 0 } +/* Auswahl als Karten: ganze Fläche klickbar, Titel und eine Zeile Erklärung. */ +.option-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 8px; max-width: 720px } +.option { display: flex; align-items: flex-start; gap: 10px; margin: 0; padding: 12px 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--paper); font-weight: 400; cursor: pointer } +.option:hover { border-color: #a9b8af } +.option:has(input:checked) { border-color: var(--accent); background: #f4f8f1; box-shadow: inset 0 0 0 1px var(--accent) } +.option input { width: 16px; min-height: 16px; margin: 2px 0 0; flex: none; accent-color: var(--accent) } +.option > span { display: grid; gap: 2px; min-width: 0 } +.option strong { font-size: 13px; font-weight: 600 } +.option > span > span { font-size: 12px; color: var(--muted); line-height: 1.45 } +.check-label input, .option input { accent-color: var(--accent) } /* A card that opens: used for everything secondary, so a screen starts calm. */ details.card > summary { @@ -376,6 +409,7 @@ details.card > summary:hover { background: #fafbf8 } .pill-live { background: #e7f0d6; color: #2c5340 } .pill-draft { background: #fff2d9; color: #6f4e18 } .pill-off { background: #f1f2f0; color: #5a635e } +.pill-error { background: #fff0eb; color: var(--danger) } /* The per-row settings. One disclosure that opens in place, instead of six competing popovers that clip each other at the edge of the card. */ @@ -593,6 +627,10 @@ details.card > summary:hover { background: #fafbf8 } .nav-label { display: none } .nav-list { display: flex; gap: 4px } .nav-item { white-space: nowrap; padding: 8px 12px } + /* In der Leiste stehen Unterpunkte direkt neben ihrem Hauptpunkt, nicht in einer eigenen Zeile. */ + .nav-list > li:has(.nav-sub) { display: flex; gap: 4px } + .nav-sub { display: flex; margin: 0 } + .nav-sub .nav-item { padding: 8px 12px } .shell-foot { margin: 0 0 0 auto; padding: 0 0 0 12px; border-top: 0; border-left: 1px solid #2a3d35; display: flex; align-items: center; gap: 10px } .shell-account { padding: 3px; gap: 2px } .shell-user { padding: 3px } @@ -718,23 +756,37 @@ details.card > summary:hover { background: #fafbf8 } .check-bad { color: var(--danger) } .log-list { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 6px } .log-list code { font-size: 11px; overflow-wrap: anywhere } -.mail-result { margin-top: 14px } -#mail .facts { margin-top: 0 } +.mail-result { margin-top: 16px } .mail-result p { margin: 0 0 6px } .mail-result p:last-child { margin-bottom: 0 } .mail-result pre { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 11px; margin: 6px 0 0 } +.mail-result-head { margin: 0 0 10px } /* Unterpunkte im Menü: eingerückt auf die Höhe des Textes, ohne Symbol. */ -.nav-sub { list-style: none; margin: 2px 0 4px; padding: 0 } -.nav-sub .nav-item { padding-left: 42px; font-size: 13px } -.mail-form { margin-top: 18px; max-width: 640px } -.mail-form fieldset { border: 0; margin: 0 0 16px; padding: 0; min-width: 0 } -.mail-form legend { font-weight: 600; font-size: 13px; margin-bottom: 6px; padding: 0 } -.mail-form .check-label { display: flex; gap: 8px; align-items: baseline; margin: 4px 0 } -.mail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px 12px } -.mail-grid label { margin-top: 0 } -.mail-grid input { width: 100%; box-sizing: border-box } -.mail-actions { display: flex; flex-wrap: wrap; gap: 8px } -.mail-actions button { margin: 0 } +.nav-sub { list-style: none; margin: 2px 0 4px; padding: 0; display: grid; gap: 2px } +.nav-sub .nav-item { padding: 7px 10px 7px 39px; font-size: 12.5px; color: var(--shell-muted) } +.nav-sub .nav-item:hover { color: var(--shell-ink) } +.nav-sub .nav-item[aria-current=page] { color: #fff; background: #21362d } +/* E-Mail-Versand: der eingestellte Weg als Zeile, darunter Abschnitte mit Trennlinie. */ +.mail-current { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin-top: 14px; padding: 12px 14px; border-radius: 10px; background: #f6f8f4 } +.mail-current-source { flex-basis: 100%; font-size: 12px } +.mail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 0 14px; max-width: 720px } +.mail-grid label { margin: 0 0 7px } +.mail-grid > div { margin-bottom: 4px } +/* Protokoll: Zustand links, daneben was, wann und - bei Fehlern - warum. */ +.mail-log { list-style: none; margin: 14px 0 0; padding: 0 } +.mail-entry { display: grid; grid-template-columns: 112px minmax(0, 1fr); gap: 4px 14px; align-items: start; padding: 12px 0; border-top: 1px solid var(--line) } +.mail-entry:first-child { border-top: 0; padding-top: 4px } +.mail-entry .pill { justify-self: start; margin-top: 1px } +.mail-entry-text p { margin: 0 } +.mail-entry-line { overflow-wrap: anywhere } +.mail-entry-time { font-size: 12px; color: var(--muted) } +.mail-entry-hint { margin-top: 6px !important; color: var(--ink) } +.mail-entry-detail { margin-top: 6px } +.mail-entry-detail summary { font-size: 12px; font-weight: 600; color: var(--muted); cursor: pointer; width: fit-content } +.mail-entry-detail pre { margin: 6px 0 0; padding: 10px 12px; border-radius: 8px; background: #f6f8f4; font-size: 11px; line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere } +@media (max-width: 560px) { + .mail-entry { grid-template-columns: minmax(0, 1fr) } +} /* Ohne SMTP brauchen die Felder niemand; wo :has() fehlt, bleiben sie einfach sichtbar. */ .mail-form:has(input[name=weg]:checked:not([value=smtp])) .smtp-fields { display: none } @@ -848,6 +900,9 @@ details.card > summary:hover { background: #fafbf8 } .nav-list { display: grid; gap: 2px } /* Vierundvierzig Pixel: die Flaeche, die ein Daumen zuverlaessig trifft. */ .nav-item { min-height: 44px; display: flex; align-items: center; gap: 12px; padding: 10px 14px; white-space: normal } + .nav-list > li:has(.nav-sub) { display: block } + .nav-sub { display: grid; margin: 2px 0 4px } + .nav-sub .nav-item { padding: 10px 14px 10px 44px } .shell-foot { margin-left: auto; border-left: 0; padding-left: 0 } .shell-version { display: none } } diff --git a/src/Controller/MailController.php b/src/Controller/MailController.php index 2b2600f..9f218a9 100644 --- a/src/Controller/MailController.php +++ b/src/Controller/MailController.php @@ -113,6 +113,19 @@ final class MailController extends AbstractController return $this->backToMail($request); } + /** „Letzte Versuche“ leeren: var/log/mail.log und mail.log.1. */ + #[Route('/%kant.admin_path%/system/e-mail/protokoll', name: 'admin_mail_clear', methods: ['POST'])] + public function clear(Request $request): Response + { + $this->denyAccessUnlessGranted('ROLE_ADMIN'); + if (!$this->isCsrfTokenValid('system', $request->request->getString('_csrf_token'))) { + return $this->backToMail($request, 'Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.'); + } + $this->mail->clearLog(); + + return $this->backToMail($request, 'Das Protokoll ist leer.', 'ok'); + } + /** @param array{an: string, von: string} $eingabe */ private function mailAdressFehler(array $eingabe): ?string { diff --git a/src/Ops/MailCheck.php b/src/Ops/MailCheck.php index c6549bd..8cfc239 100644 --- a/src/Ops/MailCheck.php +++ b/src/Ops/MailCheck.php @@ -212,6 +212,16 @@ final class MailCheck return $eintraege; } + /** Protokoll leeren: die Datei und ihre ältere Hälfte. */ + public function clearLog(): void + { + foreach ([$this->logFile, $this->logFile.'.1'] as $datei) { + if (is_file($datei)) { + @unlink($datei); + } + } + } + public function logFile(): string { return $this->logFile; diff --git a/templates/admin/_mail_entry.html.twig b/templates/admin/_mail_entry.html.twig new file mode 100644 index 0000000..6398526 --- /dev/null +++ b/templates/admin/_mail_entry.html.twig @@ -0,0 +1,15 @@ +{# Ein Versuch, gleich gebaut für „Letzte Versuche“ und „Alle Wege ausprobiert“: Zustand, worum es + ging, wann - bei Fehlern der Hinweis vorn und die Meldung des Servers zugeklappt dahinter. #} +
+ {% if titel is defined %}{{ titel }}{% else %}{{ eintrag.anlass }} an {{ eintrag.an }}{% if eintrag.von %} von {{ eintrag.von }}{% endif %}{% if eintrag.weg is defined and eintrag.weg %} · {{ eintrag.weg }}{% endif %}{% endif %}
+
{{ eintrag.zeit }} UTC
{% endif %} + {% if not eintrag.ok %} + {% if eintrag.hinweis %}{{ eintrag.hinweis }}
{% endif %} +{{ eintrag.meldung }}{{ mailNotice.text }}
+{% endif %} + +