+ Inspect disk usage and remove files from the library. +
++ {status} +
+ ) : null} + ++ Library size +
++ {formatBytes(storage.watched_folder.library_size)} +
++ {storage.watched_folder.path} +
++ {storage.library_items} items +
++ Disk +
++ {formatBytes(storage.watched_folder.used)} /{" "} + {formatBytes(storage.watched_folder.total)} +
++ {storage.watched_folder.percent_used}% used ·{" "} + {formatBytes(storage.watched_folder.free)} free +
++ Media folder +
++ {formatBytes(storage.media.size)} +
++ {storage.media.path} +
++ Temp uploads +
++ {formatBytes(storage.temp.size)} · {storage.temp.files} file(s) +
+ ++ {entry.filename} +
++ {formatBytes(entry.size)} + {entry.location_count > 1 ? ` · ${entry.location_count} copies` : ""} +
++ Clean up exact copies and visually similar files. +
++ {status} +
+ ) : null} + ++ No items exist at multiple paths. +
+ ) : ( ++ {group.filename} +
++ {errorMessage(similarMutation.error)} +
+ ) : null} ++ No matches at {similarMutation.data.threshold}% similarity. +
+ ) : ( ++ No groups at {threshold}% similarity. +
+ ) : ( ++ Already in your library +
++ This upload looks like it is already in the library — + you can discard it below. +
+ +
diff --git a/frontend/src/features/users/UsersPage.tsx b/frontend/src/features/users/UsersPage.tsx
new file mode 100644
index 0000000..5a39246
--- /dev/null
+++ b/frontend/src/features/users/UsersPage.tsx
@@ -0,0 +1,180 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useState } from "react";
+
+import { Button, EmptyState, Spinner, inputClass } from "@/components/ui";
+import { api, errorMessage } from "@/lib/api";
+import { cn } from "@/lib/cn";
+import { formatDate } from "@/lib/format";
+import type { AdminUser, Paginated } from "@/lib/types";
+import { useAuth } from "@/store/auth";
+
+const ROLE_OPTIONS = [
+ { value: "user", label: "User" },
+ { value: "uploader", label: "Uploader" },
+ { value: "staff", label: "Staff" },
+];
+
+function UserRow({ profile }: { profile: AdminUser }) {
+ const queryClient = useQueryClient();
+ const [avatarJId, setAvatarJId] = useState(profile.avatar_j_id ?? "");
+ const [message, setMessage] = useState
+ {profile.username}
+ {profile.is_superuser ? (
+
+ admin
+
+ ) : null}
+ {profile.is_staff && !profile.is_superuser ? (
+
+ django staff
+
+ ) : null}
+
+ {profile.email || "no email"} · joined{" "}
+ {formatDate(profile.date_joined)} · {profile.uploads_count} upload
+ {profile.uploads_count === 1 ? "" : "s"}
+ {message} {error}
+ Manage roles and set profile pictures from library J-IDs.
+
+ ) : (
+
+ {profile.username.slice(0, 1).toUpperCase()}
+
+ )}
+
Users
+