<feed xmlns='http://www.w3.org/2005/Atom'>
<title>albumen.git/app.rb, branch main</title>
<subtitle>Ruby/Sinatra photo album</subtitle>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/'/>
<entry>
<title>Fix bulk action 400: Rack maps entries[] to params['entries'] not params['entries[]']</title>
<updated>2026-06-12T14:16:12+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-06-12T14:16:12+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=be80a08c6a2a330e81d43fbfd345f05199e75947'/>
<id>be80a08c6a2a330e81d43fbfd345f05199e75947</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Move bulk selection to admin cluster detail page (correct page)</title>
<updated>2026-06-12T14:01:48+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-06-12T14:01:48+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=cfb814470864785565f33e4bebd2aca7e67c16ae'/>
<id>cfb814470864785565f33e4bebd2aca7e67c16ae</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Add bulk photo selection panel to person page (admin)</title>
<updated>2026-06-12T13:57:11+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-06-12T13:57:11+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=b9a3ce6942e917c8e5046d652b7742cfe5f960ec'/>
<id>b9a3ce6942e917c8e5046d652b7742cfe5f960ec</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Fix New Person flow: redirect to new cluster, show hero face, detect duplicate names</title>
<updated>2026-06-09T14:09:19+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-06-09T14:09:19+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=a942b4e83d8c3c71020fdc6ae93954ddfa2ea338'/>
<id>a942b4e83d8c3c71020fdc6ae93954ddfa2ea338</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Add face pool, blacklisting, and action explanations to people admin</title>
<updated>2026-06-09T13:09:23+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-06-09T13:09:23+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=c13a40a970be156a231200c20362636b198d32ec'/>
<id>c13a40a970be156a231200c20362636b198d32ec</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Add face and cluster deletion to people admin</title>
<updated>2026-06-08T22:39:58+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-06-08T22:39:58+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=cf1385bbd6d88a8db9f615512564e150c85a0b5f'/>
<id>cf1385bbd6d88a8db9f615512564e150c85a0b5f</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Add people cluster detail page with face move/merge and hover preview</title>
<updated>2026-06-08T21:09:47+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-06-08T21:09:47+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=7f6325fe213ed46ff5479ffd34b0e212426d48f2'/>
<id>7f6325fe213ed46ff5479ffd34b0e212426d48f2</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Add photo search with Boolean operators</title>
<updated>2026-06-08T19:11:51+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-06-08T19:11:51+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=00f63c03b7c5de68aea6a2305886bc1953a722b6'/>
<id>00f63c03b7c5de68aea6a2305886bc1953a722b6</id>
<content type='text'>
- 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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
- 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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Add people/face clustering feature</title>
<updated>2026-06-08T19:00:02+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-06-08T19:00:02+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=01f52565f460a0107679999588b73b770f01a98c'/>
<id>01f52565f460a0107679999588b73b770f01a98c</id>
<content type='text'>
- 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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
- 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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Separate face detection into standalone daemon</title>
<updated>2026-06-08T18:36:07+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-06-08T18:36:07+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=625b3d5176f2c274e91fcf28bda8e45cc0477722'/>
<id>625b3d5176f2c274e91fcf28bda8e45cc0477722</id>
<content type='text'>
- 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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
- 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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
</feed>
