Betrieb: Dockge-Stack, Images, Prüfung
compose.yaml aus docs/11-betrieb.md, ergänzt um Healthchecks für db-backup und offsite-backup. Das Dokument verlangt für jeden Dienst einen, die abgedruckte Fassung hatte für die beiden keinen, und ohne ihn zeigt Dockge nur "läuft", auch wenn seit Wochen nichts mehr gesichert wird. Die Migration läuft im Startkommando von web, nicht in einem Init-Container. Die Prisma-CLI samt Schema-Engine liegt dafür unter /app/migrator im Image: ein Container, der nur startet, wenn gerade eine Registry erreichbar ist, wäre auf einem Homeserver die falsche Bauart. Alle drei Images gebaut und laufen gegen eine echte Postgres-Instanz geprüft: web wendet Migrationen an und liefert /api/health aus, worker schreibt sein Lebenszeichen, pdf rendert ein echtes PDF. Die CI prüft Lint, Typen, Tests und E2E gegen den Standalone-Server, also so, wie der Container startet, und hält das Bündel-Budget aus docs/09-performance-und-ux.md ein. Phase 0 aus TASKS.md ist damit abgehakt.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
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()
|
||||
const manifestPath = path.join(appDir, '.next', '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, '.next', 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)
|
||||
}
|
||||
Reference in New Issue
Block a user