Duplicates, delete & storage, users page with J-ID avatars

Backend:
- Perceptual hashes (aHash/dHash/pHash/wHash via imagehash, no imgdd)
  stored on items, computed on upload/download and by the new
  compute_visual_hashes command
- Duplicates API: exact duplicates (multi-location items), visual matches
  for one item, union-find similarity groups with pagination
- Delete API with ownership/staff checks, per-item and per-copy deletion,
  watched-folder path validation; storage overview and temp cleanup;
  file list accepts j_ids batches
- Staged uploads are flagged visual_match with their library matches
  (threshold via VISUAL_MATCH_THRESHOLD)
- Staff users API: list with upload counts, set role and avatar by J-ID;
  User.avatar FK with signed avatar URLs
- Download threads close their DB connection and stale tasks are reaped,
  keeping behaviour Gunicorn-friendly

Frontend:
- /duplicates: exact duplicate groups with per-copy delete, visual
  similarity controls, search similar to a J-ID, paginated groups with
  selection, bulk delete and dismiss
- /delete: storage cards, delete by J-ID with preview grid, temp cleanup
- /users: staff directory with role selects and avatar J-ID inputs
- Nav + command palette entries; top-bar avatar; upload cards and the
  metadata modal show library visual matches
This commit is contained in:
2026-09-17 12:49:10 -05:00
parent 75b7ed35eb
commit cd490b0a23
28 changed files with 1966 additions and 22 deletions
@@ -1,4 +1,5 @@
import {
Copy,
FolderOpen,
Globe,
LogIn,
@@ -6,7 +7,9 @@ import {
Menu,
Search,
Settings,
Trash2,
Upload,
Users,
type LucideIcon,
} from "lucide-react";
import { useMemo, useState } from "react";
@@ -59,6 +62,18 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
icon: Upload,
run: () => navigate("/upload"),
},
{
id: "duplicates",
label: "Find duplicates",
icon: Copy,
run: () => navigate("/duplicates"),
},
{
id: "delete",
label: "Delete files & storage",
icon: Trash2,
run: () => navigate("/delete"),
},
]
: []),
{
@@ -94,6 +109,14 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
icon: Settings,
run: () => navigate("/account"),
});
if (user.is_staff || user.is_superuser || user.role === "staff") {
list.push({
id: "users",
label: "Manage users",
icon: Users,
run: () => navigate("/users"),
});
}
list.push({
id: "logout",
label: `Log out (${user.username})`,