pillarz 0.1.0.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. pillarz-0.1.0.dev1/CHANGELOG.md +240 -0
  2. pillarz-0.1.0.dev1/LICENSE +661 -0
  3. pillarz-0.1.0.dev1/MANIFEST.in +7 -0
  4. pillarz-0.1.0.dev1/PKG-INFO +71 -0
  5. pillarz-0.1.0.dev1/README.md +36 -0
  6. pillarz-0.1.0.dev1/env.example +83 -0
  7. pillarz-0.1.0.dev1/pillarz/__init__.py +309 -0
  8. pillarz-0.1.0.dev1/pillarz/__main__.py +176 -0
  9. pillarz-0.1.0.dev1/pillarz/_auth/__init__.py +2 -0
  10. pillarz-0.1.0.dev1/pillarz/_auth/passwords.py +74 -0
  11. pillarz-0.1.0.dev1/pillarz/_auth/setup.py +57 -0
  12. pillarz-0.1.0.dev1/pillarz/_db/__init__.py +1 -0
  13. pillarz-0.1.0.dev1/pillarz/_db/clock.py +67 -0
  14. pillarz-0.1.0.dev1/pillarz/_db/connection.py +241 -0
  15. pillarz-0.1.0.dev1/pillarz/_db/m0001_initial.py +194 -0
  16. pillarz-0.1.0.dev1/pillarz/_db/migrations.py +272 -0
  17. pillarz-0.1.0.dev1/pillarz/_i18n/__init__.py +11 -0
  18. pillarz-0.1.0.dev1/pillarz/_i18n/babel-js.cfg +4 -0
  19. pillarz-0.1.0.dev1/pillarz/_i18n/babel.cfg +8 -0
  20. pillarz-0.1.0.dev1/pillarz/_i18n/catalogs.py +216 -0
  21. pillarz-0.1.0.dev1/pillarz/_i18n/jinja.py +41 -0
  22. pillarz-0.1.0.dev1/pillarz/_i18n/js.py +43 -0
  23. pillarz-0.1.0.dev1/pillarz/_i18n/request.py +103 -0
  24. pillarz-0.1.0.dev1/pillarz/_i18n/tools.py +200 -0
  25. pillarz-0.1.0.dev1/pillarz/_protocol.py +315 -0
  26. pillarz-0.1.0.dev1/pillarz/_runtime.py +86 -0
  27. pillarz-0.1.0.dev1/pillarz/_themes.py +154 -0
  28. pillarz-0.1.0.dev1/pillarz/_web/__init__.py +1 -0
  29. pillarz-0.1.0.dev1/pillarz/_web/app.py +69 -0
  30. pillarz-0.1.0.dev1/pillarz/_web/context.py +14 -0
  31. pillarz-0.1.0.dev1/pillarz/_web/csp.py +148 -0
  32. pillarz-0.1.0.dev1/pillarz/_web/errors.py +132 -0
  33. pillarz-0.1.0.dev1/pillarz/_web/gate.py +134 -0
  34. pillarz-0.1.0.dev1/pillarz/_web/guard.py +241 -0
  35. pillarz-0.1.0.dev1/pillarz/_web/headers.py +62 -0
  36. pillarz-0.1.0.dev1/pillarz/_web/net.py +48 -0
  37. pillarz-0.1.0.dev1/pillarz/_web/pages.py +30 -0
  38. pillarz-0.1.0.dev1/pillarz/_web/render.py +271 -0
  39. pillarz-0.1.0.dev1/pillarz/_web/static.py +40 -0
  40. pillarz-0.1.0.dev1/pillarz/config.py +336 -0
  41. pillarz-0.1.0.dev1/pillarz/db.py +337 -0
  42. pillarz-0.1.0.dev1/pillarz/i18n.py +126 -0
  43. pillarz-0.1.0.dev1/pillarz/locale/en/LC_MESSAGES/pillarz-js.po +86 -0
  44. pillarz-0.1.0.dev1/pillarz/locale/en/LC_MESSAGES/pillarz.po +622 -0
  45. pillarz-0.1.0.dev1/pillarz/locale/pillarz-js.pot +83 -0
  46. pillarz-0.1.0.dev1/pillarz/locale/pillarz.pot +588 -0
  47. pillarz-0.1.0.dev1/pillarz/static/pillarz/css/base.css +531 -0
  48. pillarz-0.1.0.dev1/pillarz/static/pillarz/favicon.svg +1 -0
  49. pillarz-0.1.0.dev1/pillarz/static/pillarz/js/api.js +96 -0
  50. pillarz-0.1.0.dev1/pillarz/static/pillarz/js/i18n.js +158 -0
  51. pillarz-0.1.0.dev1/pillarz/static/pillarz/js/ui.js +433 -0
  52. pillarz-0.1.0.dev1/pillarz/static/pillarz/vendor/lucide/LICENSE +43 -0
  53. pillarz-0.1.0.dev1/pillarz/templates/pillarz/bare.html +25 -0
  54. pillarz-0.1.0.dev1/pillarz/templates/pillarz/base.html +38 -0
  55. pillarz-0.1.0.dev1/pillarz/templates/pillarz/error.html +14 -0
  56. pillarz-0.1.0.dev1/pillarz/templates/pillarz/icons.svg +66 -0
  57. pillarz-0.1.0.dev1/pillarz/templates/pillarz/partials/flash.html +8 -0
  58. pillarz-0.1.0.dev1/pillarz/templates/pillarz/partials/nav.html +30 -0
  59. pillarz-0.1.0.dev1/pillarz/templates/pillarz/plain.html +16 -0
  60. pillarz-0.1.0.dev1/pillarz/testing/__init__.py +149 -0
  61. pillarz-0.1.0.dev1/pillarz/testing/_web.py +112 -0
  62. pillarz-0.1.0.dev1/pillarz/text.py +81 -0
  63. pillarz-0.1.0.dev1/pillarz/web.py +182 -0
  64. pillarz-0.1.0.dev1/pillarz.egg-info/PKG-INFO +71 -0
  65. pillarz-0.1.0.dev1/pillarz.egg-info/SOURCES.txt +69 -0
  66. pillarz-0.1.0.dev1/pillarz.egg-info/dependency_links.txt +1 -0
  67. pillarz-0.1.0.dev1/pillarz.egg-info/requires.txt +10 -0
  68. pillarz-0.1.0.dev1/pillarz.egg-info/top_level.txt +1 -0
  69. pillarz-0.1.0.dev1/pyproject.toml +58 -0
  70. pillarz-0.1.0.dev1/requirements.in +13 -0
  71. pillarz-0.1.0.dev1/setup.cfg +4 -0
