The root layout replaced the entire app with the SetupScreen whenever
the config was incomplete (missing gallery.yaml or Immich credentials)
— including /admin, which lives under the same root layout. That locked
users out of the very tool that fixes an incomplete configuration.
The middleware now forwards the request pathname via an x-pathname
header, and the root layout skips the setup screen for admin paths, so
/admin renders normally (login when ADMIN_PASSWORD is set, the
enable-hint otherwise) while every other route keeps showing the setup
instructions.
Fixes#326
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix: honour gallery.yaml order for homepage albums
getStandaloneAlbums() filtered the Immich API album list but kept the
API's own (creation/update) order, so reordering the albums list in
gallery.yaml or the admin panel had no effect on the homepage. Sort by
the config list's index instead.
Fixes#352
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: serve fresh settings after admin save without restart
getConfig() short-circuited on a per-worker in-memory cache in
production. Admin saves invalidate that cache only in the worker that
handles the API request; when Next.js renders pages in a different
worker/process, its copy stayed stale until a container restart.
Drop the in-memory config cache entirely and rebuild the config per
call. Freshness comes from the existing mtime-checked YAML cache in
loadYaml() — one statSync per file, negligible next to Immich API
calls, and the same behaviour dev mode has always had.
Fixes#351
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Seven security issues from an audit, plus album pages and the map coming
up empty against Immich 3.x. Verified against a real Immich 3.0.3
instance in a production build.
Security:
- Admin session forgery via a hardcoded 'dev-fallback-secret' signing key
that bypassed the production AUTH_SECRET guard
- /api/map ignored album passwords and returned working asset tokens;
rewriting ?size=thumbnail to ?size=original downloaded full-resolution
originals from protected albums
- Admin login had no rate limit, and the client IP came from a spoofable
header, so rotating it bypassed every limit in the app
- Raw Immich asset UUIDs were serialized into the RSC payload
- Album tokens never expired server-side; a captured token replayed forever
- Path traversal into the Immich API via an unvalidated album id
- /admin ran with no enforced CSP (only Report-Only reached it)
TRUSTED_PROXIES is replaced by TRUSTED_PROXY_HOPS. The old variable
matched proxy IPs against the socket peer address, which self-hosted
Next.js never exposes, so the check could never pass: unset, the limiter
read a spoofable header; set, every request shared one bucket and a single
client could rate-limit the whole site. Migrates automatically.
Immich 3.x: GET /albums/:id no longer embeds assets. Assets now come from
POST /search/metadata with withExif: true — without that flag EXIF is
omitted and grid aspect ratios, the lightbox panel and map markers stay
empty. Drops support for Immich below 3.0, which now logs a warning.
Upgrade: requires Immich 3.0+; set TRUSTED_PROXY_HOPS (nginx = 1);
visitors of protected areas and admin sessions must log in once more.
91 unit tests (25 new); the new admin-auth and rate-limit cases were
confirmed to fail against the pre-fix code.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(admin): implement visual theme cards, album search, and system status diagnostics
* fix(admin): resolve type-checking build errors in SettingsEditor and status route
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
* fix: align env vars and config paths in CI and contributing docs
* feat: add automatic docker publishing to ghcr workflow
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
💡 What: Added a "Skip to content" link that is visually hidden by default but appears when focused via keyboard. Pointed it to the main content area using `id="main-content"` and `tabIndex={-1}`.
🎯 Why: Keyboard users and screen readers must otherwise tab through the entire navigation menu on every page load before reaching the actual page content. This is tedious and a poor UX.
📸 Before/After: Before, keyboard focus started on "Home" in the header. After, the first tab press shows the "Skip to content" link at the top left of the screen.
♿ Accessibility: Massively improves WCAG compliance (Bypass Blocks) and keyboard navigation efficiency.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Introduced a request-scoped `Map` (`authCache`) in `app/api/map/route.ts` to memoize `isAuthenticated` calls.
The `getMapData` loop iterates over all albums across all locations. Albums within the same subpage slug trigger repeated `isAuthenticated` checks, which execute expensive HMAC cryptographic operations, causing performance degradation for large datasets.
Eliminates redundant cryptographic operations during map data fetching, reducing CPU utilization and response times for the Map API.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Sanitized the Content-Type header on image and video proxy routes (`/api/image/*`, `/api/video/*`). The content type is forced to lowercase, validated against a safe prefix (`image/`, `video/`, `audio/`), and restricted to block `svg` and `xml`. Any invalid type falls back to `application/octet-stream`. This prevents an attacker from uploading an SVG file containing a malicious script and triggering a Stored XSS attack via these proxy endpoints, which are excluded from the global CSP.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Add revalidatePath('/', 'layout') calls to gallery save, settings save,
and reload routes. This tells Next.js to purge any cached page responses
so the next browser refresh picks up the new hero images and config.
- Asset Picker: Visual browser for hero images with Favorites tab (shown first)
and All Photos tab. Includes infinite scroll and UUID fallback input for
assets not easily found in the grid.
- Drag & Drop: All lists (hero images, standalone albums, subpage albums,
subpage tiles) are now reorderable via drag handles using @dnd-kit.
- Preview Links: 'Preview Site' button in save bar, '↗ Site' in header, and
per-subpage preview links with URL slug preview shown inline.
- Keyboard Shortcuts: ⌘+S / Ctrl+S to save in both Pages and Settings tabs.
- Unsaved Changes Guard: beforeunload warning prevents accidental data loss.
- New API route /api/admin/assets for browsing Immich assets with pagination
and favorites filter via search/metadata endpoint.
When saving gallery/settings via admin, the config cache was invalidated
but the Immich album cache (which stores resolved title overrides and
descriptions) was not cleared. This meant changes only appeared after
the TTL expired or a manual 'Reload' click.
Now both gallery and settings saves also call immich.invalidateAll()
to flush the album cache, making changes visible on next page load.
The container was running as 'nextjs' (UID 1001) but bind-mounted
./content from the host has host-user ownership, making it unwritable.
Solution: start as root, fix /app/content ownership in entrypoint,
then drop privileges to nextjs via su-exec before running the app.
- Remove :ro and network: host from base docker-compose.yml
- Add docker-compose.override.yml.example with network/healthcheck template
- Add docker-compose.override.yml to .gitignore
This allows git pull without conflicts on the server. Server-specific
settings (external networks, healthchecks) go in the override file.
The content volume was mounted as :ro (read-only) which prevented
the admin panel from writing gallery.yaml and settings.yaml changes.
Removed the :ro flag so the admin panel can save config and create backups.
- Subpages shown as compact visual tiles in a grid (like folders)
- Each tile shows the first album's cover image + name + album count
- Click a tile to expand/edit that subpage's details below
- Active tile highlighted with accent border
- Only one subpage expanded at a time — no more endless scrolling
- Move arrows changed to ← → (horizontal) matching the grid layout
- Smooth slide-down animation when expanding a subpage
- Hero images shown as landscape tiles (16:10 aspect ratio) in a grid
- Full cover images instead of tiny thumbnails with UUID text
- Remove button appears on hover (red X, top-right corner)
- Slide number badge shown bottom-left for ordering reference
- Much more visual and recognizable at a glance
Replace the list-style album cards with a visual grid layout:
- Albums shown as tiles with square cover images (aspect-ratio: 1)
- Grid auto-fills with min 160px columns
- Edit/remove buttons appear as overlay on hover
- Album name and photo count shown below the cover
- Much more compact — multiple albums visible without scrolling
a) Admin panel now respects the site's light/dark theme toggle via CSS
custom properties that respond to [data-theme='light'] on <html>.
All hardcoded dark colors replaced with semantic CSS variables.
b) Improved contrast in dark mode: secondary text changed from #666/#555
to #a0a0a0, muted text from #555 to #707070. All text is now clearly
readable against dark backgrounds.
c) Backup system was already implemented in yaml-service.ts (creates
timestamped backups in content/.backups/ before each save, keeps last
10 versions). Now the UI shows a confirmation message that a backup
was created after each successful save.
- Add /api/admin/thumbnail/[id] proxy endpoint for serving Immich thumbnails
- Show album cover thumbnails in AlbumCard components (subpages + standalone)
- Show actual image thumbnails in AlbumPicker modal instead of emoji placeholders
- Show small thumbnail previews for hero image entries
- Add corresponding CSS styles for all thumbnail elements
The admin panel is a static page with client-side React that gets
prerendered at build time. The nonce-based CSP from middleware doesn't
work for static pages because the nonce is generated per-request but
the HTML is served from cache without it. Exclude /admin from the
middleware matcher so its scripts can load normally.
- Lightbox renders <video> with controls for video assets
- PhotoGrid shows video indicator overlay on video thumbnails
- Skip video preloading in lightbox (streams on demand)
- Add video-related CSS styles
- Update catch-all page to pass video URLs to components
- Add /api/video/[id] route with Range header support for seeking
- Add /api/webhook route for Immich event-driven cache invalidation
- Add streamVideo() to ImmichClient with 206 Partial Content support
- Support album description overrides in gallery.yaml
- Add videoUrl() helper to lib/urls.ts
- Export cache.delete() and cache.clear() methods
- Add docs/admin-panel.md with full guide (setup, features, security, Docker)
- Update README with admin panel feature listing and setup instructions
- Update gallery-config.md with tip about visual alternative
- Update CLAUDE.md with admin architecture and API routes
- Add /admin route with password-protected visual page builder
- Page Builder: manage hero images, standalone albums, subpages, and sections
- Album Picker: browse all shared Immich albums with search and filtering
- Settings Editor: theme, grid, footer, legal, SEO configuration
- YAML service with atomic writes and automatic backups (content/.backups/)
- HMAC-signed admin sessions (24h expiry, HttpOnly cookies)
- API endpoints: /api/admin/{auth,gallery,settings,albums,reload}
- Enable with ADMIN_PASSWORD env var; disabled by default
- invalidateConfigCache() export for hot-reload after saves
The property was removed from NextRequest types in Next.js 15+ but is
still populated by hosting platforms (Vercel, Netlify, etc.).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>