Die letzte offene Zeile aus TASKS.md. Sie blieb offen, weil eine Spur über
jeden Aktenaufruf schnell etwas anderes wird als eine Bequemlichkeit.
Vier Festlegungen halten sie auf der richtigen Seite:
- Kein Verlauf. Der Primärschlüssel (userId, targetType, targetId) lässt je
Nutzer und Datensatz genau eine Zeile zu, die überschrieben wird. Die
Tabelle kann nicht sagen, wie oft oder in welcher Reihenfolge gearbeitet
wurde. Zwanzig Einträge, älteres fällt hinten heraus.
- Nur die eigene. Keine Ansicht, kein Adminbereich, keine Funktion mit
fremder Kennung. Jede Abfrage nimmt p.userId.
- Jederzeit löschbar, ohne Rückfrage und ohne Grabstein.
- Vorabrufe zählen nicht. Ohne die Prüfung auf next-router-prefetch stünde
dort, worüber jemand mit der Maus gefahren ist.
Gelesen wird über dieselben Sichtbarkeitsfragmente wie überall: eine
vorgemerkte Kennung ist keine Berechtigung. Steigt eine Akte über die
Freigabestufe, verschwindet sie ohne Lücke und ohne Platzhalter; Personalakten
brauchen zusätzlich personnel.read. Das Zugriffsprotokoll bleibt unberührt und
bleibt die einzige Auskunft darüber, wer wann was gelesen hat.
Sie steht auf der Übersicht und im leeren Feld der Kommandopalette. Der zweite
Ort ist der eigentliche.
Nebenbei: NEXT_DIST_DIR, damit ein Build neben einem laufenden
Entwicklungsserver nicht dasselbe .next-Verzeichnis abräumt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
61 lines
2.0 KiB
JavaScript
61 lines
2.0 KiB
JavaScript
import { readFileSync, existsSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { gzipSync } from 'node:zlib'
|
|
|
|
/**
|
|
* Bündel-Budget aus docs/09-performance-und-ux.md: die erste Route muss unter
|
|
* 120 kB JavaScript nach Kompression bleiben.
|
|
*
|
|
* Gemessen wird je Route die Vereinigung ihrer JavaScript-Chunks aus
|
|
* app-build-manifest.json, gzip-komprimiert. Das entspricht dem, was Next
|
|
* selbst als "First Load JS" ausweist. Cloudflare liefert Brotli aus, das ist
|
|
* noch etwas kleiner, gzip ist damit die vorsichtigere Schätzung.
|
|
*/
|
|
const BUDGET_BYTES = 120 * 1024
|
|
|
|
const appDir = process.cwd()
|
|
// Dasselbe Verzeichnis wie next.config.ts, damit auch ein Build neben einem
|
|
// laufenden Entwicklungsserver gemessen werden kann.
|
|
const distDir = process.env.NEXT_DIST_DIR ?? '.next'
|
|
const manifestPath = path.join(appDir, distDir, 'app-build-manifest.json')
|
|
|
|
if (!existsSync(manifestPath)) {
|
|
console.error('Kein Build gefunden. Zuerst `pnpm --filter @afis/web run build` laufen lassen.')
|
|
process.exit(1)
|
|
}
|
|
|
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
const routes = Object.entries(manifest.pages ?? {})
|
|
|
|
if (routes.length === 0) {
|
|
console.error('Keine Routen im Manifest gefunden.')
|
|
process.exit(1)
|
|
}
|
|
|
|
const measured = routes
|
|
.map(([route, files]) => {
|
|
let bytes = 0
|
|
for (const file of new Set(files)) {
|
|
if (!file.endsWith('.js')) continue
|
|
const full = path.join(appDir, distDir, file)
|
|
if (!existsSync(full)) continue
|
|
bytes += gzipSync(readFileSync(full)).byteLength
|
|
}
|
|
return { route, bytes }
|
|
})
|
|
.sort((a, b) => b.bytes - a.bytes)
|
|
|
|
const kb = (n) => `${(n / 1024).toFixed(1)} kB`
|
|
|
|
for (const { route, bytes } of measured) {
|
|
console.log(`${bytes > BUDGET_BYTES ? '✗' : '✓'} ${route.padEnd(28)} ${kb(bytes)}`)
|
|
}
|
|
|
|
const worst = measured[0]
|
|
console.log(`\nGrößte Route: ${worst.route} mit ${kb(worst.bytes)}, Budget ${kb(BUDGET_BYTES)}`)
|
|
|
|
if (worst.bytes > BUDGET_BYTES) {
|
|
console.error(`Budget überschritten um ${kb(worst.bytes - BUDGET_BYTES)}.`)
|
|
process.exit(1)
|
|
}
|