Compare commits

..
4 Commits
Author SHA1 Message Date
kantarciandClaude Opus 5 ea3dd4ddf2 Fassung 1.4.2: die Systempruefung nennt die SQLite-Fassung
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s
Auf dem Hosting brach die Installation im dritten Schritt ab: near
"(": syntax error. Dort laeuft SQLite in einer Fassung vor 3.16, und
Doctrine liest das Schema mit Tabellenfunktionen wie
pragma_table_info(...), die es erst seit 3.16 gibt.

Die Pruefung nennt die gefundene Fassung jetzt vor der Wahl der
Datenbank, und der Datenbankschritt bietet SQLite gar nicht erst an,
wenn sie zu alt ist - mit dem Hinweis auf MySQL. Eine Voraussetzung,
die erst mitten in der Installation auffaellt, ist keine Pruefung wert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 21:56:17 +02:00
kantarciandClaude Opus 5 c517173579 Fassung 1.4.1: dem Releasearchiv fehlten migrations.php und cli-config.php
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s
Auf dem Hosting brach die Installation im dritten Schritt ab - "The
configuration file does not exist", genau dort, wo die Tabellen
entstehen sollten. Beide Dateien standen weder in der Mitnahme- noch in
der Ausschlussliste des Releaseskripts.

Ein Test ordnet jetzt jeden Eintrag im Projektstamm einer der beiden
Listen zu: was neu dazukommt und unbedacht bleibt, laesst ihn
fehlschlagen. Der Installationsweg wurde danach vollstaendig
durchgespielt - 3504 Dateien entpackt, 14 Migrationen ausgefuehrt, 15
Tabellen angelegt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 21:43:56 +02:00
kantarciandClaude Opus 5 c5e85e4db0 Release 1.4.0 veroeffentlicht und der Installationsweg geprueft
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s
Archiv, Pruefsumme und install.php haengen am Release des Tags v1.4.0.
Einmal vollstaendig durchgespielt: install.php anonym geladen, in ein
leeres Verzeichnis gelegt, aufgerufen - laedt 4,7 MB, prueft die
Summe, entpackt 3502 Dateien mit vendor/ und loescht sich selbst. Die
frische Installation antwortet mit der Systempruefung unter /install.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 21:23:23 +02:00
kantarciandClaude Opus 5 43c20b14c7 Release veroeffentlichen: Ablauf dokumentiert, ADMIN_PATH in der Vorlage
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s
Das Archiv und install.php gehoeren als Anhang an das Git-Release des
passenden Tags - genau dort erwartet install.php sie. Ein privates
Repository liefert diese Adresse nicht ohne Anmeldung aus; das steht
jetzt in docs/UPDATE.md, samt der Alternative ueber --url=.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 20:56:56 +02:00
11 changed files with 127 additions and 5 deletions

No files matched your search

