Ladegrenzen für jede Route, Turbopack im Entwicklungsserver

Gemessen statt vermutet: die Datenschicht war nie das Problem. resolvePermissions
unter 1 ms, loadDashboard 11 ms, searchCases 5 ms, die Abteilungsabfragen 3–4 ms.

Die Sekunden kamen aus zwei Ecken.

**Fehlende Ladegrenzen.** Den meisten Routen fehlte loading.tsx - unter anderem
der Übersicht, der Suche und dem gesamten Adminbereich. Ohne diese Datei hat
der Router keine Grenze, wartet stumm auf die fertige Serverantwort, und bis
dahin steht die alte Seite: man klickt, und scheinbar passiert nichts. Neunzehn
Skelette nachgetragen, jede Route hat jetzt eine.

Dazu quittiert der geklickte Link den Klick selbst, mit einem ruhig
pulsierenden Punkt statt eines Kreisels. Regel 5 verbietet den
Blockade-Spinner, der Inhalt ersetzt; eine Quittung im Link ersetzt nichts, und
die eigentliche Antwort auf das Warten bleibt das Skelett.

**Webpack.** Der Entwicklungsserver übersetzte jede Route beim ersten Aufruf
neu: 8 s für die Übersicht, 2,8 s für die Fallliste. Mit Turbopack sind es
0,43 s beim ersten Aufruf und 0,1 s danach. Nur pnpm dev, der Produktionsbau
bleibt unverändert. Gegengeprüft, dass die Anmeldeseite mit CSS und ohne
Fehler im Log rendert.

Nebenbei die eine Abfrage, die schlecht skalierte: die Übersicht zählte
ungelesene Nachrichten mit einer Abfrage je Ort - bei hundert Akten hundert
Hin- und Rückwege. Jetzt ein einziges groupBy mit der Lesemarke im ODER-Zweig
des jeweiligen Ortes. Ein Test prüft, was dabei leicht verrutscht: die Marke
gilt je Ort, und eigene Nachrichten zählen nie mit.
This commit is contained in:
Harlekin
2026-09-03 22:21:43 +02:00
parent 4b794ec9a8
commit 75c22ff4f8
27 changed files with 542 additions and 19 deletions
+63
View File
@@ -130,4 +130,67 @@ suite('Übersicht und Beobachtungsliste', () => {
const abgestiegen = perms(nutzer.stufe4, 1)
expect((await listWatched(abgestiegen)).map((a) => a.id)).not.toContain(geheimId)
})
it('zählt Ungelesenes je Ort und lässt Eigenes aus', async () => {
// Die Zählung lief einmal als eine Abfrage je Ort und ist jetzt eine
// einzige über alle. Geprüft wird deshalb, was dabei leicht verrutscht:
// die Lesemarke gilt **je Ort**, und eigene Nachrichten zählen nie mit.
const p = perms(nutzer.stufe1, 1)
const vorlage = await db.documentTemplate.findFirstOrThrow({
where: { key: `${marke}-vorlage` },
})
const zweite = await db.case.create({
data: {
templateId: vorlage.id,
templateKey: vorlage.key,
createdById: nutzer.stufe4,
caseNumber: `Ung-2026-${Date.now().toString(36)}`,
title: 'Zweiter Vorgang',
classification: 1,
status: 'OPEN',
},
})
await db.caseParticipant.create({
data: { caseId: zweite.id, userId: nutzer.stufe1, role: 'INVESTIGATOR' },
})
const erste = await db.case.findFirstOrThrow({ where: { caseNumber: offenNummer } })
const frueher = new Date(Date.now() - 60_000)
await db.chatMessage.createMany({
data: [
// Vor der Lesemarke der ersten Akte: zählt nicht.
{
id: `${marke}-alt`,
caseId: erste.id,
authorId: nutzer.stufe4,
body: 'alt',
createdAt: frueher,
},
// Danach: zählt.
{ id: `${marke}-neu`, caseId: erste.id, authorId: nutzer.stufe4, body: 'neu' },
// Eigene Nachricht: zählt nie.
{ id: `${marke}-eigen`, caseId: erste.id, authorId: nutzer.stufe1, body: 'eigen' },
// Zweite Akte ohne Lesemarke: alles zählt.
{ id: `${marke}-z1`, caseId: zweite.id, authorId: nutzer.stufe4, body: 'a' },
{ id: `${marke}-z2`, caseId: zweite.id, authorId: nutzer.stufe4, body: 'b' },
],
})
await db.readMarker.create({
data: {
userId: nutzer.stufe1,
scopeType: 'case',
scopeId: erste.id,
lastReadAt: new Date(Date.now() - 30_000),
},
})
const daten = await loadDashboard(p)
const zuErster = daten.ungelesen.find((u) => u.scopeId === erste.id)
const zuZweiter = daten.ungelesen.find((u) => u.scopeId === zweite.id)
expect(zuErster?.count).toBe(1)
expect(zuZweiter?.count).toBe(2)
})
})