fix: Navigationsstruktur aus der Client-Komponente herausloesen
Das Server-Layout filtert die Navigation danach, welche Module der angemeldete Benutzer sehen darf. Es importierte NAV_STRUKTUR aber aus einer Datei mit "use client" - dort kommt auf dem Server kein Wert an, sondern nur ein Verweis. NAV_STRUKTUR.filter war entsprechend keine Funktion, und jede Fachseite lief in einen Serverfehler. Zweiter Fehler in derselben Zeile: die Eintraege trugen die Icons als React-Komponenten. Die lassen sich nicht ueber die Server/Client-Grenze reichen. Beides behoben, indem die Struktur nach src/lib/navigation-struktur.ts wandert und dort nur noch ein Kuerzel je Icon steht. Die Client- Komponente uebersetzt das Kuerzel. Die Wiederausfuhr aus navigation.tsx wurde bewusst weggelassen, sonst stellt sich dieselbe Falle wieder auf. Dazu scripts/screenshots.mjs: meldet sich mit dem Entwicklungszugang an und fotografiert Seiten am Schreibtisch und am Handy, hell und dunkel. Etappe: 1 Phase: Fundament
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Bildschirmfotos der Anwendung erzeugen.
|
||||
*
|
||||
* node scripts/screenshots.mjs [pfad ...]
|
||||
*
|
||||
* Meldet sich mit dem Entwicklungszugang an (SEED_DEV_TOTP_SECRET aus der
|
||||
* .env) und fotografiert die angegebenen Seiten - jeweils am Schreibtisch
|
||||
* und am Handy, hell und dunkel. Die Bilder landen unter ./screenshots.
|
||||
*
|
||||
* Nur fuer die Entwicklung gedacht: das Skript setzt voraus, dass der
|
||||
* zweite Faktor mit einem bekannten Geheimnis vorbelegt ist.
|
||||
*/
|
||||
import { chromium } from '@playwright/test'
|
||||
import { TOTP, Secret } from 'otpauth'
|
||||
import { mkdir, readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
const BASIS = process.env.SCREENSHOT_BASIS ?? 'http://localhost:3000'
|
||||
const AUSGABE = 'screenshots'
|
||||
|
||||
async function envLesen() {
|
||||
const roh = await readFile('.env', 'utf8')
|
||||
const werte = {}
|
||||
for (const zeile of roh.split(/\r?\n/)) {
|
||||
const treffer = zeile.match(/^([A-Z0-9_]+)=(.*)$/)
|
||||
if (treffer) werte[treffer[1]] = treffer[2].replace(/^"|"$/g, '')
|
||||
}
|
||||
return werte
|
||||
}
|
||||
|
||||
function code(secret, kennung) {
|
||||
return new TOTP({
|
||||
issuer: 'Betreuungs-Cockpit',
|
||||
label: kennung,
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret: Secret.fromBase32(secret),
|
||||
}).generate()
|
||||
}
|
||||
|
||||
const GERAETE = [
|
||||
{ name: 'schreibtisch', viewport: { width: 1440, height: 960 }, mobil: false },
|
||||
{ name: 'handy', viewport: { width: 390, height: 844 }, mobil: true },
|
||||
]
|
||||
|
||||
async function main() {
|
||||
const env = await envLesen()
|
||||
const email = env.SEED_ADMIN_EMAIL
|
||||
const passwort = env.SEED_ADMIN_PASSWORT
|
||||
const secret = env.SEED_DEV_TOTP_SECRET
|
||||
|
||||
if (!email || !passwort || !secret) {
|
||||
console.error(
|
||||
'In der .env fehlen SEED_ADMIN_EMAIL, SEED_ADMIN_PASSWORT oder SEED_DEV_TOTP_SECRET.',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Git Bash unter Windows wandelt "/heute" in einen Windows-Pfad um.
|
||||
// Deshalb wird hier nur der letzte Abschnitt genommen und ein Slash
|
||||
// vorangestellt - "heute" und "/heute" fuehren beide zum Ziel.
|
||||
const pfade = process.argv.slice(2).map((roh) => {
|
||||
// Aus "C:/Program Files/Git/heute" wird wieder "/heute".
|
||||
const abschnitte = roh.split(/[\\/]+/).filter(Boolean)
|
||||
const gefunden = abschnitte.findIndex((teil) => teil.toLowerCase() === 'git')
|
||||
const rest = gefunden >= 0 ? abschnitte.slice(gefunden + 1) : abschnitte
|
||||
return `/${rest.join('/')}`
|
||||
})
|
||||
if (pfade.length === 0) {
|
||||
console.error('Bitte mindestens einen Pfad angeben, z.B. /heute /fristen')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await mkdir(AUSGABE, { recursive: true })
|
||||
const browser = await chromium.launch()
|
||||
|
||||
for (const geraet of GERAETE) {
|
||||
for (const thema of ['hell', 'dunkel']) {
|
||||
const kontext = await browser.newContext({
|
||||
viewport: geraet.viewport,
|
||||
isMobile: geraet.mobil,
|
||||
hasTouch: geraet.mobil,
|
||||
deviceScaleFactor: 2,
|
||||
locale: 'de-DE',
|
||||
timezoneId: 'Europe/Berlin',
|
||||
colorScheme: thema === 'dunkel' ? 'dark' : 'light',
|
||||
})
|
||||
|
||||
// Das Farbschema merkt sich die Anwendung im localStorage.
|
||||
await kontext.addInitScript((wahl) => {
|
||||
try {
|
||||
localStorage.setItem('bc-thema', wahl)
|
||||
} catch {
|
||||
/* egal */
|
||||
}
|
||||
}, thema)
|
||||
|
||||
const seite = await kontext.newPage()
|
||||
|
||||
// --- Anmelden ------------------------------------------------------
|
||||
await seite.goto(`${BASIS}/anmelden`, { waitUntil: 'networkidle' })
|
||||
|
||||
if (pfade.includes('/anmelden')) {
|
||||
await seite.screenshot({
|
||||
path: path.join(AUSGABE, `anmelden-${geraet.name}-${thema}.png`),
|
||||
fullPage: true,
|
||||
})
|
||||
}
|
||||
|
||||
await seite.fill('input[name="email"]', email)
|
||||
await seite.fill('input[name="passwort"]', passwort)
|
||||
await seite.fill('input[name="code"]', code(secret, email))
|
||||
await Promise.all([
|
||||
seite.waitForURL((url) => !url.pathname.startsWith('/anmelden'), { timeout: 30_000 }),
|
||||
seite.click('button[type="submit"]'),
|
||||
])
|
||||
|
||||
// --- Seiten fotografieren -------------------------------------------
|
||||
for (const pfad of pfade) {
|
||||
if (pfad === '/anmelden') continue
|
||||
|
||||
const name = pfad.replace(/^\//, '').replace(/\//g, '-') || 'start'
|
||||
try {
|
||||
await seite.goto(`${BASIS}${pfad}`, { waitUntil: 'networkidle', timeout: 45_000 })
|
||||
// Kurz warten, damit Schriften und Übergänge sitzen.
|
||||
await seite.waitForTimeout(600)
|
||||
await seite.screenshot({
|
||||
path: path.join(AUSGABE, `${name}-${geraet.name}-${thema}.png`),
|
||||
fullPage: true,
|
||||
})
|
||||
console.log(` ${name}-${geraet.name}-${thema}.png`)
|
||||
} catch (fehler) {
|
||||
console.warn(` ${pfad} (${geraet.name}/${thema}) fehlgeschlagen: ${fehler.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
await kontext.close()
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close()
|
||||
console.log(`\nFertig. Bilder liegen in ./${AUSGABE}`)
|
||||
}
|
||||
|
||||
main().catch((fehler) => {
|
||||
console.error(fehler)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user