16 KiB
quick_id, type, status, subsystem, tags, requires, provides, affects, key-files, decisions, metrics, actuals, plan_head_before
| quick_id | type | status | subsystem | tags | requires | provides | affects | key-files | decisions | metrics | actuals | plan_head_before | |||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 260924-m4n | quick | complete | apps/web-tests, apps/api/dashboard, apps/api/prisma |
|
|
|
|
|
|
|
|
dd09c08311 |
Quick 260924-m4n: Flackernden Test entschärft, alte Bildspalte des Bilderrahmens entfernt – Zusammenfassung
Der Marktplatz-Test, der die Freigabe 1.3.1 blockiert hat, lädt seine Komponenten jetzt beim Einlesen der Testdatei statt im Test selbst und ist in zwei Einzelfälle aufgeteilt. Die Proxmox-Kachel-Tests erzeugen keine act-Warnungen mehr (81 weniger im CI-Protokoll). Außerdem ist Stufe 2 der Bilderrahmen-Umstellung umgesetzt: Die Spalte data ist weg, storagePath ist Pflicht. Eine Schutzprüfung bricht die Migration ab, bevor Daten verloren gehen könnten.
Aufgabe 1 – Flackernder Test TenantContextSelector (Commit b10734f)
Ursache: tenant-selector.test.tsx hat TenantContextSelector und die Marktplatzseite per await import(...) innerhalb der Tests geladen. Der erste Test einer Datei hat damit das Laden und Umwandeln der Module in seiner 5-s-Frist mitbezahlt. Lokal waren das nur rund 115 ms. Bei jeder Freigabe laufen aber drei Pipelines gleichzeitig, und unter dieser Last reißt der Test die Frist. waitFor selbst war nicht der Grund: Es bricht schon nach 1 s mit einer eigenen Meldung ab, nicht erst nach 5 s.
Behoben:
- Statische Importe:
vi.mockwird über die Importe gehoben, das Laden fällt damit in die Einlesephase der Datei. - Der Doppelfall steckt jetzt in zwei
it: „renders tenant options for SUPER_ADMIN“ sowie „renders nothing for ADMIN and does not load the tenant list“. Der ADMIN-Fall prüft zusätzlich, dass kein Abruf stattfindet. - Die gleiche Umstellung gilt für
marketplace.test.tsxundmarketplace-filters.test.tsx, die aus demselben Ordner stammen. Inmarketplace-filtersistuserEventjetzt peruserEvent.setup({ advanceTimers: vi.advanceTimersByTime })an die simulierte Uhr gekoppelt. Vorher hat jede Eingabe auf das langsame Nachschieben in Echtzeit durchshouldAdvanceTimegewartet. Der Test „typing in search … after debounce“ läuft allein jetzt in 358 ms statt vorher rund 1,1 s in der Gesamtsuite. testTimeoutwurde nicht global erhöht.
Proxmox-Kachel (proxmox-widget.test.tsx): renderWidget() rendert jetzt innerhalb von await act(async () => …). Dadurch laufen die drei Zustandsänderungen nach dem ersten Laden (setServers, setLoadFailed, setNow) innerhalb von act() und nicht mehr danach ins Leere. Den Test „Verlassen des Bearbeitungsmodus“ habe ich genauso umgestellt, und die Komponente wird ebenfalls statisch importiert.
- act-Warnungen in der ganzen Web-Suite: vorher 102, davon 81 von ProxmoxWidget. Nachher 21, davon 0 von ProxmoxWidget. Die restlichen 21 stammen aus VehicleTable (8), WidgetSettingsPanel (8), DashboardPage (3), CalendarWidget (1) und Header (1). Sie lagen außerhalb des Auftrags und sind nicht angefasst.
Messung: vitest run --reporter=verbose --reporter=json über die ganze Web-Suite mit 91 Dateien und 865 Tests, alle grün.
Die 10 langsamsten Tests nach der Änderung im normalen Lauf mit 12 Kernen:
| # | Dauer | Datei | Test |
|---|---|---|---|
| 1 | 1136 ms | components/settings/smtp-settings-form.test.tsx | Test 2: PUT-Payload trägt den Wert; leeres Feld -> null |
| 2 | 1044 ms | components/bug-report/bug-report-button.test.tsx | Test 1: Bild VOR dem Dialog … |
| 3 | 882 ms | components/settings/proxmox-widget-config-form.test.tsx | lädt die Server einmal und zeigt die Auswahl … |
| 4 | 839 ms | app/(portal)/marketplace/marketplace-filters.test.tsx | typing in search filters the grid … after debounce |
| 5 | 799 ms | components/proxmox/proxmox-server-picker.test.tsx | zeigt je Server ein Kästchen … |
| 6 | 798 ms | modules/tender-radar/settings/components/SourceConfigForm.test.tsx | editing the interval and clicking Speichern … |
| 7 | 749 ms | modules/dkv-fleet/settings/components/VehicleTable.test.tsx | clicking trash icon opens confirm dialog … |
| 8 | 746 ms | components/settings/picture-frame-config-form.test.tsx | Test 4: Pfeile … |
| 9 | 742 ms | app/(portal)/admin/users/user-access-modal.test.tsx | rolls back the direct checkbox … |
| 10 | 741 ms | app/(portal)/admin/users/users-page.test.tsx | zeigt den Servertext im offenen Löschdialog … |
Zusätzlich lief die ganze Suite gedrosselt auf 2 Kerne (taskset -c 0-1, 6 Worker), um die Last beim Freigeben nachzustellen. Alle 865 Tests waren grün, der langsamste brauchte 1302 ms (smtp-settings-form Test 2). Die Tests von tenant-selector lagen dort bei höchstens 950 ms.
Kein Test lag über 2 s, weder im normalen noch im gedrosselten Lauf. Damit gibt es nichts weiter zu beheben oder aufzulisten.
Beobachtung, nicht behoben: Das Muster „Komponente per await import() im Test laden“ steckt noch in 37 weiteren Web-Testdateien. Es macht jeweils den ersten Test einer Datei unter Last anfälliger. Keiner dieser Tests liegt heute nahe am Limit. Umgestellt sind nur der Marktplatz-Ordner und die Proxmox-Kachel.
Aufgabe 2 – DashboardImage Stufe 2 (Commit dd54ec5)
Migration 20260924120000_dashboard_image_drop_data:
- Eine Schutzprüfung im
DO-Block: Existiert noch eine Zeile mitstoragePath IS NULL, bricht die Migration mitRAISE EXCEPTIONab, und zwar mit der Meldung aus dem Plan. ALTER COLUMN "storagePath" SET NOT NULL, danachDROP COLUMN "data".DROP POLICY IF EXISTS system_read_policy ON "DashboardImage".
Die Migration musste einen anderen Namen bekommen als im Todo vorgemerkt (20260922120100), weil sie hinter allen vorhandenen Migrationen liegen muss (die letzte war 20260923160000).
Prüfung des Zeilenschutzes (RLS), Ergebnis:
DashboardImagehatFORCE ROW LEVEL SECURITY, das gilt auch für den Eigentümer. Heute läuftmigrate deployalstessera. Die Rolle ist lokal gemessen Superuser mit BYPASSRLS und Eigentümerin der Tabelle, sieht also alle Zeilen.- Die Falle ist real, lokal nachgewiesen: Ein Eigentümer ohne BYPASSRLS bekommt bei
SELECT EXISTS (… storagePath IS NULL)das Ergebnisf, obwohl eine solche Zeile existiert. Die Schutzprüfung wäre dann stumm wirkungslos. Getestet habe ich das mit einer Wegwerf-Rollem4n_ownerin einer Transaktion, die danach zurückgerollt wurde. - Lösung: Die Prüfung setzt
set_config('row_security', 'off', true). Sie sieht damit entweder alle Zeilen, oder PostgreSQL bricht laut ab. Die Meldung „ERROR: query would be affected by row-level security policy for table "DashboardImage"“ ist unter derselben Wegwerf-Rolle nachgewiesen. Alstesseragreift die Schutzprüfung wie gewollt, auch das ist nachgewiesen. Die Datenbank sieht also nie „0 Zeilen, weiter“.
Negativtest der Schutzprüfung in einer Wegwerf-Datenbank tessera_m4n_neg, die danach gelöscht wurde. Alle Migrationen bis 20260923160000 wurden angewendet, dann eine Zeile mit Pfad und eine ohne Pfad angelegt:
Applying migration `20260924120000_dashboard_image_drop_data`
Error: P3018
Database error code: P0001
ERROR: DashboardImage: es gibt noch Zeilen ohne storagePath — Umzug (quick-260922-hk4) zuerst mit einer Version >= 1.3.1 laufen lassen, dann erneut deployen
Danach war nichts geändert: data und storagePath waren weiterhin NULLbar, und system_read_policy stand noch.
Nebenbefund, im Betriebshandbuch beschrieben: Nach dem Abbruch verweigert auch 1.3.1 den Start mit P3009 („failed migrations in the target database“). Den Ausweg habe ich vollständig durchgespielt. Zuerst wird der fehlgeschlagene Eintrag in _prisma_migrations als zurückgenommen markiert: UPDATE … SET rolled_back_at = now() WHERE migration_name = '…' AND finished_at IS NULL. Danach meldet der Stand von 1.3.1 „No pending migrations“. Anschließend habe ich den Umzug nachgestellt, und die neue Version lief sauber durch: NOT NULL gesetzt, Spalte entfernt, nur noch tenant_isolation_policy vorhanden. Der Ablauf steht in drei Schritten in docs/anleitung-betrieb.md Kapitel 4.
Lokal angewendet über die Container-IP 172.19.0.2 mit tessera:tessera_dev:
| Zeitpunkt | pg_total_relation_size |
Zeilen |
|---|---|---|
vorher (0 ohne Pfad, 1 Zeile mit 502 Bytes in data) |
65536 (64 kB) | 2 |
| nach DROP | 65536 (64 kB) | 2 |
nach VACUUM FULL "DashboardImage" |
65536 (64 kB) | 2 |
Lokal ist keine Verkleinerung messbar. In data standen nur 502 Bytes, und die Tabelle belegt schon mit Tabelle, TOAST und zwei Indizes die Mindestgröße ganzer 8-kB-Seiten. Auf alpha und live mit echten Bildern ist der Gewinn größer. Dort gilt ebenfalls, dass PostgreSQL den Platz erst nach VACUUM FULL an das Dateisystem zurückgibt. Ob man das dort ausführt, entscheidest du; ich habe keinen Server angefasst.
Dienst (dashboard-images.service.ts):
onApplicationBootstrap()samtforSystem()ist entfernt, ebenso die Selbstheilung ausdataingetBytes.uploadvergibt die UUID selbst mitrandomUUID()und legt die Zeile gleich mitstoragePathan, weil das Feld jetzt Pflicht ist. Das nachträglicheupdateentfällt. Das Verhalten bei halb fertigen Zuständen bleibt gleich: erst die Zeile, dann die Datei, und scheitert das Schreiben, wird die Zeile wieder gelöscht und 500 zurückgegeben.- Schema:
data Bytes?ist gestrichen,storagePath String?wird zustoragePath String.
Tests: Die Fälle 18 und 21–23 entfallen wie geplant. Dazu fallen 10b und 10c weg, weil sie die Selbstheilung aus data geprüft haben, die es nicht mehr gibt. Test 16 liest jetzt eine JPEG-Datei statt „Datei schlägt Zeilenbytes“, Test 13 prüft die UUID und den Pfad im create, und Test 12 prüft die neue Aufrufreihenfolge ohne update und ohne forSystem. Die Dienst-Spec hat damit 19 statt 25 Tests.
Erlaubnisliste und Klassifikation:
FORSYSTEM_ALLOWED_CALL_SITES: Der Eintragdashboard-images.service.tsist entfernt, jetzt 5 Dateien mit 6 Aufrufen.docs/mandantentrennung-zugriffsklassifikation.md: Der Stand des Paarsdashboard-images.service.ts/dashboardImagegeht vonsystem-gebundenzurück aufgebunden. Die Zahlen habe ich mit der Gate-Schleife (for d in apps/api/src/*/) neu gemessen:- Bereich
dashboardgeht auf 1/29/0. Im Dokument stand 1/28/1, gemessen waren vor dieser Änderung aber schon 1/31/1, weil quick-260923-lrr zwei Treffer (favoriteLink) hinzugefügt hatte, ohne die Zeile nachzuziehen. Diese Änderung selbst zieht 2 gebundene Treffer und 1 System-Treffer ab. - Bereich
favoritesgeht auf 0/12/0. Im Dokument stand 0/8/0, auch hier war die Zeile nach quick-260923-lrr nicht nachgezogen worden. - Die Summe geht auf 61/213/6, vorher 61/208/7.
- Die Aufzählung der Tabellen mit
system_read_policyist korrigiert. Es sind jetzt sechs Tabellen (lokal gemessen),ProxmoxServerfehlte bisher in der Aufzählung, undDashboardImageist nur noch als vorübergehender Fall erwähnt. - Gegenprobe: Den Stand habe ich absichtlich auf
system-gebundenzurückgedreht.rls-access-inventory.spec.tswurde rot („dokumentiert=system-gebunden, gemessen=gebunden“), danach habe ich die Datei zurückgesetzt.
- Bereich
- Einen veralteten Kommentarverweis auf
DashboardImagesServiceals Vorbild habe ich inproxmox.service.tsentfernt.
CHANGELOG: bewusst nicht ergänzt, weil die Änderung für Nutzer nicht sichtbar ist. Betriebshandbuch: Kapitel 4 hat einen Hinweis für die erste Version nach 1.3.1 bekommen: was die Meldung bedeutet und die drei Schritte zur Wiederherstellung.
Tore
- API-Tests komplett: 84 Dateien, 1364 Tests, grün, einschließlich
rls-access-inventory.spec.tsmit 30 Tests. - Web-Tests komplett: 91 Dateien, 865 Tests, grün, auch gedrosselt auf 2 Kerne.
pnpm turbo run type-check lint: 9/9 Aufgaben erfolgreich.- Biome-Warnungen: web 53 (Grenze 53), api 82 (Grenze 82).
Abweichungen vom Plan
- [Rule 3 – blockierend] Todo-Ordner: Den Ordner
.planning/todos/done/aus dem Plan gibt es nicht, die Ablage im Projekt heißt.planning/todos/completed/. Beide Todos liegen deshalb dort, jeweils mit einem Nachtrag „Erledigt in quick-260924-m4n“. - [Rule 1 – Bug] Weg zurück nach einem Abbruch: Die Meldung der Schutzprüfung rät, zuerst 1.3.1 laufen zu lassen. Allein das hätte aber nicht funktioniert, weil 1.3.1 danach mit P3009 abbricht. Der fehlende Zwischenschritt steht jetzt im Betriebshandbuch und im Kopfkommentar der Migration, und ich habe ihn durchgespielt.
- [Rule 1 – Bug] Upload-Ablauf: Mit
storagePathals Pflichtfeld hätte der bisherige Ablauf (Zeile ohne Pfad anlegen, Pfad nachtragen) an der Datenbank scheitern müssen. Deshalb vergibt der Dienst die UUID jetzt selbst. - Zusätzliche Tests entfallen: Neben 18 und 21–23 sind auch 10b und 10c weg (siehe oben).
- Mehr Marktplatz-Dateien umgestellt: Neben
tenant-selectorsind auchmarketplace.test.tsxundmarketplace-filters.test.tsxumgestellt. Es ist dasselbe Muster im selben Ordner. - Drift in der Klassifikation nachgeholt: Die Zeilen
favoritesunddashboardstimmten schon vorher nicht mit der Messung überein. Beides ist jetzt nachgeholt und im Dokument begründet.
Offen / für dich
- Live konnte ich von hier aus nicht prüfen. Die nächste Freigabe nach 1.3.1 setzt voraus, dass live einmal auf 1.3.1 gelaufen ist. Wenn nicht, bricht die Migration mit einer klaren Meldung ab, der Weg zurück steht in Kapitel 4.
- Optional nach dem Einspielen auf alpha und live:
VACUUM FULL "DashboardImage";, damit PostgreSQL den Platz der alten Bilddaten tatsächlich an das Dateisystem zurückgibt.
Self-Check: PASSED
- FOUND: apps/api/prisma/migrations/20260924120000_dashboard_image_drop_data/migration.sql
- FOUND: .planning/todos/completed/2026-09-23-flackernder-test-tenant-selector-zeitueberschreitung.md
- FOUND: .planning/todos/completed/2026-09-22-dashboard-image-data-spalte-entfernen.md
- FOUND:
b10734f,dd54ec5(gemessen:git rev-list --count dd09c08..HEAD= 2)