Files
silo-server/web/src/hooks/queries/admin/serverNotificationChannels.ts
T
QuickandClaude Fable 5 3d2368aed7 feat(notifications): admin server channels broadcasting new content and request activity
Add admin-owned broadcast destinations ("community channels"): Discord or
generic webhooks fed straight from release_events by a per-channel watermark
sweep, announcing newly added movies/episodes as grouped digest posts plus
configurable media request lifecycle events (submitted/approved/declined/
fulfilled).

- Extend release_events with a kind discriminator and add a movie
  availability spine (movie_availability + kind-keyed
  notification_content_seed_state; first full scan seeds silently so
  upgrades never flood the movie back catalog)
- Sweep worker reads events by (created_at, id) cursor with batch-window
  grouping, per-channel backoff, and auto-disable; request events post
  best-effort via new requests.LifecycleNotifier hooks
- Reuse the webhook stack throughout: URL encryption (new AAD namespace),
  SSRF guard, embed limits, HMAC signing; shared type/name validation
  extracted for both services
- Admin CRUD API under /admin/notifications/server-channels and a Server
  Channels section in the notifications admin settings UI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:04:35 -04:00

87 lines
2.7 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api } from "@/api/client";
import type {
NotificationWebhookTestResult,
ServerNotificationChannel,
ServerNotificationChannelInput,
} from "@/api/types";
import { adminKeys } from "../keys";
import { toast } from "sonner";
export function useServerNotificationChannels() {
return useQuery({
queryKey: adminKeys.serverNotificationChannels(),
queryFn: () =>
api<{ channels: ServerNotificationChannel[] }>("/admin/notifications/server-channels").then(
(d) => d.channels ?? [],
),
});
}
export function useCreateServerNotificationChannel() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (input: ServerNotificationChannelInput) =>
api<ServerNotificationChannel>("/admin/notifications/server-channels", {
method: "POST",
body: JSON.stringify(input),
}),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: adminKeys.serverNotificationChannels() });
},
});
}
export function useUpdateServerNotificationChannel() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, ...input }: ServerNotificationChannelInput & { id: string }) =>
api<ServerNotificationChannel>(`/admin/notifications/server-channels/${id}`, {
method: "PUT",
body: JSON.stringify(input),
}),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: adminKeys.serverNotificationChannels() });
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : "Failed to update channel");
},
});
}
export function useDeleteServerNotificationChannel() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) =>
api(`/admin/notifications/server-channels/${id}`, { method: "DELETE" }),
onSuccess: () => {
toast.success("Channel deleted");
void queryClient.invalidateQueries({ queryKey: adminKeys.serverNotificationChannels() });
},
onError: () => {
toast.error("Failed to delete channel");
},
});
}
export function useTestServerNotificationChannel() {
return useMutation({
mutationFn: (id: string) =>
api<NotificationWebhookTestResult>(`/admin/notifications/server-channels/${id}/test`, {
method: "POST",
}),
});
}
export function useRotateServerNotificationChannelSecret() {
return useMutation({
mutationFn: (id: string) =>
api<{ signing_secret: string }>(`/admin/notifications/server-channels/${id}/rotate-secret`, {
method: "POST",
}),
onError: (error) => {
toast.error(error instanceof Error ? error.message : "Failed to rotate signing secret");
},
});
}