Générateur de portfolios PDF Figures Libres (app Nuxt autonome)
App Nuxt 3 + Nuxt UI qui lit les projets du CMS Grav et compose des portfolios A3 paysage exportés en PDF (gabarit book_v3) : composeur WYSIWYG, templates cover/toc/projet/grille/page libre, pipeline Playwright + Ghostscript, gel du contenu à la génération, partage public opt-in. Dépôt autonome : Dockerfile (base Playwright + Ghostscript + ffmpeg + rsync) inclus. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
23
scripts/dev-composer-shots.mjs
Normal file
23
scripts/dev-composer-shots.mjs
Normal file
@@ -0,0 +1,23 @@
|
||||
// Captures du composeur : panneau page couverture projet et panneau grille.
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
const [, , id] = process.argv
|
||||
const base = process.env.BASE_URL ?? 'http://localhost:3000'
|
||||
|
||||
const browser = await chromium.launch()
|
||||
const page = await browser.newPage({ viewportSize: { width: 1600, height: 950 } })
|
||||
await page.request.post(`${base}/api/auth/login`, {
|
||||
data: { password: process.env.NUXT_PORTFOLIO_PASSWORD ?? 'figureslibres' },
|
||||
})
|
||||
await page.goto(`${base}/portfolios/${id}`, { waitUntil: 'networkidle' })
|
||||
|
||||
await page.getByText('Projet — couverture').first().click()
|
||||
await page.waitForTimeout(1200)
|
||||
await page.screenshot({ path: '/app/data/cache/composer-cover.png' })
|
||||
|
||||
await page.getByText('Projet — grille').first().click()
|
||||
await page.waitForTimeout(1200)
|
||||
await page.screenshot({ path: '/app/data/cache/composer-grid.png' })
|
||||
|
||||
console.log('ok')
|
||||
await browser.close()
|
||||
23
scripts/dev-pages-shots.mjs
Normal file
23
scripts/dev-pages-shots.mjs
Normal file
@@ -0,0 +1,23 @@
|
||||
// Capture chaque page d'un portfolio dans le composeur (auto-contrôle dev).
|
||||
// Usage : node scripts/dev-pages-shots.mjs <portfolioId>
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
const [, , id] = process.argv
|
||||
const base = process.env.BASE_URL ?? 'http://localhost:3000'
|
||||
|
||||
const browser = await chromium.launch()
|
||||
const page = await browser.newPage({ viewportSize: { width: 1600, height: 900 } })
|
||||
await page.request.post(`${base}/api/auth/login`, {
|
||||
data: { password: process.env.NUXT_PORTFOLIO_PASSWORD ?? 'figureslibres' },
|
||||
})
|
||||
await page.goto(`${base}/portfolios/${id}`, { waitUntil: 'networkidle' })
|
||||
|
||||
const items = page.locator('aside .group')
|
||||
const count = await items.count()
|
||||
for (let i = 0; i < count; i++) {
|
||||
await items.nth(i).click()
|
||||
await page.waitForTimeout(1200)
|
||||
await page.screenshot({ path: `/app/data/cache/page-${i + 1}.png` })
|
||||
console.log(`page ${i + 1}/${count}`)
|
||||
}
|
||||
await browser.close()
|
||||
24
scripts/dev-picker-shot.mjs
Normal file
24
scripts/dev-picker-shot.mjs
Normal file
@@ -0,0 +1,24 @@
|
||||
// Capture de la modale « Ajouter un projet » du composeur (auto-contrôle dev).
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
const [, , id, out = '/app/data/cache/picker.png'] = process.argv
|
||||
const base = process.env.BASE_URL ?? 'http://localhost:3000'
|
||||
|
||||
const browser = await chromium.launch()
|
||||
const page = await browser.newPage({ viewportSize: { width: 1440, height: 900 } })
|
||||
await page.request.post(`${base}/api/auth/login`, {
|
||||
data: { password: process.env.NUXT_PORTFOLIO_PASSWORD ?? 'figureslibres' },
|
||||
})
|
||||
await page.goto(`${base}/portfolios/${id}`, { waitUntil: 'networkidle' })
|
||||
await page.getByRole('button', { name: 'Ajouter', exact: true }).click()
|
||||
await page.getByRole('menuitem', { name: /Projet/ }).click()
|
||||
await page.waitForTimeout(1500)
|
||||
// Filtre « type de travail » si fourni en 3e argument
|
||||
const filterLabel = process.argv[4]
|
||||
if (filterLabel) {
|
||||
await page.getByRole('button', { name: filterLabel, exact: true }).click()
|
||||
await page.waitForTimeout(800)
|
||||
}
|
||||
await page.screenshot({ path: out })
|
||||
await browser.close()
|
||||
console.log(`screenshot: ${out}`)
|
||||
50
scripts/dev-raw-pdf.mjs
Normal file
50
scripts/dev-raw-pdf.mjs
Normal file
@@ -0,0 +1,50 @@
|
||||
// Capture le PDF brut Chromium de /print/<id> (avant Ghostscript) pour diagnostic.
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
const [, , id, out = '/app/data/cache/raw-debug.pdf'] = process.argv
|
||||
const token = process.env.NUXT_INTERNAL_TOKEN ?? 'dev-internal-token'
|
||||
|
||||
const browser = await chromium.launch()
|
||||
const page = await browser.newPage({
|
||||
viewportSize: { width: 1280, height: 720 },
|
||||
extraHTTPHeaders: { 'x-internal-token': token },
|
||||
})
|
||||
page.on('console', m => console.log('[console]', m.type(), m.text()))
|
||||
page.on('requestfailed', r => console.log('[failed]', r.url(), r.failure()?.errorText))
|
||||
page.on('response', (r) => { if (!r.ok()) console.log('[http]', r.status(), r.url()) })
|
||||
|
||||
const url = `http://localhost:3000/print/${id}?internal-token=${encodeURIComponent(token)}`
|
||||
const response = await page.goto(url, { waitUntil: 'networkidle', timeout: 120_000 })
|
||||
console.log('goto:', response?.status())
|
||||
|
||||
await page.evaluate(async () => {
|
||||
await Promise.all(Array.from(document.fonts).map(f => f.load().catch(() => {})))
|
||||
await document.fonts.ready
|
||||
const images = Array.from(document.querySelectorAll('img'))
|
||||
await Promise.all(images.map(img => img.complete
|
||||
? Promise.resolve()
|
||||
: new Promise((resolve) => { img.onload = img.onerror = resolve })))
|
||||
})
|
||||
|
||||
// État des fontes + présence de texte dans le DOM
|
||||
const info = await page.evaluate(() => ({
|
||||
fonts: Array.from(document.fonts).map(f => `${f.family} ${f.weight} ${f.style}: ${f.status}`),
|
||||
h2: Array.from(document.querySelectorAll('h2')).map(el => el.textContent?.slice(0, 40)),
|
||||
bodyTextLen: document.body.innerText.length,
|
||||
}))
|
||||
console.log(JSON.stringify(info, null, 2))
|
||||
|
||||
await page.emulateMedia({ media: 'print' })
|
||||
// Bug Chromium : texte en webfonts absent du PDF sans réinvalidation des
|
||||
// styles (cf. pdf-pipeline.ts)
|
||||
await page.evaluate(async () => {
|
||||
for (const sheet of document.styleSheets) sheet.disabled = true
|
||||
void document.body.offsetHeight
|
||||
for (const sheet of document.styleSheets) sheet.disabled = false
|
||||
void document.body.offsetHeight
|
||||
await document.fonts.ready
|
||||
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))
|
||||
})
|
||||
await page.pdf({ path: out, printBackground: true, preferCSSPageSize: true, timeout: 120_000 })
|
||||
console.log('raw pdf →', out)
|
||||
await browser.close()
|
||||
18
scripts/dev-screenshot.mjs
Normal file
18
scripts/dev-screenshot.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
// Capture d'écran de contrôle (dev) : node scripts/dev-screenshot.mjs <path> <out.png>
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
const [, , path = '/', out = '/app/data/cache/screenshot.png'] = process.argv
|
||||
const base = process.env.BASE_URL ?? 'http://localhost:3000'
|
||||
|
||||
const browser = await chromium.launch()
|
||||
const page = await browser.newPage({ viewportSize: { width: 1440, height: 900 } })
|
||||
|
||||
// Session : login via l'API puis navigation
|
||||
await page.request.post(`${base}/api/auth/login`, {
|
||||
data: { password: process.env.NUXT_PORTFOLIO_PASSWORD ?? 'figureslibres' },
|
||||
})
|
||||
await page.goto(`${base}${path}`, { waitUntil: 'networkidle' })
|
||||
await page.waitForTimeout(1000)
|
||||
await page.screenshot({ path: out, fullPage: false })
|
||||
await browser.close()
|
||||
console.log(`screenshot: ${out}`)
|
||||
20
scripts/dev-test-generate.mjs
Normal file
20
scripts/dev-test-generate.mjs
Normal file
@@ -0,0 +1,20 @@
|
||||
// Test E2E du flux de génération PDF depuis le composeur (auto-contrôle dev).
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
const [, , id] = process.argv
|
||||
const base = process.env.BASE_URL ?? 'http://localhost:3000'
|
||||
|
||||
const browser = await chromium.launch()
|
||||
const page = await browser.newPage({ viewportSize: { width: 1600, height: 900 } })
|
||||
await page.request.post(`${base}/api/auth/login`, {
|
||||
data: { password: process.env.NUXT_PORTFOLIO_PASSWORD ?? 'figureslibres' },
|
||||
})
|
||||
await page.goto(`${base}/portfolios/${id}`, { waitUntil: 'networkidle' })
|
||||
|
||||
await page.getByRole('button', { name: 'Générer le PDF' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
await page.getByRole('button', { name: 'Générer', exact: true }).click()
|
||||
await page.getByRole('link', { name: 'Télécharger' }).waitFor({ timeout: 180_000 })
|
||||
await page.screenshot({ path: '/app/data/cache/generate-modal.png' })
|
||||
console.log('génération OK, lien de téléchargement affiché')
|
||||
await browser.close()
|
||||
Reference in New Issue
Block a user