import { useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, CheckCircle2, CloudUpload, ExternalLink, FileJson, History, Loader2, RefreshCcw, ShieldAlert, } from "lucide-react"; import type { CloudUpstreamActivationDecision, CloudUpstreamActivationEntityType, CloudUpstreamPreview, CloudUpstreamRun, CloudUpstreamStep, } from "@paperclipai/shared"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { cloudUpstreamsApi } from "@/api/cloudUpstreams"; import { instanceSettingsApi } from "@/api/instanceSettings"; import { useBreadcrumbs } from "@/context/BreadcrumbContext"; import { useCompany } from "@/context/CompanyContext"; import { applyCompanyPrefix, extractCompanyPrefixFromPath } from "@/lib/company-routes"; import { Link, useLocation } from "@/lib/router"; import { queryKeys } from "@/lib/queryKeys"; const PENDING_CONNECTION_KEY = "paperclip-cloud-upstream-pending-connection"; const STEPS: Array<{ key: CloudUpstreamStep; label: string }> = [ { key: "connect", label: "Connect" }, { key: "scan", label: "Scan" }, { key: "preview", label: "Preview" }, { key: "push", label: "Push" }, { key: "verify", label: "Verify" }, { key: "activate", label: "Activate" }, ]; const ACTIVATION_CATEGORIES: Array<{ key: CloudUpstreamActivationEntityType; label: string; singular: string; detail: string; }> = [ { key: "agents", label: "Agents", singular: "agent", detail: "Confirm cloud secrets and adapter credentials before unpausing imported agents.", }, { key: "routines", label: "Routines", singular: "routine", detail: "Review schedules and trigger settings before enabling imported routines.", }, { key: "monitors", label: "Monitors", singular: "monitor", detail: "Activate after the target stack has been smoke tested.", }, ]; export function CloudUpstream() { const { selectedCompany, selectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); const location = useLocation(); const [remoteUrl, setRemoteUrl] = useState(""); const [preview, setPreview] = useState(null); const [activeRun, setActiveRun] = useState(null); const [notice, setNotice] = useState(null); const [actionError, setActionError] = useState(null); useEffect(() => { setBreadcrumbs([ { label: selectedCompany?.name ?? "Company", href: "/dashboard" }, { label: "Settings", href: "/company/settings" }, { label: "Cloud upstream" }, ]); }, [selectedCompany?.name, setBreadcrumbs]); const experimentalQuery = useQuery({ queryKey: queryKeys.instance.experimentalSettings, queryFn: () => instanceSettingsApi.getExperimental(), }); const cloudSyncEnabled = experimentalQuery.data?.enableCloudSync === true; const upstreamQuery = useQuery({ queryKey: selectedCompanyId ? queryKeys.cloudUpstreams(selectedCompanyId) : ["cloud-upstreams", "__disabled__"], queryFn: () => cloudUpstreamsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId && cloudSyncEnabled, }); const connection = upstreamQuery.data?.connections[0] ?? null; const latestRun = activeRun ?? upstreamQuery.data?.runs[0] ?? null; const callbackParams = useMemo(() => new URLSearchParams(location.search), [location.search]); const code = callbackParams.get("code"); const state = callbackParams.get("state"); const callbackError = callbackParams.get("error"); const settingsPath = useMemo(() => { const pathPrefix = extractCompanyPrefixFromPath(location.pathname); return applyCompanyPrefix("/company/settings/cloud-upstream", pathPrefix ?? selectedCompany?.issuePrefix ?? null); }, [location.pathname, selectedCompany?.issuePrefix]); const finishMutation = useMutation({ mutationFn: (input: { pendingConnectionId: string; code: string; state: string }) => cloudUpstreamsApi.finishConnect(input), onSuccess: async () => { localStorage.removeItem(PENDING_CONNECTION_KEY); setNotice("Cloud upstream connection approved."); setActionError(null); await invalidateUpstreams(); window.history.replaceState(null, "", settingsPath); }, onError: (error) => setActionError(error instanceof Error ? error.message : "Failed to finish connection."), }); const { mutate: finishConnect, isError: finishConnectFailed, isPending: finishConnectPending, isSuccess: finishConnectSucceeded, } = finishMutation; useEffect(() => { if (!cloudSyncEnabled || !code || !state || finishConnectPending || finishConnectSucceeded || finishConnectFailed) return; const pendingConnectionId = localStorage.getItem(PENDING_CONNECTION_KEY); if (!pendingConnectionId) { setActionError("No pending cloud upstream connection was found. Start the connection again."); return; } finishConnect({ pendingConnectionId, code, state }); }, [cloudSyncEnabled, code, finishConnect, finishConnectFailed, finishConnectPending, finishConnectSucceeded, state]); useEffect(() => { if (callbackError) { setActionError(`Cloud upstream connection was not approved: ${callbackError}`); } }, [callbackError]); const startMutation = useMutation({ mutationFn: () => cloudUpstreamsApi.startConnect({ companyId: selectedCompanyId!, remoteUrl, redirectUri: `${window.location.origin}${settingsPath}`, }), onSuccess: (result) => { localStorage.setItem(PENDING_CONNECTION_KEY, result.pendingConnectionId); setActionError(null); window.location.assign(result.authorizationUrl); }, onError: (error) => setActionError(error instanceof Error ? error.message : "Failed to start connection."), }); const previewMutation = useMutation({ mutationFn: (input: { connectionId: string; companyId: string }) => cloudUpstreamsApi.preview(input.connectionId, { companyId: input.companyId }), onSuccess: (nextPreview) => { setPreview(nextPreview); setActionError(null); }, onError: (error) => setActionError(previewErrorMessage(error)), }); const runMutation = useMutation({ mutationFn: (input: { connectionId: string; companyId: string; retryOfRunId?: string | null }) => cloudUpstreamsApi.createRun(input.connectionId, { companyId: input.companyId, retryOfRunId: input.retryOfRunId ?? null, }), onSuccess: async (run) => { setActiveRun(run); setNotice(run.status === "succeeded" ? "Push run completed. Review activation before unpausing automations." : "Push run failed. Review the run events and retry after correcting the issue."); setActionError(null); await invalidateUpstreams(); }, onError: (error) => setActionError(error instanceof Error ? error.message : "Failed to run push."), }); const activationMutation = useMutation({ mutationFn: (input: { run: CloudUpstreamRun; entityType: CloudUpstreamActivationEntityType }) => cloudUpstreamsApi.activateEntities(input.run.connectionId, input.run.id, { companyId: input.run.companyId, entityType: input.entityType, }), onSuccess: async (run) => { setActiveRun(run); setNotice("Activation checklist updated."); setActionError(null); await invalidateUpstreams(); }, onError: (error) => setActionError(error instanceof Error ? error.message : "Failed to activate imported entities."), }); async function invalidateUpstreams() { if (!selectedCompanyId) return; await queryClient.invalidateQueries({ queryKey: queryKeys.cloudUpstreams(selectedCompanyId) }); } if (!selectedCompanyId || !selectedCompany) { return
Select a company to configure cloud upstream.
; } if (experimentalQuery.isLoading) { return
Loading experimental settings...
; } if (!cloudSyncEnabled) { return (

Cloud upstream

Cloud sync is disabled. Enable it in{" "} Instance Settings {" "} to show upstream connection and push tools.
); } return (

Cloud upstream

Push {selectedCompany.name} into a Paperclip Cloud stack. Automations stay paused until activation.

{connection?.target.origin ? ( ) : null}
{notice ? (
{notice}
) : null} {actionError ? (
{actionError}
) : null}
Connection
{connection ? (
{connection.target.stackDisplayName ?? connection.target.stackSlug ?? connection.target.stackId}
{connection.target.product} · {connection.target.origin} · token {connection.tokenStatus}
Schema {connection.target.schemaMajor}. Max chunk {formatBytes(connection.target.maxChunkBytes)}.
{previewMutation.isPending ? : null}
) : (
setRemoteUrl(event.target.value)} placeholder="https://paperclip.paperclip.app/PC521D/dashboard" aria-label="Paperclip Cloud stack URL" />
)}
{preview ? (
Preview
) : null} {latestRun ? (
Progress and finish
{latestRun.status === "failed" || latestRun.status === "cancelled" ? ( ) : latestRun.status === "succeeded" ? ( ) : null}
{latestRun.status}
Run {latestRun.id.slice(0, 8)} · {latestRun.completedAt ? `completed ${formatDate(latestRun.completedAt)}` : "in progress"}
{latestRun.progressPercent}%
{latestRun.events.map((event) => (
{formatDate(event.at)} {event.phase} {event.message}
))}
{latestRun.status === "succeeded" ? ( activationMutation.mutate({ run: latestRun, entityType })} /> ) : null}
) : null} {upstreamQuery.data?.runs.length ? (
History
{upstreamQuery.data.runs.map((run) => ( ))}
) : null}
); } function PreviewProgressHint() { const [elapsed, setElapsed] = useState(0); useEffect(() => { const startedAt = Date.now(); const interval = window.setInterval(() => setElapsed(Math.round((Date.now() - startedAt) / 1000)), 1000); return () => window.clearInterval(interval); }, []); const message = elapsed < 15 ? "Building manifest..." : elapsed < 45 ? `Building manifest... ${elapsed}s. Large companies can take up to a minute.` : `Still building manifest... ${elapsed}s. PAP-scale companies routinely take ~60s.`; return
{message}
; } function Stepper({ activeStep }: { activeStep: CloudUpstreamStep }) { const activeIndex = STEPS.findIndex((step) => step.key === activeStep); return (
{STEPS.map((step, index) => { const complete = index < activeIndex; const active = index === activeIndex; return (
{complete ? ( ) : ( )} {step.label}
); })}
); } function SummaryGrid({ summary }: { summary: CloudUpstreamPreview["summary"] }) { return (
{summary.map((item) => (
{item.count}
{item.label}
))}
); } function WarningsPanel({ warnings }: { warnings: CloudUpstreamPreview["warnings"] }) { return (
Warnings
{warnings.map((warning) => (
{warning.title}
{warning.detail}
))}
); } function ConflictTable({ conflicts }: { conflicts: CloudUpstreamPreview["conflicts"] }) { return (
Conflicts
{conflicts.length === 0 ? (
No target conflicts detected for this preview.
) : (
{conflicts.map((conflict) => (
{conflict.entityType} {conflict.sourceLabel} {conflict.targetLabel} {conflict.plannedAction}
))}
)}
); } function ActivationChecklist({ run, pendingEntityType, isPending, onActivate, }: { run: CloudUpstreamRun; pendingEntityType: CloudUpstreamActivationEntityType | null; isPending: boolean; onActivate: (entityType: CloudUpstreamActivationEntityType) => void; }) { const rows = buildActivationRows(run); return (
Activation checklist
{rows.map((row) => { const pending = isPending && pendingEntityType === row.key; const activated = row.status === "activated"; return (
{row.label}
{row.statusLabel}
{row.count === 0 ? `0 imported ${row.pluralLabel} in this run.` : row.detail}
); })}
); } export function buildActivationRows(run: CloudUpstreamRun) { const activationChecklist = activationChecklistFromReport(run.report); return ACTIVATION_CATEGORIES.map((category) => { const decision = activationChecklist[category.key]; const count = summaryCount(run.summary, category.key); const status = decision?.status === "activated" ? "activated" : "paused"; const pluralLabel = `${category.singular}${count === 1 ? "" : "s"}`; return { ...category, count, pluralLabel, status, detail: `${count} imported ${pluralLabel} are paused by default. ${category.detail}`, statusLabel: status === "activated" ? `${count} activated` : count === 0 ? "0 imported" : `${count} paused`, }; }); } function summaryCount(summary: CloudUpstreamRun["summary"], key: CloudUpstreamActivationEntityType): number { return summary.find((item) => item.key === key)?.count ?? 0; } function activationChecklistFromReport(report: CloudUpstreamRun["report"]): Partial> { const value = optionalRecord(report.activationChecklist); const decisions: Partial> = {}; for (const key of ["agents", "routines", "monitors"] as const) { const item = optionalRecord(value[key]); if (!item) continue; decisions[key] = { entityType: key, count: typeof item.count === "number" ? item.count : 0, status: item.status === "activated" ? "activated" : "paused", activatedAt: typeof item.activatedAt === "string" ? item.activatedAt : null, }; } return decisions; } function optionalRecord(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; } function downloadRunReport(run: CloudUpstreamRun) { const blob = new Blob([JSON.stringify(run.report, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = `cloud-upstream-run-${run.id}.json`; anchor.click(); URL.revokeObjectURL(url); } function formatDate(value: string) { return new Date(value).toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); } function formatBytes(value: number) { if (value >= 1024 * 1024) return `${Math.round(value / (1024 * 1024))} MiB`; if (value >= 1024) return `${Math.round(value / 1024)} KiB`; return `${value} B`; } function previewErrorMessage(error: unknown): string { const code = error instanceof Error ? error.message : null; if (code === "payload_too_large" || code === "bad_request") { return "Local company is too large to preview as a single request. Click Push to continue (the Push step uploads in chunks), or see the docs for chunked-preview options."; } return code ?? "Failed to preview push."; }