feat: add UI for capture time override

This commit is contained in:
William Valentin
2026-02-04 08:57:27 -08:00
parent 6030581429
commit 8eae0c7c97
2 changed files with 272 additions and 0 deletions

View File

@@ -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",