import { For, Show, createEffect, createSignal } from "solid-js"; import { AlertsService, AutomationService, CreateInboundHookRequest, CreateIntegrationRequest, FunctionsService, InboundService, IntegrationsService, UpsertAlertRuleRequest, WebhooksService, type AlertRule, type Function as PrimoraFunction, type InboundHook, type ScheduledJob, type Integration, type IntegrationAnalytics, type Webhook, type WebhookDelivery, } from "@primora/api-client"; import { demoService } from "../lib/demo-mode"; import { Badge } from "../components/Badge"; import { Modal } from "../components/Modal"; import { Input, Select } from "../components/Input"; import { IconPlus, IconRefresh, IconTrash, IconPlay, IconChevronRight, IconCheck, IconCopy, } from "../components/Icons"; interface IntegrationsPageProps { projectID?: string; canManage: boolean; demoMode: boolean; } type Tab = "integrations" | "webhooks"; const err = (e: unknown) => (e instanceof Error ? e.message : String(e)); const intSvc = (demo: boolean) => (demo ? (demoService as unknown as typeof IntegrationsService) : IntegrationsService); const hookSvc = (demo: boolean) => (demo ? (demoService as unknown as typeof WebhooksService) : WebhooksService); const fmtTime = (ts?: string | null) => (ts ? new Date(ts).toLocaleString() : "—"); const fmtNum = (n?: number) => (n == null ? "—" : n.toLocaleString()); const statusVariant = (s?: string) => s === "ok" || s === "delivered" ? "success" : s === "error" || s === "failed" ? "error" : s === "pending" ? "warning" : "neutral"; const EVENT_TYPES = ["issue.created", "deploy.marker", "webhook.test", "alert.fired", "alert.resolved"] as const; export function IntegrationsPage(props: IntegrationsPageProps) { const [tab, setTab] = createSignal("integrations"); const [integrations, setIntegrations] = createSignal([]); const [webhooks, setWebhooks] = createSignal([]); const [deliveries, setDeliveries] = createSignal>({}); const [analytics, setAnalytics] = createSignal>({}); const [analyticsSite, setAnalyticsSite] = createSignal>({}); const [expandedIntegration, setExpandedIntegration] = createSignal(); const [expandedWebhook, setExpandedWebhook] = createSignal(); const [alertRules, setAlertRules] = createSignal([]); const [inboundHooks, setInboundHooks] = createSignal([]); const [scheduledJobs, setScheduledJobs] = createSignal([]); const [showCreateIntegration, setShowCreateIntegration] = createSignal(false); const [showCreateWebhook, setShowCreateWebhook] = createSignal(false); const [showCreateAlert, setShowCreateAlert] = createSignal(false); const [showCreateInbound, setShowCreateInbound] = createSignal(false); const [inboundForm, setInboundForm] = createSignal({ name: "", mode: CreateInboundHookRequest.mode.EVENT as CreateInboundHookRequest.mode, job_id: "", function_id: "", secret: "" }); const [functions, setFunctions] = createSignal([]); const [copiedUrl, setCopiedUrl] = createSignal(""); const [alertForm, setAlertForm] = createSignal({ name: "", kind: UpsertAlertRuleRequest.kind.HEARTBEAT_SILENCE as UpsertAlertRuleRequest.kind, component: "", threshold: "5", window: "15", }); const [integrationForm, setIntegrationForm] = createSignal({ name: "", base_url: "", api_key: "", site_id: "" }); const [webhookForm, setWebhookForm] = createSignal({ url: "", secret: "", events: [] as string[], enabled: true }); const [markerForm, setMarkerForm] = createSignal({ version: "", ref: "", environment: "", note: "" }); const [createdSecret, setCreatedSecret] = createSignal(""); const [loading, setLoading] = createSignal(false); const [busy, setBusy] = createSignal(); const [message, setMessage] = createSignal(""); const [error, setError] = createSignal(""); const refresh = async () => { if (!props.projectID) return; setLoading(true); setError(""); try { const [int, hooks, rules, inbound, jobs, fns] = await Promise.all([ intSvc(props.demoMode).listIntegrations({ projectId: props.projectID }), hookSvc(props.demoMode).listWebhooks({ projectId: props.projectID }), props.demoMode ? Promise.resolve({ items: [] }) : AlertsService.listAlertRules({ projectId: props.projectID }), props.demoMode ? Promise.resolve({ items: [] }) : InboundService.listInboundHooks({ projectId: props.projectID }), props.demoMode ? Promise.resolve({ items: [] }) : AutomationService.listScheduledJobs({ projectId: props.projectID }), props.demoMode ? Promise.resolve({ items: [] }) : FunctionsService.listFunctions({ projectId: props.projectID }), ]); setIntegrations(int.items ?? []); setWebhooks(hooks.items ?? []); setAlertRules(rules.items ?? []); setInboundHooks(inbound.items ?? []); setScheduledJobs(jobs.items ?? []); setFunctions(fns.items ?? []); } catch (e) { setError(err(e)); } finally { setLoading(false); } }; createEffect(() => { if (props.projectID) void refresh(); }); const run = async (key: string, task: () => Promise, done?: string) => { setBusy(key); setMessage(""); setError(""); try { await task(); if (done) setMessage(done); await refresh(); } catch (e) { setError(err(e)); } finally { setBusy(undefined); } }; const createIntegration = (e: Event) => { e.preventDefault(); const form = integrationForm(); void run("create-integration", async () => { await intSvc(props.demoMode).createIntegration({ projectId: props.projectID!, requestBody: { name: form.name.trim(), type: CreateIntegrationRequest.type.RYBBIT, base_url: form.base_url.trim(), api_key: form.api_key.trim() || undefined, site_id: form.site_id.trim() || undefined, }, }); setShowCreateIntegration(false); setIntegrationForm({ name: "", base_url: "", api_key: "", site_id: "" }); }, "Integration created"); }; const testIntegration = (id: string) => run(`test-${id}`, async () => { const res = await intSvc(props.demoMode).testIntegration({ projectId: props.projectID!, integrationId: id }); setMessage(res.ok ? `Health check passed (${res.status_code ?? "—"})` : `Health check failed: ${res.error ?? "unknown"}`); }); const deleteIntegration = (id: string) => run( `del-int-${id}`, () => intSvc(props.demoMode).deleteIntegration({ projectId: props.projectID!, integrationId: id }), "Integration deleted", ); const loadAnalytics = async (integration: Integration, site?: string) => { if (!props.projectID) return; setBusy(`analytics-${integration.id}`); setError(""); try { const res = await intSvc(props.demoMode).getIntegrationAnalytics({ projectId: props.projectID, integrationId: integration.id, site: site || analyticsSite()[integration.id] || integration.site_id || undefined, days: 30, }); setAnalytics((c) => ({ ...c, [integration.id]: res })); } catch (e) { setError(err(e)); } finally { setBusy(undefined); } }; const toggleAnalytics = (integration: Integration) => { if (expandedIntegration() === integration.id) { setExpandedIntegration(undefined); return; } setExpandedIntegration(integration.id); void loadAnalytics(integration); }; const createWebhook = (e: Event) => { e.preventDefault(); const form = webhookForm(); void run("create-webhook", async () => { const res = await hookSvc(props.demoMode).createWebhook({ projectId: props.projectID!, requestBody: { url: form.url.trim(), secret: form.secret.trim() || undefined, events: form.events as Array<"issue.created" | "deploy.marker" | "webhook.test">, enabled: form.enabled, }, }); if (res.secret) setCreatedSecret(res.secret); setShowCreateWebhook(false); setWebhookForm({ url: "", secret: "", events: [], enabled: true }); }, "Webhook created"); }; const toggleWebhook = (hook: Webhook) => run( `toggle-${hook.id}`, () => hookSvc(props.demoMode).updateWebhook({ projectId: props.projectID!, webhookId: hook.id, requestBody: { enabled: !hook.enabled }, }), hook.enabled ? "Webhook disabled" : "Webhook enabled", ); const deleteWebhook = (id: string) => run( `del-hook-${id}`, () => hookSvc(props.demoMode).deleteWebhook({ projectId: props.projectID!, webhookId: id }), "Webhook deleted", ); const createAlertRule = (e: Event) => { e.preventDefault(); const form = alertForm(); const threshold = parseInt(form.threshold, 10) || 0; const config = form.kind === UpsertAlertRuleRequest.kind.HEARTBEAT_SILENCE ? { component: form.component.trim() || undefined, threshold_minutes: threshold } : { component: form.component.trim() || undefined, threshold_count: threshold, window_minutes: parseInt(form.window, 10) || 15 }; void run("create-alert", async () => { await AlertsService.createAlertRule({ projectId: props.projectID!, requestBody: { name: form.name.trim(), kind: form.kind, config }, }); setShowCreateAlert(false); setAlertForm({ name: "", kind: UpsertAlertRuleRequest.kind.HEARTBEAT_SILENCE, component: "", threshold: "5", window: "15" }); }, "Alert rule created"); }; const deleteAlertRule = (id: string) => run( `del-alert-${id}`, () => AlertsService.deleteAlertRule({ projectId: props.projectID!, ruleId: id }), "Alert rule deleted", ); const createInboundHook = (e: Event) => { e.preventDefault(); const form = inboundForm(); void run("create-inbound", async () => { await InboundService.createInboundHook({ projectId: props.projectID!, requestBody: { name: form.name.trim(), mode: form.mode, job_id: form.mode === CreateInboundHookRequest.mode.JOB ? form.job_id : undefined, function_id: form.mode === CreateInboundHookRequest.mode.FUNCTION ? form.function_id : undefined, secret: form.secret.trim() || undefined, }, }); setShowCreateInbound(false); setInboundForm({ name: "", mode: CreateInboundHookRequest.mode.EVENT, job_id: "", function_id: "", secret: "" }); }, "Inbound hook created"); }; const deleteInboundHook = (id: string) => run( `del-inbound-${id}`, () => InboundService.deleteInboundHook({ projectId: props.projectID!, hookId: id }), "Inbound hook deleted", ); const copyHookUrl = (hook: InboundHook) => { void navigator.clipboard?.writeText(hook.url).then(() => { setCopiedUrl(hook.id); setTimeout(() => setCopiedUrl(""), 1500); }); }; const testWebhook = (id: string) => run( `test-hook-${id}`, async () => { await hookSvc(props.demoMode).testWebhook({ projectId: props.projectID!, webhookId: id }); await loadDeliveries(id); }, "Test delivery queued", ); const loadDeliveries = async (webhookId: string) => { if (!props.projectID) return; try { const res = await hookSvc(props.demoMode).listWebhookDeliveries({ projectId: props.projectID, webhookId, limit: 25 }); setDeliveries((c) => ({ ...c, [webhookId]: res.items ?? [] })); } catch (e) { setError(err(e)); } }; const toggleDeliveries = (hook: Webhook) => { if (expandedWebhook() === hook.id) { setExpandedWebhook(undefined); return; } setExpandedWebhook(hook.id); void loadDeliveries(hook.id); }; const recordMarker = (e: Event) => { e.preventDefault(); const form = markerForm(); void run( "marker", async () => { await hookSvc(props.demoMode).createDeployMarker({ projectId: props.projectID!, requestBody: { version: form.version.trim() || undefined, ref: form.ref.trim() || undefined, environment: form.environment.trim() || undefined, note: form.note.trim() || undefined, }, }); setMarkerForm({ version: "", ref: "", environment: "", note: "" }); }, "Deploy marker recorded — deploy.marker fired", ); }; const toggleEvent = (ev: string) => setWebhookForm((c) => ({ ...c, events: c.events.includes(ev) ? c.events.filter((x) => x !== ev) : [...c.events, ev], })); return (
{message()}
{error()}
Webhook secret — shown once. {createdSecret()}
Connectors
External services attached to this project. Credentials are encrypted at rest and never leave the server.
0} fallback={

No connectors yet. Add a Rybbit instance to pull analytics into this project.

} >
{(item) => ( <> )}
Name Type Base URL Status Actions
{item.name} {item.type} {item.base_url} {item.status}
{ setAnalyticsSite((c) => ({ ...c, [item.id]: site })); void loadAnalytics(item, site); }} />
Outbound webhooks
Primora signs each delivery with X-Primora-Signature (HMAC-SHA256 over the raw body).
0} fallback={

No webhooks yet. Add an HTTPS endpoint to receive issue.created and deploy.marker events.

} >
{(hook) => ( <> )}
Endpoint Events State Actions
{hook.url} 0} fallback={all}> {(ev) => {ev}} {hook.enabled ? "enabled" : "disabled"}
Alert rules
Telemetry conditions that fire alert.fired / alert.resolved events — subscribe a webhook above to receive them.
0} fallback={

No alert rules yet. Watch for heartbeat silence or error spikes across your components.

} >
{(rule) => ( )}
Name Condition Scope State Actions
{rule.name} {rule.kind === "heartbeat_silence" ? `no heartbeat for ${rule.config.threshold_minutes ?? "?"}m` : `≥${rule.config.threshold_count ?? "?"} errors in ${rule.config.window_minutes ?? 15}m`} all components}> {rule.config.component} 0} fallback={{rule.enabled ? "watching" : "disabled"}} > firing: {rule.firing.join(", ")}
Inbound hooks
Public ingest endpoints — POST from external systems to republish as inbound.received events or trigger a scheduled job.
0} fallback={

No inbound hooks yet.

} >
{(hook) => ( )}
Name Mode Security Last received Actions
{hook.name} {hook.mode === "job" ? "job trigger" : hook.mode === "function" ? `ƒ ${functions().find((f) => f.id === hook.function_id)?.name ?? "function"}` : "event"} {" "}disabled token only}> signed {fmtTime(hook.last_received_at)}
Deploy marker
Record a deployment and fire deploy.marker to subscribed webhooks.
setMarkerForm((c) => ({ ...c, version: e.currentTarget.value }))} /> setMarkerForm((c) => ({ ...c, ref: e.currentTarget.value }))} /> setMarkerForm((c) => ({ ...c, environment: e.currentTarget.value }))} /> setMarkerForm((c) => ({ ...c, note: e.currentTarget.value }))} />
setShowCreateIntegration(false)} title="Add connector" size="md">
setIntegrationForm((c) => ({ ...c, name: e.currentTarget.value }))} /> setIntegrationForm((c) => ({ ...c, base_url: e.currentTarget.value }))} /> setIntegrationForm((c) => ({ ...c, api_key: e.currentTarget.value }))} /> setIntegrationForm((c) => ({ ...c, site_id: e.currentTarget.value }))} />
setShowCreateWebhook(false)} title="Add webhook" size="md">
setWebhookForm((c) => ({ ...c, url: e.currentTarget.value }))} /> setWebhookForm((c) => ({ ...c, secret: e.currentTarget.value }))} />
Events
{(ev) => ( )}

