/** * Screenshots der laufenden App für die Produktseite (`public/marketing/*.png`). * * Meldet sich OHNE Passworteingabe an: die Session wird wie in `scripts/smoke-auth.ts` gebaut * (finalizeIdentityLogin + next-auth/jwt) und als Cookie in den Browser gesetzt. Gezeigt werden * ausschließlich Demo-Daten des Mandanten `demo`. * * npm run dev # Dev-Server muss laufen * npx tsx scripts/marketing-shots.ts # BASE=http://localhost:3000 */ import "dotenv/config"; import { mkdir } from "node:fs/promises"; import path from "node:path"; import { chromium, type BrowserContext } from "playwright-core"; import { encode } from "next-auth/jwt"; import { prisma } from "../src/server/db"; import { finalizeIdentityLogin } from "../src/server/auth"; const BASE = process.env.BASE ?? "http://localhost:3000"; const COOKIE = "authjs.session-token"; const OUT = path.join(process.cwd(), "public", "marketing"); async function sessionCookie(email: string, tenant: string) { const identity = await prisma.identity.findUniqueOrThrow({ where: { email } }); const user = await finalizeIdentityLogin(identity.id, tenant); if (!user) throw new Error(`keine Mitgliedschaft für ${email}`); const token = { sub: user.id, name: user.name, email: user.email, userId: user.id, identityId: user.identityId, tenantId: user.tenantId, tenantSlug: user.tenantSlug, activeMembershipId: user.activeMembershipId, memberships: user.memberships, roles: user.roles, permissions: user.permissions, isPlatformAdmin: user.isPlatformAdmin, mfaEnrolled: user.mfaEnrolled, }; return encode({ token, secret: process.env.AUTH_SECRET!, salt: COOKIE, maxAge: 60 * 60 }); } type Shot = { name: string; path: string; wait?: string; click?: string; fullPage?: boolean }; async function shoot(ctx: BrowserContext, shots: Shot[], label: string) { const page = await ctx.newPage(); for (const s of shots) { await page.goto(`${BASE}${s.path}`, { waitUntil: "domcontentloaded", timeout: 120_000 }); if (s.wait) await page.waitForSelector(s.wait, { timeout: 60_000 }).catch(() => undefined); if (s.click) await page.click(s.click, { timeout: 15_000 }).catch(() => undefined); // Dev-Overlay von Next.js gehört nicht ins Produktbild await page.addStyleTag({ content: "nextjs-portal, [data-nextjs-toast], #__next-build-watcher { display: none !important; }" }); await page.waitForTimeout(1200); await page.screenshot({ path: path.join(OUT, `${s.name}.png`), fullPage: s.fullPage ?? false }); console.log(`✔ ${label}: ${s.name}.png (${s.path})`); } await page.close(); } async function main() { await mkdir(OUT, { recursive: true }); const tenant = await prisma.tenant.findUniqueOrThrow({ where: { slug: "demo" }, select: { id: true } }); const order = await prisma.workOrder.findFirstOrThrow({ where: { tenantId: tenant.id, status: "in_progress", deletedAt: null }, select: { id: true }, }); const browser = await chromium.launch({ channel: "chrome", headless: true }); const value = await sessionCookie("admin@demo.example", "demo"); const techValue = await sessionCookie("monteur@demo.example", "demo"); const cookie = { name: COOKIE, value, domain: "localhost", path: "/", httpOnly: true, secure: false, sameSite: "Lax" as const }; const desktop = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2, locale: "de-DE" }); await desktop.addCookies([cookie]); await shoot(desktop, [ { name: "plantafel", path: "/planning", wait: "text=Plantafel" }, { name: "live-lage", path: "/planning/live", wait: "text=Live-Lage", click: "text=Kacheln" }, { name: "auftrag", path: `/work-orders/${order.id}`, wait: "text=Status" }, { name: "abrechnung", path: "/billing", wait: "text=Abrechnung" }, { name: "dashboard", path: "/dashboard", wait: "text=Dashboard" }, { name: "auftraege", path: "/work-orders", wait: "text=Aufträge" }, { name: "berichte", path: "/reports", wait: "text=Berichte" }, { name: "zeitfreigabe", path: "/work-orders/time-approvals", wait: "text=Zeiten" }, { name: "audit", path: "/settings/audit", wait: "text=Audit" }, { name: "teams", path: "/teams", wait: "text=Teams" }, ], "Desktop"); await desktop.close(); const mobile = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 3, locale: "de-DE", isMobile: true, hasTouch: true }); await mobile.addCookies([{ ...cookie, value: techValue }]); await shoot(mobile, [ { name: "mobil-heute", path: "/m", wait: "text=Heute" }, { name: "mobil-auftrag", path: `/m/orders/${order.id}`, wait: "text=Auftrag" }, { name: "mobil-lotse", path: "/m/lotse", wait: "text=Lotse" }, { name: "mobil-zeiten", path: "/m/time", wait: "text=Meine Zeiten" }, ], "Mobil"); await mobile.close(); await browser.close(); console.log(`\nFertig — Dateien in public/marketing/`); } main() .then(() => prisma.$disconnect()) .then(() => process.exit(0)) .catch(async (err) => { console.error(err instanceof Error ? err.message : err); await prisma.$disconnect().catch(() => undefined); process.exit(1); });