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.
- pillarz-0.1.0.dev1/CHANGELOG.md +240 -0
- pillarz-0.1.0.dev1/LICENSE +661 -0
- pillarz-0.1.0.dev1/MANIFEST.in +7 -0
- pillarz-0.1.0.dev1/PKG-INFO +71 -0
- pillarz-0.1.0.dev1/README.md +36 -0
- pillarz-0.1.0.dev1/env.example +83 -0
- pillarz-0.1.0.dev1/pillarz/__init__.py +309 -0
- pillarz-0.1.0.dev1/pillarz/__main__.py +176 -0
- pillarz-0.1.0.dev1/pillarz/_auth/__init__.py +2 -0
- pillarz-0.1.0.dev1/pillarz/_auth/passwords.py +74 -0
- pillarz-0.1.0.dev1/pillarz/_auth/setup.py +57 -0
- pillarz-0.1.0.dev1/pillarz/_db/__init__.py +1 -0
- pillarz-0.1.0.dev1/pillarz/_db/clock.py +67 -0
- pillarz-0.1.0.dev1/pillarz/_db/connection.py +241 -0
- pillarz-0.1.0.dev1/pillarz/_db/m0001_initial.py +194 -0
- pillarz-0.1.0.dev1/pillarz/_db/migrations.py +272 -0
- pillarz-0.1.0.dev1/pillarz/_i18n/__init__.py +11 -0
- pillarz-0.1.0.dev1/pillarz/_i18n/babel-js.cfg +4 -0
- pillarz-0.1.0.dev1/pillarz/_i18n/babel.cfg +8 -0
- pillarz-0.1.0.dev1/pillarz/_i18n/catalogs.py +216 -0
- pillarz-0.1.0.dev1/pillarz/_i18n/jinja.py +41 -0
- pillarz-0.1.0.dev1/pillarz/_i18n/js.py +43 -0
- pillarz-0.1.0.dev1/pillarz/_i18n/request.py +103 -0
- pillarz-0.1.0.dev1/pillarz/_i18n/tools.py +200 -0
- pillarz-0.1.0.dev1/pillarz/_protocol.py +315 -0
- pillarz-0.1.0.dev1/pillarz/_runtime.py +86 -0
- pillarz-0.1.0.dev1/pillarz/_themes.py +154 -0
- pillarz-0.1.0.dev1/pillarz/_web/__init__.py +1 -0
- pillarz-0.1.0.dev1/pillarz/_web/app.py +69 -0
- pillarz-0.1.0.dev1/pillarz/_web/context.py +14 -0
- pillarz-0.1.0.dev1/pillarz/_web/csp.py +148 -0
- pillarz-0.1.0.dev1/pillarz/_web/errors.py +132 -0
- pillarz-0.1.0.dev1/pillarz/_web/gate.py +134 -0
- pillarz-0.1.0.dev1/pillarz/_web/guard.py +241 -0
- pillarz-0.1.0.dev1/pillarz/_web/headers.py +62 -0
- pillarz-0.1.0.dev1/pillarz/_web/net.py +48 -0
- pillarz-0.1.0.dev1/pillarz/_web/pages.py +30 -0
- pillarz-0.1.0.dev1/pillarz/_web/render.py +271 -0
- pillarz-0.1.0.dev1/pillarz/_web/static.py +40 -0
- pillarz-0.1.0.dev1/pillarz/config.py +336 -0
- pillarz-0.1.0.dev1/pillarz/db.py +337 -0
- pillarz-0.1.0.dev1/pillarz/i18n.py +126 -0
- pillarz-0.1.0.dev1/pillarz/locale/en/LC_MESSAGES/pillarz-js.po +86 -0
- pillarz-0.1.0.dev1/pillarz/locale/en/LC_MESSAGES/pillarz.po +622 -0
- pillarz-0.1.0.dev1/pillarz/locale/pillarz-js.pot +83 -0
- pillarz-0.1.0.dev1/pillarz/locale/pillarz.pot +588 -0
- pillarz-0.1.0.dev1/pillarz/static/pillarz/css/base.css +531 -0
- pillarz-0.1.0.dev1/pillarz/static/pillarz/favicon.svg +1 -0
- pillarz-0.1.0.dev1/pillarz/static/pillarz/js/api.js +96 -0
- pillarz-0.1.0.dev1/pillarz/static/pillarz/js/i18n.js +158 -0
- pillarz-0.1.0.dev1/pillarz/static/pillarz/js/ui.js +433 -0
- pillarz-0.1.0.dev1/pillarz/static/pillarz/vendor/lucide/LICENSE +43 -0
- pillarz-0.1.0.dev1/pillarz/templates/pillarz/bare.html +25 -0
- pillarz-0.1.0.dev1/pillarz/templates/pillarz/base.html +38 -0
- pillarz-0.1.0.dev1/pillarz/templates/pillarz/error.html +14 -0
- pillarz-0.1.0.dev1/pillarz/templates/pillarz/icons.svg +66 -0
- pillarz-0.1.0.dev1/pillarz/templates/pillarz/partials/flash.html +8 -0
- pillarz-0.1.0.dev1/pillarz/templates/pillarz/partials/nav.html +30 -0
- pillarz-0.1.0.dev1/pillarz/templates/pillarz/plain.html +16 -0
- pillarz-0.1.0.dev1/pillarz/testing/__init__.py +149 -0
- pillarz-0.1.0.dev1/pillarz/testing/_web.py +112 -0
- pillarz-0.1.0.dev1/pillarz/text.py +81 -0
- pillarz-0.1.0.dev1/pillarz/web.py +182 -0
- pillarz-0.1.0.dev1/pillarz.egg-info/PKG-INFO +71 -0
- pillarz-0.1.0.dev1/pillarz.egg-info/SOURCES.txt +69 -0
- pillarz-0.1.0.dev1/pillarz.egg-info/dependency_links.txt +1 -0
- pillarz-0.1.0.dev1/pillarz.egg-info/requires.txt +10 -0
- pillarz-0.1.0.dev1/pillarz.egg-info/top_level.txt +1 -0
- pillarz-0.1.0.dev1/pyproject.toml +58 -0
- pillarz-0.1.0.dev1/requirements.in +13 -0
- 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.
|