feat: expose and display duplicates

This commit is contained in:
William Valentin
2026-02-04 23:38:24 -08:00
parent 1952fbaf30
commit 83f3ff1f69
4 changed files with 179 additions and 0 deletions

View File

@@ -35,6 +35,7 @@ type VideoPlaybackVariant = { kind: "original" } | { kind: "video_mp4"; size: nu
type VariantsResponse = Array<{ kind: string; size: number; key: string }>;
type Tag = { id: string; name: string };
type Album = { id: string; name: string };
type DupesResponse = { items: Array<{ id: string }> };
function startOfDayUtc(iso: string) {
const d = new Date(iso);
@@ -74,6 +75,8 @@ export function MediaPanel(props: { selectedDayIso: string | null }) {
const [overrideError, setOverrideError] = useState<string | null>(null);
const [overrideBusy, setOverrideBusy] = useState(false);
const [baseCaptureTs, setBaseCaptureTs] = useState<string | null>(null);
const [dupes, setDupes] = useState<Array<{ id: string }> | null>(null);
const [dupesError, setDupesError] = useState<string | null>(null);
const range = useMemo(() => {
if (!props.selectedDayIso) return null;
@@ -174,6 +177,15 @@ export function MediaPanel(props: { selectedDayIso: string | null }) {
return { url, variant: { kind: "original" } };
}
async function loadDupes(assetId: string) {
setDupesError(null);
setDupes(null);
const res = await fetch(`/api/assets/${assetId}/dupes`, { cache: "no-store" });
if (!res.ok) throw new Error(`dupes_fetch_failed:${res.status}`);
const json = (await res.json()) as DupesResponse;
setDupes(json.items);
}
async function openViewer(asset: Asset) {
if (asset.status === "failed") {
setViewerError(`${asset.id}: ${asset.error_message ?? "asset_failed"}`);
@@ -196,6 +208,9 @@ export function MediaPanel(props: { selectedDayIso: string | null }) {
setOverrideInput(asset.capture_ts_utc ?? "");
setOverrideError(null);
void loadAdminLists();
void loadDupes(asset.id).catch((err) => {
setDupesError(err instanceof Error ? err.message : String(err));
});
return;
}
@@ -206,6 +221,9 @@ export function MediaPanel(props: { selectedDayIso: string | null }) {
setOverrideInput(asset.capture_ts_utc ?? "");
setOverrideError(null);
void loadAdminLists();
void loadDupes(asset.id).catch((err) => {
setDupesError(err instanceof Error ? err.message : String(err));
});
} catch (err) {
setViewer(null);
setViewerError(
@@ -629,6 +647,45 @@ export function MediaPanel(props: { selectedDayIso: string | null }) {
{viewer.asset.id}
</div>
<div
style={{
borderTop: "1px solid #eee",
paddingTop: 12,
display: "grid",
gap: 6,
}}
>
<strong style={{ fontSize: 13 }}>Duplicates</strong>
{dupesError ? (
<div style={{ color: "#b00", fontSize: 12 }}>
{dupesError}
</div>
) : null}
{dupes === null ? (
<div style={{ color: "#666", fontSize: 12 }}>
Loading...
</div>
) : dupes.length === 0 ? (
<div style={{ color: "#666", fontSize: 12 }}>
No duplicates.
</div>
) : (
<div
style={{
display: "grid",
gap: 4,
fontSize: 12,
color: "#444",
}}
>
<div>{dupes.length} duplicate(s)</div>
{dupes.map((dupe) => (
<div key={dupe.id}>{dupe.id.slice(0, 8)}</div>
))}
</div>
)}
</div>
<div
style={{
borderTop: "1px solid #eee",