fix: complete display and channel group management

This commit is contained in:
ekklesia
2026-09-29 18:15:26 -05:00
parent 5760059ee3
commit aa8cb1c96f
10 changed files with 243 additions and 19 deletions
+50
View File
@@ -487,6 +487,56 @@ describe("Signage dashboard", () => {
expect(container.textContent).toContain("SLIDE_DECODE_FAILED");
});
it("explains display registration and deletes a channel group only after confirmation", async () => {
const secondGroup = {
...GROUP,
channels: [{ id: "chan-2", isDefault: true, key: "default", name: "Default" }],
id: "group-2",
name: "Foyer",
playback: { ...GROUP.playback, groupId: "group-2" },
};
let currentGroups = [GROUP, secondGroup];
const deleted: string[] = [];
const host = createFakeHost({
permissions: ["signage.view", "signage.displays.manage"],
routes: [
{ method: "GET", path: "settings", respond: () => jsonResponse(FAKE_SETTINGS) },
{
method: "GET",
path: "channel-groups",
respond: () => jsonResponse({ groups: currentGroups }),
},
{ method: "GET", path: "timelines", respond: () => jsonResponse({ timelines: [] }) },
{ method: "GET", path: "playback/group-1", respond: () => jsonResponse(PLAYBACK_DETAIL) },
{ method: "GET", path: "viewers", respond: () => jsonResponse({ viewers: [] }) },
{
method: "DELETE",
path: "channel-groups/group-1",
respond: ({ path }) => {
deleted.push(path);
currentGroups = [secondGroup];
return new Response(null, { status: 204 });
},
},
],
});
await render(<SignageDashboard context={host.context} />);
await click(
[...container.querySelectorAll("[role='tab']")].find(
(candidate) => candidate.textContent === "Displays",
),
);
expect(container.textContent).toContain("/display/signage");
expect(container.textContent).toContain("Provider audience key");
await click(buttonNamed("Delete group"));
expect(deleted).toEqual([]);
expect(container.textContent).toContain("Displays assigned here will move");
await click(buttonNamed("Confirm delete"));
expect(deleted).toEqual(["channel-groups/group-1"]);
});
it("uploads one request per file and reports each one's own outcome", async () => {
const uploaded: string[] = [];
const host = createFakeHost({
+1 -1
View File
@@ -103,7 +103,7 @@ export function createMemoryPlaybackStore(
return Promise.resolve();
},
deleteChannelGroup(groupId: string): Promise<void> {
deleteChannelGroup(groupId: string, _fallbackChannelId: string): Promise<void> {
groups.delete(groupId);
playback.delete(groupId);
for (const [id, channel] of channels) {
+14
View File
@@ -46,6 +46,20 @@ export function createMemorySignageStore(
...playbackStore,
...viewerStore,
async deleteChannelGroup(groupId: string, fallbackChannelId: string): Promise<void> {
const removedChannelIds = new Set(
[...playbackStore.channels.values()]
.filter((channel) => channel.groupId === groupId)
.map((channel) => channel.id),
);
for (const viewer of viewerStore.viewers.values()) {
if (viewer.channelId !== null && removedChannelIds.has(viewer.channelId)) {
await viewerStore.updateViewer(viewer.id, { channelId: fallbackChannelId });
}
}
await playbackStore.deleteChannelGroup(groupId, fallbackChannelId);
},
addImageReference(imageId: string, kind: "slot" | "static-hold"): void {
const current = references.get(imageId) ?? { slots: 0, staticHolds: 0 };
references.set(
+28
View File
@@ -128,6 +128,34 @@ describe("viewers", () => {
expect(store.viewers.size).toBe(1);
});
it("moves an assigned display atomically when its channel group is deleted", async () => {
const original = (await store.listChannelGroups())[0];
if (!original) {
throw new Error("Expected the default group.");
}
const session = await openViewerSession({
at: new Date(clock),
deviceId: null,
newId,
store,
viewerKey: "kiosk-lobby-0003",
});
expect(original.channels.some((channel) => channel.id === session.channel.id)).toBe(true);
const fallbackGroupId = newId();
const fallbackChannelId = newId();
await store.createChannelGroup({
channelId: fallbackChannelId,
channelName: "Default",
groupId: fallbackGroupId,
name: "Foyer",
});
await store.deleteChannelGroup(original.group.id, fallbackChannelId);
expect(await store.findChannelGroup(original.group.id)).toBeUndefined();
expect((await store.findViewer(session.viewer.id))?.channelId).toBe(fallbackChannelId);
});
it("stores only the safe device identifier a paired principal carries", async () => {
const session = await openViewerSession({
at: new Date(clock),