summaryrefslogtreecommitdiffstats
path: root/views
diff options
context:
space:
mode:
Diffstat (limited to 'views')
-rw-r--r--views/admin/people.erb111
-rw-r--r--views/layout.erb2
-rw-r--r--views/people.erb25
-rw-r--r--views/person.erb26
4 files changed, 164 insertions, 0 deletions
diff --git a/views/admin/people.erb b/views/admin/people.erb
new file mode 100644
index 0000000..6b7a4d0
--- /dev/null
+++ b/views/admin/people.erb
@@ -0,0 +1,111 @@
+<div class="admin-people">
+ <div class="admin-nav">
+ <a href="/admin/edit/" class="btn btn-sm">← Albums</a>
+ <% unless @clusters.empty? %>
+ <a href="/people" class="btn btn-sm" target="_blank">Public Page ↗</a>
+ <% end %>
+ </div>
+
+ <h1>People</h1>
+
+ <section class="admin-update">
+ <h2>Clustering</h2>
+ <p class="update-hint">
+ <%= @total %> cluster<%= @total == 1 ? '' : 's' %> &middot;
+ <%= @named_count %> named
+ <% if @updated_at %>&middot; last run <%= @updated_at %><% end %>
+ </p>
+ <p class="update-hint">
+ Re-clustering reads all faces.json files and groups similar faces together.
+ Existing names are preserved. Run again whenever the face daemon has processed a significant batch of new photos.
+ </p>
+ <button id="cluster-btn" class="btn" onclick="startRecluster()">Re-cluster Faces</button>
+ <div id="cluster-panel" class="update-panel hidden">
+ <div id="cluster-status" class="update-status"></div>
+ <pre id="cluster-log" class="update-log"></pre>
+ </div>
+ </section>
+
+ <% if @clusters.empty? %>
+ <p class="empty-album" style="margin-top: 40px">
+ No face data yet — the face daemon is still processing, or run Re-cluster once it has finished a pass.
+ </p>
+ <% else %>
+ <div class="people-admin-list">
+ <% @clusters.each do |c| %>
+ <div class="people-admin-row<%= c[:name] ? '' : ' unnamed-cluster' %>">
+ <div class="face-samples">
+ <% c[:samples].each do |s| %>
+ <img src="/face/<%= ERB::Util.html_escape(s[:rel]) %>?box=<%= ERB::Util.html_escape(s[:box].join(',')) %>"
+ width="72" height="72" loading="lazy">
+ <% end %>
+ <% if c[:count] > c[:samples].length %>
+ <span class="face-more">+<%= c[:count] - c[:samples].length %></span>
+ <% end %>
+ </div>
+ <div class="people-admin-meta">
+ <span class="face-count"><%= c[:count] %> photo<%= c[:count] == 1 ? '' : 's' %></span>
+ <form method="post" action="/admin/people/<%= ERB::Util.url_encode(c[:uuid]) %>" class="name-form">
+ <input type="text" name="name"
+ value="<%= ERB::Util.html_escape(c[:name].to_s) %>"
+ placeholder="Enter name…" class="name-input">
+ <button type="submit" class="btn btn-sm">Save</button>
+ <% if c[:slug] %>
+ <a href="/people/<%= ERB::Util.url_encode(c[:slug]) %>" class="btn btn-sm" target="_blank">↗</a>
+ <% end %>
+ </form>
+ </div>
+ </div>
+ <% end %>
+ <% if @total > @clusters.length %>
+ <p class="update-hint" style="margin-top: 14px">
+ Showing <%= @clusters.length %> of <%= @total %> clusters (named first, then largest unnamed).
+ </p>
+ <% end %>
+ </div>
+ <% end %>
+</div>
+
+<script>
+async function startRecluster() {
+ const btn = document.getElementById('cluster-btn');
+ const panel = document.getElementById('cluster-panel');
+ const log = document.getElementById('cluster-log');
+ const status = document.getElementById('cluster-status');
+
+ btn.disabled = true;
+ btn.textContent = 'Clustering…';
+ log.textContent = '';
+ status.textContent = 'Running…';
+ status.className = 'update-status running';
+ panel.classList.remove('hidden');
+
+ const res = await fetch('/admin/people/recluster', { method: 'POST' });
+ const { job_id } = await res.json();
+
+ let seen = 0;
+ const poll = setInterval(async () => {
+ const r = await fetch('/admin/update/' + job_id);
+ const data = await r.json();
+ const fresh = data.lines.slice(seen);
+ if (fresh.length) {
+ log.textContent += fresh.join('\n') + '\n';
+ log.scrollTop = log.scrollHeight;
+ seen = data.lines.length;
+ }
+ if (data.status !== 'running') {
+ clearInterval(poll);
+ btn.disabled = false;
+ btn.textContent = 'Re-cluster Faces';
+ if (data.status === 'done') {
+ status.textContent = 'Done ✓';
+ status.className = 'update-status done';
+ setTimeout(() => location.reload(), 1200);
+ } else {
+ status.textContent = 'Error ✗';
+ status.className = 'update-status error';
+ }
+ }
+ }, 1500);
+}
+</script>
diff --git a/views/layout.erb b/views/layout.erb
index 086fe8c..e0c5399 100644
--- a/views/layout.erb
+++ b/views/layout.erb
@@ -17,8 +17,10 @@
<header class="site-header">
<a href="/browse/" class="site-logo">Albumen</a>
<nav class="site-nav">
+ <% if FACES_ENABLED %><a href="/people">People</a><% end %>
<% if admin? %>
<a href="/admin/edit/<%= defined?(@rel) ? @rel : '' %>">Edit Album</a>
+ <% if FACES_ENABLED %><a href="/admin/people">Manage People</a><% end %>
<a href="/admin/logout">Logout</a>
<% else %>
<a href="/admin/login?return_to=<%= CGI.escape("/admin/edit/#{defined?(@rel) ? @rel : ''}") %>" class="nav-admin">Admin</a>
diff --git a/views/people.erb b/views/people.erb
new file mode 100644
index 0000000..4d47769
--- /dev/null
+++ b/views/people.erb
@@ -0,0 +1,25 @@
+<div class="album-header">
+ <h1>People</h1>
+</div>
+
+<% if @people.empty? %>
+ <p class="empty-album">
+ No named people yet.
+ <% if admin? %>
+ <a href="/admin/people">Go to Manage People</a> to name face clusters.
+ <% end %>
+ </p>
+<% else %>
+ <div class="grid people-grid">
+ <% @people.each do |p| %>
+ <a href="/people/<%= ERB::Util.url_encode(p[:slug]) %>" class="person-tile">
+ <div class="thumb-wrap">
+ <img src="/face/<%= ERB::Util.html_escape(p[:face][:rel]) %>?box=<%= ERB::Util.html_escape(p[:face][:box].join(',')) %>"
+ alt="<%= ERB::Util.html_escape(p[:name]) %>" loading="lazy">
+ </div>
+ <div class="person-name"><%= ERB::Util.html_escape(p[:name]) %></div>
+ <div class="person-count"><%= p[:count] %> photo<%= p[:count] == 1 ? '' : 's' %></div>
+ </a>
+ <% end %>
+ </div>
+<% end %>
diff --git a/views/person.erb b/views/person.erb
new file mode 100644
index 0000000..1bb25e2
--- /dev/null
+++ b/views/person.erb
@@ -0,0 +1,26 @@
+<div class="album-header">
+ <div class="breadcrumbs">
+ <a href="/people">People</a>
+ <span class="sep">/</span>
+ <%= ERB::Util.html_escape(@person_name) %>
+ </div>
+ <h1><%= ERB::Util.html_escape(@person_name) %></h1>
+ <p class="album-desc"><%= @count %> photo<%= @count == 1 ? '' : 's' %></p>
+</div>
+
+<% if @photos.empty? %>
+ <p class="empty-album">No photos found.</p>
+<% else %>
+ <div class="grid">
+ <% @photos.each do |photo| %>
+ <% album_url = photo[:dir_rel].empty? ? '/browse/' : "/browse/#{ERB::Util.html_escape(photo[:dir_rel])}" %>
+ <a href="<%= album_url %>?photo=<%= ERB::Util.url_encode(photo[:filename]) %>"
+ class="card" style="text-decoration:none">
+ <div class="thumb-wrap">
+ <img src="/thumb/<%= ERB::Util.html_escape(photo[:rel]) %>"
+ loading="lazy" alt="<%= ERB::Util.html_escape(photo[:filename]) %>">
+ </div>
+ </a>
+ <% end %>
+ </div>
+<% end %>