feat: expose and display duplicates
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user