summaryrefslogtreecommitdiffstats
path: root/app.rb
diff options
context:
space:
mode:
authorKen D'Ambrosio <ken@jots.org>2026-06-08 21:09:47 +0000
committerKen D'Ambrosio <ken@jots.org>2026-06-08 21:09:47 +0000
commit7f6325fe213ed46ff5479ffd34b0e212426d48f2 (patch)
tree46430a22dc791ce5c8018eeb7bce2c857fd17cd6 /app.rb
parent00f63c03b7c5de68aea6a2305886bc1953a722b6 (diff)
Add people cluster detail page with face move/merge and hover preview
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>
Diffstat (limited to 'app.rb')
-rw-r--r--app.rb77
1 files changed, 77 insertions, 0 deletions
diff --git a/app.rb b/app.rb
index 3978c54..7ce9d22 100644
--- a/app.rb
+++ b/app.rb
@@ -895,6 +895,83 @@ post '/admin/people/:uuid' do
redirect '/admin/people'
end
+get '/admin/people/:uuid' do
+ require_admin!
+ data = load_people_data
+ people = data['people'] || {}
+ halt 404 unless people.key?(params[:uuid])
+
+ pd = people[params[:uuid]]
+ @title = pd['name'] || 'Unnamed cluster'
+ @uuid = params[:uuid]
+ @name = pd['name']
+ @members = pd['members'] || []
+ @count = @members.length
+
+ @named_others = people
+ .reject { |k, _| k == params[:uuid] }
+ .select { |_, v| v['name'] }
+ .map { |k, v| { uuid: k, name: v['name'] } }
+ .sort_by { |x| x[:name].downcase }
+
+ @all_others = people
+ .reject { |k, _| k == params[:uuid] }
+ .map { |k, v| { uuid: k, name: v['name'] || "(unnamed ยท #{(v['members'] || []).length})" } }
+ .sort_by { |x| x[:name].downcase }
+
+ erb :'admin/person_detail'
+end
+
+post '/admin/people/:uuid/move' do
+ require_admin!
+ src = params[:uuid]
+ rel = params['rel']
+ box = JSON.parse(params['box']).map(&:to_i)
+ to = params['to'].to_s.strip
+
+ return redirect "/admin/people/#{src}" if to.empty?
+
+ data = load_people_data
+ people = data['people'] || {}
+ halt 404 unless people.key?(src)
+
+ member = people[src]['members'].find { |m| m['rel'] == rel && m['box'].map(&:to_i) == box }
+ halt 404 unless member
+ people[src]['members'].delete(member)
+
+ if to == 'new'
+ new_uid = SecureRandom.uuid
+ people[new_uid] = { 'name' => nil, 'slug' => nil, 'members' => [member] }
+ else
+ halt 404 unless people.key?(to)
+ people[to]['members'] << member
+ end
+
+ data['people'] = people
+ atomic_write(PEOPLE_PATH, JSON.pretty_generate(data))
+
+ people[src] ? redirect("/admin/people/#{src}") : redirect('/admin/people')
+end
+
+post '/admin/people/:uuid/merge' do
+ require_admin!
+ src = params[:uuid]
+ into = params['into'].to_s.strip
+
+ return redirect '/admin/people' if into.empty?
+
+ data = load_people_data
+ people = data['people'] || {}
+ halt 404 unless people.key?(src) && people.key?(into)
+
+ people[into]['members'].concat(people[src]['members'])
+ people.delete(src)
+ data['people'] = people
+
+ atomic_write(PEOPLE_PATH, JSON.pretty_generate(data))
+ redirect "/admin/people/#{into}"
+end
+
get '/people' do
data = load_people_data
people = (data['people'] || {}).select { |_, p| p['name'] && p['slug'] }