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:
@@ -60,7 +60,16 @@ export function AppShell() {
|
||||
const navItems = [
|
||||
{ to: "/", label: "Library" },
|
||||
{ to: "/online", label: "Online" },
|
||||
...(user?.can_upload ? [{ to: "/upload", label: "Upload" }] : []),
|
||||
...(user?.can_upload
|
||||
? [
|
||||
{ to: "/upload", label: "Upload" },
|
||||
{ to: "/duplicates", label: "Duplicates" },
|
||||
{ to: "/delete", label: "Delete" },
|
||||
]
|
||||
: []),
|
||||
...(user && (user.is_staff || user.is_superuser || user.role === "staff")
|
||||
? [{ to: "/users", label: "Users" }]
|
||||
: []),
|
||||
];
|
||||
|
||||
const navigationType = useNavigationType();
|
||||
@@ -120,9 +129,17 @@ export function AppShell() {
|
||||
title="Account settings"
|
||||
className="flex items-center gap-2 transition hover:opacity-80"
|
||||
>
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-ctp-surface1 font-mono text-xs font-semibold text-ctp-lavender">
|
||||
{user.username.slice(0, 1).toUpperCase()}
|
||||
</span>
|
||||
{user.avatar_url ? (
|
||||
<img
|
||||
src={user.avatar_url}
|
||||
alt={user.username}
|
||||
className="h-7 w-7 rounded-full border border-ctp-surface1 object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-ctp-surface1 font-mono text-xs font-semibold text-ctp-lavender">
|
||||
{user.username.slice(0, 1).toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
<span className="hidden text-sm text-ctp-subtext1 sm:block">
|
||||
{user.username}
|
||||
</span>
|
||||
|
||||
@@ -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})`,
|
||||
|
||||
Reference in New Issue
Block a user