- Add vite-plugin-pwa: generates manifest, Workbox service worker, precaches app shell
- Generate icon-192, icon-512, apple-touch-icon PNGs from existing SVG logo
- Add theme-color, apple-mobile-web-app-capable, apple-touch-icon meta tags; viewport-fit=cover for notch support
- Mobile layout (< 768px): bottom tab bar (Library, Albums, Memories, Map, More) replaces top nav; sidebar becomes slide-in filter drawer with backdrop; bulk action bar clears bottom nav; safe-area insets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Scans MUSIC_DIR (mounted at /music in Docker) with music-metadata to
read embedded BPM/genre tags and classify tracks as upbeat/cinematic/
dreamy. Falls back to untagged general pool when mood bucket is thin.
Slideshow plays real audio via <audio> element with range-request
streaming; Web Audio synth is kept as fallback when MUSIC_DIR is unset.
Shows now-playing title/artist in the slideshow bottom bar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Albums tab with manual album CRUD (create/delete, cover photo grid)
and Smart Albums auto-generated by year, city, and camera model using
SQLite window functions — no extra storage, computed from existing data.
Lightbox gets a download-original button (streams raw file with
Content-Disposition: attachment).
DetailPanel Camera section expands to show ISO, aperture, shutter speed,
focal length, and lens model extracted at ingest time; five new DB columns
added via safe ALTER TABLE migration.
Bulk selection: hover a photo to reveal a circle checkbox; selecting
any photo activates a floating action bar for star/unstar and add-to-album
across the whole selection. PhotoCard wrapped in React.memo to limit
re-renders to only affected cards.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
react-leaflet-cluster uses exports.default instead of module.exports,
so Vite's default import resolves to {default: fn} not fn directly.
Unwrap with ?. default ?? to get the actual component function.
This was never triggered before because the MapContainer only renders
when there are GPS points — previously there were none (GPS bug).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Leaflet CSS inside a lazy component has a load-order race with the map
initialization. Moving it to main.jsx ensures it's always ready.
Also adds a MapErrorBoundary so any Leaflet crash shows an error message
instead of unmounting the whole app (black screen), and filters out any
non-finite GPS coordinates before passing them to the map.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ffprobe stores fractional seconds (e.g. 2.033s for a "2s" clip), so
<= 2 was too strict. Math.floor matches what the badge shows the user.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The pick option creates an allowlist for the merged EXIF output, which
drops latitude/longitude even when gps:true is set. Fixed by calling
exifr.gps() as a separate step (bypasses the pick filter entirely).
Also adds a startup backfill that re-extracts GPS for already-indexed
photos where gps_lat IS NULL, including reverse geocoding for place names.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New /api/memories/today endpoint queries photos by month+day (client
sends its local date to avoid UTC offset issues in Docker)
- MemoriesView shows photos grouped by year with a "Play Memories"
slideshow that crossfades between photos every 5 seconds
- Ambient music generated via Web Audio API — A-minor pad with slow
vibrato shimmer and a pentatonic melody with convolver reverb
- Mute toggle, keyboard nav (← → Esc), progress dots, year badge
- fix: map popup images now use explicit px dimensions (100% was
collapsing to 0 inside Leaflet's unsized popup content box)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace near-invisible grays (#2a2a2a, #333, #3a3a3a, #444, #555) with
readable values (#555–#777) on the dark background.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Auto-discovers libraries via --list-libraries then runs icloudpd once
per library, each into its own subfolder under icloud_dest. Covers
PrimarySync, SharedSync-*, and SharedCollection-* automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use explicit type: volume for the luma_data named volume so compose
managers (CasaOS etc.) don't misinterpret it as a bind mount
- Use type: bind / read_only: true for the photo directory
- Both server and worker now reference /media/Plex/Gallery consistently
- Worker was previously left with the placeholder path, causing it to
watch a non-existent directory and index nothing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Places:
- worker/geocode.js: Nominatim reverse geocoder, 1 req/sec rate limit,
in-memory cache keyed to 3-decimal-place coordinates
- Ingest calls reverseGeocode() for any photo with GPS data; stores
place_city and place_country on the photo row
- DB migrations add place_city / place_country columns + index on place_city
- Stats API returns cities grouped by count
- buildFilter() supports ?city= parameter
- FilterSidebar shows a Places section with city list + country sublabel
Folder browser:
- GET /api/fs/browse?path=… lists subdirectories (path-normalized to prevent traversal)
- Settings panel: 📁 button opens inline directory browser below the add
input — navigate with up arrow / directory clicks, Select fills the input
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The CREATE INDEX on photos(starred) ran inside the main db.exec() block,
which fires before the ALTER TABLE migration that adds the starred column.
On databases created before the starred feature, this crashed the process
on startup with "no such column: starred" — causing the worker container
to exit silently with no logs.
Move the index creation to after the migration so the column always exists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings panel (⚙ in header):
- Watch Folders: add/remove paths, toggle enabled; worker polls every 30s
and calls watcher.add/unwatch without restart
- Ingestion: thumb/preview size and quality, applied per-file at ingest time
- Duplicate Detection: configurable Hamming distance threshold
- Danger Zone: clear all dismissed duplicate pairs
Multi-folder scanning:
- watch_folders table replaces single PHOTO_DIR env var
- PHOTO_DIR env var seeds the first entry on startup
- settings table with defaults for all tunable values
- Worker reads settings from DB on each drain cycle so changes are live
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Load all known filepaths into an in-memory Set on startup (one bulk query).
Chokidar's initial 'add' burst now hits an O(1) Set lookup instead of going
through the serial drain queue with a per-file DB round-trip — a 100k-photo
library restarts in seconds rather than re-processing every file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Left filter sidebar: All Photos / Favorites / Date by Year / Tags / Collections
with counts, See more/less toggles, and blue active-row highlight
- SSE ingest badge in header: pulsing dot + filename + indexed count during scan;
auto-refreshes the library when a scan finishes
- Starred/Favorites: starred column on photos, star button in lightbox toolbar,
Favorites row in sidebar, ?starred=1 filter on /api/photos
- GET /api/library/stats endpoint (totals, years, tags, collections)
- GET /api/ingest/stream SSE endpoint (polls ingest_status table every 1.5s)
- Worker writes ingest_status (active, current file, scanned count) to SQLite
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>