Rebuild DEEG as a fifth installation, add stepped forms and a plugin system, and clear the review backlog

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>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-16 07:32:35 +02:00
1 parent 66ff68abf8
commit c8a8fa7460
74 files changed
+5352 -243

No files matched your search

+84
View File
@@ -2,6 +2,90 @@
Stand: 15.09.2026. Arbeitstitel KantSites; Produktname offen.
## ADR 104 – Die Rückstände der Gesamtprüfung abgearbeitet: Archiv als Seite, Bereiche vollständig, Palette bis in die Fehlerfarbe, Betrieb auf MySQL (16.09.2026)
Umut: „Bitte umsetzen“ – die Liste aus ADR 100, die bis dahin nur dokumentiert war.
**Archivseiten sind Seiten.** `/beitraege`, `/kategorie/…` und `/schlagwort/…` bauen ein Dokument aus Bausteinen (Kopfabschnitt mit Überschrift und Zahl, ein Abschnitt mit blätternder Beitragsliste) und geben es an `renderPage()` – dieselbe Aufbaukette wie jede Seite: Kopf und Fuß mit ihren Stilen, verknüpfte Sektionen, eigenes CSS, Schriften, Stylesheet-Bündel, Nach-oben-Knopf, Einwilligung, Skripte, Richtlinie. Die eigene Vorlage `archive.html.twig` ist weg. Ein Archiv hat den Typ `archive`; Brotkrumen führen „Beiträge → Kategorie“, und das steht als `BreadcrumbList` in den strukturierten Daten.
**Blättern ist eine Folge von Seiten.** Hat eine Seite eine blätternde Beitragsliste, nennt die Canonical ab Seite 2 die Seitenzahl (`?seite=n`), und `<link rel="prev">`/`<link rel="next">` verbinden die Folge. Vorher hielt eine Suchmaschine Seite 3 für ein Duplikat von Seite 1.
**Bereiche sind vollständig.** Formulare, Beitragslisten und Brotkrumen in Kopf, Fuß und verknüpften Sektionen werden jetzt wie im Inhalt aufgelöst; die Meldungen eines abgewiesenen Formulars finden ihren Block über die Formularadresse statt über die Block-ID, wo auch immer er steht. Eigene Skripte eines Bereichs laufen als `/assets/bereich/<id>.js` – vorher wurden sie gespeichert, geprüft und nie ausgeführt.
**Mehrfach verknüpfte Sektionen ohne doppelte IDs.** `PartialRepository::expandRepeatedLinks()` zählt in Renderreihenfolge (Kopf, Inhalt, Fuß, verschachtelt bis zur Grenze) und gibt der zweiten und jeder weiteren Verknüpfung eine Kopie mit angehängten Kennungen (`id-2`) und ohne Sprungziele; ihre Stile werden mitkompiliert. Auch die Editorvorschau macht das.
**Eine Grenze zwischen Tablet und Telefon: 767 px.** Design-Tokens, Website-Stile, Slider und Galerie sprangen bei 650, der Stilcompiler und die Sichtbarkeit bei 767 – zwischen 651 und 767 galt die Hälfte. Jetzt gilt überall 1024/767, wie es der Editor schon immer sagte.
**Die Palette reicht bis in die Fehlerfarbe.** `color-scheme`, Skip-Link, Aufruf-Button, die Fehlerfarben der Formulare und der Text auf dunklen Abschnitten kommen aus Variablen; `Theme::css()` leitet aus der Leuchtdichte des Papiers ab, ob das Schema dunkel ist und welches Rot dort steht (`--k-scheme`, `--k-error`, `--k-error-bg`, `--k-error-line`). Buttons dunkeln beim Überfahren in die Tinte statt in Schwarz. Neu: Druckstile – Menü, Einwilligung, Nach-oben und Formularknöpfe bleiben weg, Verweise zeigen ihre Adresse.
**Betrieb.** Sicherung und Wiederherstellung auf MySQL/MariaDB über `mysqldump --single-transaction --result-file` und `mysql --execute=source`; damit laufen Archiv-Update und Rückweg auch dort. Der Git-Updater sichert über `Backup` (VACUUM INTO bzw. Dump) statt mit einer rohen Dateikopie und spielt beim Rückweg dieselbe Sicherung ein. Eine Sperre (`var/update.lock`, flock) hält den zweiten Klick und den Archiv-Weg neben dem Git-Weg fern. Das Archiv wird direkt in eine Datei geladen (`Download::toFile()`), nicht mehr als Zeichenkette in den Speicher. `public/.htaccess` wird zusammengeführt: eingeschaltete HTTPS- und HSTS-Zeilen bleiben eingeschaltet. Nach einem gelungenen Update räumt `finish()` `var/update/` (bis auf die zwei jüngsten) und die Zwischenspeicher früherer Fassungen auf. Der MySQL-Job der CI prüft jetzt Gesundheit, Sicherung und die Tests.
**Editor.** Die Vorschau zeigt eigenes CSS, Bereichs- und Sektionsstile und Formulare mit ihren Farben – „sieht in der Vorschau anders aus“ ist keine Ausrede mehr. Kontaktdaten, Formular, verknüpfte Sektion und externer Inhalt sind direkt nach dem Einfügen gültig: leer heißt „noch nichts gewählt“, der Editor sagt es, die Website zeigt nichts. Der Server gibt nach dem Speichern die bereinigte Fassung zurück, und der Editor übernimmt sie als gespeicherten Stand (`EditorHistory.adopt()`), statt sie erst nach dem Neuladen zu zeigen.
**Kleines.** `legal.label` ist auf 60 Zeichen begrenzt, die verrutschten Docblocks in `PageController` stehen an ihren Methoden.
**Folgen.** `tests/BacklogTest.php` hält alles fest. Nicht gebaut: eine Wiederherstellung auf MySQL ohne `mysql`-Client (dann liegt der Dump zum Einspielen von Hand bereit), Druckstile für Beiträge mit Bildkopf.
## ADR 103 – Arbeitspaket H zu Ende: Container, Einstellungen, Adressen, Vorlagen und Designpakete für Plugins – und warum es keine PHP-Hooks gibt (16.09.2026)
Umut: „Dann mach H.“ Der erste Teil (ADR 102) brachte Manifest, Schalter und geprüfte Bausteine. Offen waren Container, Einstellungen, eigene Routen, Tabellen, Hooks und Templates.
**Grundentscheidung: ein Plugin bleibt Manifest plus Twig.** Es bringt keinen PHP-Code mit. Das ist kein Zufall, sondern die Antwort auf zwei Punkte des Entwicklungsplans zugleich: Ein Plugin ohne PHP kann nicht als „sandboxed“ bezeichnet werden müssen, weil es gar nichts ausführt, was der Kern nicht ohnehin ausführt (Twig mit `strict_variables`, ohne Zugriff auf Datenbank, Anfrage oder Dateisystem). Und ein Update des Kerns kann ein solches Plugin nicht brechen, solange die vier Übergaben stehen – `props`, `settings`, `children`, `block`. Damit fallen zwei Wünsche des Plans bewusst weg: **Hooks** in Speichern und Veröffentlichen und **eigene Tabellen**. Beides braucht Code, der im Kern läuft. Wer das braucht, baut einen Kernbaustein (ADR 049) oder wartet auf einen dritten Teil mit einer echten Erweiterungsschnittstelle in PHP – dann mit der Ehrlichkeit, dass er Installationscode ist.
**Container.** `"container": true` am Baustein, optional `"children": [Typen]`. Der Kern rendert die Kinder selbst – mit demselben Kontextbündel wie jeden Kernbaustein – und reicht sie der Vorlage fertig als `children`. So kommt eine Plugin-Vorlage nie an Formulare, Beitragslisten oder die Navigation. Beim Schreiben prüft `DocumentValidator`, dass ein Nicht-Container keine Kinder trägt und ein Container nur die erlaubten; der Editor spiegelt die Regel in `canContain`, also im Werkzeugkasten und beim Ziehen.
**Einstellungen.** `"settings": {Schlüssel: Schema}` mit denselben sieben Arten wie Eigenschaften. Der Bildschirm „Plugins“ zeigt für jedes aktive Plugin ein Formular daraus; gespeichert wird in der neuen Spalte `plugin.settings` (Migration `20260916000100`), geprüft gegen das Schema wie eine Eigenschaft – aus einem Formular kommen Zahlen und Schalter als Text, das wird umgewandelt. Vorlagen und Adressen bekommen die Werte als `settings`, fehlende Schlüssel mit ihrer Vorgabe.
**Eigene Adressen.** `"routes": {name: {path, template, type}}`. Der Pfad hat die Form einer Seite (Slugs, bis zu vier Ebenen) und darf nichts belegen, was der Kern beantwortet; die Antwortart ist eine aus html, json, text, xml. `PageController::page()` fragt die Registry, wenn es zu einem Pfad keine veröffentlichte Seite gibt – eine Seite geht also immer vor, die Adresse kommt vor Weiterleitung und 404. Die Vorlage bekommt `settings`, die Abfrageparameter (nur saubere Namen, gekürzt) und die Route; die Antwort trägt dieselben Schutzkopfzeilen wie jede Seite, kein Skript, bei allem außer html `noindex`. Ausgeschaltet: 404.
**Seitenvorlagen.** `"presets": {name: {label, blocks}}` – fertige Bausteine, die der Editor als Ganzes einfügt, hinter den eingebauten Vorlagen mit demselben Knopf. Jedes Einfügen vergibt frische Kennungen; geprüft wird beim Speichern wie jedes Dokument. Ein Plugin kann so seinen Baustein gleich im passenden Abschnitt anbieten.
**Designpakete.** `"theme": {colors, fonts, base, buttons}` im Format des Designbereichs. „Designpaket übernehmen“ auf dem Bildschirm „Plugins“ ruft `Theme::save()` – dieselbe Prüfung wie beim Speichern unter Design, dieselben Fehler. Inhalte bleiben unangetastet; genau das ist die Abnahme des Plans („Templatewechsel erhält die Inhalte“), und der Test wechselt auf einer Seite mit Plugin-Baustein das Paket und vergleicht das Dokument davor und danach. Ein Plugin darf nur aus einem Designpaket bestehen (`plugins/nachtblau`); übernehmen geht nur eingeschaltet.
**Referenz.** `plugins/vergleich` 1.1.0 hat jetzt Einstellungen (Standardbeschriftungen, Griff), einen Container „Vergleichsgalerie“ nur für Bildvergleiche, die Vorlage „Vorher-Nachher-Abschnitt“ und die Adresse `/vergleich/daten` (JSON mit den Einstellungen). `plugins/nachtblau` ist das Designpaket.
**Folgen.** `docs/PLUGINS.md` beschreibt alles. Nicht gebaut und nicht geplant, solange Plugins ohne PHP bleiben: Hooks, Tabellen, Ressourcen in der Verwaltung, eigene Verwaltungsbildschirme.
## ADR 102 – Das Pluginsystem, erste Fassung: Manifest, Schalter, geprüfte Bausteine (15.09.2026)
Umut: „Plugin hätte ich gerne aber umgesetzt. Also Möglichkeit.“ Bis hierher gab es nur den Erhaltungsvertrag aus ADR 011: ein Baustein mit Namensraum wurde aufbewahrt, aber nie gerendert. Jetzt gibt es die Laufzeit dazu – Arbeitspaket H, erster Teil.
**Ein Plugin ist ein Ordner.** `plugins/<name>/plugin.json` beschreibt Titel, Fassung, kleinste KantSites-Fassung und die Bausteine: je Baustein Beschriftung, Symbol, Twig-Vorlage, optional ein Stylesheet und ein Skript unter `assets/`, und ein Schema der Eigenschaften mit sieben Arten (Text, mehrzeilig, Zahl mit Grenzen, an/aus, Auswahl, Adresse, Bild aus der Mediathek). Der Typ des Bausteins ist `<plugin>.<baustein>` – dieselbe Form, die der Erhaltungsvertrag seit Schema v4 kennt. `PluginRegistry` liest die Manifeste bei jedem Aufruf (ein Dutzend Dateien, kein Cache nötig) und meldet ein kaputtes Manifest mit Grund statt es zu übergehen; ein solches Plugin lässt sich nicht einschalten.
**Der Schalter liegt in der Datenbank.** Migration `20260915000100` legt `plugin` an (Name, aktiv, Fassung beim Einschalten, Zeitpunkt). Ob ein Plugin läuft, entscheidet die Installation, nicht der Code – und nicht eine Datei, die ein Update überschreiben könnte. Der Bildschirm „Plugins“ (Administrator) zeigt jeden Ordner mit seinen Bausteinen und schaltet ein und aus; ein ausgeschaltetes Plugin lässt seine Bausteine unangetastet in den Dokumenten (Erhaltungsvertrag wie bisher).
**Der Kern prüft, die Vorlage vertraut.** Beim Schreiben eines Dokuments prüft `DocumentValidator` die Eigenschaften eines Plugin-Bausteins gegen das Schema – so streng wie bei einem Kernbaustein, aber nur, wenn das Plugin aktiv ist. Beim *Lesen* nicht (`validate(..., writing: true)` nur an den Schreibstellen): ein Plugin, das nach dem Speichern eingeschaltet oder geändert wurde, darf eine gespeicherte Seite nicht unlesbar machen. Dass unpassende Werte trotzdem nie in die Ausgabe gelangen, sichert der Renderer: `kant_plugin_props(block)` prüft beim Rendern erneut und liefert bei einem Fehler null – dann steht ein Kommentar statt des Bausteins. Die Vorlage bekommt genau vier Dinge: die geprüften `props`, `block` (für die `id`), `designClasses` und `editing`, dazu `kant_plugin_media()` für Bilder. Keine Datenbank, keine Einstellungen, keine Anfrage. Unbekannte Eigenschaften in einem gespeicherten Dokument werden ignoriert, damit ein Plugin eine Eigenschaft streichen kann.
**Dateien nur dort, wo der Baustein steht.** `PageController` sammelt aus allen Dokumenten der Seite die Plugin-Bausteine und verlinkt ihre Stylesheets und Skripte (`/plugin-assets/<name>/assets/...?v=<Fassung>`); ein Skript schaltet die Richtlinie auf `script-src 'self'`, sonst bleibt sie bei `'none'`. `PluginAssetController` liefert nur Dateien aktiver Plugins unter `assets/` mit erlaubter Endung, mit Typ, `nosniff` und einem Jahr Haltbarkeit; Manifest und Vorlagen sind nicht erreichbar. Plugins liegen bewusst außerhalb von `public/`. In der Editorvorschau wird das Stylesheet geladen, das Skript nicht – wie bei eigenem Code.
**Der Editor kennt die Bausteine aus dem Dokumentaufruf.** Der Editor-Endpunkt liefert mit dem Dokument die Definitionen der aktiven Plugins; `registerPlugins()` trägt Beschriftung, Symbol und Vorgaben ein und gibt ihnen die Gruppe „Erweiterungen“ im Werkzeugkasten. Das Eigenschaftenpanel entsteht aus dem Schema mit derselben Feldfabrik wie für Kernbausteine; ein Bild geht über die Mediathek. Ein Baustein, dessen Plugin fehlt, bleibt der bekannte Platzhalter. Design-Tokens (Abstände, Ausrichtung, Ecken) gelten jetzt auch für Plugin-Bausteine – die Klassen sind allgemein, die Prüfung lässt namensraumhaltige Typen durch.
**Kein Sandkasten, und das steht so da.** Eine Plugin-Vorlage läuft mit den Rechten der Anwendung. Der Entwicklungsplan verlangt, das nicht anders zu nennen; der Bildschirm „Plugins“ sagt es dem Betreiber.
**Referenzplugin „Bildvergleich“** (`plugins/vergleich`): zwei Bilder übereinander, ein Regler dazwischen – Beschriftungen, Startposition, Ecken als Eigenschaften, ein Stylesheet, ein Skript, das nur `--pos` über das CSSOM setzt (Inline-Stile bleiben durch die Richtlinie verboten). Ohne Skript steht der Vergleich bei der Startposition, das native Bereichsfeld bleibt bedienbar.
**Updates.** Das Releasearchiv enthält `plugins/`; das Archiv-Update übernimmt den Ordner Datei für Datei wie `public/`, sodass mitgelieferte Plugins aktualisiert werden und ein selbst installiertes stehen bleibt.
**Nicht in dieser Fassung:** Container-Bausteine, eigene Routen, Einstellungen, Tabellen, Hooks in Speichern und Veröffentlichen, Ressourcen in der Verwaltung, Templates (der zweite Teil von H). Anleitung für Entwickler: `docs/PLUGINS.md`.
## ADR 101 – Formulare mit Schritten und Bedingungen: der Preisfinder ist ein Formular, kein Plugin (15.09.2026)
Umut wollte für die DEEG-Website „kein Kontaktformular weiteres, sondern so ein ausgebautes Form“ – den Preisfinder der laufenden Website: erst die Objektart, dann Eckdaten je Objektart, dann Kontakt, zum Schluss eine Zusammenfassung. Und er fragte, ob das ein eigenes Plugin braucht.
**Warum kein Plugin.** Ein Plugin ist in diesem CMS bis heute nur der Erhaltungsvertrag aus ADR 011: ein Baustein mit Namensraum wird gespeichert, im Editor als Platzhalter gezeigt und öffentlich als Kommentar ausgegeben. Registrierung, Laufzeit, Eigenschaftenpanel und Ressourcenladen sind Arbeitspaket H und nicht gebaut. Ein Preisfinder als Plugin hätte zuerst das ganze Pluginsystem gebraucht – und wäre dann eine Hülle um genau das, was der Formularbaukasten schon kann: Felder prüfen, speichern, verschicken, anzeigen. Wie bei der Beitragsseite (ADR 049) ist die Antwort ein Ausbau des Kerns. Ein Formular, das in Schritten fragt und Felder je nach Antwort zeigt, braucht jede Kundenwebsite.
**Was dazukam.** Drei Feldtypen: Zahl (mit Unter-, Obergrenze und Einheit), Auswahl als Optionsfelder und Mehrfachauswahl. Ein Feld vom Typ „Neuer Schritt“ eröffnet einen Abschnitt: seine Beschriftung ist der Titel, sein Hilfetext die Einleitung, alle Felder bis zum nächsten Schritt gehören dazu. Jedes Feld und jeder Schritt kann eine Bedingung tragen (`showIf: {field, values}`): es gilt nur, wenn ein *früheres* Auswahlfeld einen der Werte hat. Und eine Einstellung „Zusammenfassung vor dem Absenden“.
**Der Server entscheidet, nicht das Skript.** `FormRepository::check()` geht die Felder in Reihenfolge durch und merkt sich die gewählten Optionen; ein Feld hinter einer Bedingung, die nicht zutrifft, wird weder verlangt noch geprüft noch gespeichert – auch wenn der Browser einen Wert mitschickt. Ein Schritt hinter einer Bedingung nimmt seine Felder mit. Beim Speichern der Definition wird jede Bedingung geprüft: das Feld muss davor stehen, ein Auswahlfeld sein, und jeder Wert eine seiner Optionen – sonst könnte die Bedingung nie zutreffen, und ein Pflichtfeld dahinter wäre unerreichbar oder immer offen. Optionen dürfen kein Komma enthalten, weil die Verwaltung daran trennt und die Mehrfachauswahl damit gespeichert wird.
**Ohne Skript vollständig.** Die Vorlage rendert Schritte als `fieldset` mit `legend`, Optionsfelder und Mehrfachauswahl als Gruppen, Zahlen als `type="number"` mit Grenzen; Bedingungen stehen als Datenattribute am Feld. Ohne JavaScript steht das ganze Formular auf der Seite, jeder Schritt ein Abschnitt, alles absendbar – der Server prüft ohnehin selbst. `form-steps.js` kommt nur auf Seiten, deren Formular Schritte, Bedingungen oder die Zusammenfassung hat (`FormRepository::needsScript()`), und die Richtlinie erlaubt es dann; es zeigt einen Schritt nach dem anderen mit Fortschritt, „Zurück“ und „Weiter“, prüft den aktuellen Schritt mit der Browserprüfung, deaktiviert Felder hinter nicht zutreffenden Bedingungen (deaktiviert, nicht nur versteckt – sie werden nicht gesendet und nicht geprüft) und baut als letzten Schritt die Zusammenfassung aus den geltenden Feldern mit „Ändern“ je Schritt. Nach einer Ablehnung durch den Server öffnet es den ersten Schritt mit einem Fehler. Eine Pflicht-Mehrfachauswahl trägt `required` nicht am Kästchen – das hieße „alle ankreuzen“ – sondern als `data-required` an der Gruppe; Skript und Server prüfen sie dort.
**Bildauswahl.** Umut zeigte den Solar-Rechner von Elektro Bernhard: „Wieviele Personen leben im Haushalt“ als vier Bildkarten, die gewählte mit Rahmen. Optionsfelder und Mehrfachauswahl tragen dafür je Option ein Bild aus der Mediathek (`images: {Option: Kennung}`); sobald eine Option eines hat, rendert die Vorlage die Auswahl als Karten mit Bild und Text. Das native Kästchen bleibt in der Karte – sichtbar oben links, mit Fokusring auf der Karte –, damit Tastatur und Vorlesesoftware dieselbe Auswahl bedienen wie die Maus. Die Seite holt die kleinsten Fassungen aller Optionsbilder eines Formulars mit einer Abfrage (`PageController::optionImages()`); ein gelöschtes Bild lässt die Karte mit Text stehen. In der Verwaltung wählt man das Bild je Option aus einer Liste der Mediathek; ohne Skript ist es eine Zeile „Option = Kennung“.
**Rückweg.** Bestehende Definitionen bleiben gültig: `present()` ergänzt fehlende Schlüssel (`min`, `max`, `unit`, `images`, `showIf`, `summary`), es gibt keine Migration. Ein Formular ohne Schritte und Bedingungen rendert wie zuvor und lädt kein Skript. Die Höchstzahl der Felder steigt von 25 auf 40; ein Preisfinder mit vier Objektarten braucht sie.
**Folgen.** Die Editorvorschau zeigt Formulare weiterhin nicht (bekannter Rückstand). Der Preisfinder der DEEG-Website ist damit ein gewöhnliches Formular `bewertung` mit drei Schritten, 20 Feldern und Zusammenfassung, angelegt im Aufbauskript und in der Verwaltung ohne Skript bearbeitbar. Nicht gebaut: Bedingungen auf Zahlenbereiche oder mehrere Felder, Dateiuploads, ein Schritt-für-Schritt-Ablauf ohne JavaScript.
## ADR 100 – Gesamtprüfung: Rollen wie versprochen, Installer bleibt zu, Einwilligung nur von hier (15.09.2026)
**Anlass:** Umut wollte eine umfangreiche Prüfung – Fehler, Durchgängigkeit, Sauberkeit – und ausdrücklich die Frage, ob jemand ohne Administrator etwas verändern kann. Sechs getrennte Durchsichten (Backend, Betrieb, Editor, öffentliche Ausgabe, Dokumentation, Autorisierung jeder Route) lieferten rund sechzig Befunde; die mit Wirkung auf Sicherheit oder Bedienbarkeit sind umgesetzt, der Rest steht in STATUS als Rückstand.
+3 -1
View File
@@ -86,6 +86,8 @@ Abnahme: Die Startseite des Piloten ist ohne Code nachbaubar. Keine neue öffent
### D – Formulare — **erledigt am 12.09.2026, ohne Dateiuploads**
**Ergänzt am 15.09.2026 (ADR 101):** Feldtypen Zahl, Optionsfelder und Mehrfachauswahl, Schritte, Bedingungen auf frühere Auswahlfelder und eine Zusammenfassung vor dem Absenden – für den Preisfinder der DEEG-Website, als Ausbau des Kerns statt als Plugin. Dateiuploads bleiben offen.
*Braucht: nichts; das größte eigenständige Paket.*
Umgesetzt: Feldtypen Text, E-Mail, Telefon, Mehrzeiliges, Auswahl und Einwilligung, serverseitige Validierung mit Meldung je Feld, Einwilligungszeitpunkt, E-Mail-Versand mit Antwortadresse, gespeicherte Einsendungen in der Verwaltung und Spamschutz aus Honeypot, signiertem Zeitstempel und Rate-Limit. **Offen geblieben:** Dateiuploads (eigener nicht öffentlicher Ablageort und Typprüfung, Begründung in ADR 017), Löschen und Umbenennen von Formularen, Export und Aufbewahrungsfrist für Einsendungen. Der tatsächliche Versand ist ungeprüft und gehört zu Paket J.
@@ -139,7 +141,7 @@ Umut hat den Umfang von G erweitert. Vier Punkte, in dieser Reihenfolge:
Abnahme: Kopf- und Fußbereich sind im Editor gestaltbar und öffentlich sichtbar. Eine verknüpfte Sektion auf zwei Seiten ändert sich an beiden Stellen gleichzeitig. Ein Textelement lässt sich formatiert und als HTML bearbeiten, und keine Auszeichnung außerhalb der Positivliste übersteht das Speichern. Eigenes CSS wirkt, eigenes Skript wirkt nur für Administratoren gesetzt und nur auf der veröffentlichten Seite. Die Verwaltung ist ohne Anleitung bedienbar.
### H – Plugins und Templates
### H – Plugins und Templates — **erledigt am 16.09.2026** (ADR 102 und 103: Manifest, Schalter, geprüfte Bausteine, Dateien je Seite, Editoranbindung, Container, Einstellungen, eigene Adressen, Seitenvorlagen, Designpakete; Referenzplugins „Bildvergleich“ und „Nachtblau“. Bewusst nicht: PHP-Hooks und eigene Tabellen – Plugins bleiben Manifest plus Twig, ADR 103)
*Braucht: den Erhaltungsvertrag (erledigt).*
+183
View File
@@ -0,0 +1,183 @@
# Plugins schreiben
Ein Plugin bringt eigene Bausteine, Vorlagen, Adressen und Designpakete in den Editor und auf die
Website. Es ist ein Ordner unter `plugins/` mit einer `plugin.json`, Twig-Vorlagen und optional
Stylesheets und Skripten. Eingeschaltet und eingestellt wird es in der Verwaltung unter **Plugins**
(Administrator). Hintergrund und Grenzen: ADR 102 und ADR 103 in `docs/ARCHITECTURE.md`.
**Ein Plugin bringt keinen PHP-Code mit.** Alles ist Manifest plus Twig. Deshalb gibt es keine
Hooks in Speichern oder Veröffentlichen und keine eigenen Tabellen – und deshalb kann ein Update
des Kerns ein Plugin nicht brechen, solange die Übergaben unten stehen.
## Der Ordner
```
plugins/
vergleich/ ← Name des Plugins: Kleinbuchstaben und Ziffern, mit Buchstabe beginnend
plugin.json ← das Manifest
blocks/bildvergleich.html.twig
blocks/galerie.html.twig
routes/daten.json.twig
assets/bildvergleich.css ← nur unter assets/ liegt, was die Website ausliefert
assets/bildvergleich.js
```
Die mitgelieferten Plugins sind die Vorlagen zum Abschreiben: `plugins/vergleich` (Bausteine,
Container, Einstellungen, Adresse, Seitenvorlage) und `plugins/nachtblau` (nur ein Designpaket).
## Das Manifest
```json
{
"name": "vergleich",
"title": "Bildvergleich",
"description": "Vorher und nachher in einem Bild.",
"version": "1.1.0",
"requires": "1.17.0",
"settings": {
"labelBefore": { "kind": "text", "label": "Standardbeschriftung vorher", "default": "Vorher" }
},
"blocks": {
"bildvergleich": {
"label": "Bildvergleich",
"icon": "⇔",
"template": "blocks/bildvergleich.html.twig",
"css": "assets/bildvergleich.css",
"script": "assets/bildvergleich.js",
"props": {
"before": { "kind": "media", "label": "Bild vorher" },
"position": { "kind": "number", "label": "Startposition (%)", "min": 5, "max": 95, "default": 50 }
}
},
"galerie": {
"label": "Vergleichsgalerie",
"template": "blocks/galerie.html.twig",
"container": true,
"children": ["vergleich.bildvergleich"],
"props": { "columns": { "kind": "select", "label": "Spalten", "choices": { "2": "Zwei", "3": "Drei" } } }
}
},
"presets": {
"abschnitt": { "label": "Vorher-Nachher-Abschnitt", "blocks": [ { "id": "s", "type": "section", "children": [ ] } ] }
},
"routes": {
"daten": { "path": "/vergleich/daten", "template": "routes/daten.json.twig", "type": "json" }
},
"theme": { "colors": [ ], "fonts": [ ], "base": { }, "buttons": { } }
}
```
- `name` muss dem Ordnernamen entsprechen. Der Typ eines Bausteins ist `name.baustein`, hier
`vergleich.bildvergleich`; so heißt er im Dokument und in der Ausgabe.
- `version` in der Form `1.2.3`. Sie hängt an den Adressen der Dateien (`?v=1.1.0`), damit der
Browser nach einem Update die neue Fassung holt.
- `requires` (optional): die kleinste KantSites-Fassung. Passt sie nicht, erscheint das Plugin in
der Verwaltung mit dem Grund und lässt sich nicht einschalten.
- Ein Manifest braucht mindestens einen Baustein, eine Adresse, eine Vorlage oder ein
Designpaket. Fehlt eine genannte Datei, ist das Manifest ungültig.
### Eigenschaften und Einstellungen (Schema)
Eigenschaften (`props` eines Bausteins) und Einstellungen (`settings` des Plugins) haben dasselbe
Schema: eine Art (`kind`), eine Beschriftung (`label`), optional `help` und `default`.
| Art | Feld | Prüfung | Weitere Schlüssel |
|---|---|---|---|
| `text` | einzeilig | Text bis `maxLength` (Standard 200) | `maxLength` |
| `textarea` | mehrzeilig | Text bis `maxLength` (Standard 2000) | `maxLength` |
| `number` | Zahl | zwischen `min` und `max` | `min`, `max` |
| `boolean` | Kästchen | an oder aus | – |
| `select` | Liste | einer der Werte | `choices`: `{"wert": "Beschriftung"}` |
| `url` | einzeilig | lokaler Pfad, `#sprungmarke`, `https://`, `mailto:`, `tel:` | `maxLength` |
| `media` | Bild aus der Mediathek | Kennung eines Bildes oder leer | – |
Höchstens 30 Eigenschaften je Baustein. Unbekannte Eigenschaften in einem gespeicherten Dokument
werden ignoriert (so kann ein Plugin eine Eigenschaft streichen), bekannte müssen passen, sonst
lässt sich die Seite nicht speichern. Einstellungen bearbeitet der Administrator auf dem Bildschirm
„Plugins“; sie werden genauso geprüft.
### Container
`"container": true` macht einen Baustein zum Behälter. `"children"` (optional) zählt auf, was
hinein darf – Kernbausteine wie `text` oder Bausteine des Plugins; ohne die Liste darf alles hinein.
Der Editor zeigt die Regel im Werkzeugkasten, der Server prüft sie beim Speichern.
### Seitenvorlagen (`presets`)
Fertige Bausteine, die der Editor als Ganzes einfügt – hinter den eingebauten Vorlagen mit demselben
Knopf. `blocks` ist eine Liste von Bausteinen wie in einem Dokument (höchstens 64 KiB); Kennungen
vergibt der Editor bei jedem Einfügen neu, du kannst also feste schreiben.
### Eigene Adressen (`routes`)
`path` hat die Form einer Seite (`/name` oder `/name/unter/seite`, Slugs) und darf nichts belegen,
was der Kern beantwortet (`/admin`, `/media`, `/assets`, `/formular`, `/beitraege` …). `type` ist
`html`, `json`, `text` oder `xml`. Eine veröffentlichte Seite mit derselben Adresse geht vor.
Ausgeschaltet antwortet die Adresse mit 404; alles außer `html` trägt `noindex`.
### Designpaket (`theme`)
`colors`, `fonts`, `base` und optional `buttons` im Format des Designbereichs (siehe die
Aufbauskripte unter `scripts/` oder `plugins/nachtblau`). „Designpaket übernehmen“ ersetzt die
Palette der Website – dieselbe Prüfung wie beim Speichern unter Design, die Inhalte bleiben.
## Die Vorlagen
### Bausteine
Die Vorlage eines Bausteins bekommt genau sechs Dinge:
- `props` – die geprüften Eigenschaften, jede vorhanden, fehlende mit ihrem Vorgabewert
- `settings` – die Einstellungen des Plugins, jede vorhanden
- `children` – bei einem Container die fertig gerenderten Kinder (leer sonst); ausgeben mit
`{{ children }}`
- `block` – der Baustein selbst; `block.id` gehört als `id` an das Wurzelelement, sonst greifen
eigenes CSS und die Editorauswahl nicht
- `designClasses` – Klassen aus Sichtbarkeit, Design-Tokens und Einblendungen, an die `class`
des Wurzelelements anhängen
- `editing` – `true` in der Editorvorschau, für Hinweise wie „bitte ein Bild wählen“
Dazu eine Funktion: `kant_plugin_media(kennung)` liefert ein Bild der Mediathek als
`{id, title, alt, variants: [{src, width, height}, …]}` (kleinste Fassung zuerst) oder `null`.
```twig
{% set bild = props.before ? kant_plugin_media(props.before) : null %}
<div id="{{ block.id }}" class="block-bildvergleich{{ designClasses }}" data-position="{{ props.position }}">
{% if bild %}<img src="{{ (bild.variants|last).src }}" alt="{{ props.alt ?: settings.labelBefore }}">{% endif %}
{{ children }}
</div>
```
### Adressen
Die Vorlage einer Adresse bekommt `settings`, `query` (die Abfrageparameter mit sauberen Namen,
gekürzt) und `route`. Für `json` und `xml` schreibt sie die Antwort selbst, etwa mit `json_encode`.
Was keine Vorlage bekommt: Datenbank, Einstellungen der Website, Anfrage. Sie läuft mit den
Rechten der Anwendung – ein Sandkasten ist das nicht, und so wird es auch nicht genannt.
## Stylesheet und Skript
Beide werden nur auf Seiten geladen, auf denen der Baustein vorkommt, über
`/plugin-assets/<name>/assets/...`. Die Content-Security-Policy erlaubt Skripte vom eigenen
Ursprung, sobald die Seite eines braucht; Inline-Stile und Inline-Skripte bleiben verboten. Ein
Skript setzt Werte deshalb über das CSSOM (`element.style.setProperty(...)`), nie über ein
`style`-Attribut in der Vorlage. In der Editorvorschau wird das Stylesheet geladen, das Skript
nicht – die Vorlage muss auch ohne Skript ein brauchbares Bild abgeben.
## Was der Kern übernimmt
- Speichern, Veröffentlichen, Fassungen, Undo/Redo – der Baustein ist ein gewöhnlicher Block.
- Eigenes CSS je Block, Design-Tokens (Abstände, Ausrichtung, Ecken), Sichtbarkeit je Gerät,
Einblendungen – alles über `designClasses` und die `id`.
- Der Editor: Eintrag im Werkzeugkasten unter „Erweiterungen“, Eigenschaftenpanel aus dem
Schema, Container-Regeln, Vorlagen, Vorschau über den Server.
- Der Erhaltungsvertrag: ist das Plugin aus oder fehlt es, bleibt der Baustein im Dokument, der
Editor zeigt einen Platzhalter, die Website einen HTML-Kommentar. Nichts geht verloren.
## Updates
Mitgelieferte Plugins liegen im Releasearchiv und werden beim Archiv-Update Datei für Datei
aktualisiert. Ein selbst installiertes Plugin bleibt dabei stehen, weil sein Ordner im Archiv
nicht vorkommt. Beim Git-Update ist ein fremder Ordner unter `plugins/` nicht von Git verwaltet;
`git checkout` lässt ihn in Ruhe. Die Einstellungen liegen in der Datenbank und überleben beides.
+4
View File
@@ -1,5 +1,9 @@
# Qualitätsbericht – erster ausführbarer Prototyp
## Ergänzung: DEEG Immobilien & Hausverwaltung gemessen, 15.09.2026 (1.17.0)
Lighthouse **13.4.1** (`scripts/audit.mjs`, je drei Läufe Mobil und Desktop, Mediane) gegen den lokalen Produktionsmodus der fünften Referenzwebsite „DEEG – Immobilien & Hausverwaltung in Freudenstadt“ (`var/immobilien-deeg.db`, Startkonfiguration `immobilien-deeg-prod`, Port 8095), drei Adressen: `/`, `/impressum`, `/datenschutz`. Ergebnis: **Leistung, Barrierefreiheit, Best Practices, SEO und agentisches Browsing überall 100**; Startseite LCP 1,8 s mobil und 0,5 s Desktop (das Startfoto als `img` im Hero), CLS 0, TBT 0 ms. Ladebudget der Startseite 157 KB mit 6 Anfragen und 11 Datenbankabfragen. Berichte unter `reports/lighthouse-immobilien-deeg/`. Voraussetzung für die 100 bei Barrierefreiheit: Blau mit weißer Schrift als `#0076b6` statt Logo-Blau `#0081c7`, kleine blaue Schrift `#006aa3`, Copyright-Zeile mit eigener Farbe. Nach dem Umbau der Bewertungsanfrage zum dreistufigen Preisfinder (ADR 101, zweites Skript `form-steps.js` auf der Startseite) erneut gemessen: `/` mobil und Desktop weiterhin in allen fünf Kategorien 100, Ladebudget 161 KB mit 7 Anfragen.
## Ergänzung: Ihr Malerservice Drews gemessen, 15.09.2026 (1.16.0 + zwei unveröffentlichte Korrekturen)
Lighthouse **13.4.1** (`scripts/audit.mjs`, je drei Läufe Mobil und Desktop, Mediane) gegen den lokalen Produktionsmodus der vierten Referenzwebsite „Ihr Malerservice Drews“ (`var/maler-drews.db`, Port 8092), sechs Adressen: `/`, `/aktuelles`, `/unsere-projekte`, `/kunstmalerei`, `/kontakt`, `/friedrichsbad-baden-baden`. Ergebnis: **Barrierefreiheit, Best Practices, SEO und agentisches Browsing überall 100**, Leistung 98–100 – Startseite 99 mobil und 98 Desktop, Kunstmalerei 99/99, Projekte und Beitrag 99 mobil, alle übrigen 100. Der Rest zur 100 ist überall das Foto hinter dem Seitenkopf als CSS-Hintergrund (LCP mobil 1,6–2,1 s, Desktop 0,6–1,0 s; CLS ≤ 0,01, TBT ≤ 7 ms). Berichte unter `reports/lighthouse-maler-drews/`.
+80
View File
@@ -1,5 +1,85 @@
# Projektstatus
## Fassung 1.18.0 veröffentlicht (16.09.2026)
**Anlass:** Umut: „Bau eine neue Version bitte, Commit, Push und Release.“ Der Stand aus DEEG-Nachbau, Formularschritten, Pluginsystem und den Rückständen der Gesamtpruefung geht als 1.18.0 heraus.
**Vorher gefunden und behoben:** Der komplette `phpunit`-Lauf in einem Prozess starb reproduzierbar nach 80 Sekunden mit „Maximum execution time exceeded“, an wechselnden Stellen. Ursache war nicht ein Test, sondern `TaskWorkerSubscriber`: `set_time_limit()` **setzt** die Grenze, es verlängert sie nicht. Auf einem Prozess ohne Grenze – Konsole, Testlauf, Hoster mit `max_execution_time = 0` – machte `set_time_limit($budget + 60)` aus „unbegrenzt“ achtzig Sekunden, und der Prozess starb später irgendwo weit weg vom Aufgabenplan. Derselbe Fall stand im Takt der Verwaltung (`TaskController::tick()`, 120 Sekunden). Beide Stellen heben die Grenze jetzt nur an und senken sie nie. Einzeln liefen alle Testdateien immer durch, deshalb war das bisher nie aufgefallen.
**Geprüft:** neuer Test in `tests/SchedulerTest.php` („der Worker kürzt einen Prozess ohne Zeitgrenze nicht“): faellige Aufgabe läuft, `max_execution_time` bleibt 0. `phpunit` 572 Tests, 4768 Zusicherungen in **einem** Prozess grün (vorher Abbruch), `npm test` 34, Twig-Lint mit `plugins/`, php-cs-fixer. Danach Commit, Tag `v1.18.0`, Push nach `origin` und Releasearchiv.
## Rückstände der Gesamtprüfung abgearbeitet (16.09.2026)
**Anlass:** Umut: „Bitte umsetzen“ zur Liste aus ADR 100. ADR 104 beschreibt jeden Punkt; hier die Kurzfassung.
- **Archivseiten** laufen durch `renderPage()` (eigenes Dokument aus Kopfabschnitt und blätternder Beitragsliste): Kopf/Fuß mit Stilen, Sektionen, eigenes CSS, Bündel, Nach-oben, Einwilligung, Skripte. `templates/archive.html.twig` gelöscht.
- **Formulare, Beitragslisten, Brotkrumen in Bereichen und Sektionen** werden aufgelöst; Meldungen eines abgewiesenen Formulars finden den Block über die Formularadresse. Skripte in Bereichen laufen als `/assets/bereich/<id>.js`.
- **Doppelte Block-IDs** bei mehrfach verknüpfter Sektion: `PartialRepository::expandRepeatedLinks()` erzeugt Kopien mit eigenen Kennungen (Website und Editorvorschau).
- **Breakpoints** überall 1024/767 (design.css neu gebaut, site.css, enhancements.css, preview.css, editor.css, Editortext).
- **Farben**: `color-scheme`, Skip-Link, Aufruf-Button, Fehlerfarben, `.tone-ink`-Text aus der Palette; `Theme::css()` setzt `--k-scheme` und die Fehlerfarben nach Leuchtdichte des Papiers; Formularbutton dunkelt in die Tinte. Druckstile neu.
- **Betrieb**: Sicherung/Wiederherstellung auf MySQL (mysqldump/mysql), Git-Updater sichert und rollt über `Backup`, Sperre `var/update.lock` für beide Wege, Archiv als Datei geladen, `.htaccess` zusammengeführt (HTTPS/HSTS bleiben an), `var/update/` und alte Caches nach dem Update aufgeräumt, CI-MySQL-Job mit `cms:health`, `cms:backup` und `phpunit`.
- **SEO**: Canonical mit `?seite=n` und `rel=prev/next` bei blätternden Beitragslisten; Druckstile.
- **Editor**: Vorschau mit eigenem CSS, Bereichs-/Sektionsstilen und Formularen; Kontaktdaten, Formular, verknüpfte Sektion und Embed direkt nach dem Einfügen gültig (leer = nichts gewählt); bereinigter Rich Text sofort im Editor (`EditorHistory.adopt()`).
- **Kleines**: `legal.label` begrenzt, Docblocks in `PageController` gerückt.
**Geprüft:** neue `tests/BacklogTest.php` (7 Tests, 55 Zusicherungen: Archiv mit Fuß, Sektion, Nach-oben, Canonical/prev/next, Kategorie mit BreadcrumbList; blätternde Liste auf einer Seite; Formular im Fuß samt 422-Wiederanzeige; Sektion zweimal verknüpft; leere Bausteine; dunkle Palette; Sperre und .htaccess-Zusammenführung). Bestehende Tests angepasst: leerer Kontaktblock ist gültig, Schrittfolge des Updaters mit „Sperre“, `ReleaseOnDisk` kennt `toFile()`. Komplette Kette grün: `phpunit` 571 Tests (PHP 8.4), `npm test` 34, Twig-Lint mit `plugins/`, php-cs-fixer auf allen geänderten Dateien, `npm run test:editor` bestanden (Vorschau mit Formularen und eigenem CSS), DEEG `npm run test:browser` bestanden (Startseite 168 KB, 11 Anfragen – die vier Optionsbilder des Preisfinders kamen dazu), Lighthouse `/` Desktop überall 100, mobil Leistung 99 (die vier Piktogramme laden mit; Rest 100). Nicht committet.
## Arbeitspaket H zu Ende: Container, Einstellungen, Adressen, Vorlagen, Designpakete (16.09.2026)
**Anlass:** Umut: „Dann mach H.“ Zweiter Teil des Pluginsystems (ADR 103). Grundentscheidung: ein Plugin bleibt Manifest plus Twig, ohne PHP – deshalb keine Hooks und keine eigenen Tabellen, dafür alles, was sich ohne Installationscode sicher übergeben lässt.
**Gebaut.** `PluginRegistry` liest jetzt auch `settings` (Schema wie Eigenschaften, gespeichert in der neuen Spalte `plugin.settings`, Migration `20260916000100`), `container`/`children` am Baustein, `routes` (Pfad wie eine Seite, Vorlage, Antwortart html/json/text/xml, Kernadressen gesperrt), `presets` (Bausteinlisten bis 64 KiB) und `theme` (Palette, Schriften, Grundwerte, Buttons). `block.html.twig` rendert die Kinder eines Containers selbst und reicht sie der Vorlage als `children`, dazu `settings`; `DocumentValidator` weist Kinder in Nicht-Containern und nicht erlaubte Kinder ab. `PageController::page()` beantwortet eine Plugin-Adresse, wenn keine Seite den Pfad hat (vor Weiterleitung und 404), mit Einstellungen und gesäuberten Abfrageparametern, Schutzkopfzeilen, ohne Skript, außer html mit `noindex`. Editor: `registerPlugins(list, presets)` kennt Container und erlaubte Kinder (`canContain`), `insertPreset('plugin:…')` fügt Vorlagen mit frischen Kennungen ein, der Werkzeugkasten zeigt sie hinter den eingebauten. Bildschirm „Plugins“: Einstellungsformular je aktivem Plugin, Adressen verlinkt, „Designpaket übernehmen“ (`Theme::save()`, Inhalte bleiben), beide Wege in der Rollenmatrix. Referenz: `plugins/vergleich` 1.1.0 (Einstellungen, Vergleichsgalerie als Container nur für Bildvergleiche, Vorlage „Vorher-Nachher-Abschnitt“, Adresse `/vergleich/daten`), neu `plugins/nachtblau` als reines Designpaket. Alle fünf lokalen Installationen migriert. `docs/PLUGINS.md` beschreibt das vollständige Manifest.
**Geprüft:** `tests/PluginTest.php` jetzt 8 Tests: Einstellungen (Vorgaben, Umwandlung aus dem Formular, Abweisung, Wirkung in Vorlage und Adresse, 404 bei ausgeschaltetem Plugin), Container (Kinder gerendert, Stylesheet einmal, fremdes Kind und Kinder im Nicht-Container abgewiesen, Editor-Definitionen und Vorlagen), Designpaket (übernommen, Buttons, Dokument unverändert, nur eingeschaltet, Fehler ohne Paket). `editor-model.test.mjs` prüft registrierte Container, Vorgaben, `canContain` und Vorlagen mit frischen Kennungen (34 grün). `EditorSecurityTest` mit den neuen Routen grün. `npm run test:editor` um Einstellungen speichern, die Adresse `/vergleich/daten`, die Plugin-Vorlage im Werkzeugkasten und den Container erweitert (Bildvergleich landet in der Galerie, ein Text daneben) und bestanden; ein Lauf davor blieb beim Neuladen des Editors bei „Wird geladen“ hängen, der nächste lief ohne Änderung durch – der Check protokolliert jetzt jede Konsolenfehlermeldung, damit so etwas beim nächsten Mal einen Grund hat. Komplette Kette grün: `phpunit` 565 Tests (PHP 8.4), `npm test` 34, Twig-Lint mit `plugins/`, php-cs-fixer auf allen neuen und geänderten Dateien. Nicht committet.
## Pluginsystem, erste Fassung – Manifest, Schalter, geprüfte Bausteine, Referenzplugin „Bildvergleich“ (15.09.2026)
**Anlass:** Umut: „Plugin hätte ich gerne aber umgesetzt. Also Möglichkeit. … Dann Plugin so wie du es gesagt hast, hört sich gut an.“ Arbeitspaket H, erster Teil (ADR 102).
**Gebaut.** `src/Plugin/PluginRegistry.php` liest `plugins/<name>/plugin.json` (Titel, Fassung, `requires`, Bausteine mit Beschriftung, Symbol, Vorlage, Stylesheet, Skript und Eigenschaftenschema mit sieben Arten), meldet kaputte Manifeste mit Grund und hält den Schalter in der neuen Tabelle `plugin` (Migration `20260915000100`, in `TestSchema`). `DocumentValidator` prüft die Eigenschaften eines Plugin-Bausteins beim Schreiben gegen das Schema (`validate(..., writing: true)` an allen Schreibstellen), beim Lesen nicht – ein später eingeschaltetes Plugin macht keine Seite unlesbar. `templates/blocks/block.html.twig` rendert Bausteine aktiver Plugins über `@plugins/...` mit den erneut geprüften `props` (`src/Twig/PluginExtension.php`: `kant_plugin_block`, `kant_plugin_props`, `kant_plugin_media`); passt etwas nicht, bleibt der Kommentar. `PluginAssetController` liefert nur Dateien aktiver Plugins unter `assets/` mit erlaubter Endung; `PageController` verlinkt sie je Seite und öffnet die Richtlinie für ein Skript nur dann. Design-Tokens gelten jetzt auch für Plugin-Typen. Editor: der Dokumentaufruf liefert die Definitionen (`plugins`), `registerPlugins()` in `editor-model.js` trägt sie ein (Gruppe „Erweiterungen“, Vorgaben, Schema v4), `renderPluginFields()` in `editor.js` baut das Panel aus dem Schema, ein Bild über die Mediathek; die Vorschau lädt das Stylesheet, kein Skript. Verwaltung: Bildschirm „Plugins“ (Administrator) mit Ein/Aus, CSRF, in Navigation und Rollenmatrix. Release/Update: `plugins/` im Archiv und beim Archiv-Update Datei für Datei übernommen. Entwicklerdoku `docs/PLUGINS.md`.
**Referenzplugin `plugins/vergleich`:** Baustein `vergleich.bildvergleich` – zwei Bilder aus der Mediathek, ein Regler dazwischen (natives Bereichsfeld, Skript setzt nur `--pos` über das CSSOM), Beschriftungen, Startposition, Ecken. Ohne Skript steht der Vergleich bei der Startposition.
**Geprüft:** `tests/PluginTest.php` (5 Tests, 50 Zusicherungen): Manifest lesen und schalten (Schalter überlebt eine neue Registry), drei kaputte Manifeste mit Grund und ohne Schalter, Eigenschaftenprüfung nur bei aktivem Plugin und nur beim Schreiben, Rendering mit Stylesheet/Skript/CSP gegen Kommentar bei ausgeschaltetem Plugin, Asset-Route (Typ, `immutable`, 404 für Manifest, Vorlagen, `..` und ausgeschaltete Plugins), Design-Tokens und eigenes CSS am Plugin-Baustein. `EditorSecurityTest` mit den Plugin-Routen grün (38 Tests), `npm test` 33 grün, Twig-Lint aller Vorlagen einschließlich `plugins/`. Die vier lokalen Installationen sind migriert. `npm run test:editor` um ein Plugin-Szenario erweitert und bestanden: Bildschirm „Plugins“ mit axe (die Überschriftenfolge dort war zuerst falsch, h3 unter h1), Einschalten, Baustein aus dem Werkzeugkasten, Panel aus dem Schema, zwei Bilder über die Mediathek, Vorschau mit Stylesheet und ohne Skript, Speichern und Veröffentlichen, Website mit Skript und `script-src 'self'`, Ausschalten lässt den Baustein im Dokument und zeigt den Kommentar (`reports/editor/plugins.png`, `plugin-block.png`). Danach die komplette Kette: `phpunit` zuerst mit drei Befunden – der feste Twig-Pfad `paths` in `twig.yaml` brach jede Installation ohne Ordner `plugins/` (Installer-Tests), und die Vollständigkeitsprüfung des Archiv-Updaters kannte `plugins/` nicht. Jetzt registriert `PluginExtension` den Namensraum `@plugins` zur Laufzeit nur, wenn der Ordner existiert, und der Test führt `plugins/` als Datei-für-Datei-Ordner wie `public/`. Danach `phpunit` vollständig grün (562 Tests, 4664 Zusicherungen, PHP 8.4), Twig-Lint mit `plugins/`, php-cs-fixer auf allen neuen und geänderten Dateien.
**Nicht gebaut:** Container-Bausteine, eigene Routen und Einstellungen, Tabellen, Hooks, Ressourcen in der Verwaltung, Templates. Kein Sandkasten – Plugin-Vorlagen laufen mit den Rechten der Anwendung, so steht es auf dem Bildschirm. Nicht committet.
## Formulare mit Schritten, Bedingungen und Zusammenfassung – der DEEG-Preisfinder (15.09.2026)
**Anlass:** Umut: „Plugins funktionieren noch nicht? Was fehlt denn? Ich hätte gerne kein Kontaktformular weiteres, sondern so ein ausgebautes Form.“ Dann: „Okay, dann erweitere den Kern.“ Gemeint ist der Preisfinder der laufenden DEEG-Website: Objektart wählen, Eckdaten je Objektart, Kontakt, Zusammenfassung.
**Kern (ADR 101):** Drei neue Feldtypen – Zahl (Unter-/Obergrenze, Einheit), Auswahl als Optionsfelder, Mehrfachauswahl. Ein Feld „Neuer Schritt“ eröffnet einen Abschnitt (Beschriftung = Titel, Hilfetext = Einleitung). Jedes Feld und jeder Schritt kann eine Bedingung tragen: gilt nur, wenn ein früheres Auswahlfeld einen der genannten Werte hat. Einstellung „Zusammenfassung vor dem Absenden“. Höchstens 40 statt 25 Felder. Der Server prüft die Bedingungen selbst (`FormRepository::check()`): ein Feld, das nicht galt, wird weder verlangt noch gespeichert, auch wenn der Browser einen Wert schickt; beim Speichern der Definition muss jede Bedingung auf ein früheres Auswahlfeld mit passenden Optionen zeigen. Bestehende Formulare bleiben ohne Migration gültig (`present()` ergänzt die Schlüssel).
**Öffentlich:** `templates/blocks/form.html.twig` rendert Schritte als `fieldset`/`legend`, Gruppen für Options- und Mehrfachauswahl, `type="number"` mit Grenzen und Einheit, Bedingungen als `data-show-if`/`data-show-values`. Ohne JavaScript steht das Formular vollständig auf der Seite. Neu `public/assets/form-steps.js` (nur auf Seiten, deren Formular es braucht, `FormRepository::needsScript()`; die CSP erlaubt es dann): Fortschritt, „Zurück“/„Weiter“ mit Browserprüfung je Schritt, Felder hinter nicht zutreffenden Bedingungen deaktiviert statt nur versteckt, Zusammenfassung als letzter Schritt mit „Ändern“, nach einer Serverablehnung der erste Schritt mit Fehler. `site.css`: Stile für Schritte, Fortschritt, Gruppen, Zahl mit Einheit, Zusammenfassung; `.block-form [hidden]` schlägt jetzt `display:grid`/`inline-flex`. E-Mail: Schritte als Zwischenüberschrift, Felder, die nicht galten, bleiben weg.
**Bildauswahl (Nachtrag):** Umut meinte mit „Upload“ eine Bildauswahl wie im Solar-Rechner von Elektro Bernhard (Bildkarten statt Kästchen). Optionsfelder und Mehrfachauswahl tragen jetzt je Option ein Bild aus der Mediathek; sobald eine Option eines hat, wird die Auswahl auf der Website zu anklickbaren Karten mit Bild und Text, das Kästchen bleibt darin für Tastatur und Vorlesesoftware. Im DEEG-Preisfinder hat die Objektart vier Piktogramme (`var/immobilien-deeg-assets/option-*.png`, mit GD gezeichnete Platzhalter, in der Mediathek gegen Fotos tauschbar). Test `testAChoiceWithPicturesBecomesClickableCards`, dazu zwei abgelehnte Zuordnungen.
**Verwaltung:** Feldkarte mit Untergrenze/Obergrenze/Einheit (bei Zahl), Optionen bei Liste/Optionsfeldern/Mehrfachauswahl, „Bild je Option“ als Liste der Mediathek, Bedingung („Nur zeigen, wenn das Feld … einen dieser Werte hat“); Zustellung mit Schalter „Zusammenfassung vor dem Absenden zeigen“; Vorschau zeigt Schritte, Optionsfelder, Kästchen, Einheit und Bedingungen. Alles ohne Skript bedienbar.
**DEEG:** Das Formular `bewertung` im Aufbauskript ist jetzt der Preisfinder: Schritt 1 Objektart (Wohnung, Haus, Mehrfamilienhaus, Grundstück) und Anlass (Verkaufen, Vermieten); Schritt 2 Eckdaten je Objektart – Wohnfläche, Grundstücksfläche, Zimmer, Wohneinheiten, Baujahr, Zustand, Einbauküche, Aufzug, Parkmöglichkeit, Bebaubarkeit, jeweils hinter Bedingungen; Schritt 3 Lage und Kontakt; dann die Zusammenfassung. Die Feldliste folgt dem Preisfinder von immobiliendeeg.de (Wohnung-Seite, 15.09.2026). Das Kontaktformular bleibt, wie es war.
**Geprüft:** neue `tests/FormStepsTest.php` (5 Tests, 75 Zusicherungen): Normalisierung beim Speichern, zehn abgelehnte Definitionen mit Begründung, Bedingungen in `check()` (Grundstück ohne Wohnfläche, Wohnung mit Pflicht, Zahl mit Komma, Grenzen, fremde Optionen), Rendering samt Skript und CSP (und ein einfaches Formular weiter ohne Skript, `script-src 'none'`), POST mit Speicherung nur der geltenden Felder und 422-Wiederanzeige mit erhaltenen Kreuzen. Im Browser (Port 8094): drei Schritte durchgespielt, Felder je Objektart, Zusammenfassung mit allen Werten, Absenden mit Erfolgsmeldung, Einsendung in `form_submission` mit leeren Feldern für alles, was nicht galt; Grundstück zeigt nur zwei Eckdatenfelder, „Weiter“ hält bei leerem Pflichtfeld. Komplette Kette: `phpunit` 556 grün (PHP 8.4), `npm test` 33 grün, `npm run test:editor` vollständig bestanden (darunter die axe-Prüfung der neuen Formularverwaltung), Twig-Lint ohne Befund, php-cs-fixer auf den geänderten Dateien (die vier Befunde in `install.php` und den drei älteren Aufbauskripten sind vorbestehend). DEEG nach dem Umbau: `npm run test:browser` bestanden (Startseite jetzt 2 Skripte – Einblendungen und Formular –, 161 KB), Lighthouse `/` mobil und Desktop weiterhin überall 100.
**Browsercheck angepasst:** `scripts/browser-check.mjs` erlaubte bisher höchstens ein Skript je Seite. Das war schon vor dem Formularskript zu eng – Slider, Reiter, Einblendungen, Bildbetrachter und Nach-oben sind je eine eigene Datei –, fiel aber erst jetzt auf, weil keine Referenzseite zwei davon lud. Neu: höchstens drei Skripte, alle aus `/assets/` des eigenen Ursprungs, und weiterhin keins, wenn die Richtlinie keins erlaubt.
**Nicht gebaut:** Bedingungen auf Zahlenbereiche oder mehrere Felder, Dateiuploads, Schritt-für-Schritt ohne JavaScript, Formulare in der Editorvorschau. Nicht committet.
## DEEG Immobilien & Hausverwaltung nachgebaut, Anfrage als Formular (15.09.2026)
**Anlass:** Umut lieferte `Immobilien-DEEG-Website.zip` (Onepager für die Deeg Hausverwaltung und Immobilien GmbH, Freudenstadt) und wollte ihn „nach dem Prinzip wie die anderen Seiten mit Kontaktformular“ aufbauen; dazu die Frage, ob die Bewertungsanfrage ein eigenes Plugin braucht.
**Antwort auf die Plugin-Frage:** Nein. Ein Plugin ist in diesem CMS ein Baustein mit Namensraum, den der Kern aufbewahrt, aber nicht rendert (ADR zum Erhaltungsvertrag, ADR 049); eine Laufzeit dafür gibt es erst mit Phase 4. Die Anfrage ist deshalb das Formular `bewertung` (Auswahl Wohnung/Haus/Mehrfamilienhaus/Grundstück – die vier Objektarten des Preisfinders der laufenden Website –, Straße und Ort, Name, E-Mail, Telefon, Hinweise, Einwilligung), der Kontakt das Formular `kontakt` (mit Auswahl Verkauf/Vermietung/Verwaltung). Beide verschicken serverseitig und liegen unter Formulare in der Verwaltung; die Vorlage verlinkte dafür noch auf www.immobiliendeeg.de.
**Nachbau (`scripts/seed-immobilien-deeg.php`, `scripts/reset-immobilien-deeg.sh`).** Fünfte Installation `var/immobilien-deeg.db`, `var/immobilien-deeg-media`, Bildquellen `var/immobilien-deeg-assets` (Logo, zwei Architekturfotos aus dem Paket), Startkonfigurationen `immobilien-deeg` (Port 8094) und `immobilien-deeg-prod` (Port 8095, Produktionsmodus zum Messen). Startseite mit Hero (Foto, Schild, Bildzeile, blauer Aufruf zur Bewertung), Leistungsleiste, drei Leistungskarten mit Akkordeon, blauem Bewertungsabschnitt mit Formularkarte, Über uns mit „SEIT 1994“, Angebotsband (Link auf www.immobiliendeeg.de/immobilien/, die Immobilien-Datenbank ist nicht Teil des Pakets), Kontakt mit Telefon, E-Mail, Adresse, Bürozeiten und Formularkarte; Kopf mit heller Leiste, Logo, Menü und blauem Knopf „Immobilie bewerten“; Fuß mit Logo, Rechtszeile und Agenturlink. Impressum mit Register und Vertretung vom Impressum der laufenden Website (HRB 430747, Amtsgericht Stuttgart, Furkan Kurt); USt-ID, Aufsicht nach § 34c GewO und Bildlizenz des übernommenen Fotos als „NOCH ZU PRÜFEN“. Datenschutz als eigene Fassung ohne Cookies und Fremddienste. Schrift Arial wie die Vorlage, nichts zu holen.
**Für 100 geändert, nicht für die Optik:** Blau als Fläche mit weißer Schrift trägt `#0076b6` (4,9:1 statt 4,2:1 beim Logo-Blau), kleine blaue Schrift auf Weiß `#006aa3`; das Logo-Blau `#0081c7` bleibt für Linien und große Überschriften. Der Button-Baustein hängt immer „↗“ an, die Vorlage kommt ausdrücklich ohne Pfeile aus – am Button `& span{display:none}`. Die Copyright-Zeile trägt ihre Farbe selbst, weil der Fuß kleinen Text sonst auf 3,8:1 aufhellt.
**Browsercheck angepasst (`scripts/browser-check.mjs`):** Vor Überlauf- und axe-Prüfung spielt das Skript alle laufenden Einblendungen zu Ende (`document.getAnimations().forEach(a => a.finish())`). Vorher hing das Ergebnis vom Zeitpunkt ab: mitten in einer Einblendung rechnete axe den Kontrast gegen die halb durchsichtige Farbe (drei Scheinbefunde am Hero), und ein von rechts hereingleitender Block zählte als horizontaler Überlauf.
**Geprüft:** `npm run test:links` gegen Port 8094: 3 Seiten, 14 Adressen, kein toter Verweis. `npm run test:browser` (`PHP_BINARY=C:/php84/php.exe`): sechs Seiten-/Viewport-Prüfungen mit axe, Tastatur, ohne JavaScript bestanden, keine redaktionellen Befunde; Ladebudget der Startseite 157 KB, 6 Anfragen, 11 Abfragen. Lighthouse 13.4.1 (`scripts/audit.mjs`, je drei Läufe Mobil und Desktop, Mediane) gegen den Produktionsmodus auf Port 8095 für `/`, `/impressum`, `/datenschutz`: **Leistung, Barrierefreiheit, Best Practices, SEO und agentisches Browsing überall 100** (Startseite LCP 1,8 s mobil / 0,5 s Desktop, CLS 0, TBT 0 ms); Berichte in `reports/lighthouse-immobilien-deeg/`. Kontaktformular einmal abgeschickt: Erfolgsmeldung, Eingang in `form_submission`. Screenshots in 1440 und 390 px gegen die Vorlage verglichen. php-cs-fixer auf dem Skript ohne Befund.
**Nicht geprüft:** der Editor auf den DEEG-Dokumenten im Browser, E-Mail-Zustellung (lokal `sendmail://default`, der Eingang bleibt trotzdem gespeichert), Firefox/Safari, ein echtes Touchgerät, das Hosting. Nicht committet.
## 1.17.0 veröffentlicht (15.09.2026)
Tag `v1.17.0` (Commit `d07154e`), Release: https://git.kant-it.de/kantarci/KantSites/releases/tag/v1.17.0 – Adresse mit oder ohne www aus `SITE_ORIGIN`, Rollen wie die Rollenhilfe sie verspricht, Befunde der Gesamtprüfung (ADR 099, ADR 100).
+4
View File
@@ -133,3 +133,7 @@ Der Archiv-Updater erhält die Konfiguration auch bei diesem Übergang. Der erst
In alten Git-Installationen war `.env` noch versioniert. Beim erstmaligen Wechsel kann ein alter Git-Updater diese neue Schutzlogik noch nicht besitzen. Nutze dafür den Archiv-Updater. Bei einem manuellen Git-Checkout zuerst `.env` und gegebenenfalls `.env.local` außerhalb des Checkout-Verzeichnisses sichern, beide nach dem Checkout wiederherstellen und erst danach Setup/Migration/Cache starten. Ab 1.7.0 erhält der Git-Updater die Dateien selbst vor dem ersten neuen PHP-Prozess. `.env` nicht erneut ins Repository aufnehmen.
Bei FTP-Updates die `.env` aus dem neuen Archiv überspringen. Die Vorlage im Archiv ist ausschließlich für Erstinstallationen bestimmt. Ein Rückweg benötigt den alten Code plus die beiden gesicherten Konfigurationsdateien.
## MySQL und MariaDB (seit 16.09.2026, ADR 104)
Sicherung und Wiederherstellung brauchen auf einer MySQL-Installation die Clients `mysqldump` und `mysql` im Pfad des PHP-Prozesses (auf geteiltem Hosting beim Anbieter erfragen; ein voller Pfad lässt sich nicht eintragen, aber `mysqldump.phar`/`mysql.phar` im Projektstamm würden gefunden). Die Sicherung schreibt `datenbank.sql` mit `--single-transaction`; die Wiederherstellung spielt sie mit `mysql --execute=source` ein. Fehlt der Client zum Einspielen, sagt die Wiederherstellung das und nennt den Pfad des Dumps. Der Archiv-Updater und der Git-Updater sichern vorher über denselben Weg und spielen beim Rückweg dieselbe Sicherung ein. Beide Wege sperren sich gegenseitig über `var/update.lock`; ein zweiter Aufruf bekommt die Meldung „Ein anderes Update läuft gerade“.