| Age | Commit message (Collapse) | Author | Files | Lines |
|
params['entries[]']
Rack::Utils.parse_nested_query maps form fields named 'entries[]' to
params['entries'] (without brackets). The route was reading
params['entries[]'] which was always nil, making entries always empty
and triggering the halt 400 guard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
The overlaid checkbox had pointer-events:none so clicks fell through to
the photo link. Replaced with a real <input type=checkbox> rendered
below each face crop. Checkbox change events drive selection state;
clicking anywhere on the card except the photo link also toggles it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
The bulk selection panel now lives on /admin/people/:uuid — the face
crop grid page — which is what was actually requested. A sticky left
panel shows the cluster name, the name form, a selection counter, and
bulk action controls. Clicking a face crop toggles selection; clicking
the photo link still opens the album. Bulk actions: move selected faces
to a named person, move to pool, or blacklist. The per-face individual
dropdowns are replaced by the panel. Merge-entire-cluster and
Blacklist-cluster moved to collapsible/button in the panel too.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
On /people/:slug, admins see a sticky left panel and selectable photo
tiles. Clicking a tile (or its checkbox overlay) toggles selection;
clicking without modifier still opens the photo in a new tab. The panel
shows the selection count and two actions:
- Reassign to person: moves the selected photos' face entries from the
current person's cluster to the chosen person.
- Move to album: moves the photo files on disk and updates album.json,
faces.json, and people.json rel paths accordingly. Album paths are
offered via a datalist autocomplete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
duplicate names
After moving a face to "New Person", the user is now taken directly to
that cluster's detail page. If it's a single unnamed cluster, the face
is shown prominently at the top. Typing an existing name on the name
form triggers a confirm dialog: OK merges into the existing person's
cluster, Cancel saves as a new separate person with the same name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Removed faces now go to an "Unidentified pool" cluster rather than
disappearing. Deleting a cluster blacklists all its members so they are
skipped by future re-clustering runs. Pool faces can be assigned to a
named person or individually blacklisted. A plain-English info box on
the detail page explains what each action does and that no photo files
are ever modified.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
On the cluster detail page: "Remove face" option in each face's move
dropdown removes it from the cluster entirely; "Delete cluster" button
(red, with confirmation) removes the whole cluster from people.json.
Moving the last face out of a cluster also auto-deletes it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Each cluster in /admin/people now links to a detail page showing all
faces in a grid. From there you can rename the cluster, move individual
faces to another named person (or spin off a new cluster), or merge the
entire cluster into another. Hovering any face crop shows the original
full photo for context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Server-side search index built from all album.json files + people.json,
cached in memory for 5 minutes. Each photo document includes filename,
album path words, title, caption, camera, date parts (year/month-name/
full date), and person names.
- Recursive-descent Boolean parser: AND (explicit or implicit between
consecutive terms), OR, NOT, with standard precedence.
- GET /search?q=... returns a photo grid (max 300 results) linking each
photo back to its album lightbox.
- Search box added to the site header; hidden on mobile.
- Results show filename, date, person names, and album path per photo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- scripts/cluster_faces.py: greedy centroid clustering (numpy) with 3
refinement passes; preserves existing UUID/name mappings across re-runs;
writes MEDIA_ROOT/people.json atomically.
- app.rb: GET /face/* serves cropped+padded face thumbnails (100x100,
cached under cache/faces/); GET|POST /admin/people for cluster
management; POST /admin/people/recluster runs cluster_faces.py as a
background job; POST /admin/people/:uuid saves names+slugs; GET /people
public grid of named people; GET /people/:slug photos for one person.
- views/admin/people.erb: lists all clusters (named first, then by size),
face crop samples, inline name form, re-cluster button with live log.
- views/people.erb: public grid of named people.
- views/person.erb: photo grid for one person, linking back to album
lightbox for each photo.
- views/layout.erb: People link in nav (conditional on FACES_ENABLED).
- public/css/style.css: styles for people admin list and public tiles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Strip all face code from update.rb; add shared log helper writing to
/opt/albumen/log/albumen.log with [update] prefix. update.rb now owns
only album.json; face_daemon.rb owns faces.json.
- New scripts/face_daemon.rb: polls MEDIA_ROOT for unprocessed images,
calls faces.py in batches, writes per-directory faces.json sidecars
atomically. Graceful SIGTERM/SIGINT shutdown between directories.
- New config/face_daemon.service: systemd unit running as albumen user,
Restart=on-failure, logs via SyslogIdentifier=albumen-faces.
- app.rb: add FACES_ENABLED constant; load_faces() helper reads faces.json;
album_files() merges face data into each entry as :faces field.
- Update README.md and DESIGN.md to document the new daemon architecture,
faces.json schema, and service management commands.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- process_dir() takes idx/total args and prints [N/total] prefix on every
Scanning/Skipping line so long runs are easy to monitor via tail -f
- faces_pending?() checks album.json for any image with faces: null when
faces.enabled is true; if found, the sentinel skip is bypassed so those
images get processed even though the directory mtime hasn't changed
- This handles the case where face detection is newly enabled on a library
that was previously indexed without it — no --force needed on subsequent
runs after the initial catch-up
Resume after abort: sentinel is only touched after atomic_write_json, so
an aborted directory reruns cleanly. Already-completed directories skip
normally; partially-detected batches rerun from scratch (safe/idempotent).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- faces.py: use model="cnn" (more accurate, better at angles/small faces/poor
lighting) instead of HOG; model comment explains the trade-off clearly
- faces.py: accept multiple image paths; process with ThreadPoolExecutor
(dlib releases GIL during C++ inference → genuine thread parallelism);
output JSON dict {path: [faces]} for batch calls
- update.rb: batch_detect_faces() collects all unprocessed images per
directory and calls faces.py once per directory rather than once per image,
avoiding repeated model load overhead
- update.rb: FACES_WORKERS read from config.yml faces.workers (default 4;
set to 20 in this install's config.yml on a 64-core Xeon)
- update.rb: process_dir() now takes idx/total and prints [N/total] prefix
on every Scanning/Skipping line for progress monitoring
To monitor a long run:
nohup ruby /opt/albumen/scripts/update.rb > /tmp/faces_update.log 2>&1 &
tail -f /tmp/faces_update.log
Resume/restart is fully safe: sentinel files are only written after
atomic_write_json, so an aborted directory reruns cleanly from scratch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- scripts/faces.py: Python helper using face_recognition (dlib/HOG) to
detect faces and return 128-D encodings as JSON; called by update.rb
- scripts/update.rb: enrich_faces() stores face boxes and encodings in
album.json per image (null = not yet processed, [] = processed/none found);
skips files already processed; gated on faces.enabled in config.yml
- Reads CONFIG_PATH (same env var as app.rb) to check faces.enabled flag
- Feature is off by default; enabled in this install via config.yml
- README.md, DESIGN.md: document installation, opt-in config, data model,
and planned clustering/people-management pipeline
People management UI and clustering script are the next milestone.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Hovering over a thumbnail in the admin file table pops up the full
300×300 cached version near the cursor, making it easy to confirm
identity before deleting or editing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- update.rb: skip exiftool on images marked exif_absent (set after first
failed attempt); prevents repeated slow scans of old photos with no EXIF
- update.rb: explicit directory argument now implies force — passing a path
always rescans that subtree regardless of sentinel mtime
- app.rb: /admin/update no longer hardcodes --force; sentinel-based skipping
is used by default, making UI updates finish in seconds instead of minutes
- admin/album.erb: add "Force rescan all" checkbox to Run Update button;
checked state passes force=1 to the server and restores --force behavior
- README.md, DESIGN.md: document sentinel skipping, exif_absent flag, and
explicit-directory force behavior
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Lightbox: .lb-media now fills the full stage (flex, 100%x100%) and
#lb-img/#lb-video use width/height:100% + object-fit:contain so at
least one axis always reaches the edge. Click-to-close updated to
check event.target===stage instead of stopPropagation.
Slideshow: #ss-img/#ss-video likewise changed from max-width/max-height
to width/height:100% so small or portrait media fills the stage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
update.rb records transcoded_to in album.json (even on re-runs where
the MP4 already exists) so the marker survives across scans.
app.rb filters files with transcoded_to from non-admin views.
album.erb renders them greyed-out with an amber "⚠ original" badge in
admin mode. admin/album.erb marks the edit-table row and shows the
target filename under the original.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
On each run, any .avi, .mkv, or .mov file without a same-named .mp4
sibling is transcoded with ffmpeg (H.264/AAC, CRF 23, faststart).
The original is kept on disk and hidden in album.json so only the
playable MP4 appears in the UI; admins can un-hide the original if
needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Checking Delete and saving permanently removes the file and its
thumbnail; a JS confirm dialog gates the submit. Deleted files are
stripped from params before save_edits so they don't linger in
album.json.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Admin edit form: "Folder name" field renames the directory on save;
also moves the thumbnail cache subtree to match the new path
- Admin edit page: "Run Update" button spawns update.rb in a background
thread, streams output into a terminal-style log panel via 1.5s polling;
shows Done/Error status when complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Root album leaves @og_image_rel nil so layout.erb falls back to
/img/albumen.png; photo-based OG image only applies to sub-albums.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- public/img/albumen.png: app logo
- public/favicon.ico: 32x32 + 16x16 multi-size
- public/apple-touch-icon.png: 180x180 for iOS home screen
- layout.erb: favicon link tags; og:image falls back to albumen.png
when no specific photo is selected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- slideshow_view now uses all_media_entries for non-root dirs so the
slideshow traverses sub-albums instead of only direct files
- album.erb condition simplified: show launcher when albums OR media
entries exist (previously hidden for folders with only sub-albums)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Groups media files in an album directory by EXIF date, reverse-geocodes
the first GPS fix of each day via Nominatim, and moves files into
"Day N - Location" subdirectories. Dry-run by default; pass --go to
execute. Supports a LOCATION_OVERRIDES map for cleaning up Nominatim
names that are overly granular.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Album cards show recursive photo count (bubbles up through sub-albums).
- Lightbox info panel shows camera, aperture, shutter speed, and ISO;
update.rb now extracts and stores these EXIF fields.
- Video thumbnail cards show a duration badge (e.g. "1:23").
- Slideshow launcher redesigned: button on its own line, with Shuffle /
Full screen / Interval options on a second line, all inside a rounded
border to make the grouping clear.
- Fixed album-actions alignment so Interval sits level with the checkboxes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
When not shuffled, reverse SS_ENTRIES so the most recently added
photos play first instead of last.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Interval input (default 5 s) now lives beside Shuffle/Full screen on
the album page; passed as ?interval= param to the slideshow and seeded
into the hidden ss-interval input on load.
- Added "Interval" label text next to the input.
- Fixed slideshow controls being pushed off-screen on mobile by using
100dvh (dynamic viewport height) with 100vh as a fallback, so the
layout accounts for mobile browser chrome.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
When a media file is deleted from disk, also delete its cached
thumbnail from cache/thumbs/ so stale .th.jpg files don't accumulate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
The next timer was starting as soon as ssShow() was called, so preload
time + 500ms crossfade ate into the visible interval. Now the timer
starts only after applyEntry() fires (photo is actually on screen),
guaranteeing each photo gets a full interval of visibility.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Acquire a screen wake lock when the slideshow is playing; release on
pause or page hide; re-acquire when the tab returns to the foreground.
Failures are silently ignored so older browsers degrade gracefully.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Use Page Visibility API to cancel the timer when the tab hides and
reschedule fresh on return, so no queued ticks fire in a burst.
Also fix stale #photo= → ?photo= in stage click-through.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Root slideshow: all_media_entries walks the full media tree so
/slideshow/ shows every photo across all albums; Slideshow button
always appears on the root album page
- Shuffle and Full screen checkboxes sit next to the Slideshow button
on the album page; options pass as ?shuffle=1&fullscreen=1 URL params
- Fullscreen uses a tap-to-activate overlay (browsers block auto-entry
on page load); webkit-prefixed for Safari; ⛶ button and F key for
mid-session toggle
- Fullscreen mode hides controls, counter, caption bar, and site header
- Exiting fullscreen auto-pauses so the current photo stays visible
- Click/tap anywhere in the stage navigates to the photo's album
lightbox; reads the live src attribute instead of ssIdx to avoid a
race where ssIdx advances during the cross-fade while the old photo
is still on screen
- layout.erb excluded from slideshow (layout: false) so the site header
never appears there
- CSS cache-busted with ?v=2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
EXIF DateTimeOriginal has no timezone — it's the camera's wall clock.
Storing it via .iso8601 attached +00:00 (server TZ), causing browsers
to shift the time to their local zone when parsing. Switch to
strftime('%Y-%m-%dT%H:%M:%S') so no offset is written. JS strips any
existing +00:00 suffix from already-stored values so old data is also
displayed correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Tap "ℹ Info" in the caption bar to toggle a semi-transparent panel at
the bottom of the image showing filename (if different from title),
date taken, and pixel dimensions. Panel resets to hidden on each
photo change. Button is hidden automatically when no metadata is
available. Width/height are now included in the ENTRIES payload.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
SVG eye icon sits inside the right edge of the password field.
Tap/click to reveal; tap again to conceal. Helps mobile users
confirm what they're typing without a separate "show password" step.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Typing in the search box instantly hides non-matching album cards.
Shown only when an album has more than 4 sub-albums (no point otherwise).
Pure client-side — no server round-trips.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|