paperclip/ui/src/pages/CloudUpstreamUxLab.tsx
Dotta e43b392a79
[codex] Add local Cloud Upstream sync (#6548)
## Thinking Path

> - Paperclip is the control plane for AI-agent companies.
> - Operators need a path to move local company state toward Paperclip
Cloud without losing local-first control.
> - The Cloud Upstream flow needs API, persistence, CLI, and board UI
surfaces that agree on the same manifest/run model.
> - The existing branch had the feature work plus UX and error-handling
follow-ups.
> - This pull request packages the remaining Cloud Upstream sync work
into one standalone branch.
> - The benefit is an inspectable local-to-cloud sync workflow with
preview, conflicts, activation, and captured UX review states.

## What Changed

- Added Cloud Upstream shared types, server routes/services, and
persisted run schema/migration.
- Added Paperclip Cloud CLI sync helpers and local connection storage.
- Added the Cloud Upstream board UI, settings entry points, query keys,
and UX lab page.
- Added preview/activation checklist behavior, redirect handling,
manifest-only preview support, friendly errors, in-flight hints, and
entity count summaries.

## Verification

- `pnpm --filter @paperclipai/plugin-sdk build`
- `NODE_ENV=test pnpm exec vitest run cli/src/__tests__/cloud.test.ts
server/src/__tests__/instance-settings-routes.test.ts
server/src/__tests__/instance-settings-service.test.ts
ui/src/pages/CloudUpstream.test.tsx
ui/src/components/CompanySettingsSidebar.test.tsx`
- `NODE_ENV=test pnpm exec vitest run
server/src/__tests__/cloud-upstreams.test.ts`

Worktree setup note: the isolated worktree install skipped native sqlite
build scripts, so I copied the already-built local sqlite binding from
the main checkout before running
`server/src/__tests__/cloud-upstreams.test.ts`. The test then passed.

## Risks

- Medium: this adds a database migration and a broad feature path across
CLI/server/UI.
- Merge order: this is the only PR in this split with a DB migration;
merge it before any future Cloud Upstream migration follow-up.
- Mitigation: the PR is based directly on current `origin/master`, has
targeted route/service/UI tests, and keeps the feature behind existing
experimental Cloud Sync settings.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI GPT-5 Codex via `codex_local`, tool-enabled coding session;
exact context window not exposed by this runtime.

## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, screenshot artifacts are
intentionally omitted per reviewer request
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-05-22 09:56:22 -05:00

822 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useMemo } from "react";
import {
AlertTriangle,
CheckCircle2,
CloudUpload,
ExternalLink,
FileJson,
History,
Loader2,
RefreshCcw,
ShieldAlert,
} from "lucide-react";
import type {
CloudUpstreamActivationDecision,
CloudUpstreamActivationEntityType,
CloudUpstreamConflict,
CloudUpstreamConnection,
CloudUpstreamPreview,
CloudUpstreamRun,
CloudUpstreamStep,
CloudUpstreamSummaryCount,
CloudUpstreamWarning,
} from "@paperclipai/shared";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useLocation } from "@/lib/router";
type FixtureStateKey =
| "settings-pane"
| "connect-wizard"
| "schema-mismatch"
| "preview"
| "preview-clean"
| "progress"
| "retry"
| "finish";
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: "Keep paused until cloud secrets and adapter credentials are verified.",
},
{
key: "routines",
label: "Routines",
singular: "routine",
detail: "Review schedules before enabling triggers.",
},
{
key: "monitors",
label: "Monitors",
singular: "monitor",
detail: "Activate after the target instance has been smoke tested.",
},
];
const FIXTURE_LABELS: Record<FixtureStateKey, string> = {
"settings-pane": "1 · Settings → Cloud upstream pane (enabled)",
"connect-wizard": "2 · Connect wizard — remote URL entry + PKCE launch",
"schema-mismatch": "3 · Connect wizard — schema-mismatch hard block",
preview: "4 · Preview — conflicts, warnings, planned actions",
"preview-clean": "5 · Preview — clean run with no conflicts",
progress: "6 · Durable progress — mid-run from run events",
retry: "7 · Retry without duplicating ledger entries",
finish: "8 · Finish / activation checklist with run report",
};
const PARSE_ORDER: FixtureStateKey[] = [
"settings-pane",
"connect-wizard",
"schema-mismatch",
"preview",
"preview-clean",
"progress",
"retry",
"finish",
];
export function CloudUpstreamUxLab() {
const location = useLocation();
const { state, showChrome } = useMemo(() => {
const params = new URLSearchParams(location.search);
const raw = (params.get("state") ?? "settings-pane") as FixtureStateKey;
return {
state: PARSE_ORDER.includes(raw) ? raw : "settings-pane",
showChrome: params.get("chrome") === "on",
};
}, [location.search]);
const fixture = useMemo(() => buildFixture(state), [state]);
return (
<div className="mx-auto max-w-6xl space-y-6 p-6">
{showChrome ? <FixtureNav active={state} /> : null}
<CloudUpstreamRender fixture={fixture} />
</div>
);
}
function FixtureNav({ active }: { active: FixtureStateKey }) {
return (
<div className="rounded-md border border-dashed border-border/70 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
<div className="mb-1 font-semibold uppercase tracking-wide">UX lab · cloud upstream</div>
<div className="flex flex-wrap gap-x-3 gap-y-1">
{PARSE_ORDER.map((key) => (
<a
key={key}
href={`?state=${key}`}
className={
active === key
? "rounded bg-primary/10 px-2 py-0.5 font-medium text-primary"
: "rounded px-2 py-0.5 hover:bg-accent/40"
}
>
{FIXTURE_LABELS[key]}
</a>
))}
</div>
</div>
);
}
interface Fixture {
selectedCompanyName: string;
connection: CloudUpstreamConnection | null;
preview: CloudUpstreamPreview | null;
latestRun: CloudUpstreamRun | null;
history: CloudUpstreamRun[];
notice: string | null;
actionError: string | null;
}
function CloudUpstreamRender({ fixture }: { fixture: Fixture }) {
const { connection, preview, latestRun, history, notice, actionError, selectedCompanyName } = fixture;
const activeStep: CloudUpstreamStep = latestRun?.activeStep
?? (preview ? "preview" : connection?.tokenStatus === "connected" ? "scan" : "connect");
return (
<div className="space-y-6">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-1">
<div className="flex items-center gap-2">
<CloudUpload className="h-5 w-5 text-muted-foreground" />
<h1 className="text-lg font-semibold">Cloud upstream</h1>
</div>
<p className="max-w-2xl text-sm text-muted-foreground">
Push {selectedCompanyName} into a Paperclip Cloud stack. Automations stay paused until activation.
</p>
</div>
{connection?.target.origin ? (
<Button variant="outline" size="sm" asChild>
<a href={connection.target.origin} target="_blank" rel="noreferrer">
<ExternalLink className="h-4 w-4" />
Open cloud
</a>
</Button>
) : null}
</div>
{notice ? (
<div className="rounded-md border border-emerald-500/30 bg-emerald-500/5 px-3 py-2 text-sm text-emerald-700 dark:text-emerald-300">
{notice}
</div>
) : null}
{actionError ? (
<div className="rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-sm text-destructive">
{actionError}
</div>
) : null}
<Stepper activeStep={activeStep} />
<section className="space-y-3">
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Connection</div>
<div className="rounded-md border border-border px-4 py-4">
{connection ? (
<div className="grid gap-3 lg:grid-cols-[1fr_auto] lg:items-start">
<div>
<div className="text-sm font-medium">
{connection.target.stackDisplayName ?? connection.target.stackSlug ?? connection.target.stackId}
</div>
<div className="mt-1 text-xs text-muted-foreground">
{connection.target.product} · {connection.target.origin} · token {connection.tokenStatus}
</div>
<div className="mt-2 text-xs text-muted-foreground">
Schema {connection.target.schemaMajor}. Max chunk {formatBytes(connection.target.maxChunkBytes)}.
</div>
</div>
<Button variant="outline" size="sm">
<RefreshCcw className="h-4 w-4" />
Preview push
</Button>
</div>
) : (
<div className="grid gap-3 md:grid-cols-[1fr_auto]">
<Input
defaultValue="https://paperclip.paperclip.app/PC521D/dashboard"
placeholder="https://paperclip.paperclip.app/PC521D/dashboard"
aria-label="Paperclip Cloud stack URL"
autoFocus
/>
<Button disabled>
<Loader2 className="h-4 w-4 animate-spin" />
Discovering
</Button>
</div>
)}
</div>
</section>
{preview ? (
<section className="space-y-3">
<div className="flex items-center justify-between gap-3">
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Preview</div>
<Button disabled={!preview.schemaCompatible}>
<CloudUpload className="h-4 w-4" />
Push to cloud
</Button>
</div>
<SummaryGrid summary={preview.summary} />
<WarningsPanel warnings={preview.warnings} />
<ConflictTable conflicts={preview.conflicts} />
</section>
) : null}
{latestRun ? (
<section className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Progress and finish</div>
<div className="flex flex-wrap gap-2">
<Button variant="outline" size="sm">
<FileJson className="h-4 w-4" />
Download report
</Button>
{latestRun.status === "failed" || latestRun.status === "cancelled" ? (
<Button variant="outline" size="sm">
<RefreshCcw className="h-4 w-4" />
Retry
</Button>
) : latestRun.status === "succeeded" ? (
<Button variant="outline" size="sm">
<RefreshCcw className="h-4 w-4" />
Re-run
</Button>
) : null}
</div>
</div>
<div className="rounded-md border border-border px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-sm font-medium capitalize">{latestRun.status}</div>
<div className="mt-1 text-xs text-muted-foreground">
Run {latestRun.id.slice(0, 8)} · {latestRun.completedAt
? `completed ${formatDate(latestRun.completedAt)}`
: latestRun.status === "running"
? "in progress"
: "in progress"}
</div>
</div>
<div className="text-sm tabular-nums">{latestRun.progressPercent}%</div>
</div>
<div className="mt-3 h-2 rounded-full bg-muted">
<div className="h-2 rounded-full bg-primary" style={{ width: `${latestRun.progressPercent}%` }} />
</div>
<div className="mt-4 divide-y divide-border">
{latestRun.events.map((event) => (
<div key={event.id} className="grid gap-2 py-2 text-sm sm:grid-cols-[7rem_8rem_1fr]">
<span className="text-xs text-muted-foreground">{formatDate(event.at)}</span>
<span className="text-xs capitalize text-muted-foreground">{event.phase}</span>
<span>{event.message}</span>
</div>
))}
</div>
</div>
{latestRun.status === "succeeded" ? <ActivationChecklist run={latestRun} /> : null}
</section>
) : null}
{history.length ? (
<section className="space-y-3">
<div className="flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
<History className="h-3.5 w-3.5" />
History
</div>
<div className="divide-y divide-border rounded-md border border-border">
{history.map((run) => (
<div
key={run.id}
className="grid w-full gap-1 px-4 py-3 text-left text-sm hover:bg-accent/40 sm:grid-cols-[1fr_auto]"
>
<span>Run {run.id.slice(0, 8)} · {run.status}</span>
<span className="text-xs text-muted-foreground">{formatDate(run.createdAt)}</span>
</div>
))}
</div>
</section>
) : null}
</div>
);
}
function Stepper({ activeStep }: { activeStep: CloudUpstreamStep }) {
const activeIndex = STEPS.findIndex((step) => step.key === activeStep);
return (
<div className="grid gap-2 rounded-md border border-border px-3 py-3 sm:grid-cols-6">
{STEPS.map((step, index) => {
const complete = index < activeIndex;
const active = index === activeIndex;
return (
<div key={step.key} className="flex items-center gap-2 text-xs">
{complete ? (
<CheckCircle2 className="h-4 w-4 text-emerald-600" />
) : (
<span className={active ? "h-4 w-4 rounded-full border-2 border-primary" : "h-4 w-4 rounded-full border border-border"} />
)}
<span className={active ? "font-medium text-foreground" : "text-muted-foreground"}>{step.label}</span>
</div>
);
})}
</div>
);
}
function SummaryGrid({ summary }: { summary: CloudUpstreamSummaryCount[] }) {
return (
<div className="grid gap-2 sm:grid-cols-4">
{summary.map((item) => (
<div key={item.key} className="rounded-md border border-border px-3 py-2">
<div className="text-lg font-semibold tabular-nums">{item.count}</div>
<div className="text-xs text-muted-foreground">{item.label}</div>
</div>
))}
</div>
);
}
function WarningsPanel({ warnings }: { warnings: CloudUpstreamWarning[] }) {
return (
<div className="rounded-md border border-border px-4 py-3">
<div className="mb-2 flex items-center gap-2 text-sm font-medium">
<ShieldAlert className="h-4 w-4 text-muted-foreground" />
Warnings
</div>
<div className="divide-y divide-border">
{warnings.map((warning) => (
<div key={warning.code} className="grid gap-2 py-2 sm:grid-cols-[1.25rem_12rem_1fr]">
<AlertTriangle className={warning.severity === "blocker" ? "h-4 w-4 text-destructive" : "h-4 w-4 text-amber-600"} />
<div className="text-sm font-medium">{warning.title}</div>
<div className="text-sm text-muted-foreground">{warning.detail}</div>
</div>
))}
</div>
</div>
);
}
function ConflictTable({ conflicts }: { conflicts: CloudUpstreamConflict[] }) {
return (
<div className="rounded-md border border-border px-4 py-3">
<div className="mb-2 text-sm font-medium">Conflicts</div>
{conflicts.length === 0 ? (
<div className="text-sm text-muted-foreground">No target conflicts detected for this preview.</div>
) : (
<div className="divide-y divide-border">
{conflicts.map((conflict) => (
<div key={conflict.id} className="grid gap-2 py-2 text-sm sm:grid-cols-[8rem_1fr_1fr_8rem]">
<span className="text-muted-foreground">{conflict.entityType}</span>
<span>{conflict.sourceLabel}</span>
<span>{conflict.targetLabel}</span>
<span className="capitalize">{conflict.plannedAction}</span>
</div>
))}
</div>
)}
</div>
);
}
function ActivationChecklist({ run }: { run: CloudUpstreamRun }) {
const rows = buildActivationRows(run);
return (
<div className="rounded-md border border-border px-4 py-3">
<div className="mb-2 text-sm font-medium">Activation checklist</div>
<div className="divide-y divide-border">
{rows.map((row) => {
const activated = row.status === "activated";
return (
<div key={row.key} className="grid gap-2 py-2 text-sm sm:grid-cols-[8rem_1fr_auto] sm:items-center">
<div>
<div className="font-medium">{row.label}</div>
<div className="text-xs text-muted-foreground">{row.statusLabel}</div>
</div>
<div className="text-muted-foreground">
{row.count === 0 ? `0 imported ${row.pluralLabel} in this run.` : row.detail}
</div>
<div className="flex flex-wrap gap-2 sm:justify-end">
<Button variant={activated ? "secondary" : "default"} size="sm" disabled={row.count === 0 || activated}>
{activated ? "Activated" : "Activate"}
</Button>
<Button variant="ghost" size="sm" disabled={activated}>
Keep paused
</Button>
</div>
</div>
);
})}
</div>
</div>
);
}
function buildActivationRows(run: CloudUpstreamRun) {
const decisions = decisionsFromReport(run.report);
return ACTIVATION_CATEGORIES.map((category) => {
const decision = decisions[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 decisionsFromReport(report: Record<string, unknown>): Partial<Record<CloudUpstreamActivationEntityType, CloudUpstreamActivationDecision>> {
const value = optionalRecord(report.activationChecklist);
const decisions: Partial<Record<CloudUpstreamActivationEntityType, CloudUpstreamActivationDecision>> = {};
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 summaryCount(summary: CloudUpstreamSummaryCount[], key: CloudUpstreamActivationEntityType): number {
return summary.find((item) => item.key === key)?.count ?? 0;
}
function optionalRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function formatDate(value: string) {
return new Date(value).toLocaleString(undefined, {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
timeZone: "UTC",
});
}
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`;
}
const STACK_TARGET = {
stackId: "stk_2vKqz9D8mNFqQ7Rp",
stackSlug: "paperclip-prod",
stackDisplayName: "Paperclip Prod",
companyId: "co_4hT2yX",
primaryHost: "paperclip.paperclip.app",
origin: "https://paperclip.paperclip.app",
product: "paperclip-cloud",
schemaMajor: 7,
maxChunkBytes: 5 * 1024 * 1024,
};
const STACK_TARGET_SCHEMA_BEHIND = {
...STACK_TARGET,
schemaMajor: 5,
};
function connectedConnection(target = STACK_TARGET): CloudUpstreamConnection {
return {
id: "cu_conn_8d3f1b6a",
companyId: "co_4hT2yX",
remoteUrl: "https://paperclip.paperclip.app/PC521D/dashboard",
target,
tokenStatus: "connected",
scopes: ["upstream.push", "upstream.preview"],
authorizedGlobalUserId: "user_9pXqYzAbCdEf",
expiresAt: "2026-08-18T19:00:00.000Z",
createdAt: "2026-05-18T18:45:00.000Z",
updatedAt: "2026-05-18T19:02:18.000Z",
lastRunId: null,
};
}
const PREVIEW_SUMMARY: CloudUpstreamSummaryCount[] = [
{ key: "users", label: "Users", count: 14 },
{ key: "agents", label: "Agents", count: 6 },
{ key: "routines", label: "Routines", count: 4 },
{ key: "monitors", label: "Monitors", count: 2 },
];
const PREVIEW_WARNINGS_NORMAL: CloudUpstreamWarning[] = [
{
code: "imported_automations_paused",
severity: "warning",
title: "Automations stay paused",
detail: "Imported agents, routines, and monitors require explicit activation after the push.",
},
{
code: "unmatched_users_import_as_historical_authors",
severity: "warning",
title: "Unmatched users become historical authors",
detail: "Invite now remains a secondary action after the transfer is complete.",
},
{
code: "secret_values_redacted",
severity: "warning",
title: "Secret values are not transferred",
detail: "The push carries secret requirements only. Configure cloud secrets before activating automations.",
},
];
const PREVIEW_WARNINGS_SCHEMA: CloudUpstreamWarning[] = [
{
code: "schema_mismatch",
severity: "blocker",
title: "Cloud stack upgrade required",
detail: "This local build uses upstream schema 7, but the cloud stack reports schema 5.",
},
...PREVIEW_WARNINGS_NORMAL,
];
const PREVIEW_CONFLICTS: CloudUpstreamConflict[] = [
{
id: "conflict_user_serena",
entityType: "user",
sourceLabel: "serena@magicmachine.co (unmatched)",
targetLabel: "→ historical author Serena R.",
plannedAction: "create",
reason: "Target stack has no matching identity. Will arrive as historical author; invite available after push.",
},
{
id: "conflict_user_dotta",
entityType: "user",
sourceLabel: "dotta@magicmachine.co",
targetLabel: "↦ dotta@magicmachine.co (cloud)",
plannedAction: "update",
reason: "Existing cloud identity matches local user; will be merged.",
},
{
id: "conflict_agent_qa",
entityType: "agent",
sourceLabel: "QA · qa-bot",
targetLabel: "↦ QA · qa-bot (cloud)",
plannedAction: "update",
reason: "Mapped to existing cloud agent. Imported run history will be appended.",
},
{
id: "conflict_routine_nightly_reports",
entityType: "routine",
sourceLabel: "Nightly status report",
targetLabel: "(new in cloud)",
plannedAction: "create",
reason: "Routine does not exist in the target stack and will be created in paused state.",
},
];
function basePreview(): CloudUpstreamPreview {
return {
connectionId: "cu_conn_8d3f1b6a",
sourceCompanyId: "co_local_pc521d",
target: STACK_TARGET,
schemaCompatible: true,
summary: PREVIEW_SUMMARY,
warnings: PREVIEW_WARNINGS_NORMAL,
conflicts: PREVIEW_CONFLICTS,
generatedAt: "2026-05-18T19:03:14.000Z",
};
}
function schemaMismatchPreview(): CloudUpstreamPreview {
return {
...basePreview(),
target: STACK_TARGET_SCHEMA_BEHIND,
schemaCompatible: false,
summary: [],
conflicts: [],
warnings: PREVIEW_WARNINGS_SCHEMA,
};
}
function cleanPreview(): CloudUpstreamPreview {
return {
...basePreview(),
conflicts: [],
warnings: PREVIEW_WARNINGS_NORMAL.slice(0, 1),
};
}
const PROGRESS_EVENTS = [
{ id: "evt_01", at: "2026-05-18T19:10:02.000Z", phase: "scan" as CloudUpstreamStep, type: "completed" as const, message: "Scanned 14 users, 6 agents, 4 routines, 2 monitors." },
{ id: "evt_02", at: "2026-05-18T19:10:11.000Z", phase: "preview" as CloudUpstreamStep, type: "completed" as const, message: "Preview generated with 4 conflicts and 3 warnings." },
{ id: "evt_03", at: "2026-05-18T19:10:31.000Z", phase: "push" as CloudUpstreamStep, type: "created" as const, message: "users · 8 created, 6 mapped to existing identities." },
{ id: "evt_04", at: "2026-05-18T19:10:48.000Z", phase: "push" as CloudUpstreamStep, type: "updated" as const, message: "agents · 4 created paused, 2 updated paused." },
{ id: "evt_05", at: "2026-05-18T19:10:58.000Z", phase: "push" as CloudUpstreamStep, type: "updated" as const, message: "routines · 3 created paused, 1 updated." },
{ id: "evt_06", at: "2026-05-18T19:11:09.000Z", phase: "push" as CloudUpstreamStep, type: "created" as const, message: "monitors · 2 created paused." },
{ id: "evt_07", at: "2026-05-18T19:11:18.000Z", phase: "verify" as CloudUpstreamStep, type: "updated" as const, message: "Verifying transferred ledger checksums…" },
];
function runningRun(): CloudUpstreamRun {
return {
id: "run_3kQ8mNpW9bX2zL4Y",
connectionId: "cu_conn_8d3f1b6a",
companyId: "co_local_pc521d",
status: "running",
activeStep: "push",
progressPercent: 62,
dryRun: false,
summary: PREVIEW_SUMMARY,
warnings: PREVIEW_WARNINGS_NORMAL,
conflicts: PREVIEW_CONFLICTS,
events: PROGRESS_EVENTS,
targetUrl: "https://paperclip.paperclip.app/PC521D/dashboard",
report: {},
retryOfRunId: null,
createdAt: "2026-05-18T19:10:01.000Z",
updatedAt: "2026-05-18T19:11:18.000Z",
completedAt: null,
};
}
function failedRun(): CloudUpstreamRun {
return {
id: "run_5fXqR2bT7aD8zP1K",
connectionId: "cu_conn_8d3f1b6a",
companyId: "co_local_pc521d",
status: "failed",
activeStep: "push",
progressPercent: 78,
dryRun: false,
summary: PREVIEW_SUMMARY,
warnings: PREVIEW_WARNINGS_NORMAL,
conflicts: PREVIEW_CONFLICTS,
events: [
...PROGRESS_EVENTS,
{
id: "evt_08",
at: "2026-05-18T19:11:30.000Z",
phase: "push",
type: "failed",
message: "Apply rejected: cloud rejected chunk 4 of 6 (HTTP 502). Ledger entries from chunks 13 retained; chunk 4 not committed.",
},
],
targetUrl: "https://paperclip.paperclip.app/PC521D/dashboard",
report: { ledgerCheckpoint: "chunk-3" },
retryOfRunId: null,
createdAt: "2026-05-18T19:10:01.000Z",
updatedAt: "2026-05-18T19:11:30.000Z",
completedAt: null,
};
}
function succeededRun(): CloudUpstreamRun {
return {
id: "run_7aBcD9eFgH2iJ3kL",
connectionId: "cu_conn_8d3f1b6a",
companyId: "co_local_pc521d",
status: "succeeded",
activeStep: "activate",
progressPercent: 100,
dryRun: false,
summary: PREVIEW_SUMMARY,
warnings: PREVIEW_WARNINGS_NORMAL,
conflicts: PREVIEW_CONFLICTS,
events: [
...PROGRESS_EVENTS,
{
id: "evt_08",
at: "2026-05-18T19:11:25.000Z",
phase: "verify",
type: "completed",
message: "Ledger checksums match. Push committed.",
},
{
id: "evt_09",
at: "2026-05-18T19:11:31.000Z",
phase: "activate",
type: "completed",
message: "Activation checklist pending operator approval — automations remain paused.",
},
],
targetUrl: "https://paperclip.paperclip.app/PC521D/dashboard",
report: {
activationChecklist: {
agents: { count: 6, status: "paused", activatedAt: null },
routines: { count: 4, status: "paused", activatedAt: null },
monitors: { count: 2, status: "paused", activatedAt: null },
},
},
retryOfRunId: null,
createdAt: "2026-05-18T19:10:01.000Z",
updatedAt: "2026-05-18T19:11:31.000Z",
completedAt: "2026-05-18T19:11:31.000Z",
};
}
function buildFixture(state: FixtureStateKey): Fixture {
switch (state) {
case "settings-pane":
return {
selectedCompanyName: "Paperclip · PC521D",
connection: connectedConnection(),
preview: null,
latestRun: null,
history: [],
notice: "Cloud upstream connection approved.",
actionError: null,
};
case "connect-wizard":
return {
selectedCompanyName: "Paperclip · PC521D",
connection: null,
preview: null,
latestRun: null,
history: [],
notice: null,
actionError: null,
};
case "schema-mismatch":
return {
selectedCompanyName: "Paperclip · PC521D",
connection: connectedConnection(STACK_TARGET_SCHEMA_BEHIND),
preview: schemaMismatchPreview(),
latestRun: null,
history: [],
notice: null,
actionError: "Cloud stack is on schema 5 but this local build pushes schema 7. Upgrade the cloud stack to continue.",
};
case "preview":
return {
selectedCompanyName: "Paperclip · PC521D",
connection: connectedConnection(),
preview: basePreview(),
latestRun: null,
history: [],
notice: null,
actionError: null,
};
case "preview-clean":
return {
selectedCompanyName: "Paperclip · PC521D",
connection: connectedConnection(),
preview: cleanPreview(),
latestRun: null,
history: [],
notice: "Preview completed. No target conflicts detected.",
actionError: null,
};
case "progress":
return {
selectedCompanyName: "Paperclip · PC521D",
connection: connectedConnection(),
preview: null,
latestRun: runningRun(),
history: [],
notice: null,
actionError: null,
};
case "retry":
return {
selectedCompanyName: "Paperclip · PC521D",
connection: connectedConnection(),
preview: null,
latestRun: failedRun(),
history: [
{ ...failedRun(), id: "run_9pYqXwVtSrQ" },
],
notice: null,
actionError: "Push run failed. Review the events. Retry resumes from ledger checkpoint chunk-3 — chunks 13 will not be re-applied.",
};
case "finish":
return {
selectedCompanyName: "Paperclip · PC521D",
connection: connectedConnection(),
preview: null,
latestRun: succeededRun(),
history: [
{ ...succeededRun(), id: "run_aZcXvBnMqWeR" },
],
notice: "Push run completed. Review activation before unpausing automations.",
actionError: null,
};
}
}