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
|
|
|
|
|
|
*
|
release: v0.11.0 (#459)
Ships `dev` as **v0.11.0**. `main` contains nothing that `dev` does not,
so this is a clean sync.
## Headline changes since v0.10.0
- **Journal & Journal Studio** — photo essays at `/journal`,
split-screen block editor with live preview, drafts, per-entry
passwords, cover images
- **First-run setup wizard at `/install`** — configure a fresh
deployment from the browser; credentials are verified against Immich
before anything is written, and the wizard is gated by a one-time token
- **About page editor and toggle** in the admin panel
- **Per-album photo order** independent of Immich, with a drag & drop
editor
- **Custom favicon upload**
- **Experimental portfolio features** — justified grid, `cover` hero,
unlisted subpages, per-album grid overrides, cover focal points,
external nav links
- **Security** — asset tokens length-capped before decoding (#423),
journal author markdown escaped rather than filtered, journal file paths
contained, admin session key derived with scrypt (#432), Next.js 16.3.0
for CVE-2025-13465
- **Fixed** — client proofing honours its own configuration again
(#454); journal photo rendering (#432)
Full detail in [CHANGELOG.md](CHANGELOG.md#0110--2026-08-15).
## Verified
`npx tsc --noEmit` clean · `npx vitest run` 435 tests pass · `npm run
build` succeeds · lint 0 errors · `prettier --check` clean · local
`docker build` of the release tree.
## After merge
Tag `v0.11.0` on `main` and publish the release — that triggers
`docker-publish.yml`, which pushes the image and moves `latest`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:09:18 +02:00
|
|
|
|
* Captures every screenshot referenced by README.md, docs/theming.md and
|
|
|
|
|
|
* docs/journal.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
|
|
|
|
|
|
* and the journal.
|
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
|
release: v0.11.0 (#459)
Ships `dev` as **v0.11.0**. `main` contains nothing that `dev` does not,
so this is a clean sync.
## Headline changes since v0.10.0
- **Journal & Journal Studio** — photo essays at `/journal`,
split-screen block editor with live preview, drafts, per-entry
passwords, cover images
- **First-run setup wizard at `/install`** — configure a fresh
deployment from the browser; credentials are verified against Immich
before anything is written, and the wizard is gated by a one-time token
- **About page editor and toggle** in the admin panel
- **Per-album photo order** independent of Immich, with a drag & drop
editor
- **Custom favicon upload**
- **Experimental portfolio features** — justified grid, `cover` hero,
unlisted subpages, per-album grid overrides, cover focal points,
external nav links
- **Security** — asset tokens length-capped before decoding (#423),
journal author markdown escaped rather than filtered, journal file paths
contained, admin session key derived with scrypt (#432), Next.js 16.3.0
for CVE-2025-13465
- **Fixed** — client proofing honours its own configuration again
(#454); journal photo rendering (#432)
Full detail in [CHANGELOG.md](CHANGELOG.md#0110--2026-08-15).
## Verified
`npx tsc --noEmit` clean · `npx vitest run` 435 tests pass · `npm run
build` succeeds · lint 0 errors · `prettier --check` clean · local
`docker build` of the release tree.
## After merge
Tag `v0.11.0` on `main` and publish the release — that triggers
`docker-publish.yml`, which pushes the image and moves `latest`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:09:18 +02:00
|
|
|
|
* SCREENSHOT_ONLY=journal SCREENSHOT_JOURNAL_ALBUM="Yufuin" npx tsx scripts/screenshots.ts
|
|
|
|
|
|
*
|
|
|
|
|
|
* The journal section writes a throwaway entry built from a real album's
|
|
|
|
|
|
* photos, shoots the public index, the entry and the studio, then deletes it.
|
|
|
|
|
|
* SCREENSHOT_JOURNAL_ALBUM picks the album by name; without it the largest one
|
|
|
|
|
|
* is used — which is usually an import dump rather than a presentable set, so
|
|
|
|
|
|
* set it whenever the output is going to be committed. Whatever it names ends
|
|
|
|
|
|
* up in a published screenshot at full size: pick an album you would show a
|
|
|
|
|
|
* stranger, not one that happens to be large.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Three further entries are written from albums matched by name, so the index
|
|
|
|
|
|
* shot shows a list rather than one lonely card. One of them is a draft and
|
|
|
|
|
|
* one carries a password, so the admin list shows the states the guide
|
|
|
|
|
|
* describes. They are skipped on an installation without those albums — see
|
|
|
|
|
|
* EXTRA_JOURNAL_ENTRIES.
|
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 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';
|
2026-10-02 18:14:23 +02:00
|
|
|
|
import { PRESET_IDS } from '../lib/config/presets';
|
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 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();
|
|
|
|
|
|
|
2026-10-02 18:14:23 +02:00
|
|
|
|
const THEMES = [...PRESET_IDS];
|
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 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`;
|
|
|
|
|
|
|
release: v0.11.0 (#459)
Ships `dev` as **v0.11.0**. `main` contains nothing that `dev` does not,
so this is a clean sync.
## Headline changes since v0.10.0
- **Journal & Journal Studio** — photo essays at `/journal`,
split-screen block editor with live preview, drafts, per-entry
passwords, cover images
- **First-run setup wizard at `/install`** — configure a fresh
deployment from the browser; credentials are verified against Immich
before anything is written, and the wizard is gated by a one-time token
- **About page editor and toggle** in the admin panel
- **Per-album photo order** independent of Immich, with a drag & drop
editor
- **Custom favicon upload**
- **Experimental portfolio features** — justified grid, `cover` hero,
unlisted subpages, per-album grid overrides, cover focal points,
external nav links
- **Security** — asset tokens length-capped before decoding (#423),
journal author markdown escaped rather than filtered, journal file paths
contained, admin session key derived with scrypt (#432), Next.js 16.3.0
for CVE-2025-13465
- **Fixed** — client proofing honours its own configuration again
(#454); journal photo rendering (#432)
Full detail in [CHANGELOG.md](CHANGELOG.md#0110--2026-08-15).
## Verified
`npx tsc --noEmit` clean · `npx vitest run` 435 tests pass · `npm run
build` succeeds · lint 0 errors · `prettier --check` clean · local
`docker build` of the release tree.
## After merge
Tag `v0.11.0` on `main` and publish the release — that triggers
`docker-publish.yml`, which pushes the image and moves `latest`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:09:18 +02:00
|
|
|
|
const JOURNAL_DIR = path.join(CONTENT_DIR, 'journal');
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Slug of the throwaway entry written for the journal screenshots. It is
|
|
|
|
|
|
* created before the shots and deleted afterwards, so a run never leaves a
|
|
|
|
|
|
* demo story published on a real site. Namespaced to make an orphan from a
|
|
|
|
|
|
* killed run obvious.
|
|
|
|
|
|
*/
|
|
|
|
|
|
const DEMO_JOURNAL_SLUG = 'screenshot-demo-entry';
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Album to pull demo photos from, matched against the Immich album name.
|
|
|
|
|
|
* Unset picks the album with the most photos, which reliably gives the entry
|
|
|
|
|
|
* enough frames to fill a fullbleed, a pair and a contained block — but the
|
|
|
|
|
|
* largest album on a real library is typically an unsorted import, so a run
|
|
|
|
|
|
* whose output gets committed should always name one. The photos are published
|
|
|
|
|
|
* at full size in the screenshots; nothing about them is anonymised, unlike the
|
|
|
|
|
|
* site identity in DEMO_SETTINGS.
|
|
|
|
|
|
*/
|
|
|
|
|
|
const JOURNAL_ALBUM = process.env.SCREENSHOT_JOURNAL_ALBUM;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Companion entries, so the index shot shows a list of stories rather than a
|
|
|
|
|
|
* single card on an otherwise empty page. Unlike the main entry — whose album
|
|
|
|
|
|
* is chosen at run time and whose prose is therefore deliberately about
|
|
|
|
|
|
* photographing rather than about a place — each of these is bound to one
|
|
|
|
|
|
* album and its text describes that place, which only works while the two are
|
|
|
|
|
|
* kept together. An installation without a matching album simply skips them
|
|
|
|
|
|
* and the index shows the main entry alone.
|
|
|
|
|
|
*/
|
|
|
|
|
|
const EXTRA_JOURNAL_ENTRIES: {
|
|
|
|
|
|
slug: string;
|
|
|
|
|
|
/** Matched against the Immich album name, case-insensitive substring. */
|
|
|
|
|
|
album: string;
|
|
|
|
|
|
markdown: (ids: string[]) => string;
|
|
|
|
|
|
}[] = [
|
|
|
|
|
|
{ slug: 'screenshot-demo-kurokawa', album: 'kurokawa', markdown: kurokawaJournalMarkdown },
|
|
|
|
|
|
{ slug: 'screenshot-demo-kada', album: 'kada', markdown: kadaJournalMarkdown },
|
|
|
|
|
|
{ slug: 'screenshot-demo-koyasan', album: 'koyasan', markdown: koyasanJournalMarkdown },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
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
|
|
|
|
/** 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
|
|
|
|
}
|
release: v0.11.0 (#459)
Ships `dev` as **v0.11.0**. `main` contains nothing that `dev` does not,
so this is a clean sync.
## Headline changes since v0.10.0
- **Journal & Journal Studio** — photo essays at `/journal`,
split-screen block editor with live preview, drafts, per-entry
passwords, cover images
- **First-run setup wizard at `/install`** — configure a fresh
deployment from the browser; credentials are verified against Immich
before anything is written, and the wizard is gated by a one-time token
- **About page editor and toggle** in the admin panel
- **Per-album photo order** independent of Immich, with a drag & drop
editor
- **Custom favicon upload**
- **Experimental portfolio features** — justified grid, `cover` hero,
unlisted subpages, per-album grid overrides, cover focal points,
external nav links
- **Security** — asset tokens length-capped before decoding (#423),
journal author markdown escaped rather than filtered, journal file paths
contained, admin session key derived with scrypt (#432), Next.js 16.3.0
for CVE-2025-13465
- **Fixed** — client proofing honours its own configuration again
(#454); journal photo rendering (#432)
Full detail in [CHANGELOG.md](CHANGELOG.md#0110--2026-08-15).
## Verified
`npx tsc --noEmit` clean · `npx vitest run` 435 tests pass · `npm run
build` succeeds · lint 0 errors · `prettier --check` clean · local
`docker build` of the release tree.
## After merge
Tag `v0.11.0` on `main` and publish the release — that triggers
`docker-publish.yml`, which pushes the image and moves `latest`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:09:18 +02:00
|
|
|
|
|
|
|
|
|
|
// ── Journal (public views + studio) ──
|
|
|
|
|
|
if (wanted('journal')) {
|
|
|
|
|
|
await applySettings({ 'theme.preset': SHOWCASE_THEME });
|
|
|
|
|
|
await captureJournal(browser);
|
|
|
|
|
|
}
|
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> {
|
docs(contributing): tell contributors to target dev (#412)
* fix(screenshots): sharp-Typen fuer 0.34 und 0.35 gleichzeitig (#410)
sharp 0.35 ist von `export =` auf ESM umgestellt: der Modul-Namespace
ist nicht mehr aufrufbar, die Factory sitzt unter .default. Der
Type-Check brach damit in scripts/screenshots.ts weg, sobald der
Next-Bump (#402) sharp von 0.34.5 auf 0.35.3 mitzieht -- sharp ist hier
keine deklarierte Dependency, sondern kommt ueber Next mit.
Die Form wird jetzt aus dem installierten Paket abgeleitet statt
festgeschrieben, zur Laufzeit wie im Typ. Geprueft mit beiden
Versionen: tsc laeuft mit 0.34.5 und mit 0.35.3 fehlerfrei durch.
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore(deps): bump the nextjs group across 1 directory with 2 updates (#402)
Bumps the nextjs group with 2 updates in the / directory: [next](https://github.com/vercel/next.js) and [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next).
Updates `next` from 16.2.12 to 16.3.0
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.2.12...v16.3.0)
Updates `eslint-config-next` from 16.2.12 to 16.3.0
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.3.0/packages/eslint-config-next)
---
updated-dependencies:
- dependency-name: eslint-config-next
dependency-version: 16.3.0
dependency-type: direct:development
update-type: version-update:semver-minor
dependency-group: nextjs
- dependency-name: next
dependency-version: 16.3.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nextjs
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps-dev): bump prettier from 3.8.3 to 3.9.6 (#405)
Bumps [prettier](https://github.com/prettier/prettier) from 3.8.3 to 3.9.6.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.3...3.9.6)
---
updated-dependencies:
- dependency-name: prettier
dependency-version: 3.9.6
dependency-type: direct:development
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* docs(contributing): PRs gehen gegen dev
Nirgends stand, welcher Branch das Ziel ist -- die offene PR #398 zeigt
folgerichtig auf main. main bewegt sich aber nur ueber Releases: es wird
von dev fast-forwarded, getaggt, und der Tag stoesst den Image-Publish
an. Eine PR dagegen legt unveroeffentlichten Code in genau den Branch,
der dem publizierten Image entsprechen soll.
Steht jetzt als eigener Abschnitt in CONTRIBUTING.md, im Setup-Snippet
und in beiden Checklisten. Mit Hinweis, dass ein falsches Ziel ueber
"Edit" an der PR umgestellt werden kann, statt die Arbeit neu zu machen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 10:41:01 +02:00
|
|
|
|
// sharp 0.35 switched from `export =` to ESM: the module namespace stopped
|
|
|
|
|
|
// being callable and the factory moved to `.default`. Resolve whichever
|
|
|
|
|
|
// shape the installed version has, so this type-checks and runs on both.
|
|
|
|
|
|
type SharpFactory = typeof import('sharp') extends { default: infer Factory }
|
|
|
|
|
|
? Factory
|
|
|
|
|
|
: typeof import('sharp');
|
|
|
|
|
|
let sharp: SharpFactory;
|
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 {
|
docs(contributing): tell contributors to target dev (#412)
* fix(screenshots): sharp-Typen fuer 0.34 und 0.35 gleichzeitig (#410)
sharp 0.35 ist von `export =` auf ESM umgestellt: der Modul-Namespace
ist nicht mehr aufrufbar, die Factory sitzt unter .default. Der
Type-Check brach damit in scripts/screenshots.ts weg, sobald der
Next-Bump (#402) sharp von 0.34.5 auf 0.35.3 mitzieht -- sharp ist hier
keine deklarierte Dependency, sondern kommt ueber Next mit.
Die Form wird jetzt aus dem installierten Paket abgeleitet statt
festgeschrieben, zur Laufzeit wie im Typ. Geprueft mit beiden
Versionen: tsc laeuft mit 0.34.5 und mit 0.35.3 fehlerfrei durch.
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore(deps): bump the nextjs group across 1 directory with 2 updates (#402)
Bumps the nextjs group with 2 updates in the / directory: [next](https://github.com/vercel/next.js) and [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next).
Updates `next` from 16.2.12 to 16.3.0
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.2.12...v16.3.0)
Updates `eslint-config-next` from 16.2.12 to 16.3.0
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.3.0/packages/eslint-config-next)
---
updated-dependencies:
- dependency-name: eslint-config-next
dependency-version: 16.3.0
dependency-type: direct:development
update-type: version-update:semver-minor
dependency-group: nextjs
- dependency-name: next
dependency-version: 16.3.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: nextjs
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps-dev): bump prettier from 3.8.3 to 3.9.6 (#405)
Bumps [prettier](https://github.com/prettier/prettier) from 3.8.3 to 3.9.6.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.3...3.9.6)
---
updated-dependencies:
- dependency-name: prettier
dependency-version: 3.9.6
dependency-type: direct:development
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* docs(contributing): PRs gehen gegen dev
Nirgends stand, welcher Branch das Ziel ist -- die offene PR #398 zeigt
folgerichtig auf main. main bewegt sich aber nur ueber Releases: es wird
von dev fast-forwarded, getaggt, und der Tag stoesst den Image-Publish
an. Eine PR dagegen legt unveroeffentlichten Code in genau den Branch,
der dem publizierten Image entsprechen soll.
Steht jetzt als eigener Abschnitt in CONTRIBUTING.md, im Setup-Snippet
und in beiden Checklisten. Mit Hinweis, dass ein falsches Ziel ueber
"Edit" an der PR umgestellt werden kann, statt die Arbeit neu zu machen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 10:41:01 +02:00
|
|
|
|
const mod = (await import('sharp')) as unknown as { default?: SharpFactory };
|
|
|
|
|
|
sharp = mod.default ?? (mod as unknown as SharpFactory);
|
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
|
|
|
|
} 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');
|
|
|
|
|
|
|
2026-09-29 22:50:30 +02:00
|
|
|
|
// Login lands on the overview (#712). It is not captured: its "needs
|
|
|
|
|
|
// attention" list is the live doctor, which names albums, IDs and secrets'
|
|
|
|
|
|
// lengths — nothing for a public screenshot.
|
release: v0.11.0 (#459)
Ships `dev` as **v0.11.0**. `main` contains nothing that `dev` does not,
so this is a clean sync.
## Headline changes since v0.10.0
- **Journal & Journal Studio** — photo essays at `/journal`,
split-screen block editor with live preview, drafts, per-entry
passwords, cover images
- **First-run setup wizard at `/install`** — configure a fresh
deployment from the browser; credentials are verified against Immich
before anything is written, and the wizard is gated by a one-time token
- **About page editor and toggle** in the admin panel
- **Per-album photo order** independent of Immich, with a drag & drop
editor
- **Custom favicon upload**
- **Experimental portfolio features** — justified grid, `cover` hero,
unlisted subpages, per-album grid overrides, cover focal points,
external nav links
- **Security** — asset tokens length-capped before decoding (#423),
journal author markdown escaped rather than filtered, journal file paths
contained, admin session key derived with scrypt (#432), Next.js 16.3.0
for CVE-2025-13465
- **Fixed** — client proofing honours its own configuration again
(#454); journal photo rendering (#432)
Full detail in [CHANGELOG.md](CHANGELOG.md#0110--2026-08-15).
## Verified
`npx tsc --noEmit` clean · `npx vitest run` 435 tests pass · `npm run
build` succeeds · lint 0 errors · `prettier --check` clean · local
`docker build` of the release tree.
## After merge
Tag `v0.11.0` on `main` and publish the release — that triggers
`docker-publish.yml`, which pushes the image and moves `latest`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:09:18 +02:00
|
|
|
|
await adminLogin(page);
|
2026-09-29 22:50:30 +02:00
|
|
|
|
|
|
|
|
|
|
// The page builder is a structure list and a panel (#717): open the first
|
|
|
|
|
|
// subpage, so the panel shows a page with its albums rather than the
|
|
|
|
|
|
// empty state.
|
|
|
|
|
|
await page.goto(`${BASE_URL}/admin/pages`, { waitUntil: 'networkidle' });
|
|
|
|
|
|
await page.locator('.pb-tree').waitFor({ timeout: 20000 });
|
|
|
|
|
|
const firstSubpage = page.locator('.pb-tree .pb-group').nth(1).locator('.pb-row-main').first();
|
|
|
|
|
|
if ((await firstSubpage.count()) > 0) await firstSubpage.click();
|
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.waitForTimeout(2500);
|
|
|
|
|
|
await waitForImages(page);
|
|
|
|
|
|
await capture(page, 'admin-page-builder');
|
|
|
|
|
|
|
|
|
|
|
|
await captureAlbumPicker(page);
|
|
|
|
|
|
|
2026-09-29 22:50:30 +02:00
|
|
|
|
await capturePage(page, '/admin/settings/general', 'admin-settings', 1500);
|
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
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
release: v0.11.0 (#459)
Ships `dev` as **v0.11.0**. `main` contains nothing that `dev` does not,
so this is a clean sync.
## Headline changes since v0.10.0
- **Journal & Journal Studio** — photo essays at `/journal`,
split-screen block editor with live preview, drafts, per-entry
passwords, cover images
- **First-run setup wizard at `/install`** — configure a fresh
deployment from the browser; credentials are verified against Immich
before anything is written, and the wizard is gated by a one-time token
- **About page editor and toggle** in the admin panel
- **Per-album photo order** independent of Immich, with a drag & drop
editor
- **Custom favicon upload**
- **Experimental portfolio features** — justified grid, `cover` hero,
unlisted subpages, per-album grid overrides, cover focal points,
external nav links
- **Security** — asset tokens length-capped before decoding (#423),
journal author markdown escaped rather than filtered, journal file paths
contained, admin session key derived with scrypt (#432), Next.js 16.3.0
for CVE-2025-13465
- **Fixed** — client proofing honours its own configuration again
(#454); journal photo rendering (#432)
Full detail in [CHANGELOG.md](CHANGELOG.md#0110--2026-08-15).
## Verified
`npx tsc --noEmit` clean · `npx vitest run` 435 tests pass · `npm run
build` succeeds · lint 0 errors · `prettier --check` clean · local
`docker build` of the release tree.
## After merge
Tag `v0.11.0` on `main` and publish the release — that triggers
`docker-publish.yml`, which pushes the image and moves `latest`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:09:18 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Journal: the public index and entry, plus the studio behind them.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Needs an admin session for two separate reasons — the studio shots are
|
|
|
|
|
|
* behind the login, and the demo entry can only be written with *raw* Immich
|
|
|
|
|
|
* asset UUIDs, which the public site never exposes (it hands out encrypted
|
|
|
|
|
|
* tokens). The admin API is the only place those UUIDs are reachable, so the
|
|
|
|
|
|
* entry is generated from a real album after logging in.
|
|
|
|
|
|
*
|
|
|
|
|
|
* The entry is deleted again in the `finally`, including when a capture throws.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function captureJournal(browser: Browser): Promise<void> {
|
|
|
|
|
|
section('Journal: index, entry, studio');
|
|
|
|
|
|
if (!ADMIN_PASSWORD) {
|
|
|
|
|
|
console.log(' ⏭ ADMIN_PASSWORD not set — skipping the journal screenshots.');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const entryPath = path.join(JOURNAL_DIR, `${DEMO_JOURNAL_SLUG}.md`);
|
|
|
|
|
|
if (fs.existsSync(entryPath)) {
|
|
|
|
|
|
console.log(` ⏭ ${DEMO_JOURNAL_SLUG}.md already exists — leaving it alone.`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const written: string[] = [];
|
|
|
|
|
|
try {
|
|
|
|
|
|
await withPage(browser, VIEWPORT, async (page) => {
|
|
|
|
|
|
await adminLogin(page);
|
|
|
|
|
|
|
|
|
|
|
|
const assetIds = await demoAssetIds(page);
|
|
|
|
|
|
if (assetIds.length < 4) {
|
|
|
|
|
|
console.log(' ⏭ Not enough photos found for a demo entry — skipping.');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fs.mkdirSync(JOURNAL_DIR, { recursive: true });
|
|
|
|
|
|
fs.writeFileSync(entryPath, demoJournalMarkdown(assetIds));
|
|
|
|
|
|
written.push(entryPath);
|
|
|
|
|
|
await writeExtraEntries(page, written);
|
|
|
|
|
|
await sleep(CONFIG_RELOAD_DELAY);
|
|
|
|
|
|
|
|
|
|
|
|
// Studio first: the session is already open on this page.
|
|
|
|
|
|
await capturePage(page, '/admin/journal', 'admin-journal-list', 1500);
|
|
|
|
|
|
await capturePage(page, `/admin/journal/${DEMO_JOURNAL_SLUG}`, 'admin-journal-studio', 3000);
|
|
|
|
|
|
await captureStudioModals(page);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Public views in a fresh context, so the admin cookie cannot leak in and
|
|
|
|
|
|
// reveal draft-only chrome in a shot meant to show what a visitor sees.
|
|
|
|
|
|
await withPage(browser, VIEWPORT, async (page) => {
|
|
|
|
|
|
await capturePage(page, '/journal', 'journal-index', 2500);
|
|
|
|
|
|
await captureJournalEntry(page);
|
|
|
|
|
|
});
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
for (const file of written) {
|
|
|
|
|
|
if (fs.existsSync(file)) {
|
|
|
|
|
|
fs.rmSync(file, { force: true });
|
|
|
|
|
|
console.log(` 🧹 Removed the demo entry (${path.basename(file)})`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* The frontmatter form, which the studio hides behind its Story Settings
|
|
|
|
|
|
* button and which no other shot reaches. Best effort: a renamed button costs
|
|
|
|
|
|
* this one shot, not the run.
|
|
|
|
|
|
*
|
|
|
|
|
|
* The studio's other dialog, the photo picker, is deliberately not captured.
|
|
|
|
|
|
* Its tabs are Favorites and All Photos — both show the operator's own library
|
|
|
|
|
|
* rather than a chosen album, and these screenshots are published.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function captureStudioModals(page: Page): Promise<void> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await page
|
|
|
|
|
|
.getByRole('button', { name: /story settings/i })
|
|
|
|
|
|
.first()
|
|
|
|
|
|
.click();
|
|
|
|
|
|
await page.locator('.journal-modal-card').waitFor({ timeout: 5000 });
|
|
|
|
|
|
await waitForImages(page);
|
|
|
|
|
|
await page.waitForTimeout(800);
|
|
|
|
|
|
await capture(page, 'admin-journal-settings');
|
|
|
|
|
|
await page
|
|
|
|
|
|
.getByRole('button', { name: /^done$/i })
|
|
|
|
|
|
.first()
|
|
|
|
|
|
.click();
|
|
|
|
|
|
await page.locator('.journal-modal-card').waitFor({ state: 'hidden', timeout: 5000 });
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
console.log(' ⏭ Story Settings dialog did not open — skipping that shot.');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* The album-bound companion entries. Each is skipped without failing the run
|
|
|
|
|
|
* when its album is missing (another installation) or when a file with that
|
|
|
|
|
|
* slug already exists (an author's own entry is never overwritten).
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function writeExtraEntries(page: Page, written: string[]): Promise<void> {
|
|
|
|
|
|
for (const extra of EXTRA_JOURNAL_ENTRIES) {
|
|
|
|
|
|
const file = path.join(JOURNAL_DIR, `${extra.slug}.md`);
|
|
|
|
|
|
if (fs.existsSync(file)) {
|
|
|
|
|
|
console.log(` ⏭ ${extra.slug}.md already exists — leaving it alone.`);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
const photos = await demoAlbumPhotos(page, extra.album);
|
|
|
|
|
|
if (!photos || photos.ids.length < 4) continue;
|
|
|
|
|
|
const picked = spreadSample(photos.ids, 4);
|
|
|
|
|
|
if (photos.coverId) picked[0] = photos.coverId;
|
|
|
|
|
|
fs.writeFileSync(file, extra.markdown(picked));
|
|
|
|
|
|
written.push(file);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* The rendered entry, scrolled to the quote so the shot shows the block types
|
|
|
|
|
|
* the guide describes. The top of the page is only a title and a cover image,
|
|
|
|
|
|
* which says nothing about the journal that the index shot does not already
|
|
|
|
|
|
* show; the quote sits between the prose and a photo block, so framing it puts
|
|
|
|
|
|
* three of the block types in one viewport.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function captureJournalEntry(page: Page): Promise<void> {
|
|
|
|
|
|
await page.goto(`${BASE_URL}/journal/${DEMO_JOURNAL_SLUG}`, { waitUntil: 'networkidle' });
|
|
|
|
|
|
await revealDeferredContent(page);
|
|
|
|
|
|
await waitForImages(page);
|
|
|
|
|
|
const quote = page.locator('.essay-quote').first();
|
|
|
|
|
|
if ((await quote.count()) > 0) {
|
|
|
|
|
|
await quote.scrollIntoViewIfNeeded();
|
|
|
|
|
|
// Lift the quote to the upper third, so the photo block below it is in frame.
|
|
|
|
|
|
await page.evaluate(() => window.scrollBy(0, -180));
|
|
|
|
|
|
await waitForImages(page);
|
|
|
|
|
|
}
|
|
|
|
|
|
await page.waitForTimeout(3000);
|
|
|
|
|
|
await capture(page, 'journal-entry');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Log in and wait for the panel chrome. Shared by the admin and journal runs. */
|
|
|
|
|
|
async function adminLogin(page: Page): Promise<void> {
|
|
|
|
|
|
await page.goto(`${BASE_URL}/admin`, { waitUntil: 'networkidle' });
|
|
|
|
|
|
await page.locator('.admin-login-card input[type="password"]').fill(ADMIN_PASSWORD!);
|
|
|
|
|
|
await page.locator('.admin-login-card button[type="submit"]').click();
|
2026-09-29 22:50:30 +02:00
|
|
|
|
await page.locator('.admin-sidebar').waitFor({ timeout: 20000 });
|
release: v0.11.0 (#459)
Ships `dev` as **v0.11.0**. `main` contains nothing that `dev` does not,
so this is a clean sync.
## Headline changes since v0.10.0
- **Journal & Journal Studio** — photo essays at `/journal`,
split-screen block editor with live preview, drafts, per-entry
passwords, cover images
- **First-run setup wizard at `/install`** — configure a fresh
deployment from the browser; credentials are verified against Immich
before anything is written, and the wizard is gated by a one-time token
- **About page editor and toggle** in the admin panel
- **Per-album photo order** independent of Immich, with a drag & drop
editor
- **Custom favicon upload**
- **Experimental portfolio features** — justified grid, `cover` hero,
unlisted subpages, per-album grid overrides, cover focal points,
external nav links
- **Security** — asset tokens length-capped before decoding (#423),
journal author markdown escaped rather than filtered, journal file paths
contained, admin session key derived with scrypt (#432), Next.js 16.3.0
for CVE-2025-13465
- **Fixed** — client proofing honours its own configuration again
(#454); journal photo rendering (#432)
Full detail in [CHANGELOG.md](CHANGELOG.md#0110--2026-08-15).
## Verified
`npx tsc --noEmit` clean · `npx vitest run` 435 tests pass · `npm run
build` succeeds · lint 0 errors · `prettier --check` clean · local
`docker build` of the release tree.
## After merge
Tag `v0.11.0` on `main` and publish the release — that triggers
`docker-publish.yml`, which pushes the image and moves `latest`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:09:18 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Raw asset UUIDs for the demo entry, via the admin API. `page.request` shares
|
|
|
|
|
|
* the context's cookies, so the session from adminLogin() carries over.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function demoAssetIds(page: Page, albumName = JOURNAL_ALBUM): Promise<string[]> {
|
|
|
|
|
|
return (await demoAlbumPhotos(page, albumName))?.ids ?? [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* The matched album's photos plus the cover its owner picked in Immich, which
|
|
|
|
|
|
* is a better opening image for an entry than whatever happens to be first in
|
|
|
|
|
|
* capture order.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function demoAlbumPhotos(
|
|
|
|
|
|
page: Page,
|
|
|
|
|
|
albumName = JOURNAL_ALBUM,
|
|
|
|
|
|
): Promise<{ ids: string[]; coverId?: string } | null> {
|
|
|
|
|
|
type AdminAlbum = {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
albumName: string;
|
|
|
|
|
|
assetCount: number;
|
|
|
|
|
|
thumbnailAssetId?: string | null;
|
|
|
|
|
|
};
|
|
|
|
|
|
const albumsRes = await page.request.get(`${BASE_URL}/api/admin/albums`);
|
|
|
|
|
|
if (!albumsRes.ok()) return null;
|
|
|
|
|
|
const albums = (await albumsRes.json()) as { albums?: AdminAlbum[] } | AdminAlbum[];
|
|
|
|
|
|
const list = Array.isArray(albums) ? albums : (albums.albums ?? []);
|
|
|
|
|
|
if (list.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const wantedName = albumName?.toLowerCase();
|
|
|
|
|
|
const album = wantedName
|
|
|
|
|
|
? list.find((a) => a.albumName.toLowerCase().includes(wantedName))
|
|
|
|
|
|
: [...list].sort((a, b) => (b.assetCount ?? 0) - (a.assetCount ?? 0))[0];
|
|
|
|
|
|
if (!album) {
|
|
|
|
|
|
console.log(` ⏭ No album matching "${albumName}" — skipping that entry.`);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log(` 🔎 Demo entry photos from: ${album.albumName}`);
|
|
|
|
|
|
|
|
|
|
|
|
const assetsRes = await page.request.get(`${BASE_URL}/api/admin/albums/${album.id}/assets`);
|
|
|
|
|
|
if (!assetsRes.ok()) return null;
|
|
|
|
|
|
const payload = (await assetsRes.json()) as
|
|
|
|
|
|
{ assets?: { id: string; type?: string }[] } | { id: string; type?: string }[];
|
|
|
|
|
|
const assets = Array.isArray(payload) ? payload : (payload.assets ?? []);
|
|
|
|
|
|
// Videos render a player, not a photo block.
|
|
|
|
|
|
const ids = assets.filter((a) => (a.type ?? 'IMAGE').toUpperCase() === 'IMAGE').map((a) => a.id);
|
|
|
|
|
|
return { ids, coverId: album.thumbnailAssetId ?? undefined };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* A demo entry exercising every block type the parser knows — heading,
|
|
|
|
|
|
* paragraph, quote with attribution, fullbleed photo, photo pair, contained
|
|
|
|
|
|
* photo — so the studio and the rendered page both show something
|
|
|
|
|
|
* representative rather than a single paragraph.
|
|
|
|
|
|
*
|
|
|
|
|
|
* The prose is deliberately about *photographing* rather than about any
|
|
|
|
|
|
* particular place. The source album is chosen with SCREENSHOT_JOURNAL_ALBUM
|
|
|
|
|
|
* and could be a coastline, a city or a mountain range; text naming a
|
|
|
|
|
|
* landscape would contradict the photos beside it in whatever album the
|
|
|
|
|
|
* operator picks. Captions are written the same way.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function demoJournalMarkdown(ids: string[]): string {
|
|
|
|
|
|
const [cover, fullbleed, pairA, pairB, ...rest] = ids;
|
|
|
|
|
|
const contained = rest[0];
|
|
|
|
|
|
return `---
|
|
|
|
|
|
title: "Notes from a Week of Photographing"
|
|
|
|
|
|
subtitle: "Seven days, one camera, and what came back from it"
|
|
|
|
|
|
author: "Jane Doe"
|
|
|
|
|
|
date: "2026-03-14"
|
|
|
|
|
|
coverAssetId: "${cover}"
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# Setting Out
|
|
|
|
|
|
|
|
|
|
|
|
Packing for a week means deciding in advance what kind of pictures you intend
|
|
|
|
|
|
to make. One body, two lenses, and the quiet certainty that both choices will
|
|
|
|
|
|
turn out to have been slightly wrong by the second afternoon.
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
## Finding the Light
|
|
|
|
|
|
|
|
|
|
|
|
The early hours were the only reliable ones. By midday the contrast had
|
|
|
|
|
|
collapsed and every exposure came back flatter than it had looked through the
|
|
|
|
|
|
viewfinder — so the middle of each day became the part spent walking, looking,
|
|
|
|
|
|
and not raising the camera at all.
|
|
|
|
|
|
|
|
|
|
|
|
> A photograph does not record what you saw. It records what was there, which
|
|
|
|
|
|
> is a slower and more honest thing. -- A note from the second evening
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
### What Stayed
|
|
|
|
|
|
|
|
|
|
|
|
Not the wide views, in the end, but the repetitions — the same twenty minutes
|
|
|
|
|
|
of light, the same short walk back, the same handful of frames worth keeping
|
|
|
|
|
|
out of a few hundred.
|
|
|
|
|
|
${contained ? `\n\n` : ''}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* `count` photos spread over the album rather than its first few. Albums are
|
|
|
|
|
|
* in capture order, so the opening frames are the journey there — a station,
|
|
|
|
|
|
* a train window — which makes a poor cover for an entry about the place. The
|
|
|
|
|
|
* first fifth is skipped for the same reason.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function spreadSample(ids: string[], count: number): string[] {
|
|
|
|
|
|
const start = Math.floor(ids.length / 5);
|
|
|
|
|
|
const usable = ids.slice(start);
|
|
|
|
|
|
const step = Math.max(1, Math.floor(usable.length / count));
|
|
|
|
|
|
return Array.from({ length: count }, (_, i) => usable[Math.min(i * step, usable.length - 1)]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Companion entry for the Kurokawa Onsen album. Shorter than the main one: it
|
|
|
|
|
|
* exists to fill the index, and only its card is ever screenshot. The prose
|
|
|
|
|
|
* stays at the level of the place and the season, since which frames it gets
|
|
|
|
|
|
* depends on the album's order.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function kurokawaJournalMarkdown(ids: string[]): string {
|
|
|
|
|
|
const [cover, fullbleed, pairA, pairB] = ids;
|
|
|
|
|
|
return `---
|
|
|
|
|
|
title: "Three Days in Kurokawa Onsen"
|
|
|
|
|
|
subtitle: "A village built into a river gorge, photographed between baths"
|
|
|
|
|
|
author: "Jane Doe"
|
|
|
|
|
|
date: "2025-11-22"
|
|
|
|
|
|
coverAssetId: "${cover}"
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# Down to the River
|
|
|
|
|
|
|
|
|
|
|
|
The village sits in the cut of a small river, dark wood and tiled roofs
|
|
|
|
|
|
stacked up both banks, and every path eventually leads back down to the water.
|
|
|
|
|
|
Late November had the maples going over at different speeds — one tree still
|
|
|
|
|
|
green, the next one already halfway to bare.
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
## Between Baths
|
|
|
|
|
|
|
|
|
|
|
|
You buy a wooden pass and it lets you into three of the baths, so the days
|
|
|
|
|
|
arrange themselves around walking from one to the next. Most of the frames here
|
|
|
|
|
|
were made on those walks, in the hour before it got dark and the lanterns came
|
|
|
|
|
|
on along the river.
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Companion entry for the Kada album — see kurokawaJournalMarkdown(). Carries
|
|
|
|
|
|
* a password so the admin list shows the lock marker its documentation
|
|
|
|
|
|
* describes. `demo` is plaintext on purpose: the entry lives for the length of
|
|
|
|
|
|
* one run, and a scrypt hash here would only hide what the shot is meant to
|
|
|
|
|
|
* show. Real entries should store a hash.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function kadaJournalMarkdown(ids: string[]): string {
|
|
|
|
|
|
const [cover, fullbleed, pairA, pairB] = ids;
|
|
|
|
|
|
return `---
|
|
|
|
|
|
title: "Kada, Out of Season"
|
|
|
|
|
|
subtitle: "A fishing harbour on the Wakayama coast, mostly at 200mm"
|
|
|
|
|
|
author: "Jane Doe"
|
|
|
|
|
|
date: "2023-10-19"
|
|
|
|
|
|
password: "demo"
|
|
|
|
|
|
coverAssetId: "${cover}"
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# The Harbour Road
|
|
|
|
|
|
|
|
|
|
|
|
Kada is a working harbour before it is anything else: crates stacked against
|
|
|
|
|
|
the sea wall, rope coiled where it was dropped, boats tied up in the middle of
|
|
|
|
|
|
a weekday. Nothing is arranged for a visitor, which is most of why it is worth
|
|
|
|
|
|
photographing.
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
## At 200mm
|
|
|
|
|
|
|
|
|
|
|
|
A long lens turned out to be the right choice here. It kept the distance
|
|
|
|
|
|
honest — the roof tiles, the bare tree behind them, the cats asleep on warm
|
|
|
|
|
|
concrete all stayed where they were instead of being walked up to.
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Companion entry for the Koyasan album, kept as a draft. The admin list
|
|
|
|
|
|
* documents a Draft pill and the public index documents that drafts are hidden
|
|
|
|
|
|
* from it — neither is visible while every demo entry is published. Being a
|
|
|
|
|
|
* draft it never reaches the public index shot, so it also does not crowd it.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function koyasanJournalMarkdown(ids: string[]): string {
|
|
|
|
|
|
const [cover, fullbleed] = ids;
|
|
|
|
|
|
return `---
|
|
|
|
|
|
title: "Koyasan, First Notes"
|
|
|
|
|
|
subtitle: "Rough cut — still choosing which frames survive"
|
|
|
|
|
|
author: "Jane Doe"
|
|
|
|
|
|
date: "2023-10-08"
|
|
|
|
|
|
draft: true
|
|
|
|
|
|
coverAssetId: "${cover}"
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# Under the Cedars
|
|
|
|
|
|
|
|
|
|
|
|
The old graveyard runs for two kilometres under cedars big enough to make the
|
|
|
|
|
|
path feel indoors, and the stone figures along it are dressed and looked after
|
|
|
|
|
|
by someone. Notes for now — the light changed every few minutes and I want to
|
|
|
|
|
|
see the frames on a proper screen before deciding what this is about.
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
/** Best effort: the picker is a modal behind a button whose label may change. */
|
|
|
|
|
|
async function captureAlbumPicker(page: Page): Promise<void> {
|
2026-09-29 22:50:30 +02:00
|
|
|
|
// "Add album" lives in the standalone-albums panel of the page builder.
|
|
|
|
|
|
const standalone = page.locator('.pb-row-solo').last();
|
|
|
|
|
|
if ((await standalone.count()) > 0) {
|
|
|
|
|
|
await standalone.click();
|
|
|
|
|
|
await page.waitForTimeout(800);
|
|
|
|
|
|
}
|
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 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
|
|
|
|
});
|