fix(ui): keep Signage state consistent across tabs, and confirm destructive actions

Activating a timeline from Playback left Timelines showing it as inactive,
because each tab read its own stale copy of the channel groups; operators
read that as activation not sticking. Tabs also mounted every panel afresh, so
each switch flashed a spinner and collapsed the page before regrowing. Panels
now share one stale-while-revalidate resource cache, and Playback follows the
management realtime channel so Now showing tracks the wall.

Deleting an image, a timeline, a slot, a source, or an audience happened on a
single click; each is now a two-step in-place confirmation (DESIGN 21.3).

The audience key was presented as a field to fill in. Audiences are now named
in plain words, explained (images look the same for every audience), and the
content key is derived from the name with an Advanced override. The roster
labels signed-in browsers and lets an offline entry be removed after
confirmation, so test tabs and retired screens no longer accumulate. The
preview keeps nothing in the Dashboard origin's storage, and the narrow-screen
notice no longer names the platform.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-12 21:32:04 -05:00
co-authored by Claude Opus 5.5
parent 67d89f1020
commit 7c17282616
16 changed files with 3446 additions and 3034 deletions
+14 -8
View File
@@ -9,7 +9,7 @@ import {
import type { SignageApi } from "../api.js";
import { useAction, useResource } from "../hooks.js";
import type { ImageRecord, SettingsResponse } from "../types.js";
import { FailureNote, formatInstant, renderLoadable } from "./shared.js";
import { ConfirmButton, FailureNote, formatInstant, renderLoadable } from "./shared.js";
/**
* The native image library and its upload queue.
@@ -28,7 +28,11 @@ export function ImagesPanel({
readonly settings: SettingsResponse;
readonly canManage: boolean;
}): ReactElement {
const images = useResource(() => api.get<{ images: readonly ImageRecord[] }>("images"), []);
const images = useResource(
() => api.get<{ images: readonly ImageRecord[] }>("images"),
[],
"images",
);
const [uploads, setUploads] = useState<readonly UploadItem[]>([]);
const queueRef = useRef<UploadQueue | undefined>(undefined);
const [renaming, setRenaming] = useState<{ id: string; value: string } | undefined>(undefined);
@@ -204,13 +208,15 @@ export function ImagesPanel({
Rename
</Button>
)}
<Button
<ConfirmButton
confirmLabel="Delete this image"
disabled={remove.pending}
onClick={() => remove.run(() => api.send("DELETE", `images/${image.id}`))}
variant="danger"
>
Delete
</Button>
label="Delete"
onConfirm={() =>
remove.run(() => api.send("DELETE", `images/${image.id}`))
}
testId={`signage-image-delete-${image.id}`}
/>
</div>
) : undefined}
</li>