mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 21:05:21 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Apps gives agents governed access to external tools through stored credentials. > - Routines start work when an external service sends an event. > - Fireflies provides meeting transcripts and summaries through an official hosted MCP server. > - This PR adds that connection and accepts signed meeting events through the shared app webhook flow. > - Agents can review completed meetings with the same permissions and audit records as other work. ## Linked Issues or Issue Description **Problem or motivation** Operators need agents to read Fireflies meetings and start follow-up work when a summary is ready. The Apps catalog lacks Fireflies. The shared app webhook flow needs to accept its signed deliveries. **Proposed solution** Use the official Fireflies MCP endpoint with OAuth or a vaulted bearer API key. Extend the existing Another app or script flow with signed webhook support. Verify the raw-body signature and pass the JSON payload as external data. Select Meeting Summarized in Fireflies. Deduplicate identical signed deliveries, including setup deliveries. **Alternatives considered** A separate REST connector would duplicate the governed MCP path. Polling, legacy V1 payloads, and automatic provider-side webhook registration are outside this change. **Roadmap alignment** This extends the existing MCP Tool Gateway & Apps and Scheduled Routines surfaces. It adds a provider to those systems. It does not introduce a second integration framework. **Additional context** A GitHub search found no existing Fireflies issues or PRs. Provider references and verification limits are in `doc/connections/FIREFLIES.md`. ## What Changed - Add the official Fireflies catalog definition, generated registry, provider evidence, and branded artwork. - Reuse Access → Connect, dynamic discovery, Permissions, vault storage, policy, and audit behavior. - Classify Fireflies sharing, movement, and access revocation as writes. - Preserve Off and Ask first restrictions during OAuth reauthorization and API-key replacement. New actions retain normal defaults. - Add `app_webhook` authentication to the shared Another app or script flow. Accept bearer tokens or raw-body HMAC-SHA256. Preserve earlier `fireflies_hmac` triggers and revision snapshots for compatibility. Existing text columns need no migration. - Verify `X-Hub-Signature` or `X-Hub-Signature-256` against the exact request body. Preserve generic event payloads and deduplicate identical signed requests. - Keep the routine wizard generic. Show one webhook URL and secret in Another app or script. Keep all new app webhook event names provider-neutral. Keep provider setup instructions in the connector documentation. - Pass generic webhook JSON to the task in an explicit external-data block, capped at 16,384 characters. Keep strict meeting validation for existing legacy Fireflies triggers. ## Verification - Feature implementation commit `0882dc8a1`: all 54 CI checks passed; two conditional Storybook checks skipped. This includes full tests, typecheck, build, browser E2E, canary dry run, and security checks. Greptile rated this commit 5/5; all review threads are resolved. - Full local `pnpm -r typecheck`, `pnpm build`, and token gates passed on the final code. Targeted connector, gateway, webhook, revision, and UI suites passed during implementation. After the provider-neutral follow-up, all 84 app-webhook and routine-service tests passed; the final payload-to-task assertion also passed in the 72-test routine suite and a clean-config rerun. - The long local `pnpm test:run` invocation started before the final edits and was stopped after the final-commit CI suites passed. It reported one generic webhook test failure while those files were changing; that test and the entire routine suite passed on the final source, including a clean-config reproduction. The interrupted local run is not counted as a full-suite pass. - In the embedded browser, completed official OAuth consent and discovered 20 live actions. Real meeting listing, transcript retrieval, and summary/action-item retrieval succeeded as the selected agent. Turning a live read Off blocked its test; catalog refresh preserved the restriction. - Embedded-browser Another app or script setup, back/save/resume, narrow layout, and a signed synthetic Fireflies delivery succeeded. The UI reported authentication passed without creating a task. Fixtures cover signature tampering, malformed requests, ordinary app event names, duplicate/setup deliveries, rotation, revisions, pause/archive, and company isolation. - Existing MCP browser suite: 8 passed and 2 provider-dependent cases skipped. Branding checks passed; connector artwork and webhook setup were checked at desktop/mobile widths and in light/dark modes. - An unauthenticated POST to a correctly formatted public webhook URL reached the staging tenant verifier through the existing Cloud gateway. - A real Fireflies webhook delivery remains unverified. A staging callback is available for the operator walkthrough. Live API-key authorization, credential expiry, and a new meeting's summary completion were not tested against the provider. Fixtures cover these protocol and lifecycle paths where applicable. - Storybook follow-up `c54174faa`: 27 production-component stories cover every UI change, with a source-to-story map in the connector documentation. Static Storybook build, UI typecheck, token gates, and Playwright checks for all stories and the mobile footer pass. All PR checks passed for this Storybook follow-up; Greptile reviewed `c54174faa` at 5/5. ## Risks - Fireflies may change its hosted MCP tools or OAuth behavior. Tool discovery stays dynamic. Experimental search/fetch tools are not required. - Public webhook setup requires HTTPS and a separate signing secret. Fireflies normally emits events for meetings owned by the configuring account. - Reauthorization touches shared MCP permission code. Regression tests cover existing restrictions, new actions, connection removal, and other gateway callers. - Webhook receipt grants no tool access. The routine agent still needs an authorized Fireflies connection. - New generic triggers rely on provider event subscriptions. Without a sender-supplied idempotency key, changed request bytes count as a new event. Existing legacy Fireflies triggers retain summary-only filtering and per-meeting deduplication. ## Model Used OpenAI Codex, model `gpt-6-astra`. Used reasoning, repository editing, code execution, and embedded-browser testing. The runtime did not expose a context-window size. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
598 lines
23 KiB
TypeScript
598 lines
23 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
||
import {
|
||
CalendarClock,
|
||
Check,
|
||
CheckCircle2,
|
||
AlertCircle,
|
||
Globe,
|
||
GitBranch,
|
||
Radio,
|
||
Webhook,
|
||
} from "lucide-react";
|
||
import {
|
||
SetupWizardNavigation,
|
||
SetupWizardFooter,
|
||
} from "@/components/SetupWizard";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { useBreadcrumbs } from "@/context/BreadcrumbContext";
|
||
import { cn } from "@/lib/utils";
|
||
import { AgentInstructions, CopyField } from "./WebhookFields";
|
||
import { WebhookUrlWarning } from "./WebhookUrlWarning";
|
||
|
||
export type TriggerDraft = {
|
||
kind: "choose" | "schedule" | "webhook";
|
||
step: number;
|
||
availableStep: number;
|
||
sender: "custom" | "github";
|
||
/** Retained when resuming webhooks created before generic signed-app support. */
|
||
signingMode?: "bearer" | "app_webhook" | "fireflies_hmac";
|
||
frequency: string;
|
||
time: string;
|
||
weekday: string;
|
||
timezone: string;
|
||
created: boolean;
|
||
};
|
||
export const defaultTriggerDraft: TriggerDraft = {
|
||
kind: "choose",
|
||
step: 0,
|
||
availableStep: 0,
|
||
sender: "custom",
|
||
frequency: "weekdays",
|
||
time: "09:00",
|
||
weekday: "Monday",
|
||
timezone: "America/Chicago",
|
||
created: false,
|
||
};
|
||
export function webhookAgentInstructions(
|
||
sender: TriggerDraft["sender"],
|
||
routineTitle: string,
|
||
webhookUrl: string,
|
||
webhookSecret: string,
|
||
setupPending = true,
|
||
signingMode: TriggerDraft["signingMode"] = "app_webhook",
|
||
) {
|
||
const common = [
|
||
`Connect the sending app to the Paperclip routine ${JSON.stringify(routineTitle)}.`,
|
||
`Webhook URL: ${webhookUrl}`,
|
||
"Send an HTTP POST request with a JSON object as the body (not an array or string).",
|
||
"Content-Type: application/json",
|
||
];
|
||
const auth =
|
||
sender === "github"
|
||
? [
|
||
"In GitHub, open your repository → Settings → Webhooks → Add webhook.",
|
||
"Use the webhook URL above as Payload URL and select application/json as Content type.",
|
||
`Secret: ${webhookSecret}`,
|
||
"Paste this value into GitHub’s Secret field. GitHub signs requests with X-Hub-Signature-256; do not use Bearer authentication.",
|
||
"Select the events that should start this routine, enable the webhook, and save.",
|
||
"To check the connection, open Recent Deliveries and redeliver an event.",
|
||
]
|
||
: [
|
||
`Secret key: ${webhookSecret}`,
|
||
...(signingMode === "bearer" ? [] : [
|
||
`If the app asks for a signing secret, paste the secret key above. Paperclip accepts HMAC-SHA256 over the exact request body in ${signingMode === "fireflies_hmac" ? "X-Hub-Signature" : "X-Hub-Signature or X-Hub-Signature-256"}, formatted sha256=<hex digest>.`,
|
||
]),
|
||
...(signingMode === "fireflies_hmac" ? [] : [
|
||
`For apps with custom headers, use Authorization: Bearer ${webhookSecret}`,
|
||
]),
|
||
"Subscribe only to the events that should start this routine. Public services need a publicly reachable HTTPS URL.",
|
||
"In the sending app, add a webhook using this URL, POST method, JSON body, and headers, then save it.",
|
||
"Send a unique Idempotency-Key header for each event and reuse it on retries, so retrying a setup test after activation cannot start the routine.",
|
||
'Example JSON body: {"event":"deployment.completed","environment":"production"}',
|
||
"To check the connection, send a test event from the app or perform the action that triggers a delivery.",
|
||
];
|
||
return [
|
||
...common,
|
||
...auth,
|
||
"Open Check connection in Paperclip to see whether the event arrived and authentication passed.",
|
||
...(setupPending
|
||
? [
|
||
"During setup, deliveries only test the connection. They do not start the routine or create a task.",
|
||
"Finish setup in Paperclip to enable this webhook for future events. Test events are not replayed.",
|
||
]
|
||
: [
|
||
"This webhook is enabled. Deliveries can start the routine and create tasks.",
|
||
]),
|
||
"Store the key securely; do not put it in source control or logs.",
|
||
].join("\n");
|
||
}
|
||
export function describeSchedule(draft: TriggerDraft) {
|
||
return `${draft.frequency === "daily" ? "Every day" : draft.frequency === "weekly" ? `Every ${draft.weekday}` : "Every weekday"} at ${draft.time}`;
|
||
}
|
||
export function RoutineTriggerWizard({
|
||
initialDraft,
|
||
onSaveExit,
|
||
onFinish,
|
||
onCreateWebhook,
|
||
onRotateKey,
|
||
routineTitle,
|
||
routineId,
|
||
routineActive = true,
|
||
webhookUrl = "",
|
||
webhookSecret = "",
|
||
checkResult = "waiting",
|
||
}: {
|
||
initialDraft: TriggerDraft;
|
||
routineTitle: string;
|
||
routineId: string;
|
||
routineActive?: boolean;
|
||
webhookUrl?: string;
|
||
webhookSecret?: string;
|
||
onCreateWebhook?: (draft: TriggerDraft) => Promise<void>;
|
||
onRotateKey?: () => Promise<void>;
|
||
onSaveExit: (draft: TriggerDraft) => void | Promise<void>;
|
||
onFinish: (draft: TriggerDraft) => void | Promise<void>;
|
||
checkResult?: "waiting" | "received" | "rejected" | "no_event";
|
||
}) {
|
||
const [draft, setDraft] = useState(initialDraft);
|
||
const [busy, setBusy] = useState(false);
|
||
const [saveError, setSaveError] = useState("");
|
||
const { setBreadcrumbs } = useBreadcrumbs();
|
||
const perform = useCallback(
|
||
async (action: () => void | Promise<void>) => {
|
||
if (busy) return;
|
||
setBusy(true);
|
||
setSaveError("");
|
||
try {
|
||
await action();
|
||
} catch (error) {
|
||
setSaveError(
|
||
error instanceof Error
|
||
? error.message
|
||
: "Couldn’t save. Please try again.",
|
||
);
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
},
|
||
[busy],
|
||
);
|
||
const saveAndExit = useCallback(() => {
|
||
void perform(() => onSaveExit(draft));
|
||
}, [draft, onSaveExit, perform]);
|
||
useEffect(() => {
|
||
setBreadcrumbs([
|
||
{
|
||
label: routineTitle,
|
||
href: `/routines/${routineId}/triggers`,
|
||
onClick: (event) => {
|
||
if (
|
||
event.button !== 0 ||
|
||
event.metaKey ||
|
||
event.ctrlKey ||
|
||
event.shiftKey ||
|
||
event.altKey
|
||
)
|
||
return;
|
||
event.preventDefault();
|
||
saveAndExit();
|
||
},
|
||
},
|
||
{ label: "Add trigger" },
|
||
]);
|
||
}, [saveAndExit, setBreadcrumbs, routineTitle, routineId]);
|
||
const schedule = draft.kind === "schedule";
|
||
const github = draft.sender === "github";
|
||
const labels = schedule
|
||
? ["Choose trigger", "Set schedule", "Review schedule"]
|
||
: ["Choose trigger", "Connect your app", "Check connection"];
|
||
function patch(values: Partial<TriggerDraft>) {
|
||
setDraft((current) => ({ ...current, ...values }));
|
||
}
|
||
function advance() {
|
||
void perform(async () => {
|
||
if (draft.kind === "webhook" && draft.step === 0 && !draft.created)
|
||
await onCreateWebhook?.(draft);
|
||
const step = draft.step + 1;
|
||
patch({
|
||
step,
|
||
availableStep: Math.max(draft.availableStep, step),
|
||
created:
|
||
draft.created || (draft.kind === "webhook" && draft.step === 0),
|
||
});
|
||
});
|
||
}
|
||
const title =
|
||
draft.step === 0
|
||
? "When should this routine run?"
|
||
: schedule
|
||
? draft.step === 1
|
||
? "Set a schedule"
|
||
: "Review your schedule"
|
||
: draft.step === 1
|
||
? `Connect ${github ? "GitHub" : "your app"}`
|
||
: "Check your connection";
|
||
const subtitle =
|
||
draft.step === 0
|
||
? `Choose how to start “${routineTitle}”. You can add another trigger later.`
|
||
: schedule
|
||
? draft.step === 1
|
||
? "Choose when Paperclip should start this routine automatically."
|
||
: "This schedule starts the routine automatically. You can pause or change it later."
|
||
: draft.step === 1
|
||
? "Copy these details into the sending app, then save its webhook settings."
|
||
: "Test that events arrive and authentication works. This won’t start the routine.";
|
||
const selectClass =
|
||
"w-full rounded-md border border-input bg-background px-3 py-2 text-sm";
|
||
const goBack = (
|
||
<Button variant="outline" onClick={() => patch({ step: draft.step - 1 })}>
|
||
Back
|
||
</Button>
|
||
);
|
||
return (
|
||
<div className="min-w-0 w-full max-w-2xl space-y-6">
|
||
<SetupWizardNavigation
|
||
takeover
|
||
disabled={busy}
|
||
ariaLabel="Trigger setup progress"
|
||
labels={labels}
|
||
step={draft.step}
|
||
availableStep={draft.availableStep}
|
||
onSelect={(step) => patch({ step })}
|
||
/>
|
||
<fieldset disabled={busy} className="min-w-0 space-y-6">
|
||
<div className="space-y-1">
|
||
<h1 className="text-xl font-bold">{title}</h1>
|
||
<p className="text-sm text-muted-foreground">{subtitle}</p>
|
||
</div>
|
||
{!schedule && draft.step > 0 && <WebhookUrlWarning url={webhookUrl} />}
|
||
{draft.step === 0 && (
|
||
<fieldset className="space-y-3">
|
||
<legend className="sr-only">Trigger type</legend>
|
||
{(
|
||
[
|
||
{
|
||
kind: "schedule",
|
||
label: "On a schedule",
|
||
detail: "Every day, on weekdays, or once a week.",
|
||
Icon: CalendarClock,
|
||
},
|
||
{
|
||
kind: "webhook",
|
||
label: "When another app sends a webhook",
|
||
detail:
|
||
"When something happens in GitHub, another app, or a script.",
|
||
Icon: Webhook,
|
||
},
|
||
] as const
|
||
).map(({ kind, label, detail, Icon }) => (
|
||
<label
|
||
key={kind}
|
||
className={cn(
|
||
"flex cursor-pointer items-start gap-3 rounded-md border p-4 focus-within:ring-2 focus-within:ring-ring",
|
||
draft.kind === kind
|
||
? "border-primary bg-accent/30"
|
||
: "border-border hover:bg-accent/20",
|
||
)}
|
||
>
|
||
<input
|
||
type="radio"
|
||
name="trigger-kind"
|
||
checked={draft.kind === kind}
|
||
disabled={draft.created && kind !== draft.kind}
|
||
onChange={() =>
|
||
patch({
|
||
kind,
|
||
availableStep:
|
||
kind === draft.kind ? draft.availableStep : 0,
|
||
})
|
||
}
|
||
className="sr-only"
|
||
/>
|
||
<Icon className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||
<span className="flex-1">
|
||
<span className="block text-sm font-medium">{label}</span>
|
||
<span className="block text-xs text-muted-foreground">
|
||
{detail}
|
||
</span>
|
||
</span>
|
||
{draft.kind === kind && <Check className="h-4 w-4" />}
|
||
</label>
|
||
))}
|
||
</fieldset>
|
||
)}
|
||
{schedule && draft.step === 1 && (
|
||
<div className="space-y-5">
|
||
<div className="grid gap-4 sm:grid-cols-2">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="repeat">Repeat</Label>
|
||
<select
|
||
id="repeat"
|
||
className={selectClass}
|
||
value={draft.frequency}
|
||
onChange={(event) => patch({ frequency: event.target.value })}
|
||
>
|
||
<option value="daily">Every day</option>
|
||
<option value="weekdays">Weekdays (Monday–Friday)</option>
|
||
<option value="weekly">Every week</option>
|
||
</select>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="run-time">Time</Label>
|
||
<Input
|
||
id="run-time"
|
||
type="time"
|
||
value={draft.time}
|
||
onChange={(event) => patch({ time: event.target.value })}
|
||
/>
|
||
</div>
|
||
</div>
|
||
{draft.frequency === "weekly" && (
|
||
<div className="space-y-2">
|
||
<Label htmlFor="run-day">Day</Label>
|
||
<select
|
||
id="run-day"
|
||
className={selectClass}
|
||
value={draft.weekday}
|
||
onChange={(event) => patch({ weekday: event.target.value })}
|
||
>
|
||
{[
|
||
"Monday",
|
||
"Tuesday",
|
||
"Wednesday",
|
||
"Thursday",
|
||
"Friday",
|
||
"Saturday",
|
||
"Sunday",
|
||
].map((day) => (
|
||
<option key={day}>{day}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
)}
|
||
<div className="space-y-2">
|
||
<Label htmlFor="timezone">Time zone</Label>
|
||
<select
|
||
id="timezone"
|
||
className={selectClass}
|
||
value={draft.timezone}
|
||
onChange={(event) => patch({ timezone: event.target.value })}
|
||
>
|
||
{Array.from(
|
||
new Set([
|
||
draft.timezone,
|
||
"America/Chicago",
|
||
"America/New_York",
|
||
"America/Los_Angeles",
|
||
"Europe/London",
|
||
"UTC",
|
||
]),
|
||
).map((zone) => (
|
||
<option key={zone}>{zone}</option>
|
||
))}
|
||
</select>
|
||
<p className="text-xs text-muted-foreground">
|
||
The time follows this zone, including daylight saving changes.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{schedule && draft.step === 2 && (
|
||
<div className="space-y-5">
|
||
<div className="flex items-start gap-3 rounded-md bg-muted/40 p-4">
|
||
<CalendarClock className="h-5 w-5 text-muted-foreground" />
|
||
<div>
|
||
<p className="text-sm font-medium">{describeSchedule(draft)}</p>
|
||
<p className="text-xs text-muted-foreground">
|
||
{draft.timezone}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<p className="text-sm text-muted-foreground">
|
||
Each scheduled run creates a task for the routine’s assigned
|
||
agent. Any existing webhook triggers will continue to work.
|
||
</p>
|
||
</div>
|
||
)}
|
||
{draft.step === 0 && draft.kind === "webhook" && (
|
||
<fieldset className="space-y-2">
|
||
<legend className="mb-2 text-sm font-medium">
|
||
What’s sending the webhook?
|
||
</legend>
|
||
<div className="grid gap-2 sm:grid-cols-2">
|
||
{(
|
||
[
|
||
{
|
||
sender: "custom",
|
||
label: "Another app or script",
|
||
Icon: Globe,
|
||
},
|
||
{ sender: "github", label: "GitHub", Icon: GitBranch },
|
||
] as const
|
||
).map(({ sender, label, Icon }) => (
|
||
<label
|
||
key={sender}
|
||
className={cn(
|
||
"flex cursor-pointer items-center gap-3 rounded-md border p-3 focus-within:ring-2 focus-within:ring-ring",
|
||
draft.sender === sender
|
||
? "border-primary bg-accent/30"
|
||
: "border-border",
|
||
draft.created && "cursor-default",
|
||
)}
|
||
>
|
||
<input
|
||
className="sr-only"
|
||
type="radio"
|
||
name="sender"
|
||
checked={draft.sender === sender}
|
||
disabled={draft.created}
|
||
onChange={() => patch({ sender })}
|
||
/>
|
||
<Icon className="h-4 w-4" />
|
||
<span className="flex-1 text-sm">{label}</span>
|
||
{draft.sender === sender && <Check className="h-4 w-4" />}
|
||
</label>
|
||
))}
|
||
</div>
|
||
</fieldset>
|
||
)}
|
||
{draft.kind === "webhook" && draft.step === 0 && (
|
||
<p className="text-sm text-muted-foreground">
|
||
Public services need a publicly reachable HTTPS webhook URL.
|
||
</p>
|
||
)}
|
||
{!schedule && draft.step === 1 && (
|
||
<div className="space-y-5">
|
||
{webhookSecret && (
|
||
<AgentInstructions
|
||
value={webhookAgentInstructions(
|
||
draft.sender,
|
||
routineTitle,
|
||
webhookUrl,
|
||
webhookSecret,
|
||
true,
|
||
draft.signingMode,
|
||
)}
|
||
/>
|
||
)}
|
||
<CopyField
|
||
label={github ? "Payload URL" : "Webhook URL"}
|
||
value={webhookUrl}
|
||
/>
|
||
{!github && draft.signingMode !== "bearer" && (
|
||
<p className="text-sm text-muted-foreground">
|
||
Paste this key into your app’s signing secret field.
|
||
{draft.signingMode !== "fireflies_hmac" && <>
|
||
{" "}If your app uses custom headers instead, set Authorization to Bearer followed
|
||
by a space and this key.
|
||
</>}
|
||
</p>
|
||
)}
|
||
{webhookSecret ? (
|
||
<CopyField
|
||
label={github ? "Secret" : draft.signingMode === "bearer" ? "Authorization header value" : "Secret key"}
|
||
value={!github && draft.signingMode === "bearer" ? `Bearer ${webhookSecret}` : webhookSecret}
|
||
/>
|
||
) : (
|
||
<div className="space-y-2">
|
||
<p className="text-sm text-muted-foreground">
|
||
The key is hidden after leaving setup. If you haven’t saved it
|
||
in your app, generate a replacement.
|
||
</p>
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => void perform(() => onRotateKey?.())}
|
||
>
|
||
Generate new key
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
{!schedule && draft.step === 2 && (
|
||
<div className="space-y-5">
|
||
<div className="space-y-1 rounded-md border border-border p-4">
|
||
<p className="text-sm font-medium">Connection test only</p>
|
||
<p className="text-sm text-muted-foreground">
|
||
Events received during setup won’t start the routine or create
|
||
tasks.
|
||
</p>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<p className="text-sm font-medium">
|
||
Send an event from {github ? "GitHub" : "your app"}
|
||
</p>
|
||
<p className="text-sm text-muted-foreground">
|
||
{github
|
||
? "Open this webhook in your repository settings. Under Recent Deliveries, choose Redeliver on an event."
|
||
: "Look for “Send test” in your app’s webhook settings. If it doesn’t have one, do the action that should trigger the webhook—for example, complete a deployment."}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground">
|
||
Keep this page open to see the test result.
|
||
</p>
|
||
</div>
|
||
<div
|
||
role="status"
|
||
className="flex items-start gap-3 rounded-md bg-muted/40 p-4"
|
||
>
|
||
{checkResult === "received" ? (
|
||
<CheckCircle2 className="h-5 w-5 shrink-0 text-(--status-task-done)" />
|
||
) : checkResult === "rejected" ? (
|
||
<AlertCircle className="h-5 w-5 shrink-0 text-destructive" />
|
||
) : (
|
||
<Radio className="h-5 w-5 shrink-0 text-muted-foreground" />
|
||
)}
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium">
|
||
{checkResult === "received"
|
||
? "Test event received · Connection working"
|
||
: checkResult === "rejected"
|
||
? "Event arrived, but the key was rejected"
|
||
: checkResult === "no_event"
|
||
? "No event received yet"
|
||
: "Waiting for an event from your app…"}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground">
|
||
{checkResult === "received"
|
||
? "Authentication passed. No routine run or task was created."
|
||
: checkResult === "rejected"
|
||
? "Go back to Connect your app, update the key in your sending app, and resend. No task was created."
|
||
: "Waiting to verify delivery and authentication. The routine is not running."}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<details>
|
||
<summary className="cursor-pointer text-xs text-muted-foreground">
|
||
Troubleshoot delivery
|
||
</summary>
|
||
<div className="space-y-3 pt-3">
|
||
<p className="text-xs text-muted-foreground">
|
||
Check that the webhook is enabled in your sending app and that
|
||
its URL matches. Scripts must send POST with a JSON body.
|
||
</p>
|
||
<CopyField label="Webhook URL" value={webhookUrl} />
|
||
</div>
|
||
</details>
|
||
</div>
|
||
)}
|
||
{!schedule && draft.step === 2 && (
|
||
<p className="text-xs text-muted-foreground">
|
||
{routineActive
|
||
? "Finish setup to enable this webhook. Future events will start the routine; this test event won’t be replayed."
|
||
: "Finish setup to save this webhook. The routine is paused; enable its automatic triggers when you’re ready. This test event won’t be replayed."}
|
||
</p>
|
||
)}
|
||
{schedule && draft.step === 2 && !routineActive && (
|
||
<p className="text-sm text-muted-foreground">
|
||
The routine is paused. Enable its automatic triggers when you’re
|
||
ready to use this schedule.
|
||
</p>
|
||
)}
|
||
{saveError && (
|
||
<p role="alert" className="text-sm text-destructive">
|
||
{saveError}
|
||
</p>
|
||
)}
|
||
<SetupWizardFooter onSaveExit={saveAndExit}>
|
||
{draft.step > 0 && goBack}
|
||
{draft.step === 0 ? (
|
||
<Button disabled={draft.kind === "choose"} onClick={advance}>
|
||
Continue
|
||
</Button>
|
||
) : schedule ? (
|
||
draft.step === 1 ? (
|
||
<Button disabled={!draft.time} onClick={advance}>
|
||
Review schedule
|
||
</Button>
|
||
) : (
|
||
<Button onClick={() => void perform(() => onFinish(draft))}>
|
||
Add schedule
|
||
</Button>
|
||
)
|
||
) : draft.step === 1 ? (
|
||
<Button onClick={advance}>Check connection</Button>
|
||
) : (
|
||
<Button onClick={() => void perform(() => onFinish(draft))}>
|
||
{checkResult === "received"
|
||
? "Finish setup"
|
||
: "Finish without checking"}
|
||
</Button>
|
||
)}
|
||
</SetupWizardFooter>
|
||
</fieldset>
|
||
</div>
|
||
);
|
||
}
|