From c13a40a970be156a231200c20362636b198d32ec Mon Sep 17 00:00:00 2001 From: Ken D'Ambrosio Date: Tue, 9 Jun 2026 13:09:23 +0000 Subject: Add face pool, blacklisting, and action explanations to people admin 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 --- app.rb | 52 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 11 deletions(-) (limited to 'app.rb') diff --git a/app.rb b/app.rb index 192814a..7ee7851 100644 --- a/app.rb +++ b/app.rb @@ -827,9 +827,16 @@ get '/admin/people' do @named_count = people.count { |_, p| p['name'] } @updated_at = data['updated_at'] + pool_data = people.delete('__pool__') + @pool = pool_data ? { count: (pool_data['members'] || []).length, + samples: (pool_data['members'] || []).first(6).map { |m| { rel: m['rel'], box: m['box'] } } } : nil + named = people.select { |_, p| p['name'] }.sort_by { |_, p| p['name'].downcase } unnamed = people.reject { |_, p| p['name'] }.sort_by { |_, p| -(p['members']&.length || 0) } + @total = people.length + @named_count = people.count { |_, p| p['name'] } + @clusters = (named + unnamed).first(200).map do |uid, p| members = p['members'] || [] { uuid: uid, name: p['name'], slug: p['slug'], @@ -902,20 +909,21 @@ get '/admin/people/:uuid' do halt 404 unless people.key?(params[:uuid]) pd = people[params[:uuid]] - @title = pd['name'] || 'Unnamed cluster' + @is_pool = params[:uuid] == '__pool__' + @title = @is_pool ? 'Unidentified pool' : (pd['name'] || 'Unnamed cluster') @uuid = params[:uuid] @name = pd['name'] @members = pd['members'] || [] @count = @members.length - @named_others = people - .reject { |k, _| k == params[:uuid] } + regular = people.reject { |k, _| k == params[:uuid] || k == '__pool__' } + + @named_others = regular .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] } + @all_others = regular .map { |k, v| { uuid: k, name: v['name'] || "(unnamed · #{(v['members'] || []).length})" } } .sort_by { |x| x[:name].downcase } @@ -939,8 +947,12 @@ post '/admin/people/:uuid/move' do halt 404 unless member people[src]['members'].delete(member) - if to == 'remove' - # just drop it — already deleted from src above + if to == 'blacklist' + data['blacklist'] ||= [] + data['blacklist'] << member + elsif to == 'remove' + people['__pool__'] ||= { 'name' => '__pool__', 'slug' => nil, 'members' => [] } + people['__pool__']['members'] << member elsif to == 'new' new_uid = SecureRandom.uuid people[new_uid] = { 'name' => nil, 'slug' => nil, 'members' => [member] } @@ -956,13 +968,31 @@ post '/admin/people/:uuid/move' do people[src] ? redirect("/admin/people/#{src}") : redirect('/admin/people') end -post '/admin/people/:uuid/delete' do +post '/admin/people/__pool__/blacklist_all' do require_admin! data = load_people_data people = data['people'] || {} - people.delete(params[:uuid]) - data['people'] = people - atomic_write(PEOPLE_PATH, JSON.pretty_generate(data)) + pool = people.delete('__pool__') + if pool + data['blacklist'] ||= [] + data['blacklist'].concat(pool['members'] || []) + data['people'] = people + atomic_write(PEOPLE_PATH, JSON.pretty_generate(data)) + end + redirect '/admin/people' +end + +post '/admin/people/:uuid/delete' do + require_admin! + data = load_people_data + people = data['people'] || {} + cluster = people.delete(params[:uuid]) + if cluster + data['blacklist'] ||= [] + data['blacklist'].concat(cluster['members'] || []) + data['people'] = people + atomic_write(PEOPLE_PATH, JSON.pretty_generate(data)) + end redirect '/admin/people' end -- cgit v1.2.3