Enable webhooks from dashboard page
This commit is contained in:
parent
13f1efdd15
commit
fa7f24e898
4 changed files with 291 additions and 17 deletions
|
|
@ -866,6 +866,8 @@ export const api = {
|
|||
|
||||
// ── Admin: Webhooks ─────────────────────────────────────────────────
|
||||
getWebhooks: () => fetchJSON<WebhooksResponse>("/api/webhooks"),
|
||||
enableWebhooks: () =>
|
||||
fetchJSON<WebhookEnableResponse>("/api/webhooks/enable", { method: "POST" }),
|
||||
createWebhook: (body: WebhookCreate) =>
|
||||
fetchJSON<WebhookRoute & { secret: string }>("/api/webhooks", {
|
||||
method: "POST",
|
||||
|
|
@ -1288,6 +1290,17 @@ export interface WebhooksResponse {
|
|||
subscriptions: WebhookRoute[];
|
||||
}
|
||||
|
||||
export interface WebhookEnableResponse {
|
||||
ok: boolean;
|
||||
platform: "webhook";
|
||||
enabled: true;
|
||||
needs_restart: boolean;
|
||||
restart_started?: boolean;
|
||||
restart_action?: string;
|
||||
restart_pid?: number | null;
|
||||
restart_error?: string;
|
||||
}
|
||||
|
||||
export interface WebhookCreate {
|
||||
name: string;
|
||||
description?: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
import { useCallback, useEffect, useLayoutEffect, useState } from "react";
|
||||
import { Webhook, Plus, Trash2, X, Copy, Check } from "lucide-react";
|
||||
import {
|
||||
AlertTriangle,
|
||||
Check,
|
||||
Copy,
|
||||
Plus,
|
||||
RotateCw,
|
||||
Trash2,
|
||||
Webhook,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@nous-research/ui/ui/components/badge";
|
||||
import { Button } from "@nous-research/ui/ui/components/button";
|
||||
import { Select, SelectOption } from "@nous-research/ui/ui/components/select";
|
||||
|
|
@ -51,6 +60,11 @@ function CopyButton({ value }: { value: string }) {
|
|||
export default function WebhooksPage() {
|
||||
const [data, setData] = useState<WebhooksResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [enabling, setEnabling] = useState(false);
|
||||
const [restartNeeded, setRestartNeeded] = useState(false);
|
||||
const [restartMessage, setRestartMessage] = useState<string | null>(null);
|
||||
const [restartError, setRestartError] = useState<string | null>(null);
|
||||
const [restarting, setRestarting] = useState(false);
|
||||
const { toast, showToast } = useToast();
|
||||
const { setEnd } = usePageHeader();
|
||||
|
||||
|
|
@ -78,7 +92,7 @@ export default function WebhooksPage() {
|
|||
const subscriptions = data?.subscriptions ?? [];
|
||||
|
||||
const loadWebhooks = useCallback(() => {
|
||||
api
|
||||
return api
|
||||
.getWebhooks()
|
||||
.then(setData)
|
||||
.catch(() => showToast("Failed to load webhooks", "error"))
|
||||
|
|
@ -89,6 +103,78 @@ export default function WebhooksPage() {
|
|||
loadWebhooks();
|
||||
}, [loadWebhooks]);
|
||||
|
||||
const watchRestartOutcome = useCallback(async () => {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
try {
|
||||
const st = await api.getActionStatus("gateway-restart", 5);
|
||||
if (st.running) continue;
|
||||
if (st.exit_code !== 0 && st.exit_code !== null) {
|
||||
setRestartMessage(null);
|
||||
setRestartNeeded(true);
|
||||
setRestartError(`Gateway restart failed with exit ${st.exit_code}.`);
|
||||
showToast(
|
||||
`Gateway restart failed (exit ${st.exit_code}) — restart manually`,
|
||||
"error",
|
||||
);
|
||||
} else {
|
||||
setRestartMessage(null);
|
||||
setRestartNeeded(false);
|
||||
setRestartError(null);
|
||||
}
|
||||
return;
|
||||
} catch {
|
||||
// The dashboard may briefly lose its connection while the gateway restarts.
|
||||
}
|
||||
}
|
||||
setRestartMessage(null);
|
||||
}, [showToast]);
|
||||
|
||||
const handleRestart = useCallback(async () => {
|
||||
setRestarting(true);
|
||||
try {
|
||||
await api.restartGateway();
|
||||
setRestartNeeded(false);
|
||||
setRestartError(null);
|
||||
setRestartMessage("Gateway restarting…");
|
||||
showToast("Gateway restarting…", "success");
|
||||
setTimeout(() => void loadWebhooks(), 4000);
|
||||
void watchRestartOutcome();
|
||||
} catch (e) {
|
||||
setRestartNeeded(true);
|
||||
setRestartError(String(e));
|
||||
showToast(`Failed to restart: ${e}`, "error");
|
||||
} finally {
|
||||
setRestarting(false);
|
||||
}
|
||||
}, [loadWebhooks, showToast, watchRestartOutcome]);
|
||||
|
||||
const handleEnableWebhooks = useCallback(async () => {
|
||||
setEnabling(true);
|
||||
setRestartNeeded(false);
|
||||
setRestartError(null);
|
||||
try {
|
||||
const result = await api.enableWebhooks();
|
||||
await loadWebhooks();
|
||||
if (result.restart_started) {
|
||||
setRestartMessage("Webhooks enabled; gateway restarting…");
|
||||
showToast("Webhooks enabled; gateway restarting…", "success");
|
||||
setTimeout(() => void loadWebhooks(), 4000);
|
||||
void watchRestartOutcome();
|
||||
} else {
|
||||
const detail = result.restart_error ? `: ${result.restart_error}` : ".";
|
||||
setRestartMessage(null);
|
||||
setRestartNeeded(true);
|
||||
setRestartError(`Gateway restart failed${detail}`);
|
||||
showToast(`Webhooks enabled; gateway restart failed${detail}`, "error");
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(`Failed to enable webhooks: ${e}`, "error");
|
||||
} finally {
|
||||
setEnabling(false);
|
||||
}
|
||||
}, [loadWebhooks, showToast, watchRestartOutcome]);
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
setName("");
|
||||
setDescription("");
|
||||
|
|
@ -171,7 +257,7 @@ export default function WebhooksPage() {
|
|||
<Button
|
||||
className="uppercase"
|
||||
size="sm"
|
||||
disabled={!enabled}
|
||||
disabled={!enabled || enabling}
|
||||
prefix={<Plus />}
|
||||
onClick={() => {
|
||||
setCreated(null);
|
||||
|
|
@ -184,7 +270,7 @@ export default function WebhooksPage() {
|
|||
return () => {
|
||||
setEnd(null);
|
||||
};
|
||||
}, [setEnd, enabled, loading]);
|
||||
}, [setEnd, enabled, enabling, loading]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
|
@ -375,17 +461,61 @@ export default function WebhooksPage() {
|
|||
)}
|
||||
|
||||
{!enabled && (
|
||||
<Card>
|
||||
<CardContent className="py-6 flex items-start gap-3 text-sm">
|
||||
<Webhook className="h-5 w-5 shrink-0 text-warning" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Webhook platform disabled</span>
|
||||
<span className="text-muted-foreground">
|
||||
The webhook platform must be enabled on the Channels page before
|
||||
you can create subscriptions. Enable it there, then return to
|
||||
this page.
|
||||
<Card className="border-warning/50">
|
||||
<CardContent className="flex flex-col gap-4 py-6 text-sm sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<Webhook className="h-5 w-5 shrink-0 text-warning" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Webhook receiver disabled</span>
|
||||
<span className="text-muted-foreground">
|
||||
Webhooks are their own gateway platform. Enable them here to
|
||||
accept incoming HTTP events; chat channels are only needed
|
||||
when a subscription delivers to Telegram, Discord, Slack, or
|
||||
another channel.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="uppercase shrink-0"
|
||||
onClick={handleEnableWebhooks}
|
||||
disabled={enabling}
|
||||
prefix={enabling ? <Spinner /> : <Webhook className="h-4 w-4" />}
|
||||
>
|
||||
{enabling ? "Enabling…" : "Enable webhooks"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{restartMessage && !restartNeeded && (
|
||||
<Card className="border-border">
|
||||
<CardContent className="flex items-center gap-2 p-4 text-sm text-muted-foreground">
|
||||
<RotateCw className="h-4 w-4 shrink-0 text-warning" />
|
||||
<span>{restartMessage}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{restartNeeded && (
|
||||
<Card className="border-warning/50">
|
||||
<CardContent className="flex flex-col gap-3 p-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-warning" />
|
||||
<span>
|
||||
{restartError ??
|
||||
"Webhooks are enabled, but the gateway still needs a restart before the receiver can come online."}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="uppercase shrink-0"
|
||||
onClick={handleRestart}
|
||||
disabled={restarting}
|
||||
prefix={restarting ? <Spinner /> : <RotateCw className="h-4 w-4" />}
|
||||
>
|
||||
{restarting ? "Restarting…" : "Restart gateway"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
|
@ -400,8 +530,8 @@ export default function WebhooksPage() {
|
|||
</H2>
|
||||
|
||||
<p className="text-xs text-muted-foreground -mt-1">
|
||||
Disabled webhooks reject incoming events; the gateway hot-reloads
|
||||
changes (no restart needed).
|
||||
Subscription changes hot-reload once the webhook receiver is running.
|
||||
Disabled subscriptions reject incoming events.
|
||||
</p>
|
||||
|
||||
{subscriptions.length === 0 && (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue