feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
/**
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
* Documentation Screenshot Generator
|
|
|
|
|
|
*
|
|
|
|
|
|
* Captures every screenshot referenced by README.md and docs/theming.md:
|
|
|
|
|
|
* theme presets, hero styles, grid layouts, light mode, the standard pages
|
|
|
|
|
|
* (subpage overview, about, map), the lightbox with its EXIF panel, a mobile
|
|
|
|
|
|
* viewport and — if ADMIN_PASSWORD is set — the admin panel.
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
*
|
|
|
|
|
|
* Uses Playwright to wait for images to fully load (not just blurhash placeholders).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Usage:
|
|
|
|
|
|
* npx tsx scripts/screenshots.ts
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
* SCREENSHOT_GRID_PATH=/japan/koyasan-2023 npx tsx scripts/screenshots.ts
|
|
|
|
|
|
* SCREENSHOT_PORT=3100 npx tsx scripts/screenshots.ts
|
|
|
|
|
|
* SCREENSHOT_ONLY=admin,lightbox npx tsx scripts/screenshots.ts
|
|
|
|
|
|
*
|
|
|
|
|
|
* The album used for the grid shots is discovered by walking the running site,
|
|
|
|
|
|
* since album slugs come from Immich album names. SCREENSHOT_GRID_PATH picks a
|
|
|
|
|
|
* specific one instead, SCREENSHOT_SUBPAGE_PATH the collection overview.
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
*
|
|
|
|
|
|
* Prerequisites:
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
* - Nothing else listening on SCREENSHOT_PORT (the script manages its own
|
|
|
|
|
|
* server; a foreign server on that port would be screenshot instead)
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
* - Playwright browsers installed: npx playwright install chromium
|
|
|
|
|
|
*
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
* Note: this rewrites content/settings.yaml, content/gallery.yaml and
|
|
|
|
|
|
* content/about.md while it runs and restores all three afterwards — including
|
|
|
|
|
|
* on Ctrl+C. On a host serving live traffic from the same files, the public site
|
|
|
|
|
|
* changes with every capture. Run it against a copy of the repo instead.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Because the screenshots end up in a public repository, the run replaces the
|
|
|
|
|
|
* site identity (title, footer, watermark, legal block, about page) with neutral
|
|
|
|
|
|
* demo values and blanks out every stored password — see DEMO_SETTINGS.
|
|
|
|
|
|
*
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
* Output:
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
* docs/screenshots/*.png
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
*/
|
|
|
|
|
|
|
2026-03-07 08:18:23 +01:00
|
|
|
|
import { spawn, type ChildProcess } from 'child_process';
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
import fs from 'fs';
|
|
|
|
|
|
import path from 'path';
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
import yaml from 'js-yaml';
|
|
|
|
|
|
import type { Browser, Page } from '@playwright/test';
|
|
|
|
|
|
|
|
|
|
|
|
// Next reads .env.local itself; this script runs outside Next, so ADMIN_PASSWORD
|
|
|
|
|
|
// and friends have to be pulled in explicitly. Must run before the constants below.
|
|
|
|
|
|
loadEnvLocal();
|
|
|
|
|
|
|
|
|
|
|
|
const THEMES = ['studio', 'studio-modern', 'minimal', 'editorial', 'classic', 'noir', 'monograph'];
|
|
|
|
|
|
const HERO_STYLES = ['split', 'fullbleed', 'minimal', 'stacked', 'typographic', 'mosaic'];
|
|
|
|
|
|
const GRID_LAYOUTS = ['masonry', 'uniform', 'showcase', 'filmstrip', 'editorial-flow'];
|
|
|
|
|
|
/** Preset used for everything that is not itself a theme comparison. */
|
|
|
|
|
|
const SHOWCASE_THEME = 'studio-modern';
|
|
|
|
|
|
|
|
|
|
|
|
const PORT = process.env.SCREENSHOT_PORT ?? '3000';
|
|
|
|
|
|
const BASE_URL = `http://localhost:${PORT}`;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Album detail page used for the grid screenshots. Discovered by walking the
|
|
|
|
|
|
* running site, because album slugs come from Immich album names and differ per
|
|
|
|
|
|
* installation. Set SCREENSHOT_GRID_PATH to pick a specific album instead.
|
|
|
|
|
|
*/
|
|
|
|
|
|
const GRID_PATH_OVERRIDE = process.env.SCREENSHOT_GRID_PATH;
|
|
|
|
|
|
/** Collection overview page. Derived from the grid path when not set. */
|
|
|
|
|
|
const SUBPAGE_PATH_OVERRIDE = process.env.SCREENSHOT_SUBPAGE_PATH;
|
|
|
|
|
|
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Comma-separated section keys to capture, e.g. `SCREENSHOT_ONLY=admin,lightbox`.
|
|
|
|
|
|
* A full pass takes the better part of an hour, so re-shooting one section after
|
|
|
|
|
|
* a layout change should not mean redoing all of it. Empty means everything.
|
|
|
|
|
|
*/
|
|
|
|
|
|
const ONLY_SECTIONS = (process.env.SCREENSHOT_ONLY ?? '')
|
|
|
|
|
|
.split(',')
|
|
|
|
|
|
.map((key) => key.trim())
|
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
const wanted = (key: string) => ONLY_SECTIONS.length === 0 || ONLY_SECTIONS.includes(key);
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
|
|
|
|
|
|
const OUTPUT_DIR = path.join(process.cwd(), 'docs', 'screenshots');
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
const CONTENT_DIR = path.join(process.cwd(), 'content');
|
|
|
|
|
|
const SETTINGS_YAML = path.join(CONTENT_DIR, 'settings.yaml');
|
|
|
|
|
|
const GALLERY_YAML = path.join(CONTENT_DIR, 'gallery.yaml');
|
|
|
|
|
|
const ABOUT_MD = path.join(CONTENT_DIR, 'about.md');
|
|
|
|
|
|
/** Every file the run rewrites, and therefore has to restore. */
|
|
|
|
|
|
const MANAGED_FILES = [SETTINGS_YAML, GALLERY_YAML, ABOUT_MD];
|
|
|
|
|
|
const backupPath = (file: string) => `${file}.screenshot-bak`;
|
|
|
|
|
|
|
|
|
|
|
|
/** Pages that never carry a photo grid — skipped while looking for an album. */
|
|
|
|
|
|
const EXCLUDED_PATHS = new Set(['/', '/about', '/map', '/impressum', '/admin']);
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
const VIEWPORT = { width: 1440, height: 900 };
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
const MOBILE_VIEWPORT = { width: 375, height: 812 };
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
const IMAGE_LOAD_TIMEOUT = 8000; // ms to wait for images after networkidle
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
/** Settled once the dev server has re-read the changed YAML (mtime-based cache). */
|
|
|
|
|
|
const CONFIG_RELOAD_DELAY = 400;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Neutral stand-ins for everything identifying in settings.yaml. The screenshots
|
|
|
|
|
|
* are published, so the site must not show the operator's name, handle, postal
|
|
|
|
|
|
* address or watermark. Dotted keys are merged into the parsed YAML.
|
|
|
|
|
|
*/
|
|
|
|
|
|
const DEMO_SETTINGS: Record<string, unknown> = {
|
|
|
|
|
|
// A run loads every page dozens of times; without this it would write a few
|
|
|
|
|
|
// hundred fake page views into content/analytics.json.
|
|
|
|
|
|
analytics: false,
|
|
|
|
|
|
title: 'My Portfolio Gallery',
|
|
|
|
|
|
'seo.title': 'My Portfolio Gallery',
|
|
|
|
|
|
'footer.name': 'My Portfolio Gallery',
|
|
|
|
|
|
'footer.instagram': 'myportfolio',
|
|
|
|
|
|
'watermark.text': 'Folio ',
|
|
|
|
|
|
'legal.name': 'Jane Doe',
|
|
|
|
|
|
'legal.address': '1 Example Street',
|
|
|
|
|
|
'legal.zipCity': '12345 Example City',
|
|
|
|
|
|
'legal.country': 'Germany',
|
|
|
|
|
|
'legal.extraInfo': '',
|
|
|
|
|
|
};
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
|
|
|
|
|
|
async function main() {
|
2026-03-07 08:18:23 +01:00
|
|
|
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
|
|
|
|
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
// Read the originals to restore later. The on-disk backups are what the signal
|
|
|
|
|
|
// handlers and the top-level catch restore from — an in-memory copy is gone the
|
|
|
|
|
|
// moment the process is killed, and this script rewrites live content files.
|
|
|
|
|
|
const originals = new Map(MANAGED_FILES.map((f) => [f, fs.readFileSync(f, 'utf8')]));
|
|
|
|
|
|
for (const [file, content] of originals) fs.writeFileSync(backupPath(file), content);
|
|
|
|
|
|
installRestoreHandlers();
|
|
|
|
|
|
|
|
|
|
|
|
const originalSettings = originals.get(SETTINGS_YAML)!;
|
|
|
|
|
|
const originalGallery = originals.get(GALLERY_YAML)!;
|
|
|
|
|
|
|
|
|
|
|
|
/** Writes settings.yaml with the demo identity plus the given overrides. */
|
|
|
|
|
|
const applySettings = async (overrides: Record<string, unknown> = {}) => {
|
|
|
|
|
|
fs.writeFileSync(SETTINGS_YAML, settingsWith(originalSettings, overrides));
|
|
|
|
|
|
await sleep(CONFIG_RELOAD_DELAY);
|
|
|
|
|
|
};
|
2026-03-07 08:18:23 +01:00
|
|
|
|
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
// Passwords live in gallery.yaml in plain text and are rendered by the admin
|
|
|
|
|
|
// page builder — blank them for the whole run rather than per screenshot.
|
|
|
|
|
|
fs.writeFileSync(GALLERY_YAML, withRedactedPasswords(originalGallery));
|
|
|
|
|
|
// The about page carries a real name, location and portrait. Swap in demo text
|
|
|
|
|
|
// and use a gallery photo as the portrait so no face ends up in the docs.
|
|
|
|
|
|
fs.writeFileSync(ABOUT_MD, demoAboutMarkdown(originalGallery));
|
|
|
|
|
|
|
|
|
|
|
|
await applySettings();
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`\n⏳ Starting dev server on ${BASE_URL} ...`);
|
|
|
|
|
|
let browser: Browser | undefined;
|
2026-03-07 08:18:23 +01:00
|
|
|
|
|
|
|
|
|
|
try {
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
await ensureServerHealthy();
|
|
|
|
|
|
|
|
|
|
|
|
const { chromium } = await import('@playwright/test');
|
|
|
|
|
|
browser = await chromium.launch();
|
|
|
|
|
|
|
|
|
|
|
|
// ── Resolve the pages used throughout the run ──
|
|
|
|
|
|
const scout = await browser.newContext({ viewport: VIEWPORT });
|
|
|
|
|
|
const scoutPage = await scout.newPage();
|
|
|
|
|
|
const gridPath = GRID_PATH_OVERRIDE ?? (await discoverGridPath(scoutPage));
|
|
|
|
|
|
const subpagePath = SUBPAGE_PATH_OVERRIDE ?? (await resolveSubpagePath(scoutPage, gridPath));
|
|
|
|
|
|
await scout.close();
|
|
|
|
|
|
console.log(`🔎 Album page: ${gridPath}`);
|
|
|
|
|
|
console.log(`🔎 Collection page: ${subpagePath ?? '(none found — skipping)'}`);
|
|
|
|
|
|
|
|
|
|
|
|
// ── Theme presets: homepage + grid ──
|
|
|
|
|
|
if (wanted('themes')) {
|
|
|
|
|
|
for (const theme of THEMES) {
|
|
|
|
|
|
section(`Theme: ${theme}`);
|
|
|
|
|
|
await applySettings({ 'theme.preset': theme });
|
|
|
|
|
|
await withPage(browser, VIEWPORT, async (page) => {
|
|
|
|
|
|
await captureHome(page, `theme-${theme}-home`);
|
|
|
|
|
|
await captureGrid(page, gridPath, `theme-${theme}-grid`);
|
2026-03-07 08:18:23 +01:00
|
|
|
|
});
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-07 08:18:23 +01:00
|
|
|
|
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
// ── Light mode ──
|
|
|
|
|
|
if (wanted('light')) {
|
|
|
|
|
|
section('Light mode');
|
|
|
|
|
|
await applySettings({ 'theme.preset': SHOWCASE_THEME });
|
|
|
|
|
|
await withPage(
|
|
|
|
|
|
browser,
|
|
|
|
|
|
VIEWPORT,
|
|
|
|
|
|
async (page) => {
|
|
|
|
|
|
await captureHome(page, `theme-${SHOWCASE_THEME}-home-light`);
|
|
|
|
|
|
await captureGrid(page, gridPath, `theme-${SHOWCASE_THEME}-grid-light`);
|
|
|
|
|
|
},
|
|
|
|
|
|
// ThemeToggle reads this on module load and sets data-theme on <html>.
|
|
|
|
|
|
() => window.localStorage.setItem('theme', 'light'),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-03-07 08:18:23 +01:00
|
|
|
|
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
// ── Hero styles ──
|
|
|
|
|
|
if (wanted('hero')) {
|
|
|
|
|
|
for (const hero of HERO_STYLES) {
|
|
|
|
|
|
section(`Hero style: ${hero} (theme: studio)`);
|
|
|
|
|
|
await applySettings({ 'theme.preset': 'studio', 'theme.heroStyle': hero });
|
|
|
|
|
|
await withPage(browser, VIEWPORT, (page) => captureHome(page, `hero-${hero}-home`));
|
2026-03-08 16:42:32 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
// ── Grid layouts ──
|
|
|
|
|
|
if (wanted('grid')) {
|
|
|
|
|
|
for (const layout of GRID_LAYOUTS) {
|
|
|
|
|
|
section(`Grid layout: ${layout}`);
|
|
|
|
|
|
await applySettings({ 'theme.preset': SHOWCASE_THEME, 'grid.layout': layout });
|
|
|
|
|
|
await withPage(browser, VIEWPORT, (page) => captureGrid(page, gridPath, `grid-${layout}`));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Standard pages ──
|
|
|
|
|
|
if (wanted('pages')) {
|
|
|
|
|
|
section('Pages: collection, about, map');
|
|
|
|
|
|
await applySettings({ 'theme.preset': SHOWCASE_THEME });
|
|
|
|
|
|
await withPage(browser, VIEWPORT, async (page) => {
|
|
|
|
|
|
if (subpagePath) await captureGrid(page, subpagePath, 'page-collection');
|
|
|
|
|
|
await captureHome(page, 'page-about', '/about');
|
|
|
|
|
|
await capturePage(page, '/map', 'page-map', 6000);
|
2026-03-08 16:42:32 +01:00
|
|
|
|
});
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
}
|
2026-03-08 16:42:32 +01:00
|
|
|
|
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
// ── Lightbox with EXIF panel ──
|
|
|
|
|
|
if (wanted('lightbox')) {
|
|
|
|
|
|
section('Lightbox');
|
|
|
|
|
|
await applySettings({ 'theme.preset': SHOWCASE_THEME });
|
|
|
|
|
|
await withPage(browser, VIEWPORT, (page) => captureLightbox(page, gridPath));
|
|
|
|
|
|
}
|
2026-03-08 16:42:32 +01:00
|
|
|
|
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
// ── Mobile viewport ──
|
|
|
|
|
|
if (wanted('mobile')) {
|
|
|
|
|
|
section('Mobile (375x812)');
|
|
|
|
|
|
await applySettings({ 'theme.preset': SHOWCASE_THEME });
|
|
|
|
|
|
await withPage(browser, MOBILE_VIEWPORT, async (page) => {
|
|
|
|
|
|
await captureHome(page, 'mobile-home');
|
|
|
|
|
|
await captureGrid(page, gridPath, 'mobile-grid');
|
2026-03-08 16:42:32 +01:00
|
|
|
|
});
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Admin panel ──
|
|
|
|
|
|
if (wanted('admin')) {
|
|
|
|
|
|
await applySettings({ 'theme.preset': SHOWCASE_THEME });
|
|
|
|
|
|
await captureAdmin(browser);
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
}
|
2026-03-07 08:18:23 +01:00
|
|
|
|
} finally {
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
await browser?.close();
|
|
|
|
|
|
await stopDevServer();
|
|
|
|
|
|
restoreManagedFiles(originals);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await optimizePngs();
|
|
|
|
|
|
|
|
|
|
|
|
if (failedSections.length > 0) {
|
|
|
|
|
|
console.log(`\n⚠️ Incomplete sections: ${failedSections.join(', ')}`);
|
2026-03-07 08:18:23 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`\n🎉 Done! Screenshots saved to ${OUTPUT_DIR}/`);
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
for (const file of fs
|
|
|
|
|
|
.readdirSync(OUTPUT_DIR)
|
|
|
|
|
|
.filter((f) => f.endsWith('.png'))
|
|
|
|
|
|
.sort()) {
|
|
|
|
|
|
const kb = Math.round(fs.statSync(path.join(OUTPUT_DIR, file)).size / 1024);
|
|
|
|
|
|
console.log(` ${file} (${kb} KB)`);
|
|
|
|
|
|
}
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Re-encode the PNGs losslessly. Playwright optimises for capture speed, not
|
|
|
|
|
|
* file size — a plain re-encode takes a full-page photo grid from ~2 MB to
|
|
|
|
|
|
* ~700 KB with identical pixels. sharp ships with Next.js but is not a declared
|
|
|
|
|
|
* dependency here, so a missing install just skips this step.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function optimizePngs(): Promise<void> {
|
|
|
|
|
|
let sharp: typeof import('sharp');
|
|
|
|
|
|
try {
|
|
|
|
|
|
sharp = (await import('sharp')).default;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
console.log('\n⏭ sharp not available — screenshots left as captured.');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('\n🗜 Optimising PNGs...');
|
|
|
|
|
|
let before = 0;
|
|
|
|
|
|
let after = 0;
|
|
|
|
|
|
for (const file of fs.readdirSync(OUTPUT_DIR).filter((f) => f.endsWith('.png'))) {
|
|
|
|
|
|
const filePath = path.join(OUTPUT_DIR, file);
|
|
|
|
|
|
before += fs.statSync(filePath).size;
|
|
|
|
|
|
const optimised = await sharp(filePath).png({ compressionLevel: 9, effort: 10 }).toBuffer();
|
|
|
|
|
|
// Never grow a file — some captures are already minimal.
|
|
|
|
|
|
if (optimised.length < fs.statSync(filePath).size) fs.writeFileSync(filePath, optimised);
|
|
|
|
|
|
after += fs.statSync(filePath).size;
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log(` ${Math.round(before / 1024)} KB → ${Math.round(after / 1024)} KB`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Capture helpers ──────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
let currentSection = '';
|
|
|
|
|
|
/** Sections that threw, reported again at the end of the run. */
|
|
|
|
|
|
const failedSections: string[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
function section(title: string): void {
|
|
|
|
|
|
currentSection = title;
|
|
|
|
|
|
console.log(`\n📷 ${title}`);
|
|
|
|
|
|
console.log('─'.repeat(50));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Run a capture block in a fresh browser context, so localStorage (light mode)
|
|
|
|
|
|
* and cookies (admin session) never leak into the next block.
|
|
|
|
|
|
*
|
|
|
|
|
|
* A block is retried once — a wedged dev server is replaced in between — and
|
|
|
|
|
|
* then reported and skipped rather than aborting the run: a full pass takes many
|
|
|
|
|
|
* minutes, and losing the remaining thirty screenshots because one page
|
|
|
|
|
|
* misbehaved is worse than a gap in the output. Re-running a block simply
|
|
|
|
|
|
* overwrites the screenshots it had already taken.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function withPage(
|
|
|
|
|
|
browser: Browser,
|
|
|
|
|
|
viewport: { width: number; height: number },
|
|
|
|
|
|
fn: (page: Page) => Promise<void>,
|
|
|
|
|
|
initScript?: () => void,
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
|
|
|
|
await ensureServerHealthy();
|
|
|
|
|
|
const context = await browser.newContext({ viewport });
|
|
|
|
|
|
// The dev server compiles routes on first hit, which can exceed Playwright's
|
|
|
|
|
|
// 30s default right after a settings change.
|
|
|
|
|
|
context.setDefaultNavigationTimeout(90000);
|
|
|
|
|
|
context.setDefaultTimeout(30000);
|
|
|
|
|
|
if (initScript) await context.addInitScript(initScript);
|
|
|
|
|
|
const page = await context.newPage();
|
|
|
|
|
|
try {
|
|
|
|
|
|
await fn(page);
|
|
|
|
|
|
return;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
const reason = String(err).split('\n')[0];
|
|
|
|
|
|
if (attempt === 1) {
|
|
|
|
|
|
console.error(` ↻ ${currentSection} failed (${reason}) — retrying`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
failedSections.push(currentSection);
|
|
|
|
|
|
console.error(` ⚠️ ${currentSection} failed: ${reason}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
await context.close();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function captureHome(page: Page, name: string, urlPath = '/'): Promise<void> {
|
|
|
|
|
|
await page.goto(`${BASE_URL}${urlPath}`, { waitUntil: 'networkidle' });
|
|
|
|
|
|
await revealDeferredContent(page);
|
|
|
|
|
|
await waitForImages(page);
|
|
|
|
|
|
await capture(page, name);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function captureGrid(page: Page, urlPath: string, name: string): Promise<void> {
|
|
|
|
|
|
await capturePage(page, urlPath, name, 3000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Load a page, reveal everything that fades or lazy-loads in, then shoot it. */
|
|
|
|
|
|
async function capturePage(
|
|
|
|
|
|
page: Page,
|
|
|
|
|
|
urlPath: string,
|
|
|
|
|
|
name: string,
|
|
|
|
|
|
settleMs: number,
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
await page.goto(`${BASE_URL}${urlPath}`, { waitUntil: 'networkidle' });
|
|
|
|
|
|
await revealDeferredContent(page);
|
|
|
|
|
|
await waitForImages(page);
|
|
|
|
|
|
// Extra settle time for late-loading images in lower columns
|
|
|
|
|
|
await page.waitForTimeout(settleMs);
|
|
|
|
|
|
await capture(page, name);
|
|
|
|
|
|
}
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
|
2026-03-08 16:42:32 +01:00
|
|
|
|
/**
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
* Open the first still image of an album in the lightbox and expand the EXIF
|
|
|
|
|
|
* panel. Videos are skipped — their tile opens a player, not a photo.
|
2026-03-08 16:42:32 +01:00
|
|
|
|
*/
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
async function captureLightbox(page: Page, gridPath: string): Promise<void> {
|
|
|
|
|
|
await page.goto(`${BASE_URL}${gridPath}`, { waitUntil: 'networkidle' });
|
|
|
|
|
|
await revealDeferredContent(page);
|
|
|
|
|
|
await waitForImages(page);
|
|
|
|
|
|
|
|
|
|
|
|
const tiles = page.locator('.photo-grid__item');
|
|
|
|
|
|
const count = await tiles.count();
|
|
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
|
|
|
|
const tile = tiles.nth(i);
|
|
|
|
|
|
if ((await tile.locator('.photo-grid__item-play').count()) > 0) continue;
|
|
|
|
|
|
await tile.click();
|
|
|
|
|
|
break;
|
2026-03-08 16:42:32 +01:00
|
|
|
|
}
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
|
|
|
|
|
|
await page.keyboard.press('i');
|
|
|
|
|
|
await page.locator('#exif-panel').waitFor({ state: 'visible', timeout: 5000 });
|
|
|
|
|
|
await waitForImages(page);
|
|
|
|
|
|
await page.waitForTimeout(1500);
|
|
|
|
|
|
await capture(page, 'lightbox-exif');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Admin panel: login screen, page builder, album picker and settings editor.
|
|
|
|
|
|
* Skipped with a warning when ADMIN_PASSWORD is unset — the panel is optional
|
|
|
|
|
|
* and a missing password should not fail the whole run.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function captureAdmin(browser: Browser): Promise<void> {
|
|
|
|
|
|
section('Admin panel');
|
|
|
|
|
|
if (!ADMIN_PASSWORD) {
|
|
|
|
|
|
console.log(' ⏭ ADMIN_PASSWORD not set — skipping the admin screenshots.');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await withPage(browser, VIEWPORT, async (page) => {
|
|
|
|
|
|
await page.goto(`${BASE_URL}/admin`, { waitUntil: 'networkidle' });
|
|
|
|
|
|
await page.locator('.admin-login-card').waitFor({ timeout: 15000 });
|
|
|
|
|
|
await capture(page, 'admin-login');
|
|
|
|
|
|
|
|
|
|
|
|
await page.locator('.admin-login-card input[type="password"]').fill(ADMIN_PASSWORD);
|
|
|
|
|
|
await page.locator('.admin-login-card button[type="submit"]').click();
|
|
|
|
|
|
await page.locator('.admin-tabs').waitFor({ timeout: 20000 });
|
|
|
|
|
|
await page.waitForTimeout(2500);
|
|
|
|
|
|
await waitForImages(page);
|
|
|
|
|
|
await capture(page, 'admin-page-builder');
|
|
|
|
|
|
|
|
|
|
|
|
await captureAlbumPicker(page);
|
|
|
|
|
|
|
|
|
|
|
|
// Reload rather than trusting Escape to have closed the picker — a modal
|
|
|
|
|
|
// still on screen swallows the click on the settings tab.
|
|
|
|
|
|
await page.reload({ waitUntil: 'networkidle' });
|
|
|
|
|
|
await page.locator('.admin-tabs').waitFor({ timeout: 20000 });
|
|
|
|
|
|
await page.locator('.admin-tab', { hasText: 'Settings' }).first().click();
|
|
|
|
|
|
await page.waitForTimeout(1500);
|
|
|
|
|
|
await capture(page, 'admin-settings');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Best effort: the picker is a modal behind a button whose label may change. */
|
|
|
|
|
|
async function captureAlbumPicker(page: Page): Promise<void> {
|
|
|
|
|
|
const trigger = page.getByRole('button', { name: /add album/i }).first();
|
|
|
|
|
|
if ((await trigger.count()) === 0) {
|
|
|
|
|
|
console.log(' ⏭ No "Add album" button found — skipping the album picker shot.');
|
|
|
|
|
|
return;
|
2026-03-08 16:42:32 +01:00
|
|
|
|
}
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
try {
|
|
|
|
|
|
await trigger.click();
|
|
|
|
|
|
await page.locator('input[placeholder*="Search" i]').first().waitFor({ timeout: 10000 });
|
|
|
|
|
|
await page.waitForTimeout(2000);
|
|
|
|
|
|
await waitForImages(page);
|
|
|
|
|
|
await capture(page, 'admin-album-picker');
|
|
|
|
|
|
await page.keyboard.press('Escape');
|
|
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
console.log(' ⏭ Album picker did not open in time — skipping that shot.');
|
2026-03-08 16:42:32 +01:00
|
|
|
|
}
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function capture(page: Page, name: string): Promise<void> {
|
|
|
|
|
|
await hideDevOverlays(page);
|
|
|
|
|
|
await page.screenshot({ path: path.join(OUTPUT_DIR, `${name}.png`), fullPage: false });
|
|
|
|
|
|
console.log(` ✅ ${name}.png`);
|
2026-03-08 16:42:32 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
* Reveal content that only appears on scroll or interaction: fade-in wrappers
|
|
|
|
|
|
* stay transparent until their observer fires, and lazy images never load
|
|
|
|
|
|
* below the fold. Both would otherwise be captured empty.
|
2026-03-08 16:42:32 +01:00
|
|
|
|
*/
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
async function revealDeferredContent(page: Page): Promise<void> {
|
|
|
|
|
|
await page.evaluate(() => {
|
|
|
|
|
|
document.querySelectorAll('.fade-in').forEach((el) => el.classList.add('fade-in--visible'));
|
|
|
|
|
|
document.querySelectorAll('img[loading="lazy"]').forEach((img) => {
|
|
|
|
|
|
(img as HTMLImageElement).loading = 'eager';
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Next.js' dev indicator and the in-app dev toolbar are not part of the product. */
|
|
|
|
|
|
async function hideDevOverlays(page: Page): Promise<void> {
|
|
|
|
|
|
await page.evaluate(() => {
|
|
|
|
|
|
document.querySelectorAll('nextjs-portal').forEach((el) => el.remove());
|
|
|
|
|
|
document.querySelectorAll('[aria-label="Dev Toolbar"]').forEach((el) => el.remove());
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Environment ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Minimal .env.local reader. Next loads the file for the dev server it spawns,
|
|
|
|
|
|
* but this process needs ADMIN_PASSWORD too, and real environment variables
|
|
|
|
|
|
* must keep winning over the file.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function loadEnvLocal(): void {
|
|
|
|
|
|
const envFile = path.join(process.cwd(), '.env.local');
|
|
|
|
|
|
if (!fs.existsSync(envFile)) return;
|
|
|
|
|
|
|
|
|
|
|
|
for (const line of fs.readFileSync(envFile, 'utf8').split('\n')) {
|
|
|
|
|
|
const match = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
|
|
|
|
|
|
if (!match || line.trimStart().startsWith('#')) continue;
|
|
|
|
|
|
const [, key, rawValue] = match;
|
|
|
|
|
|
if (process.env[key] !== undefined) continue;
|
|
|
|
|
|
process.env[key] = rawValue.trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Content rewriting ────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Parse settings.yaml, merge the demo identity plus `overrides` (dotted keys)
|
|
|
|
|
|
* and dump it back. Comments are lost in the temporary file, which is fine —
|
|
|
|
|
|
* the original is restored verbatim from its backup, never re-serialised.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function settingsWith(original: string, overrides: Record<string, unknown>): string {
|
|
|
|
|
|
const doc = (yaml.load(original) ?? {}) as Record<string, unknown>;
|
|
|
|
|
|
for (const [key, value] of Object.entries({ ...DEMO_SETTINGS, ...overrides })) {
|
|
|
|
|
|
setPath(doc, key, value);
|
|
|
|
|
|
}
|
|
|
|
|
|
return yaml.dump(doc, { lineWidth: 0 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setPath(target: Record<string, unknown>, dottedKey: string, value: unknown): void {
|
|
|
|
|
|
const keys = dottedKey.split('.');
|
|
|
|
|
|
let node = target;
|
|
|
|
|
|
for (const key of keys.slice(0, -1)) {
|
|
|
|
|
|
if (typeof node[key] !== 'object' || node[key] === null) node[key] = {};
|
|
|
|
|
|
node = node[key] as Record<string, unknown>;
|
|
|
|
|
|
}
|
|
|
|
|
|
node[keys[keys.length - 1]] = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Replace every `password:` value in gallery.yaml with a throwaway one. */
|
|
|
|
|
|
function withRedactedPasswords(original: string): string {
|
|
|
|
|
|
const doc = yaml.load(original);
|
|
|
|
|
|
const walk = (node: unknown): void => {
|
|
|
|
|
|
if (Array.isArray(node)) return node.forEach(walk);
|
|
|
|
|
|
if (typeof node !== 'object' || node === null) return;
|
|
|
|
|
|
const record = node as Record<string, unknown>;
|
|
|
|
|
|
for (const [key, value] of Object.entries(record)) {
|
|
|
|
|
|
if (key === 'password' && typeof value === 'string') record[key] = 'demo';
|
|
|
|
|
|
else walk(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
walk(doc);
|
|
|
|
|
|
return yaml.dump(doc, { lineWidth: 0 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* A demo about page. The portrait is taken from the gallery's hero images so the
|
|
|
|
|
|
* layout is shown with a real photo without publishing the operator's face, and
|
|
|
|
|
|
* without hardcoding an asset UUID into the repository.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function demoAboutMarkdown(galleryYaml: string): string {
|
|
|
|
|
|
const gallery = (yaml.load(galleryYaml) ?? {}) as { hero?: unknown };
|
|
|
|
|
|
const hero = Array.isArray(gallery.hero) ? gallery.hero : [];
|
|
|
|
|
|
const portrait = typeof hero[0] === 'string' ? hero[0] : '';
|
|
|
|
|
|
|
|
|
|
|
|
const frontmatter = [
|
|
|
|
|
|
portrait ? `portrait: ${portrait}` : null,
|
|
|
|
|
|
'name: Alex Rivera',
|
|
|
|
|
|
'location: Kyoto, Japan',
|
|
|
|
|
|
'gear:',
|
|
|
|
|
|
' - Leica Q3',
|
|
|
|
|
|
' - Summilux 35mm f/1.4',
|
|
|
|
|
|
' - Summicron 50mm f/2',
|
|
|
|
|
|
].filter((line): line is string => line !== null);
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
'---',
|
|
|
|
|
|
...frontmatter,
|
|
|
|
|
|
'---',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'Photographer and traveller.',
|
|
|
|
|
|
'Obsessed with light, geometry, and the quiet in-between.',
|
|
|
|
|
|
'',
|
|
|
|
|
|
].join('\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Page discovery ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Find an album detail page by walking the running site.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Album slugs are derived from Immich album names, so no path can be hardcoded
|
|
|
|
|
|
* without tying the script to one installation. Start at the homepage, follow
|
|
|
|
|
|
* internal links, and take the first page that renders a photo grid — either a
|
|
|
|
|
|
* standalone album, or an album reached through a subpage's cover grid.
|
|
|
|
|
|
* Password-gated pages are skipped.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function discoverGridPath(page: Page): Promise<string> {
|
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
|
|
|
|
|
|
|
const internalLinks = async (selector: string): Promise<string[]> =>
|
|
|
|
|
|
(
|
|
|
|
|
|
await page.$$eval(selector, (els) =>
|
|
|
|
|
|
els.map((el) => el.getAttribute('href')).filter((h): h is string => !!h),
|
|
|
|
|
|
)
|
|
|
|
|
|
).filter((href) => href.startsWith('/') && !EXCLUDED_PATHS.has(href) && !seen.has(href));
|
|
|
|
|
|
|
|
|
|
|
|
/** A photo grid means we arrived at an album; a password field means we cannot. */
|
|
|
|
|
|
const pageKind = async (): Promise<'grid' | 'gated' | 'other'> => {
|
|
|
|
|
|
if ((await page.locator('input[type="password"]').count()) > 0) return 'gated';
|
|
|
|
|
|
if ((await page.locator('.photo-grid, .essay').count()) > 0) return 'grid';
|
|
|
|
|
|
return 'other';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
await page.goto(BASE_URL, { waitUntil: 'networkidle' });
|
|
|
|
|
|
const entryPoints = await internalLinks('a[href^="/"]');
|
|
|
|
|
|
|
|
|
|
|
|
for (const entry of entryPoints) {
|
|
|
|
|
|
seen.add(entry);
|
|
|
|
|
|
await page.goto(`${BASE_URL}${entry}`, { waitUntil: 'networkidle' });
|
|
|
|
|
|
|
|
|
|
|
|
const kind = await pageKind();
|
|
|
|
|
|
if (kind === 'gated') continue;
|
|
|
|
|
|
if (kind === 'grid') return entry;
|
|
|
|
|
|
|
|
|
|
|
|
// A subpage listing — descend into its album covers.
|
|
|
|
|
|
for (const cover of await internalLinks('.subpage-grid__item, .album-card')) {
|
|
|
|
|
|
seen.add(cover);
|
|
|
|
|
|
await page.goto(`${BASE_URL}${cover}`, { waitUntil: 'networkidle' });
|
|
|
|
|
|
if ((await pageKind()) === 'grid') return cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
'Could not find a public album page to screenshot. Every reachable page was ' +
|
|
|
|
|
|
'password-protected or empty. Point the script at a specific album with ' +
|
|
|
|
|
|
'SCREENSHOT_GRID_PATH=/my-subpage/my-album npx tsx scripts/screenshots.ts',
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* The collection overview belonging to the album, i.e. `/japan` for
|
|
|
|
|
|
* `/japan/koyasan-2023`. Standalone albums have no parent, so fall back to the
|
|
|
|
|
|
* first navigation entry that renders a subpage grid; null if there is none.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function resolveSubpagePath(page: Page, gridPath: string): Promise<string | null> {
|
|
|
|
|
|
const segments = gridPath.split('/').filter(Boolean);
|
|
|
|
|
|
if (segments.length > 1) return `/${segments[0]}`;
|
|
|
|
|
|
|
|
|
|
|
|
const links = await page.$$eval('nav a[href^="/"]', (els) =>
|
|
|
|
|
|
els.map((el) => el.getAttribute('href')).filter((h): h is string => !!h),
|
|
|
|
|
|
);
|
|
|
|
|
|
for (const href of links) {
|
|
|
|
|
|
if (EXCLUDED_PATHS.has(href) || href === gridPath) continue;
|
|
|
|
|
|
await page.goto(`${BASE_URL}${href}`, { waitUntil: 'networkidle' });
|
|
|
|
|
|
if ((await page.locator('.subpage-grid').count()) > 0) return href;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Content restore ──────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function restoreManagedFiles(originals: Map<string, string>): void {
|
|
|
|
|
|
for (const [file, content] of originals) {
|
|
|
|
|
|
fs.writeFileSync(file, content);
|
|
|
|
|
|
fs.rmSync(backupPath(file), { force: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log('\n✅ Restored original content files');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Restore the content files when the process is killed. Without this, Ctrl+C in
|
|
|
|
|
|
* the middle of a run leaves the site on whatever theme was being captured —
|
|
|
|
|
|
* which matters because these files are the live configuration.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function installRestoreHandlers(): void {
|
|
|
|
|
|
let restored = false;
|
|
|
|
|
|
const restore = () => {
|
|
|
|
|
|
if (restored) return;
|
|
|
|
|
|
restored = true;
|
|
|
|
|
|
for (const file of MANAGED_FILES) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!fs.existsSync(backupPath(file))) continue;
|
|
|
|
|
|
fs.writeFileSync(file, fs.readFileSync(backupPath(file), 'utf8'));
|
|
|
|
|
|
fs.rmSync(backupPath(file), { force: true });
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
console.error(
|
|
|
|
|
|
`\n⚠️ Could not restore ${file} automatically. Copy it back from ${backupPath(file)}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log('\n✅ Restored original content files');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'] as const) {
|
|
|
|
|
|
process.on(signal, () => {
|
|
|
|
|
|
restore();
|
|
|
|
|
|
process.exit(130);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Server lifecycle ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* One dev server for the whole run. The YAML loader caches by mtime and the
|
|
|
|
|
|
* pages are force-dynamic, so a rewritten settings.yaml is picked up by the next
|
|
|
|
|
|
* request — restarting the server per theme would only cost startup time.
|
|
|
|
|
|
*
|
|
|
|
|
|
* It does not always survive a full pass though: after a few dozen recompiles
|
|
|
|
|
|
* the dev server occasionally stops answering, which used to turn into a run of
|
|
|
|
|
|
* navigation timeouts. ensureServerHealthy() replaces it when that happens.
|
|
|
|
|
|
*/
|
|
|
|
|
|
let devServer: ChildProcess | undefined;
|
|
|
|
|
|
|
|
|
|
|
|
async function ensureServerHealthy(): Promise<void> {
|
|
|
|
|
|
if (devServer && (await serverResponds())) return;
|
|
|
|
|
|
if (devServer) {
|
|
|
|
|
|
console.log(' ♻️ Dev server stopped responding — restarting it');
|
|
|
|
|
|
await stopDevServer();
|
|
|
|
|
|
}
|
|
|
|
|
|
devServer = startDevServer();
|
|
|
|
|
|
await waitForServer(BASE_URL, 90000);
|
|
|
|
|
|
console.log(' ✅ Server ready');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function serverResponds(): Promise<boolean> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(`${BASE_URL}/api/health`, { signal: AbortSignal.timeout(20000) });
|
|
|
|
|
|
return res.ok;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return false;
|
2026-03-07 08:18:23 +01:00
|
|
|
|
}
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function startDevServer(): ChildProcess {
|
2026-03-07 08:18:23 +01:00
|
|
|
|
const server = spawn('npm', ['run', 'dev'], {
|
|
|
|
|
|
cwd: process.cwd(),
|
|
|
|
|
|
stdio: 'pipe',
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
env: { ...process.env, PORT },
|
2026-03-08 16:42:32 +01:00
|
|
|
|
detached: true,
|
2026-03-07 08:18:23 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Suppress server output unless DEBUG is set
|
|
|
|
|
|
if (process.env.DEBUG) {
|
|
|
|
|
|
server.stdout?.pipe(process.stdout);
|
|
|
|
|
|
server.stderr?.pipe(process.stderr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return server;
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
async function stopDevServer(): Promise<void> {
|
|
|
|
|
|
const server = devServer;
|
|
|
|
|
|
devServer = undefined;
|
|
|
|
|
|
if (!server) return;
|
|
|
|
|
|
if (server.pid) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
process.kill(-server.pid, 'SIGTERM');
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// Already gone
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
await new Promise((resolve) => {
|
|
|
|
|
|
const timeout = setTimeout(resolve, 3000);
|
|
|
|
|
|
server.on('close', () => {
|
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
|
resolve(null);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
console.log('🛑 Server stopped');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
async function waitForServer(url: string, timeoutMs: number): Promise<void> {
|
2026-03-07 08:18:23 +01:00
|
|
|
|
const start = Date.now();
|
|
|
|
|
|
while (Date.now() - start < timeoutMs) {
|
|
|
|
|
|
try {
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
// /api/health does not touch Immich, so it answers as soon as the server
|
|
|
|
|
|
// is up rather than waiting on the first album fetch.
|
|
|
|
|
|
const res = await fetch(`${url}/api/health`, { signal: AbortSignal.timeout(10000) });
|
2026-03-07 08:18:23 +01:00
|
|
|
|
if (res.ok) return;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// Server not ready yet
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
}
|
2026-03-07 08:18:23 +01:00
|
|
|
|
await sleep(500);
|
|
|
|
|
|
}
|
|
|
|
|
|
throw new Error(`Server did not start within ${timeoutMs}ms`);
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Wait for all <img> elements to finish loading their full-resolution images.
|
|
|
|
|
|
* This prevents capturing blurhash placeholders instead of real photos.
|
|
|
|
|
|
*/
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
async function waitForImages(page: Page): Promise<void> {
|
2026-03-07 08:18:23 +01:00
|
|
|
|
await page.waitForTimeout(1000); // Initial settle
|
|
|
|
|
|
|
|
|
|
|
|
await page.evaluate((timeout) => {
|
|
|
|
|
|
return new Promise<void>((resolve) => {
|
|
|
|
|
|
const images = Array.from(document.querySelectorAll('img'));
|
|
|
|
|
|
let pending = images.filter((img) => !img.complete).length;
|
|
|
|
|
|
|
|
|
|
|
|
if (pending === 0) {
|
|
|
|
|
|
// All images already loaded, wait a bit for any CSS transitions
|
|
|
|
|
|
setTimeout(resolve, 500);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
// Every callback below is inline and anonymous on purpose: tsx compiles
|
|
|
|
|
|
// this file with esbuild's keepNames, which rewrites *named* functions
|
|
|
|
|
|
// into `__name(fn, '…')` — and that helper does not exist in the page.
|
2026-03-07 08:18:23 +01:00
|
|
|
|
images.forEach((img) => {
|
|
|
|
|
|
if (!img.complete) {
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
img.addEventListener('load', () => {
|
|
|
|
|
|
if (--pending <= 0) setTimeout(resolve, 500);
|
|
|
|
|
|
});
|
|
|
|
|
|
img.addEventListener('error', () => {
|
|
|
|
|
|
if (--pending <= 0) setTimeout(resolve, 500);
|
|
|
|
|
|
});
|
2026-03-07 08:18:23 +01:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Safety timeout
|
|
|
|
|
|
setTimeout(resolve, timeout);
|
|
|
|
|
|
});
|
|
|
|
|
|
}, IMAGE_LOAD_TIMEOUT);
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function sleep(ms: number): Promise<void> {
|
2026-03-07 08:18:23 +01:00
|
|
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Run ──────────────────────────────────────────────────────────
|
|
|
|
|
|
main().catch((err) => {
|
2026-03-07 08:18:23 +01:00
|
|
|
|
console.error('❌ Screenshot generation failed:', err);
|
chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed.
See CHANGELOG.md for the full list and the health-check upgrade note.
2026-08-10 09:56:20 +02:00
|
|
|
|
// The finally block normally restores; this covers a throw before it is reached.
|
|
|
|
|
|
for (const file of MANAGED_FILES) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!fs.existsSync(backupPath(file))) continue;
|
|
|
|
|
|
fs.writeFileSync(file, fs.readFileSync(backupPath(file), 'utf8'));
|
|
|
|
|
|
fs.rmSync(backupPath(file), { force: true });
|
|
|
|
|
|
console.log(`✅ Restored ${path.basename(file)}`);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
console.error(`⚠️ Restore failed — copy ${file} back from ${backupPath(file)}`);
|
|
|
|
|
|
}
|
2026-03-07 08:18:23 +01:00
|
|
|
|
}
|
|
|
|
|
|
process.exit(1);
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
});
|