+3
View File
@@ -9,3 +9,6 @@ DATABASE_URL="sqlite:///%kernel.project_dir%/var/cms.db"
SITE_ORIGIN=https://example.de
MEDIA_DIRECTORY="%kernel.project_dir%/var/media"
MAILER_DSN=sendmail://default
# Die Adresse der Verwaltung. Ohne Eintrag: /admin. Aenderbar mit
# `php bin/console cms:admin:path <neu>` - der Befehl schreibt sie nach .env.local.
#ADMIN_PATH=admin
+2
View File
@@ -88,6 +88,8 @@ npm.cmd run audit
Unter Linux/macOS `npm` und `npx` ohne `.cmd`; in CI `npx playwright install --with-deps chromium`. `test:browser` und `audit` benötigen den laufenden lokalen Produktionsmodus auf Port 8080. Ein anderes Ziel lässt sich über `CMS_URL` konfigurieren. `test:editor` startet einen eigenen PHP-Prozess auf einem freien Port mit isolierter Datenbank und zufälligem Testkonto; es ändert keine echten Seiten oder Benutzer. `PHP_BINARY` kann dafür einen alternativen PHP-Pfad angeben. Testdaten bleiben unter `var/browser-tests`, Berichte/Screenshots unter `reports/`. Das Skript erneuert zuvor den Produktionscache.
**Datenbank:** SQLite ab Fassung 3.16 (Doctrine liest das Schema mit Tabellenfunktionen, die es erst seit dieser Fassung gibt) oder MySQL/MariaDB. Auf älteren Systemen – manche Shared Hoster liefern noch SQLite 3.7 – bleibt MySQL; die Systemprüfung des Installers nennt die gefundene Fassung und bietet SQLite gar nicht erst an, wenn sie zu alt ist.
Die Verwaltung liegt unter `/admin`. `php bin/console cms:admin:path meine-verwaltung` legt sie auf eine andere Adresse: der Wert landet in `.env.local`, gilt für Routen, Firewall und Schutzkopfzeilen, und wird in der `robots.txt` bewusst nicht genannt. Ohne Argument zeigt der Befehl die aktuelle Adresse.
`composer format` formatiert PHP. `composer build` wärmt den Symfony-Produktionscache; öffentliche CSS-Dateien sind direkt auslieferbar, es ist kein Frontend-Bundler nötig. Bei Änderungen an `public/assets/design-tokens.json` einmal `npm.cmd run build:design` ausführen und die erzeugte CSS-Datei mitliefern; Node ist auf dem Hosting nicht erforderlich. Nach Änderungen an Produktionskonfiguration/Templates `php bin/console cache:clear --env=prod --no-debug` ausführen. Falls lokale Tests alte Templates verwenden, vor PHPUnit auch `php bin/console cache:clear --env=test --no-debug` ausführen.
+1 -1
View File
@@ -1 +1 @@
1.4.0
1.4.2
+3
View File
@@ -37,6 +37,9 @@ Erster Commit angelegt (`82b0782`); das Projekt hat damit einen Rückfallpunkt.
- **Das erste Bild einer Seite lädt sofort**, alles darunter weiterhin verzögert. Es ist meistens das größte Ding über der Falz.
- **Slider mit Punkten ohne Skript** (ADR 044): `::scroll-marker` zeichnet je Folie einen Punkt, der aktive ist erkennbar, die Rollleiste entfällt; ältere Browser behalten das wischbare Schiebefeld. Beitragskarten sind echte Karten (Bild 16:10, 18 px Ecken). Überschriften dürfen ein Wort hervorheben (`<mark>`, in Akzentfarbe). Behoben: die Theme-Schriftskala überschrieb die Größen in Karten und Aufruf-Bändern.
- **„Weiterlesen" ist jetzt ein Link.** In der wechselseitigen Beitragsliste stand dort eine reine Beschriftung: sie sah aus wie ein Knopf und war keiner. Der Titel steckt für Vorlesesoftware im Linktext, damit nicht zwölf Links gleich heißen.
- **Behoben in 1.4.2: die Systemprüfung nennt jetzt die SQLite-Fassung.** Auf dem Hosting scheiterte die Installation im dritten Schritt mit „near \"(\": syntax error" – dort läuft SQLite in einer Fassung vor 3.16, und Doctrine liest das Schema mit Tabellenfunktionen (`pragma_table_info(...)`), die es erst seit 3.16 gibt. Die Prüfung nennt die gefundene Fassung vor der Wahl der Datenbank, und der Datenbankschritt bietet SQLite gar nicht erst an, wenn sie zu alt ist – mit dem Hinweis auf MySQL. Eine Voraussetzung, die erst mitten in der Installation auffällt, ist keine Prüfung wert.
- **Behoben in 1.4.1: dem Releasearchiv fehlten `migrations.php` und `cli-config.php`.** Auf dem Hosting brach die Installation deshalb im dritten Schritt ab – „The configuration file does not exist", genau dort, wo die Tabellen entstehen sollten. Die beiden Dateien standen weder in der Mitnahme- noch in der Ausschlussliste des Releaseskripts, also fiel niemandem auf, dass sie fehlen. Ein Test ordnet jetzt **jeden** Eintrag im Projektstamm einer der beiden Listen zu; was neu dazukommt und unbedacht bleibt, lässt ihn fehlschlagen. Der Installationsweg wurde anschließend vollständig durchgespielt: 3504 Dateien entpackt, 14 Migrationen ausgeführt, 15 Tabellen angelegt.
- **Fassung 1.4.0 ist veröffentlicht** und der Installationsweg einmal vollständig durchgespielt: `install.php` aus dem Release geladen (anonym, ohne Anmeldung), in ein leeres Verzeichnis gelegt, aufgerufen – es lädt `kantsites-1.4.0.zip` (4,7 MB, 3502 Dateien mit `vendor/`), prüft die SHA-256-Summe, entpackt und löscht sich selbst. Die frische Installation antwortet mit der Systemprüfung unter `/install`. **Das Repository ist dafür öffentlich** – ein privates liefert die Downloadadresse ohne Anmeldung nicht aus.
- **Die Verwaltung kann umziehen** (ADR 065): `php bin/console cms:admin:path meine-verwaltung` legt sie auf eine andere Adresse – geprüft wird, dass der Name frei ist und keine Seite verdeckt. Der Wert steht in `.env.local` und gilt für Routen, Firewall, Schutzkopfzeilen und Statistik. **In der `robots.txt` wird ein geänderter Name nicht genannt**; beim Standardnamen `admin` bleibt die Zeile, weil diese Adresse ohnehin bekannt ist. Die Systemseite zeigt die aktuelle Adresse an.
- **Dokumentschema v19**: Baustein **Rechtszeile** (Impressum, Datenschutz, „Einwilligung ändern") für den Fußbereich – gestaltbar und an der richtigen Stelle statt als heller Streifen unter einem dunklen Fuß; die eingebaute Leiste erscheint nur noch, wenn die Seite den Baustein nicht hat. Dazu **„Beim Klick"** je Bild (nichts / vergrößern / Adresse) und **„Bilder vergrößerbar"** je Galerie.
- **Bildbetrachter** (ADR 063): ohne JavaScript ein Verweis auf die große Bildfassung, mit JavaScript ein Betrachter über der Seite – `<dialog>` mit Blättern, Escape, Pfeiltasten und Zähler. Das Skript lädt nur auf Seiten, die ein vergrößerbares Bild enthalten.
+14
View File
@@ -62,6 +62,20 @@ Der teuerste Fall ist ein Update, das durchläuft und erst Tage später als fals
- Kein `down()`-Rückweg für Migrationen. Der Rückweg ist die Sicherung, und das ist eine Entscheidung, nicht eine Lücke.
- Kein Update ohne Sicherung, auch nicht mit Häkchen „ich weiß, was ich tue“.
## Ein Release veröffentlichen
`php scripts/build-release.php` baut auf einem Rechner mit Composer das Archiv – Anwendung plus `vendor/` ohne Entwicklungspakete – und schreibt die SHA-256-Summe in eine Kopie von `install.php`. Beides gehört anschließend an das Git-Release des passenden Tags:
```
var/releases/kantsites-<fassung>.zip → Release-Anhang
var/releases/kantsites-<fassung>.zip.sha256 → Release-Anhang
var/releases/install.php → Release-Anhang
```
`install.php` erwartet das Archiv unter `https://git.kant-it.de/kantarci/KantSites/releases/download/v<fassung>/kantsites-<fassung>.zip`. Genau dort landet es, wenn es als Anhang am Release dieses Tags hängt – eine andere Adresse lässt sich beim Bauen mit `--url=` setzen.
**Ein privates Repository liefert diese Adresse nicht aus.** Ohne Anmeldung antwortet sie mit 404, und `install.php` kann nichts herunterladen; das ist der Zweck der Prüfsumme, nicht ihr Fehler. Damit die Installation auf einem fremden Hosting funktioniert, müssen Archiv und Installer ohne Anmeldung erreichbar sein – entweder weil das Repository öffentlich ist, oder weil beide Dateien auf einem eigenen Webspace liegen und das Release mit `--url=` darauf gebaut wurde.
## Offen, bevor das gebaut wird
1. ~~**Composer auf dem Server oder `vendor/` im Tag?**~~ Entschieden: `vendor/` gehört nicht ins Repository, sondern in ein Releasearchiv, das ein Build-Rechner erzeugt (`scripts/build-release.php`, SHA-256 daneben). Der Updater über Git bleibt für Installationen mit Composer und für Updates ohne Abhängigkeitsänderung.
+5 -2
View File
@@ -64,8 +64,11 @@ if (!is_dir($outDirectory) && !mkdir($outDirectory, 0o775, true)) {
echo "Fassung {$version}\n";
// What belongs into a release: the application, and nothing that only a developer needs.
$include = ['bin', 'config', 'migrations', 'public', 'src', 'templates', 'composer.json', 'composer.lock', 'VERSION', 'README.md', 'LICENSE', '.env.example'];
$skip = ['.git', '.github', 'node_modules', 'var', 'tests', 'scripts', 'reports', 'phpunit.xml.dist', '.php-cs-fixer.dist.php', 'package.json', 'package-lock.json'];
// migrations.php und cli-config.php gehoeren dazu: ohne die erste findet Doctrine beim
// Einrichten keine Migrationsliste und bricht mit "configuration file does not exist" ab - genau
// dort, wo eine frische Installation ihre Tabellen anlegen will.
$include = ['bin', 'config', 'migrations', 'public', 'src', 'templates', 'composer.json', 'composer.lock', 'migrations.php', 'cli-config.php', 'VERSION', 'README.md', 'LICENSE', '.env.example'];
$skip = ['.git', '.github', '.claude', '.gitattributes', '.gitignore', '.php-cs-fixer.cache', 'node_modules', 'var', 'tests', 'scripts', 'reports', 'phpunit.xml.dist', '.php-cs-fixer.dist.php', 'package.json', 'package-lock.json', 'AGENTS.md', 'ENTWICKLUNGSPLAN.md', 'docs', 'composer.phar', '.venv', 'vendor', 'phpunit.cache', 'migrations.php.bak'];
$copy = static function (string $from, string $to) use (&$copy, $skip): int {
$count = 0;
+6 -1
View File
@@ -76,7 +76,12 @@ final class InstallController extends AbstractController
'error' => $error,
'values' => $request->request->all(),
'mysql' => \extension_loaded('pdo_mysql'),
'sqlite' => \extension_loaded('pdo_sqlite'),
// SQLite nur, wenn die Fassung die Tabellenfunktionen kennt, mit denen Doctrine das
// Schema liest - sonst scheitert erst der naechste Schritt, und zwar unverstaendlich.
'sqlite' => \extension_loaded('pdo_sqlite') && null !== $this->installation->sqliteVersion()
&& version_compare((string) $this->installation->sqliteVersion(), Installation::MIN_SQLITE, '>='),
'sqliteVersion' => $this->installation->sqliteVersion(),
'minSqlite' => Installation::MIN_SQLITE,
...$this->token('datenbank'),
]);
}
+35
View File
@@ -71,6 +71,25 @@ final class Installation
* Every failing check names the file or path that has to change. A check that is merely
* advisable is marked as such, so a missing `proc_open` does not look like a reason to stop.
*/
/** Ab dieser Fassung kennt SQLite die Tabellenfunktionen, mit denen Doctrine das Schema liest. */
public const MIN_SQLITE = '3.16.0';
/** Die Fassung der eingebauten SQLite-Bibliothek, oder null wenn es keine gibt. */
public function sqliteVersion(): ?string
{
if (!extension_loaded('pdo_sqlite')) {
return null;
}
try {
$version = (new \PDO('sqlite::memory:'))->query('SELECT sqlite_version()')?->fetchColumn();
} catch (\PDOException) {
return null;
}
return is_string($version) ? $version : null;
}
public function checks(): array
{
$checks = [];
@@ -83,6 +102,22 @@ final class Installation
$drivers = array_values(array_intersect(['pdo_sqlite', 'pdo_mysql'], get_loaded_extensions()));
$checks[] = $this->check('Datenbanktreiber', [] !== $drivers, 'Weder pdo_sqlite noch pdo_mysql ist vorhanden. Ohne einen von beiden gibt es keine Datenbank.', true, implode(', ', $drivers));
// Die Fassung von SQLite, und zwar vor der Wahl der Datenbank. Doctrine liest das Schema
// mit Tabellenfunktionen wie `pragma_table_info(...)`; die gibt es erst seit SQLite 3.16
// (Januar 2017). Auf einem Hosting mit aelterem SQLite bricht sonst mitten in der
// Installation eine Abfrage mit `near "(": syntax error` ab - eine Meldung, die niemandem
// sagt, was zu tun ist. Gefunden auf einem echten Hosting, nicht hier.
$sqlite = $this->sqliteVersion();
if (null !== $sqlite) {
$checks[] = $this->check(
'SQLite-Fassung',
version_compare($sqlite, self::MIN_SQLITE, '>='),
sprintf('Gefunden: %s. Für eine SQLite-Datenbank wird mindestens %s gebraucht. Diese Installation läuft trotzdem – aber mit MySQL statt SQLite; eine Datenbank legt man beim Hoster im Kundenmenü an.', $sqlite, self::MIN_SQLITE),
false,
$sqlite,
);
}
$webp = extension_loaded('gd') && (gd_info()['WebP Support'] ?? false);
$checks[] = $this->check('GD mit WebP', $webp, 'Bilder werden beim Hochladen in WebP umgewandelt. Ohne GD mit WebP-Unterstützung schlägt jeder Upload fehl. Der Rest der Website funktioniert.', false);
$checks[] = $this->check('ZIP-Erweiterung', class_exists(\ZipArchive::class), 'Wird für Sicherungen als Archiv gebraucht. Ohne sie bleibt die Sicherung ein Verzeichnis.', false);
+1 -1
View File
@@ -12,7 +12,7 @@
<legend>Art der Datenbank</legend>
<label class="check-label">
<input type="radio" name="driver" value="sqlite" {{ driver != 'mysql' ? 'checked' : '' }} {{ sqlite ? '' : 'disabled' }}>
<span><strong>SQLite</strong> – eine Datei unter <code>var/</code>. Empfohlen, wenn nichts dagegen spricht.{% if not sqlite %} <em>Auf diesem Server nicht verfügbar.</em>{% endif %}</span>
<span><strong>SQLite</strong> – eine Datei unter <code>var/</code>. Empfohlen, wenn nichts dagegen spricht.{% if not sqlite %} <em>{% if sqliteVersion %}Dieser Server hat SQLite {{ sqliteVersion }}; gebraucht wird mindestens {{ minSqlite }}. Nimm MySQL – eine Datenbank legst du beim Hoster im Kundenmenü an.{% else %}Auf diesem Server nicht verfügbar.{% endif %}</em>{% endif %}</span>
</label>
<label class="check-label">
<input type="radio" name="driver" value="mysql" {{ driver == 'mysql' ? 'checked' : '' }} {{ mysql ? '' : 'disabled' }}>
+30
View File
@@ -1047,6 +1047,36 @@ final class CmsTest extends TestCase
}
}
public function testTheReleaseArchiveCarriesEveryFileTheApplicationNeeds(): void
{
// Auf dem Hosting gefunden, nicht hier: „The configuration file does not exist" beim
// Anlegen der Tabellen. Im Releasearchiv fehlte migrations.php - die Datei, die Doctrine
// sagt, wo die Migrationen liegen. Sie stand weder in der Mitnahmeliste noch in der
// Ausschlussliste, also fiel niemandem auf, dass sie fehlt.
$skript = (string) file_get_contents(__DIR__.'/../scripts/build-release.php');
preg_match('/\$include = \[(.+?)\];/s', $skript, $mit);
preg_match('/\$skip = \[(.+?)\];/s', $skript, $ohne);
self::assertNotEmpty($mit, 'Die Mitnahmeliste des Releaseskripts ist nicht auffindbar.');
$liste = static fn (string $roh): array => array_map(
static fn (string $eintrag): string => trim($eintrag, " '
"),
explode(',', preg_replace('#//[^
]*#', '', $roh) ?? ''),
);
$eingeordnet = [...$liste($mit[1]), ...$liste($ohne[1] ?? ''), '.env', '.env.local', 'LICENSE'];
foreach (scandir(__DIR__.'/..') ?: [] as $eintrag) {
if (in_array($eintrag, ['.', '..'], true) || str_starts_with($eintrag, '.git')) {
continue;
}
self::assertContains(
$eintrag,
$eingeordnet,
sprintf('„%s" steht im Projektstamm, aber weder in der Mitnahme- noch in der Ausschlussliste von build-release.php. Ein Release ohne diese Datei fällt erst auf dem Hosting auf.', $eintrag),
);
}
}
public function testNoStylesheetHasADanglingSelector(): void
{
// Gefunden, als der Einwilligungsdialog plötzlich ungestaltet unter dem Fuß stand: beim
+27
View File
@@ -235,6 +235,33 @@ final class InstallationTest extends TestCase
self::fail('Prüfung fehlt: '.$label);
}
public function testTheSystemCheckNamesTheSqliteVersionBeforeItMatters(): void
{
// Auf einem echten Hosting gefunden: SQLite war zu alt für die Tabellenfunktionen, mit
// denen Doctrine das Schema liest. Die Installation brach erst im dritten Schritt ab, mit
// „near "(": syntax error" - eine Meldung, die niemandem sagt, was zu tun ist. Die Fassung
// gehört deshalb in die Systemprüfung, vor die Wahl der Datenbank.
$version = $this->installation->sqliteVersion();
self::assertNotNull($version, 'Auf diesem Rechner gibt es pdo_sqlite - dann muss auch die Fassung lesbar sein.');
self::assertMatchesRegularExpression('/^\d+\.\d+/', $version);
$namen = array_column($this->installation->checks(), 'label');
self::assertContains('SQLite-Fassung', $namen);
$eintrag = null;
foreach ($this->installation->checks() as $check) {
if ('SQLite-Fassung' === $check['label']) {
$eintrag = $check;
}
}
self::assertNotNull($eintrag);
self::assertSame($version, $eintrag['found']);
// Sie ist eine Empfehlung, kein Hindernis: mit MySQL läuft die Installation auch auf einem
// Server mit altem SQLite.
self::assertFalse($eintrag['required']);
self::assertSame(version_compare($version, Installation::MIN_SQLITE, '>='), $eintrag['ok']);
}
private function removeDirectory(string $path): void
{
if (!is_dir($path)) {