feat: add webhook body template and headers support for channels
This commit is contained in:
@@ -23,6 +23,26 @@ import type {
|
||||
RecyclePolicy,
|
||||
} from "@/lib/types";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Webhook preset templates (frontend-only, zero backend changes needed)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const WEBHOOK_PRESETS = {
|
||||
discord: `{
|
||||
"embeds": [{
|
||||
"title": "📺 {{event}}",
|
||||
"description": "{{#if data.item.title}}Now playing: **{{data.item.title}}**{{else}}No signal{{/if}}",
|
||||
"color": 3447003,
|
||||
"timestamp": "{{timestamp}}"
|
||||
}]
|
||||
}`,
|
||||
slack: `{
|
||||
"text": "📺 *{{event}}*{{#if data.item.title}} — {{data.item.title}}{{/if}}"
|
||||
}`,
|
||||
} as const;
|
||||
|
||||
type WebhookFormat = "default" | "discord" | "slack" | "custom";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Zod schemas
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -758,6 +778,8 @@ interface EditChannelSheetProps {
|
||||
logo_opacity?: number;
|
||||
webhook_url?: string | null;
|
||||
webhook_poll_interval_secs?: number;
|
||||
webhook_body_template?: string | null;
|
||||
webhook_headers?: string | null;
|
||||
},
|
||||
) => void;
|
||||
isPending: boolean;
|
||||
@@ -791,6 +813,9 @@ export function EditChannelSheet({
|
||||
const [logoOpacity, setLogoOpacity] = useState(100);
|
||||
const [webhookUrl, setWebhookUrl] = useState("");
|
||||
const [webhookPollInterval, setWebhookPollInterval] = useState<number | "">(5);
|
||||
const [webhookFormat, setWebhookFormat] = useState<WebhookFormat>("default");
|
||||
const [webhookBodyTemplate, setWebhookBodyTemplate] = useState("");
|
||||
const [webhookHeaders, setWebhookHeaders] = useState("");
|
||||
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
|
||||
const [fieldErrors, setFieldErrors] = useState<FieldErrors>({});
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -810,6 +835,18 @@ export function EditChannelSheet({
|
||||
setLogoOpacity(Math.round((channel.logo_opacity ?? 1) * 100));
|
||||
setWebhookUrl(channel.webhook_url ?? "");
|
||||
setWebhookPollInterval(channel.webhook_poll_interval_secs ?? 5);
|
||||
const tmpl = channel.webhook_body_template ?? "";
|
||||
setWebhookBodyTemplate(tmpl);
|
||||
setWebhookHeaders(channel.webhook_headers ?? "");
|
||||
if (!tmpl) {
|
||||
setWebhookFormat("default");
|
||||
} else if (tmpl === WEBHOOK_PRESETS.discord) {
|
||||
setWebhookFormat("discord");
|
||||
} else if (tmpl === WEBHOOK_PRESETS.slack) {
|
||||
setWebhookFormat("slack");
|
||||
} else {
|
||||
setWebhookFormat("custom");
|
||||
}
|
||||
setSelectedBlockId(null);
|
||||
setFieldErrors({});
|
||||
}
|
||||
@@ -846,6 +883,8 @@ export function EditChannelSheet({
|
||||
...(webhookUrl
|
||||
? { webhook_poll_interval_secs: webhookPollInterval === "" ? 5 : webhookPollInterval }
|
||||
: {}),
|
||||
webhook_body_template: webhookBodyTemplate || null,
|
||||
webhook_headers: webhookHeaders || null,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1107,14 +1146,72 @@ export function EditChannelSheet({
|
||||
/>
|
||||
</Field>
|
||||
{webhookUrl && (
|
||||
<Field label="Poll interval (seconds)" hint="How often to check for broadcast changes">
|
||||
<NumberInput
|
||||
value={webhookPollInterval}
|
||||
onChange={setWebhookPollInterval}
|
||||
min={1}
|
||||
placeholder="5"
|
||||
/>
|
||||
</Field>
|
||||
<>
|
||||
<Field label="Poll interval (seconds)" hint="How often to check for broadcast changes">
|
||||
<NumberInput
|
||||
value={webhookPollInterval}
|
||||
onChange={setWebhookPollInterval}
|
||||
min={1}
|
||||
placeholder="5"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-zinc-400">Payload format</p>
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
{(["default", "discord", "slack", "custom"] as WebhookFormat[]).map((fmt) => (
|
||||
<button
|
||||
key={fmt}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setWebhookFormat(fmt);
|
||||
if (fmt === "discord") setWebhookBodyTemplate(WEBHOOK_PRESETS.discord);
|
||||
else if (fmt === "slack") setWebhookBodyTemplate(WEBHOOK_PRESETS.slack);
|
||||
else if (fmt === "default") setWebhookBodyTemplate("");
|
||||
}}
|
||||
className={`rounded px-2.5 py-1 text-xs font-medium capitalize transition-colors ${
|
||||
webhookFormat === fmt
|
||||
? "bg-zinc-600 text-zinc-100"
|
||||
: "bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200"
|
||||
}`}
|
||||
>
|
||||
{fmt === "default" ? "K-TV default" : fmt}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{webhookFormat !== "default" && (
|
||||
<Field
|
||||
label="Body template (Handlebars)"
|
||||
hint="Context: event, timestamp, channel_id, data.item.title, data.item.duration_secs, …"
|
||||
>
|
||||
<textarea
|
||||
rows={6}
|
||||
value={webhookBodyTemplate}
|
||||
onChange={(e) => {
|
||||
setWebhookBodyTemplate(e.target.value);
|
||||
setWebhookFormat("custom");
|
||||
}}
|
||||
placeholder={'{\n "text": "Now playing: {{data.item.title}}"\n}'}
|
||||
className="w-full resize-y rounded-md border border-zinc-700 bg-zinc-800 px-3 py-2 font-mono text-xs text-zinc-100 placeholder:text-zinc-600 focus:border-zinc-500 focus:outline-none"
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field
|
||||
label="Extra headers (JSON)"
|
||||
hint={'e.g. {"Authorization": "Bearer token"}'}
|
||||
>
|
||||
<textarea
|
||||
rows={2}
|
||||
value={webhookHeaders}
|
||||
onChange={(e) => setWebhookHeaders(e.target.value)}
|
||||
placeholder={'{"Authorization": "Bearer xxx"}'}
|
||||
className="w-full resize-none rounded-md border border-zinc-700 bg-zinc-800 px-3 py-2 font-mono text-xs text-zinc-100 placeholder:text-zinc-600 focus:border-zinc-500 focus:outline-none"
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -158,6 +158,10 @@ export default function DashboardPage() {
|
||||
logo?: string | null;
|
||||
logo_position?: import("@/lib/types").LogoPosition;
|
||||
logo_opacity?: number;
|
||||
webhook_url?: string | null;
|
||||
webhook_poll_interval_secs?: number;
|
||||
webhook_body_template?: string | null;
|
||||
webhook_headers?: string | null;
|
||||
},
|
||||
) => {
|
||||
updateChannel.mutate(
|
||||
|
||||
Reference in New Issue
Block a user