Arbeitspaket M sagte "Abfragen je Seite zaehlen. Messen, dann zusammenfassen." Messen braucht ein Messgeraet, also gibt es jetzt eines: ein Zaehler als Doctrine-Middleware, dessen Zahl ausserhalb der Produktion im Kopf X-Kant-Queries steht. Keine Profiler-Maschinerie, eine Zahl. Die erste Messung: die Startseite brauchte 18 Abfragen. Nicht wegen einer komplizierten Seite, sondern weil SiteSettings::load() und Theme::load() je Aufrufer dieselbe Zeile erneut holten - Renderer, strukturierte Daten, Einwilligungsschalter, Vorlagen. Beide merken sich ihren Stand jetzt fuer die Dauer einer Anfrage: 18 auf 8. Dabei ist mir die klassische Falle des Merkens selbst passiert, und ein bestehender Test hat sie sofort gezeigt: ich leerte den Merker am Anfang des Speicherns. Das Speichern liest den Stand aber noch einmal und fuellte ihn damit erneut - der Rest der Anfrage bekam den alten Stand. Geleert wird jetzt nach dem Schreiben. Das Ladebudget wird gemessen statt behauptet: die Browserpruefung liest je Seite HTML-Groesse, CSS-Groesse, Anzahl Anfragen, Anzahl Skripte und Abfragezahl und prueft Obergrenzen. Startseite: 7,4 KB HTML, 22,7 KB CSS, 2 Anfragen, 8 Abfragen, 0 Skripte. Die Null ist die wichtigste Zahl - sie haelt als Test fest, was bisher nur Absicht war. Das erste Bild einer Seite ist nicht mehr lazy. Es ist meistens das groesste Ding ueber der Falz, und es zu verzoegern verzoegert genau das Pixel, auf das jemand wartet. Der Renderer bestimmt es in Renderreihenfolge, Kopfbereich zuerst. Eine feste Obergrenze ist kein Blaettern: der Fassungsverlauf hoerte bei dreissig auf, die Einsendungen bei fuenfzig, die Weiterleitungen zeigten bis zu zweitausend auf einmal. Das Erste sieht aus wie "es gibt dreissig" und ist das Einzige, was eine Historie nicht tun darf. Alle drei blaettern jetzt, mit derselben Rechnung und demselben Pager wie Beitragsarchiv und Mediathek. 347 PHP-Tests / 1422 Assertions unter PHP 8.2.12 und 8.4.22. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
96 lines
6.4 KiB
Twig
96 lines
6.4 KiB
Twig
{% extends 'admin/shell.html.twig' %}
|
||
{% set nav = 'forms' %}
|
||
{% block title %}Formulare{% endblock %}
|
||
{% block heading %}Formulare{% endblock %}
|
||
{% block lead %}Felder, Empfänger und Einwilligungstext werden hier gepflegt. Auf einer Seite steht nur ein Formularblock mit der Adresse – so kann ein gespeicherter Seiteninhalt niemals ändern, wohin eine Anfrage geht.{% endblock %}
|
||
{% block content %}
|
||
{% for form in forms|merge([null]) %}
|
||
{% set new = form is null %}
|
||
{% set rows = new ? [] : form.fields %}
|
||
<section class="stack-section">
|
||
<div class="section-head">
|
||
<h2>{{ new ? 'Neues Formular' : form.name }}{% if not new %} <span class="count">{{ form.fields|length }} Felder</span>{% endif %}</h2>
|
||
{% if not new %}<span class="muted">Block-Adresse <code>{{ form.slug }}</code></span>{% endif %}
|
||
</div>
|
||
<form action="{{ path('admin_form_save', {slug: new ? 'neu' : form.slug}) }}" method="post" class="form-def card card-body">
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('forms') }}">
|
||
<input type="hidden" name="expectedVersion" value="{{ new ? 0 : form.version }}">
|
||
<div class="fields">
|
||
<div>
|
||
<label for="name-{{ loop.index }}">Name</label>
|
||
<input id="name-{{ loop.index }}" name="name" maxlength="120" required value="{{ new ? '' : form.name }}">
|
||
</div>
|
||
{% if new %}
|
||
<div>
|
||
<label for="slug-new">Adresse</label>
|
||
<input id="slug-new" name="slug" maxlength="80" pattern="[a-z0-9]+(-[a-z0-9]+)*" required placeholder="kontakt">
|
||
<p class="field-help">Wird im Formularblock eingetragen.</p>
|
||
</div>
|
||
{% endif %}
|
||
<div>
|
||
<label for="recipient-{{ loop.index }}">Empfänger</label>
|
||
<input id="recipient-{{ loop.index }}" name="recipient" type="email" required value="{{ new ? '' : form.settings.recipient }}">
|
||
</div>
|
||
<div>
|
||
<label for="subject-{{ loop.index }}">Betreff</label>
|
||
<input id="subject-{{ loop.index }}" name="subject" maxlength="160" value="{{ new ? '' : form.settings.subject }}">
|
||
</div>
|
||
<div>
|
||
<label for="reply-{{ loop.index }}">Antwortadresse aus Feld</label>
|
||
<input id="reply-{{ loop.index }}" name="replyToField" maxlength="32" value="{{ new ? '' : form.settings.replyToField }}" placeholder="email">
|
||
<p class="field-help">Technischer Name eines E-Mail-Feldes. Dann lässt sich direkt antworten.</p>
|
||
</div>
|
||
<div>
|
||
<label for="success-{{ loop.index }}">Meldung nach dem Absenden</label>
|
||
<input id="success-{{ loop.index }}" name="success" maxlength="300" value="{{ new ? '' : form.settings.success }}">
|
||
</div>
|
||
</div>
|
||
|
||
<table class="table field-table">
|
||
<caption class="visually-hidden">Felder des Formulars. Eine Beschriftung zu leeren entfernt das Feld.</caption>
|
||
<thead><tr><th scope="col">Technischer Name</th><th scope="col">Typ</th><th scope="col">Beschriftung</th><th scope="col">Pflicht</th><th scope="col">Hilfetext</th><th scope="col">Optionen</th></tr></thead>
|
||
<tbody>
|
||
{% for row in rows|merge([{name: '', type: 'text', label: '', required: false, help: '', options: []}, {name: '', type: 'text', label: '', required: false, help: '', options: []}]) %}
|
||
{% set n = loop.index0 %}
|
||
<tr{% if row.label == '' %} class="table-new"{% endif %}>
|
||
<td><input name="fields[{{ n }}][name]" maxlength="32" value="{{ row.name }}" pattern="[a-z][a-z0-9_]*" aria-label="Technischer Name von Feld {{ n + 1 }}" placeholder="vorname"></td>
|
||
<td>
|
||
<select name="fields[{{ n }}][type]" aria-label="Typ von Feld {{ n + 1 }}">
|
||
{% for value, label in types %}<option value="{{ value }}"{{ row.type == value ? ' selected' : '' }}>{{ label }}</option>{% endfor %}
|
||
</select>
|
||
</td>
|
||
<td><input name="fields[{{ n }}][label]" maxlength="120" value="{{ row.label }}" aria-label="Beschriftung von Feld {{ n + 1 }}"></td>
|
||
<td class="check-cell"><input type="checkbox" name="fields[{{ n }}][required]" value="1"{{ row.required ? ' checked' : '' }} aria-label="Feld {{ n + 1 }} ist ein Pflichtfeld"></td>
|
||
<td><input name="fields[{{ n }}][help]" maxlength="200" value="{{ row.help }}" aria-label="Hilfetext von Feld {{ n + 1 }}"></td>
|
||
<td><input name="fields[{{ n }}][options]" maxlength="500" value="{{ row.options|join(', ') }}" aria-label="Optionen von Feld {{ n + 1 }}" placeholder="nur bei Auswahl"></td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
<p class="field-help">Ein Einwilligungsfeld ist immer ein Pflichtfeld; der Zeitpunkt der Einwilligung wird mitgespeichert. Optionen einer Auswahl durch Komma trennen. Die letzten beiden Zeilen sind für neue Felder.</p>
|
||
<button class="primary" type="submit">{{ new ? 'Formular anlegen' : 'Formular speichern' }}</button>
|
||
</form>
|
||
</section>
|
||
{% endfor %}
|
||
|
||
<div class="section-head"><h2>Einsendungen <span class="count">{{ submissions.total }}</span></h2><span class="muted">Neueste zuerst.</span></div>
|
||
{% if submissions.items is empty %}
|
||
<div class="empty"><h3>Noch keine Einsendungen.</h3><p>Hier erscheinen eingegangene Nachrichten, auch wenn der E-Mail-Versand fehlschlägt.</p></div>
|
||
{% else %}
|
||
<div class="rows">
|
||
{% for entry in submissions.items %}
|
||
<article class="row row-top">
|
||
<div class="row-main">
|
||
<h3>{{ entry.form }}</h3>
|
||
<p>{{ entry.createdAt|date('d.m.Y H:i') }} UTC{% if entry.consentAt %} · Einwilligung {{ entry.consentAt|date('d.m.Y H:i') }}{% endif %}</p>
|
||
<dl class="submission-data">{% for key, value in entry.data %}<dt>{{ key }}</dt><dd>{{ value }}</dd>{% endfor %}</dl>
|
||
</div>
|
||
<span class="pill {{ entry.delivered ? 'pill-live' : 'pill-off' }}">{{ entry.delivered ? 'Versendet' : 'Nicht versendet' }}</span>
|
||
</article>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
{% include 'admin/pager.html.twig' with {result: submissions, route: 'admin_forms', label: 'Seiten der Einsendungen'} only %}
|
||
<p class="footnote">KantSites · Arbeitsstand</p>
|
||
{% endblock %}
|