feat: add UI for capture time override
This commit is contained in:
@@ -65,6 +65,10 @@ export function MediaPanel(props: { selectedDayIso: string | null }) {
|
||||
const [albumId, setAlbumId] = useState("");
|
||||
const [adminError, setAdminError] = useState<string | null>(null);
|
||||
const [adminBusy, setAdminBusy] = useState(false);
|
||||
const [overrideInput, setOverrideInput] = useState("");
|
||||
const [overrideError, setOverrideError] = useState<string | null>(null);
|
||||
const [overrideBusy, setOverrideBusy] = useState(false);
|
||||
const [baseCaptureTs, setBaseCaptureTs] = useState<string | null>(null);
|
||||
|
||||
const range = useMemo(() => {
|
||||
if (!props.selectedDayIso) return null;
|
||||
@@ -183,6 +187,9 @@ export function MediaPanel(props: { selectedDayIso: string | null }) {
|
||||
? "video_mp4"
|
||||
: playback.variant.kind;
|
||||
setViewer({ asset, url: playback.url, variant: variantLabel });
|
||||
setBaseCaptureTs(asset.capture_ts_utc);
|
||||
setOverrideInput(asset.capture_ts_utc ?? "");
|
||||
setOverrideError(null);
|
||||
void loadAdminLists();
|
||||
return;
|
||||
}
|
||||
@@ -190,6 +197,9 @@ export function MediaPanel(props: { selectedDayIso: string | null }) {
|
||||
const variant: "original" | "thumb_med" | "poster" = "original";
|
||||
const url = await loadSignedUrl(asset.id, variant);
|
||||
setViewer({ asset, url, variant });
|
||||
setBaseCaptureTs(asset.capture_ts_utc);
|
||||
setOverrideInput(asset.capture_ts_utc ?? "");
|
||||
setOverrideError(null);
|
||||
void loadAdminLists();
|
||||
} catch (err) {
|
||||
setViewer(null);
|
||||
@@ -199,6 +209,87 @@ export function MediaPanel(props: { selectedDayIso: string | null }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOverrideCaptureTs() {
|
||||
if (!viewer) return;
|
||||
setOverrideError(null);
|
||||
setOverrideBusy(true);
|
||||
try {
|
||||
const token = sessionStorage.getItem("porthole_admin_token") ?? "";
|
||||
if (!token) throw new Error("missing_admin_token");
|
||||
|
||||
const trimmed = overrideInput.trim();
|
||||
if (!trimmed) throw new Error("enter_iso_timestamp");
|
||||
|
||||
const res = await fetch(
|
||||
`/api/assets/${viewer.asset.id}/override-capture-ts`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Porthole-Admin-Token": token,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ captureTsUtcOverride: trimmed }),
|
||||
},
|
||||
);
|
||||
if (!res.ok) throw new Error(`override_failed:${res.status}`);
|
||||
setViewer((prev) =>
|
||||
prev
|
||||
? {
|
||||
...prev,
|
||||
asset: {
|
||||
...prev.asset,
|
||||
capture_ts_utc: trimmed,
|
||||
},
|
||||
}
|
||||
: prev,
|
||||
);
|
||||
setOverrideError("Override saved.");
|
||||
} catch (err) {
|
||||
setOverrideError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setOverrideBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClearOverride() {
|
||||
if (!viewer) return;
|
||||
setOverrideError(null);
|
||||
setOverrideBusy(true);
|
||||
try {
|
||||
const token = sessionStorage.getItem("porthole_admin_token") ?? "";
|
||||
if (!token) throw new Error("missing_admin_token");
|
||||
const res = await fetch(
|
||||
`/api/assets/${viewer.asset.id}/override-capture-ts`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Porthole-Admin-Token": token,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ captureTsUtcOverride: null }),
|
||||
},
|
||||
);
|
||||
if (!res.ok) throw new Error(`override_clear_failed:${res.status}`);
|
||||
setViewer((prev) =>
|
||||
prev
|
||||
? {
|
||||
...prev,
|
||||
asset: {
|
||||
...prev.asset,
|
||||
capture_ts_utc: baseCaptureTs,
|
||||
},
|
||||
}
|
||||
: prev,
|
||||
);
|
||||
setOverrideInput(baseCaptureTs ?? "");
|
||||
setOverrideError("Override cleared.");
|
||||
} catch (err) {
|
||||
setOverrideError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setOverrideBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAdminLists() {
|
||||
setAdminError(null);
|
||||
setAdminBusy(true);
|
||||
@@ -527,6 +618,49 @@ export function MediaPanel(props: { selectedDayIso: string | null }) {
|
||||
{viewer.asset.id}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
borderTop: "1px solid #eee",
|
||||
paddingTop: 12,
|
||||
display: "grid",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<strong style={{ fontSize: 13 }}>Capture time override</strong>
|
||||
<div style={{ fontSize: 12, color: "#555" }}>
|
||||
Effective: {viewer.asset.capture_ts_utc ?? "(unset)"}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#555" }}>
|
||||
Base: {baseCaptureTs ?? "(unknown)"}
|
||||
</div>
|
||||
<div style={{ display: "grid", gap: 6 }}>
|
||||
<label style={{ fontSize: 12, color: "#555" }}>
|
||||
ISO timestamp
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="2026-02-01T12:34:56.000Z"
|
||||
value={overrideInput}
|
||||
onChange={(e) => setOverrideInput(e.target.value)}
|
||||
style={{ padding: 6, borderRadius: 6, border: "1px solid #ccc" }}
|
||||
disabled={overrideBusy}
|
||||
/>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<button type="button" onClick={handleOverrideCaptureTs}>
|
||||
Save override
|
||||
</button>
|
||||
<button type="button" onClick={handleClearOverride}>
|
||||
Clear override
|
||||
</button>
|
||||
</div>
|
||||
{overrideError ? (
|
||||
<div style={{ fontSize: 12, color: "#b00" }}>
|
||||
{overrideError}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
borderTop: "1px solid #eee",
|
||||
|
||||
Reference in New Issue
Block a user