@@ -0,0 +1,240 @@
1
+ # Changelog
2
+
3
+ Alle nennenswerten Änderungen an pillarz. Dieses CHANGELOG richtet sich an **App-Entwickler**
4
+ (Breaking Changes, neue Hooks, Upgrade-Hinweise), nicht an Endnutzer. Format nach
5
+ [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), Versionen nach SemVer (ADR 0002): vor 1.0
6
+ darf sich die öffentliche API zwischen 0.x-Versionen ohne Deprecation-Frist ändern, jede solche
7
+ Änderung steht unter „Breaking“. Abschnitte: Added, Changed, Deprecated, Removed, Fixed, Security,
8
+ Breaking, Upgrade.
9
+
10
+ ## [Unreleased]
11
+
12
+ ## [0.1.0.dev1] — 2026-10-29
13
+
14
+ ### Added
15
+ - Paket und Auslieferung: `pyproject.toml` (setuptools, Version aus `pillarz.__version__`), Laufzeit-
16
+ Abhängigkeiten als Bereiche, exakte Locks mit Hashes (`requirements.txt`, `requirements-dev.txt`,
17
+ erzeugt von `scripts/lock-deps.sh`). CI baut bei jedem Push Wheel + Sdist und lädt sie bei `v*`-Tags
18
+ öffentlich nach **pypi.org** hoch (nur PyPI, keine Forgejo-Registry). Lizenz **`AGPL-3.0-or-later`**
19
+ (`LICENSE` in Repo, sdist und Wheel; SPDX-Ausdruck nach PEP 639), Kurzbeschreibung `README.md`.
20
+ - `pillarz.Pillarz(name=…, title=…, version=…, package_dir=…, migrations=…, auth_methods=…,
21
+ weak_passwords=…)` mit `create_app()`, `on_startup()`, `on_shutdown()`, `migrate()` und `config`.
22
+ Genau eine Instanz je Prozess; Registrieren nach `create_app()` wirft `RuntimeError`. Datenklassen
23
+ `NavItem`, `AdminTab`, `AccountSection`, `DeleteInput`, `Setting`, `Theme` vorerst nur als Hüllen
24
+ (Felder folgen mit 0.1-Paketen B–F).
25
+ - `pillarz.config.Config.from_env()`: alle 0.1-Variablen (siehe `env.example`), gelesen beim Erzeugen
26
+ von `Pillarz`, nicht beim Import. Startabbruch (`ConfigError`) bei `SECRET_KEY` < 32 Zeichen,
27
+ `SETUP_TOKEN` < 16 Zeichen, ungültigen `PUBLIC_URLS` oder ohne aktive Anmeldemethode; harmlose
28
+ Fehler als Warnung in `Config.warnings` (beim Start protokolliert).
29
+ - `pillarz.db`: `Conn`/`connect()` aus labelz (`?`-Platzhalter, dict-Zeilen, eine Transaktion je
30
+ `connect()`), DDL-Platzhalter `ddl()` (`{pk}`, `{blob}`, `{bigint}`), `now()`/`now_plus()`/`stamp()`
31
+ über eine zentrale Uhr, Migrations-Helfer `has_table`, `has_column`, `add_column`, `create_index`,
32
+ `json_object_sql`, `upsert_sql`, `applied_migrations`, dazu `IntegrityError`, `ping`, `reset`
33
+ (nur Tests) und die labelz-Grundbausteine `all_rows`/`one`/`insert`/`update`/`delete`/`get` mit
34
+ ID-Grenzen.
35
+ - PostgreSQL-Verbindungspool je Prozess (`PG_POOL_SIZE`, Standard 5, `0` = ohne Pool).
36
+ - Versionierte Migrationen je Komponente (`pz_migrations`): pillarz zuerst, dann die App; eine
37
+ Transaktion je Schritt, Sperre gegen parallele Worker, Startabbruch bei Fehlern und bei unbekannten
38
+ (neueren) Einträgen. `python -m pillarz migrate [--dry-run|--status] <modul:objekt>`.
39
+ - `python -m pillarz create-admin <modul:objekt>`: erstes Admin-Konto interaktiv anlegen (nur bei leerer
40
+ Datenbank, Passwortregeln K-4: 10–200 Zeichen, nicht gleich dem Benutzernamen, kein Platzhalter).
41
+ - pillarz-Migration `0001_initial`: Tabelle `users` (öffentliche Spalten `id, username, name, email,
42
+ role, active, locale, region, created_at, last_login, session_epoch`) und private Tabellen
43
+ `pz_sessions`, `pz_flows`, `pz_throttle`, `pz_tokens`, `pz_settings`, `pz_themes`, `pz_log`, `pz_meta`.
44
+ - `pillarz.testing`: `configure()`, `clock` (`freeze`/`advance`), `reset_runtime()` – Grundstock des
45
+ Testkits.
46
+ - `pillarz.i18n` (ADR 0011): `_`, `ngettext`, `pgettext`, `lazy`, `current_locale()` und der
47
+ Kontextmanager `use_locale(sprache)`. msgids sind deutsch; Kataloge
48
+ `<package_dir>/locale/<sprache>/LC_MESSAGES/<name>.po` (Python + Jinja) und `<name>-js.po` (JS)
49
+ werden ohne `.mo` direkt aus den `.po` geladen. Kette **App → pillarz → Quelltext**: eine App kann
50
+ pillarz-Texte mit gleicher msgid umformulieren und weitere Sprachen liefern. `fuzzy`/leere Einträge
51
+ zählen als fehlend. `lazy("…")` liefert ein Objekt, das erst bei `str()`/im Template übersetzt (für
52
+ JSON vorher `str()`).
53
+ - Sprachwahl je Request: Konto (`users.locale`) → `Accept-Language` → `DEFAULT_LOCALE`, jeweils nur
54
+ unterstützte Sprachen. Unterstützt sind `de` plus die Sprachen der App-Kataloge – hat die App keine
55
+ Kataloge, die von pillarz (`en`). Ohne Request (Start, Jobs) gilt die Standardsprache.
56
+ - Jinja: `_`, `gettext`, `ngettext`, `pgettext`, `npgettext` und `{% trans %}` in allen Templates
57
+ (`newstyle`: Werte als Schlüsselwörter, `{{ _("Hallo %(name)s", name=…) }}` – escaped; ein
58
+ wörtliches `%` als `%%`).
59
+ - JavaScript: `/static/pillarz/js/i18n.js` mit `t(msgid, params)`, `nt(singular, plural, n, params)`,
60
+ `pt(kontext, msgid, params)`; Platzhalter `{name}`, bei `nt` ist `{n}` automatisch gesetzt. Die
61
+ Texte kommen aus dem JSON-Block `<script type="application/json" id="i18n">` der Seite (nur die
62
+ JS-Domänen der aktuellen Sprache, CSP-konform); Pluralregeln aus dem Katalog, ohne `eval`.
63
+ - `scripts/i18n.sh extract|update|init <sprache>|check` (Babel) – auch als Vorlage für Apps
64
+ (`PACKAGE_DIR`, `DOMAIN`). Schlüsselwörter: Python `_`, `gettext`, `ngettext`, `pgettext`,
65
+ `npgettext`, `lazy`, `_l`; JS `t`, `nt`, `pt`; Übersetzerhinweise mit `# TRANSLATORS:`.
66
+ - Englischer Katalog für alle pillarz-Texte (`pillarz/locale/en/LC_MESSAGES/pillarz.po`).
67
+ - `pillarz.testing.check_catalogs(package_dir=None, name=None, *, locales=None)`: Liste fehlender,
68
+ `fuzzy` oder leerer Übersetzungen, fehlender Pluralformen, unpassender `%(…)`-Platzhalter und
69
+ fehlender Kataloge (ohne Argumente: die aktuelle App). `pillarz.testing.untranslated()`: sammelt
70
+ Texte, die in einer anderen Sprache als Deutsch unübersetzt angezeigt wurden.
71
+ - `examples/minimal`: kleinste App mit einer App-Migration.
72
+ - Web-Rahmen (0.1-Paket B): `create_app()` setzt Guard (reine ASGI-Middleware: Request-ID, CSRF über
73
+ `Sec-Fetch-Site`/`Origin`, Body-Grenzen 1 MiB bzw. Upload-Registry, 411 ohne Länge), das Gate
74
+ **„default deny“** (globale Dependency: jede Route ohne `Depends(pillarz.web.public)` bzw.
75
+ `@pillarz.web.public_route` verlangt eine Anmeldung – anonym 401, bei HTML-GET `303 /login?next=…`),
76
+ Sicherheits-Header auf jede Antwort (`nosniff`, `Referrer-Policy: same-origin`, CSP, HSTS nur wenn alle
77
+ `PUBLIC_URLS` https sind, `X-Request-ID`, `Vary: Cookie`), Fehler-Handler wie labelz (JSON für `/api/…`
78
+ und Nicht-GET, Fehlerseite für 403/404, Pfad-Validierung → 404, `UnicodeEncodeError` → 422,
79
+ `psycopg_pool.PoolTimeout` → 503 mit `Retry-After: 5`), Static mit `no-cache` + ETag unter `/static/`
80
+ (App) und `/static/pillarz/`, die öffentlichen Seiten `/healthz` und `/themes.css`.
81
+ - Neue Registrierungen an `Pillarz`: `nav(NavItem)`, `csp(**direktiven)` (nur Ergänzungen; `'unsafe-*'`
82
+ außer `'wasm-unsafe-eval'`, `*`, reine Schemata in `script-src`/`default-src` und Änderungen an
83
+ `object-src`/`base-uri`/`frame-ancestors` → `ValueError` beim Start), `upload_limit(pfad|re, bytes)`,
84
+ `no_store(pfad|re)`, `boot(fn)`, `log_source(logger, bereich, label)`, `theme(Theme)`, `icons(pfad)`.
85
+ Pfadregeln: String mit `/` am Ende = Präfix, sonst exakt; `re.compile(…)` muss den ganzen Pfad treffen.
86
+ - `NavItem(key, href, label, icon=None, order=100, visible=None, badge=None, badge_href=None,
87
+ badge_label=None)` und `Theme(id, name, light, dark)` mit Feldern.
88
+ - `pillarz.web`: `public`, `public_route`, `render`, `fail`, `csp_extend`, `read_form`, `client_ip`,
89
+ `base_url`, `current_user`, `is_admin`, `via`, `require_user`, `require_member`, `require_admin`,
90
+ `require_role(*rollen)`. Bis Paket D gibt es keine Sitzungen: `require_*` antworten anonym immer 401;
91
+ `throttled`/`hit` sind Hüllen (`NotImplementedError`).
92
+ - `pillarz.text`: `oneline`, `multiline`, `required_line`, `safe_next`, `to_json` (aus labelz).
93
+ - Templates `pillarz/base.html` (Blöcke `title`, `head`, `body_class`, `main_class`, `content`,
94
+ `footer_extra`, `scripts`, `icons`, dazu `topbar` und `footer`), `pillarz/error.html`,
95
+ `pillarz/plain.html` (Karte ohne Navigation, Blöcke `card`, `card_class`), Icon-Sprite
96
+ `pillarz/icons.svg` (Lucide, ISC). Template-Kette App → pillarz; Namen unter `pillarz/…` kommen immer
97
+ aus pillarz. Kontext: `user`, `is_admin`, `site_name`, `app_name`, `version` (anonym nur ein
98
+ Cache-Buster), `themes_rev`, `theme_id`, `mode`, `locale`, `region`, `nav`, `nav_items`, `flash`,
99
+ `boot`, `page`, `help`, `i18n`, `_`/`ngettext`/`pgettext` (Jinja-i18n, newstyle).
100
+ - Static: `/static/pillarz/css/base.css` (alles in `@layer pillarz`, Tokens `--pz-*` aus `--t-*`,
101
+ Komponenten `.btn`, `.icon-btn`, `.field`, `.banner-*`, `.card`, `.chip`, `.tabs`, `.tbl`, `.menu`,
102
+ `.dlg`, `.toasts`, `.topbar` …), `js/api.js` (`api`, `upload`, `postQuietly`, `ApiError`), `js/ui.js`
103
+ (`boot`, `pageData`, `region`, `readJson`, `h`, `icon`, `button`, `iconButton`, `toast`, `showError`,
104
+ `trapFocus`, `dialog`, `confirmDialog`, `promptDialog`, `field`, `numInput`, `readNum`, `selectEl`,
105
+ `checkbox`, `parseNum`, `fmtNum`, `fmtNumber`, `fmtDate`, `relTime`, `debounce`, `closeMenu`,
106
+ `menuIsOpen`, `attachMenu`, `menuButton`), `js/i18n.js` (`t`, `nt`, `pt`), `favicon.svg`.
107
+ - Themes: labelz' fünf eingebaute (Papier, Wald, Meer, Schiefer, Beere) + `pz.theme()` + eigene aus
108
+ `pz_themes`; `/themes.css` nur aus geprüften Hex-Werten, Akzentschrift nach WCAG-Kontrast.
109
+ - Protokoll: Einträge der Logger `pillarz` und `<name>` ab INFO in `pz_log`, mit `request_id`,
110
+ `actor_id` und `ip` aus dem Request (K-13); `extra={"pz_actor": id}` setzt das Konto selbst.
111
+ Aufbewahrung `LOG_RETENTION_DAYS` (neu, Standard 90) und höchstens 20 000 Einträge (K-14).
112
+ - `pillarz.testing.route_audit(app)` (jede nicht öffentliche Route anonym → 401/303→`/login`/404,
113
+ dazu Routen auf von pillarz belegten URLs und doppelte Routen) und `assert_security_headers(resp)`.
114
+ - `tests/browser_smoke.py`: manueller Browser-Smoke mit Playwright (Konsolen-/CSP-Fehler, Überlauf bei
115
+ 360 px, Screenshots); `playwright` steht in `requirements-dev.txt`.
116
+ - i18n im Web-Rahmen (Paket C in B): der Guard setzt die Sprache je Request – vor CSRF/Body-Prüfung aus
117
+ `Accept-Language`, nach der Identität mit der Kontosprache (`user["locale"]`) – und setzt sie bewusst
118
+ nicht zurück, damit auch der 500er-Handler in der Sprache des Requests antwortet. Alle Templates
119
+ bekommen `_`, `gettext`, `ngettext`, `pgettext`, `npgettext`, `{% trans %}` und `current_locale()`;
120
+ `#i18n` enthält die JS-Texte der Sprache (`{"locale", "plural", "messages"}`). Kataloge werden beim
121
+ Start geladen (kaputte `.po` → Startabbruch). Englische Texte für alle Seiten und Meldungen aus B.
122
+ - **Protokoll in der Standardsprache:** `pz_log`-Einträge stehen immer in `DEFAULT_LOCALE`, nicht in
123
+ der Sprache des auslösenden Requests. Log-Texte als `lazy("…")` mit `%`-Argumenten loggen
124
+ (`log.warning(lazy("Import von %s fehlgeschlagen"), name)`) – dann sind sie extrahierbar, und der
125
+ Protokoll-Handler übersetzt sie beim Schreiben unter `use_locale(None)`. Andere Handler (Konsole)
126
+ formatieren in der aktuellen Sprache. `Config.warnings` sind weiterhin `str` mit dem deutschen
127
+ Quelltext und werden beim Protokollieren übersetzt.
128
+ - `tests/browser_smoke.py --locales de,en` (Standard): je Sprache `<html lang>`, `#i18n`-Block und
129
+ Übersetzung durch `i18n.js` unter der CSP.
130
+ - `pillarz.db.requires_fk_off` (Dekorator für Migrationsschritte) und
131
+ `pillarz.db.rebuild_table_sqlite(conn, table, create_sql, columns=None)` (ADR 0003): Tabellenumbau auf
132
+ SQLite nach dem 12-Schritte-Verfahren. Markierte Schritte laufen auf SQLite mit
133
+ `PRAGMA foreign_keys = OFF` (vor `BEGIN` gesetzt) und `PRAGMA foreign_key_check` vor dem Commit –
134
+ Verstöße rollen zurück (`MigrationError`). Ohne die Markierung verweigert `rebuild_table_sqlite` den
135
+ Umbau, weil `DROP TABLE` sonst `ON DELETE CASCADE` in Kindtabellen auslöst. Auf PostgreSQL hat die
136
+ Markierung keine Wirkung; `rebuild_table_sqlite` gibt es dort nicht (`ALTER TABLE` nutzen).
137
+ - `pillarz.testing.is_test_dir(pfad)`.
138
+ - Beim Start: Fehler im Protokoll, wenn es Konten, aber kein aktives Admin-Konto gibt und
139
+ `OIDC_ADMIN_GROUPS` leer ist (Aussperrung nach der Übernahme einer Datenbank sichtbar machen).
140
+ - **Seiten ohne Rahmen** (P-98): `render(request, template, page=…, chrome=False)` mit einem Template,
141
+ das `pillarz/bare.html` erweitert (Blöcke `title`, `head`, `body_class`, `content`, `scripts`; JSON
142
+ `#page`, `#i18n`; `data-theme`/`data-mode`/`data-version` am `<html>`). Gleiche Header und CSP wie
143
+ jede Seite, aber ohne Navigation, Badges, Topbar, Fuß, Flash, Toasts, Hilfe, `#boot` und ohne
144
+ pillarz-CSS/-JS (`base.css` bei Bedarf selbst im Block `head`). Falscher Rahmen (`base.html` mit
145
+ `chrome=False`, `bare.html` ohne, gar kein Rahmen mit `chrome=False`) → `RuntimeError` beim Rendern.
146
+ Beispiel: `/print` in `examples/minimal` (auch im Browser-Smoke).
147
+ - **Protokoll: App-Quelle** (P-107): Logger unter `<name>` ohne eigene `pz.log_source()`-Zuordnung
148
+ landen in der Quelle `<name>` (Anzeigename = `title`) statt in `system`; bildet die App `<name>`
149
+ selbst ab, gilt ihre Zuordnung.
150
+ - `pz.public_mount(pfad)`: eine eingehängte Unter-App (`app.mount`) ohne Anmeldung erreichbar machen.
151
+ - **WebSockets** (FastAPI-`@router.websocket`): Gate und Guard gelten auch dort – `Origin` muss zum
152
+ `Host` passen (fehlender `Origin` = kein Browser, erlaubt), Identität wie bei HTTP
153
+ (`websocket.state.user`), ohne Anmeldung bzw. bei fremdem Origin Schließen mit 1008. `public`
154
+ funktioniert wie bei HTTP-Routen.
155
+ - `HEAD` wird für alle Routen wie `GET` beantwortet (ohne Inhalt), z. B. `HEAD /healthz`.
156
+ - `pillarz.testing.flush_log()`: wartet, bis das Protokoll geschrieben ist (vor dem Lesen von `pz_log`).
157
+ - `route_audit()` prüft WebSocket-Routen (anonym → 1008) und meldet Mounts (außer Static und
158
+ `pz.public_mount`), `Host`- und Starlette-Routen (`Route`/`WebSocketRoute`), an denen der Gate nicht
159
+ läuft. Eingebundene Router werden mit der ganzen Kette ausgewertet.
160
+
161
+ ### Changed
162
+ - **`pillarz.testing.configure()` erzwingt ein frisches temporäres `DATA_DIR`** (`<tmp>/pillarz-test-*`,
163
+ ein gesetztes `DATA_DIR` wird überschrieben, `DB_PATH` entfernt) und leert `POSTGRES_*` – außer bei
164
+ `PZ_TEST_PG=1`. Apps, die ihre Tests gegen PostgreSQL fahren, setzen `PZ_TEST_PG=1` und eine
165
+ Datenbank, deren Name `test` als Wortteil trägt (`labelz_test`, `pillarz_test_…`).
166
+ `pillarz.db.reset()` verweigert sonst (SQLite: Datei außerhalb eines Testverzeichnisses).
167
+ Unterprozesse eines Testlaufs übernehmen das Verzeichnis des Elternprozesses.
168
+ - `python -m pillarz migrate --status|--dry-run` legen keine fehlende SQLite-Datei mehr an (Meldung,
169
+ alle Schritte offen); `migrations.status()`/Trockenlauf ohne Datenbank → `MigrationError`.
170
+ - Wartet eine Migration auf SQLite länger als 120 s auf die Dateisperre, gibt es einen
171
+ `MigrationError` mit klarer Meldung statt `sqlite3.OperationalError`.
172
+ - `.secret_key` entsteht bei mehreren gleichzeitig startenden Prozessen genau einmal (Temp-Datei 0600 +
173
+ `os.link`); eine für Gruppe/andere lesbare Schlüsseldatei ergibt eine Warnung.
174
+ - **Protokoll schreibt im Hintergrund:** der Handler ist ein `QueueHandler`, ein eigener Thread schreibt
175
+ die Einträge stapelweise. Loggen blockiert weder den Event-Loop noch eine offene Transaktion
176
+ (vorher auf SQLite 5 s Sperre, verlorener Eintrag und danach „database is locked“; auf PG eine zweite
177
+ Pool-Verbindung). Einträge sind unabhängig von der Transaktion des Aufrufers (bleiben bei Rollback).
178
+ Beim Herunterfahren wird die Warteschlange geleert; Tests lesen `pz_log` nach `testing.flush_log()`.
179
+ - Protokoll-Obergrenze 20 000 Einträge gilt getrennt für `auth`/`admin` und alle übrigen Quellen.
180
+ - `log_once` drosselt nach festen Schlüsseln (Routen-Template statt Pfad) und verdrängt bei Überlauf
181
+ die ältesten Schlüssel, statt alle zu vergessen.
182
+ - HTML-GET mit unerwartetem Fehler bekommt die Fehlerseite (500) statt JSON.
183
+ - Templates: `autoescape` für alle Templates (unabhängig von der Endung).
184
+ - Body-Grenzen gelten für alle Methoden (auch `GET`/`HEAD`/`OPTIONS`); ein `Content-Length` ohne
185
+ gültige Zahl ergibt 411.
186
+
187
+ ### Fixed
188
+ - Validatoren (Benutzername, Mail, SQL-Bezeichner, Migrationsnamen, App-/Navigations-/Theme-Kennungen,
189
+ Farben, CSP-Quellen, Sprachcode) ließen ein abschließendes `\n` durch (`re.match` mit `$`) – jetzt
190
+ `fullmatch`.
191
+ - PostgreSQL-Pool: kam nach einer toten Verbindung kein Ersatz (`PoolTimeout`), wurde die tote
192
+ Verbindung ein zweites Mal an den Pool zurückgegeben (bei gesunden Verbindungen ein `ValueError`, der
193
+ den `PoolTimeout` überdeckt).
194
+ - Ersteinrichtung auf SQLite mit mehreren Prozessen: „database is locked“ → `SetupClosed`.
195
+ - `scripts/release.sh`: mit `SKIP_TESTS=1` fehlte der Interpreter für den Build-Schritt.
196
+
197
+ ### Security
198
+ - `SECRET_KEY` aus der Umgebung: bekannte Vorlagenwerte (`replace-with-…`, `changeme`, `example` …) und
199
+ Werte mit weniger als 10 verschiedenen Zeichen brechen den Start ab.
200
+ - Passwörter mit einzelnen UTF-16-Surrogaten lehnt `password_problem` ab (nicht kodier-/hashbar).
201
+ - CI: Versionsnummer aus dem Tag erst gegen PEP 440 prüfen und nie in Python-Quelltext einsetzen;
202
+ PyPI-Token (`PYPI_TOKEN`) nur über die Umgebung an `twine`, nie in der Kommandozeile;
203
+ probe-install installiert von pypi.org und vergleicht den Hash dort mit dem im publish-Job
204
+ hochgeladenen Wheel; pip-audit auch über `requirements-dev.txt`; kein Cache-Speichern aus
205
+ Pull-Requests.
206
+ - Review Paket B: Mounts, `Host`- und Starlette-Routen liefen am Gate vorbei – der Guard weist anonyme
207
+ Anfragen an sie jetzt ab (401 bzw. `303 /login`), außer pillarz' Static und `pz.public_mount()`.
208
+ - `public` zählt nur noch als direkte Abhängigkeit der Route bzw. eines Routers der Kette – eine
209
+ Hilfs-Dependency, die intern `Depends(public)` nutzt, macht Routen nicht mehr öffentlich. Umgekehrt
210
+ wird `public` an verschachtelt eingebundenen Routern (`include_router(…, dependencies=[…])`) erkannt.
211
+ - WebSocket-Verbindungen liefen am Guard vorbei (kein Origin-Check, keine Identität).
212
+ - CSP-Ergänzungen: `javascript:` überall abgelehnt; `worker-src`/`child-src` nur `'self'`/`blob:`; Hosts
213
+ bei reinem HTTPS nur `https://` (`connect-src` zusätzlich `wss://`), `ws(s)://` nur in `connect-src`;
214
+ Hashes in `style-src` abgelehnt (sie schalteten `'unsafe-inline'` still ab).
215
+ - Pfade in Logzeilen einzeilig und gekürzt (`oneline(…)[:200]`, keine Log-Injection auf stdout).
216
+ - `PoolTimeout`/`HTTPException` aus der Identitätsauflösung im Guard → 503 mit `Retry-After` bzw. die
217
+ passende Fehlerantwort statt 500.
218
+ - Template-Namensraum: `./pillarz/…` und ähnliche Schreibweisen umgingen den Schutz der
219
+ pillarz-Templates; ein App-Ordner `templates/pillarz/` bricht den Start jetzt ab.
220
+
221
+ ### Upgrade
222
+ - **Default deny:** Routen einer App sind ab 0.1-Paket B ohne Anmeldung gesperrt. Öffentliche Routen
223
+ (Webhooks, Startseite ohne Login …) brauchen `Depends(pillarz.web.public)` bzw. `@public_route`;
224
+ `pillarz.testing.route_audit(app)` im Testlauf der App zeigt vergessene Stellen.
225
+ - App-Ordner `static/pillarz/` ist verboten (`create_app()` bricht ab); `static/logo.svg` und
226
+ `static/favicon.svg` der App ersetzen Marke bzw. Favicon von pillarz.
227
+ - App-Ordner `templates/pillarz/` ist ebenso verboten (`create_app()` bricht ab).
228
+ - `render(**ctx)` lehnt von pillarz belegte Kontextschlüssel ab (`user`, `boot`, `nav_items`, `i18n`,
229
+ `version`, `help`, `flash`, `_` … → `ValueError`).
230
+ - Eigene Mounts (`app.mount`) sind anonym gesperrt; öffentliche Unter-Apps mit `pz.public_mount(pfad)`
231
+ freigeben. `app.add_route()` statt FastAPI-Routen meldet `route_audit()`.
232
+ - Tests, die `pz_log` lesen, vorher `pillarz.testing.flush_log()` aufrufen.
233
+ - Eine vorhandene `users`-Tabelle (z. B. labelz 0.12) wird übernommen: `0001_initial` ergänzt fehlende
234
+ Spalten (`role`, `email_confirmed`, `locale`, `region`, `updated_at` …), setzt `email_confirmed = 1`
235
+ für Konten mit Adresse und `pz_meta.setup_done`, wenn es Konten gibt. Datenumbau (`is_admin` →
236
+ `role`, `oidc_links` → `pz_tokens`, `themes` → `pz_themes`, `meta.settings` → `pz_settings`) ist
237
+ Sache einer App-Migration, die nach `0001_initial` läuft (Architektur §5.4).
238
+ - Installation in einer App: normal von pypi.org, exakter Pin mit Hash (`pillarz==X.Y.Z
239
+ --hash=sha256:…` im Lock der App), Installation mit `--require-hashes` – kein zusätzlicher Index, kein
240
+ Token im Image-Build.