- The installer only opens with a per-installation key that install.php
creates and shows; the key is deleted with the first account.
- Lock files keep .env, the database, backups and logs out of reach when a
domain points at the project folder instead of public/.
- Linked sections render by their own schema version, so an old section
cannot carry unfiltered HTML into a newer page.
- Editors no longer move, trash or change areas and post data of published
pages; a scheduled publication goes live with the revision that was
scheduled (migration 20261012000100).
- Form stamps are bound to their form, IPv6 senders count per /64, and a
flood per form or overall stops storing with a visible message.
- The consent redirect rejects control characters, update targets must look
like 1.2.3, and own CSS cannot reach outside its block.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The request that swaps the files now ends with a plain page instead of running on through the
framework with old code in memory and new code on disk - that is what answered with a 500 after
a successful update. The page calls the second step itself with a one-time key: migrations,
clean-up, maintenance off, back to the system screen (ADR 112).
Turkish is complete in the fixed template texts and form messages. Two language switches on one
page get distinct landmark names. Kantarci Reisen gets a Turkish version of every page, the
header, footer, menu and enquiry form, with a DE · TR switch; the reset script applies it.
The DM Sans 600 files are needed after all and are committed with fonts.css.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A site can have further languages (Settings → Languages). A page is built once; each language
adds only its texts, keyed by the stable block IDs, plus its own address under /en/… (ADR 111,
document schema v27, migration 20261009000100 for page_translation).
- Editor: DE | EN switch, a list of all texts with original, state (missing, translated,
original changed) and filter; structure stays with the main language. Undo, drafts,
publishing and revisions cover all languages at once.
- Website: /en and translated slugs, lang, canonical, hreflang with x-default, og:locale,
sitemap; links to translated pages follow the language; a language switch block.
- Header, footer and linked sections translate the same way; menu entries carry labels per
language; fixed template texts come from a small dictionary.
- Forms: labels, help, options, button and success message per language; messages and the
step script speak the language, the submission and the email stay in the main language.
- Posts, archives, category names, consent text and the 404 page follow the language.
- Single-language sites see none of it and never touch the new table.
The forms administration becomes an overview with one row per form and its own edit page;
fields are one line each with details collapsed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Forms gained steps, conditions, image choices, number fields with units and a
summary step; the DEEG price finder uses all of it. Plugins are manifest plus
Twig: blocks, settings, containers, routes, presets and theme packs, switched
per installation. The review backlog is done: archive pages render through the
normal page pipeline, forms and post lists work inside areas and sections,
repeated sections get their own ids, breakpoints and colours come from the
palette, backup and update work on MySQL and lock each other out.
The task worker no longer shortens a process that has no time limit:
set_time_limit() sets the limit instead of extending it, which turned an
unlimited CLI run into eighty seconds and killed the full test suite far away
from the scheduler.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Umut asked where a contact form's mail is sent from: it was the recipient
address, `From` equal to `To`. That breaks as soon as the recipient is a
mailbox at another provider, because most hosts only deliver mail that
claims an address of their own domain, and the provider's SPF/DMARC then
treats it as forged.
Each form now has an optional `sender` setting, validated like the
recipient; empty keeps the old behaviour, so stored forms need no
migration. Delivery uses `sender ?: recipient`; the reply-to field is
untouched. The administration shows the field under the recipient with a
note on why it matters. The pilot launch config moves to port 8091 because
8087 was held by another session.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Forms are an editor now: one card per field with add and remove, the
technical name derived from the label, options only for a choice, and a
new "Aussehen" section with a button label and four colours (field border
and background, button and button text) that travel to the page as one
rule per form inside the hashed style block. A live preview draws the
form with the site's default button and names the text contrast.
Statistics open with six figures (views against the same span before,
average per day, busiest day, pages, referred views, missing addresses),
a chart with grid lines and a title per bar, a weekday chart and ranked
lists with share bars. The design screen shows the palette as colour
cards with seven ready-made palettes, picks the base colours from the
palette, previews every font in itself and the typography as a sample.
The sidebar ends in an account card with role and a logout icon.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
Migration 20260912000700 legt form und form_submission an. Der
Formularblock im Seiteninhalt enthält nur die Adresse: Felder,
Empfänger und Einwilligungstext liegen in der Datenbank, damit ein
gespeichertes Dokument nicht ändern kann, wonach gefragt wird oder
wohin eine Anfrage geht.
Serverseitige Validierung mit einer Meldung je Feld über
aria-describedby und aria-invalid; eine Ablehnung rendert die Seite
mit HTTP 422 und allen Eingaben neu, eine gültige Absendung wird mit
HTTP 303 zurückgeleitet. Alles ohne JavaScript.
Spamschutz ohne Dienst Dritter: Honeypot, signierter Zeitstempel
gegen zu schnelle und zu alte Absendungen, fünf Absendungen je
Absender und Stunde. Ein erkannter Bot bekommt dieselbe Antwort wie
ein Erfolg. Die Absenderadresse wird nie gespeichert, nur ein Hash.
Gespeichert wird vor dem Versand, damit ein kaputter Mailversand
keine Anfrage verliert. Kein CSRF-Token auf dem öffentlichen Weg,
weil das eine Sitzung und damit ein Cookie auf jeder öffentlichen
Seite bedeuten würde; Begründung in ADR 017. Dateiuploads fehlen
bewusst.
Ein echter Fehler von den Tests gefunden: das Stundenfenster des
Limits entstand aus lokaler Zeit, die Zeitstempel aber aus UTC - auf
einer Uhr vor UTC hätte das Limit nie gegriffen.
Geprüft: 117 PHP-Tests / 450 Assertions unter PHP 8.2.12 und 8.4.22,
14 JavaScript-Modelltests, Lint/Format/Container, composer audit.
Der E-Mail-Versand wurde nie wirklich ausgeführt; keine Browser- oder
Lighthouse-Läufe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>