No selection means the endpoint receives every event.

setShowCreateAlert(false)} title="Add alert rule" size="md">
setAlertForm((c) => ({ ...c, name: e.currentTarget.value }))} /> setAlertForm((c) => ({ ...c, component: e.currentTarget.value }))} />
setAlertForm((c) => ({ ...c, threshold: e.currentTarget.value }))} /> setAlertForm((c) => ({ ...c, window: e.currentTarget.value }))} />
setShowCreateInbound(false)} title="Add inbound hook" size="md">
setInboundForm((c) => ({ ...c, name: e.currentTarget.value }))} /> setInboundForm((c) => ({ ...c, function_id: e.currentTarget.value }))} options={[ { value: "", label: "Select a function…" }, ...functions().map((f) => ({ value: f.id, label: f.name })), ]} />

The received body becomes the function's stdin payload.

setInboundForm((c) => ({ ...c, secret: e.currentTarget.value }))} />
); } function AnalyticsPanel(props: { analytics?: IntegrationAnalytics; loading: boolean; site: string; onSiteChange: (site: string) => void; }) { const stats = () => props.analytics?.overview; const stat = (label: string, value?: number) => (
{label} {fmtNum(value)}
); return (

Loading analytics…

1}>
{stat("Sessions", stats()?.sessions)} {stat("Pageviews", stats()?.pageviews)} {stat("Users", stats()?.users)} {stat("Pages/session", stats()?.pages_per_session)} {stat("Bounce %", stats()?.bounce_rate)} {stat("Session (s)", stats()?.session_duration)}

No analytics loaded.

); } function MetricTable(props: { title: string; items?: { value: string; count: number; percentage?: number }[] }) { return (
{props.title}
0} fallback={

No data in window.

}> {(item) => ( )}
{item.value} {item.count.toLocaleString()}
); } function DeliveriesPanel(props: { deliveries?: WebhookDelivery[] }) { return (
0} fallback={

No deliveries yet.

}> {(d) => ( )}
Event Status Attempts HTTP
{d.event_type} {d.status} {d.attempts} {d.last_status_code ?? "—"}
); }