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>
- Adjusted `generateScryptHash` parameter in plaintext fallback logic in `lib/auth.ts` to use `storedPassword` instead of the intermediate hashed value. This prevents users from getting locked out when they adopt the recommended hash.
- Enhanced plaintext fallback verification to hash `password` and `storedPassword` via keyed HMAC before executing `crypto.timingSafeEqual` to avoid potential timing attacks more securely.
Co-authored-by: ralksta <16941050+ralksta@users.noreply.github.com>
Replaced the unbounded `Promise.all` in `getMapData` with a chunked
fetching approach. This prevents Out of Memory crashes by limiting
the concurrent fetching of large arrays of assets to chunks of 10.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Update in-memory rate limiter to fail secure (reject new requests) when maximum capacity is reached, rather than evicting the oldest tracking entry. This prevents an attacker from bypassing their rate limit by flooding the store with spoofed `x-forwarded-for` IPs to intentionally trigger cache eviction. Also cleaned up temporary files and updated tests to verify the behavior.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Destructures `remaining` and `resetAt` from `checkRateLimit` and adds standard rate-limiting headers (`Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`) to the 429 Too Many Requests response.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Upgraded `encodeAssetId` and `decodeAssetId` to use `aes-256-gcm` (Authenticated Encryption) instead of `aes-256-cbc`.
AES-CBC lacks authentication, leaving it potentially susceptible to padding oracle attacks or ciphertext manipulation.
The new token adds a `v2:` prefix which strictly enforces AES-GCM while supporting legacy fallback for cached urls.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>