mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 23:43:16 +01:00
## Thinking Path > - Paperclip is the open source app that people use to manage AI agents for work. > - Operators use the settings area to control a company and its Paperclip instance. > - The current navigation separates related settings and uses duplicate instance pages. > - Company exports also do independent reads in sequence and do extra work for previews. > - Hardened workspace commands can differ from their saved command after loopback binding. > - This pull request makes these related operator workflows consistent and faster. > - The benefit is one clear settings area, faster exports, and stable runtime command matching. ## Linked Issues or Issue Description Refs #338 Related: #9834 **What existing behavior does this improve?** This improves the company settings UI, company export preparation, and workspace runtime command matching. **Current behavior** Company and instance settings use separate navigation and duplicate pages. Export preparation reads many independent records in sequence. Preview generation can also build an unused organization image. A command with a forced loopback bind can fail to match its saved runtime command. **Proposed behavior** Use one settings navigation and put general instance controls on the company General page. Load independent export data with bounded concurrency, skip unused preview image work, and load the export page only when it is needed. Treat the loopback-bound form of a command as the same runtime command. **Reason and benefit** Operators get one clear settings area. Large company exports need fewer serialized reads. Export previews and initial UI loads do less work. Hardened runtime services remain linked to their saved command definitions. **Breaking changes** The obsolete instance General URL redirects to the unified settings page. Access and Heartbeats remain available, and legacy bookmarks keep their destinations. No API response shape or database schema changes. ## What Changed - Unified company and instance settings navigation and removed duplicate instance settings pages. - Embedded general instance controls in the company General page and kept access-sensitive navigation behavior. - Preserved instance Access and Heartbeats controls in the unified navigation and normalized old bookmarks to those destinations. - Improved environment and access-state handling when workspace seed requests overlap. - Added bounded export reads, a lighter preview path, deferred export preparation, and lazy export-page loading. - Matched loopback-bound runtime commands to their saved command definitions. - Added focused shared, server, and UI regression tests. ## Verification - `pnpm exec vitest run <18 changed test files>`: 18 files and 256 tests passed. - `pnpm check:token-gates`: passed all four token gates. - `pnpm -r typecheck`: passed for all workspace projects. - `pnpm build`: passed for all workspace projects. - `pnpm test:run`: tests ran without a reported failure, but the runner did not close after the server handoff tests. The process closed with status 0 after an interrupt. - Focused latest-head route tests: 2 files and 4 tests passed. - GitHub latest-head checks: all completed without failure. - Greptile: 5/5 with no unresolved review threads. ## Risks - Medium risk: settings routes and navigation changed across several operator roles. - Medium risk: bounded export concurrency increases simultaneous database reads. The limits stay below the normal pool size. - Low risk: runtime command matching accepts only the known Tailscale HTTPS loopback transformation. - No migrations are included. > 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 Codex with a GPT-5-family coding model. The runtime does not expose the exact deployed model ID or context-window size. Reasoning, tool use, and local code execution were enabled. ## 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) - [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details Exception: This task requires the existing execution branch. The harness does not permit a branch rename. - [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>
249 lines
9.4 KiB
TypeScript
249 lines
9.4 KiB
TypeScript
import type { ReactElement, ReactNode } from "react";
|
||
import { Loader2, ShieldCheck, Terminal, TriangleAlert } from "lucide-react";
|
||
import { BOOTSTRAP_FALLBACK_COMMAND } from "@/bootstrapSetup";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Card } from "@/components/ui/card";
|
||
|
||
type LabFixtureKey =
|
||
| "signed-out-private"
|
||
| "signed-in-private"
|
||
| "claiming"
|
||
| "claim-error"
|
||
| "claim-success"
|
||
| "public-invite-only";
|
||
|
||
const FIXTURE_LABELS: Record<LabFixtureKey, string> = {
|
||
"signed-out-private": "1 · authenticated/private — signed out (browser claim available)",
|
||
"signed-in-private": "2 · authenticated/private — signed in (claim CTA primary)",
|
||
claiming: "3 · authenticated/private — claim in flight",
|
||
"claim-error": "4 · authenticated/private — claim error (e.g. 409 already claimed)",
|
||
"claim-success": "5 · authenticated/private — claim succeeded, redirect pending",
|
||
"public-invite-only": "6 · authenticated/public — invite-only (no browser claim)",
|
||
};
|
||
|
||
const FIXTURE_ORDER: LabFixtureKey[] = [
|
||
"signed-out-private",
|
||
"signed-in-private",
|
||
"claiming",
|
||
"claim-error",
|
||
"claim-success",
|
||
"public-invite-only",
|
||
];
|
||
|
||
function CliFallback({ hasActiveInvite }: { hasActiveInvite: boolean }) {
|
||
return (
|
||
<div className="mt-6 border-t border-border pt-5">
|
||
<div className="flex items-center gap-2 text-sm font-medium">
|
||
<Terminal className="size-4 text-muted-foreground" aria-hidden />
|
||
<span>Prefer to finish setup from the host?</span>
|
||
</div>
|
||
<p className="mt-2 text-sm text-muted-foreground">
|
||
{hasActiveInvite
|
||
? "A bootstrap invite is already active. Check your Paperclip startup logs for the first‑admin URL, or run this command on the host to rotate it:"
|
||
: "Run this command on the host that runs Paperclip to print a one‑time first‑admin invite URL:"}
|
||
</p>
|
||
<pre className="mt-3 overflow-x-auto rounded-md border border-border bg-muted/30 p-3 font-mono text-xs">
|
||
{BOOTSTRAP_FALLBACK_COMMAND}
|
||
</pre>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function StateChrome({ children }: { children: ReactNode }) {
|
||
return (
|
||
<div className="mx-auto max-w-xl py-10">
|
||
<Card className="block p-6">{children}</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function SignedOutPrivate() {
|
||
return (
|
||
<StateChrome>
|
||
<h1 className="text-xl font-semibold">Finish setting up this Paperclip</h1>
|
||
<p className="mt-2 text-sm text-muted-foreground">
|
||
No admin has claimed this instance yet. Sign in or create your Paperclip account to become the first
|
||
admin from this browser.
|
||
</p>
|
||
<div className="mt-5">
|
||
<Button asChild>
|
||
<a href="/auth?next=/">Sign in / Create account</a>
|
||
</Button>
|
||
</div>
|
||
<CliFallback hasActiveInvite={false} />
|
||
</StateChrome>
|
||
);
|
||
}
|
||
|
||
function SignedInPrivate() {
|
||
return (
|
||
<StateChrome>
|
||
<h1 className="text-xl font-semibold">Finish setting up this Paperclip</h1>
|
||
<p className="mt-2 text-sm text-muted-foreground">
|
||
No admin has claimed this instance yet. Claim it now to become the first admin and start onboarding.
|
||
</p>
|
||
<div className="mt-5 flex flex-wrap items-center gap-3">
|
||
<Button>Claim this instance</Button>
|
||
<span className="text-sm text-muted-foreground">
|
||
Signed in as <span className="font-medium text-foreground">jane@appliance.local</span>
|
||
</span>
|
||
</div>
|
||
<p className="mt-3 text-xs text-muted-foreground">
|
||
Wrong account?{" "}
|
||
<a href="/auth?next=/" className="underline underline-offset-2">
|
||
Switch account
|
||
</a>
|
||
.
|
||
</p>
|
||
<CliFallback hasActiveInvite={false} />
|
||
</StateChrome>
|
||
);
|
||
}
|
||
|
||
function ClaimingPrivate() {
|
||
return (
|
||
<StateChrome>
|
||
<h1 className="text-xl font-semibold">Finish setting up this Paperclip</h1>
|
||
<p className="mt-2 text-sm text-muted-foreground">
|
||
No admin has claimed this instance yet. Claim it now to become the first admin and start onboarding.
|
||
</p>
|
||
<div className="mt-5 flex flex-wrap items-center gap-3">
|
||
<Button disabled>
|
||
<Loader2 className="mr-2 size-4 animate-spin" aria-hidden />
|
||
Claiming…
|
||
</Button>
|
||
<span className="text-sm text-muted-foreground">
|
||
Signed in as <span className="font-medium text-foreground">jane@appliance.local</span>
|
||
</span>
|
||
</div>
|
||
<CliFallback hasActiveInvite={false} />
|
||
</StateChrome>
|
||
);
|
||
}
|
||
|
||
function ClaimErrorPrivate() {
|
||
return (
|
||
<StateChrome>
|
||
<h1 className="text-xl font-semibold">Finish setting up this Paperclip</h1>
|
||
<p className="mt-2 text-sm text-muted-foreground">
|
||
No admin has claimed this instance yet. Claim it now to become the first admin and start onboarding.
|
||
</p>
|
||
<div className="mt-5 flex flex-wrap items-center gap-3">
|
||
<Button>Claim this instance</Button>
|
||
<span className="text-sm text-muted-foreground">
|
||
Signed in as <span className="font-medium text-foreground">jane@appliance.local</span>
|
||
</span>
|
||
</div>
|
||
<div
|
||
role="alert"
|
||
className="mt-4 flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive"
|
||
>
|
||
<TriangleAlert className="mt-0.5 size-4 flex-shrink-0" aria-hidden />
|
||
<div>
|
||
<p className="font-medium">Someone else has already claimed this instance.</p>
|
||
<p className="mt-1 text-destructive/90">
|
||
Refresh to sign in, or ask the existing admin to invite you from{" "}
|
||
<span className="font-mono">Settings → Access</span>.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<CliFallback hasActiveInvite={false} />
|
||
</StateChrome>
|
||
);
|
||
}
|
||
|
||
function ClaimSuccess() {
|
||
return (
|
||
<StateChrome>
|
||
<div className="flex items-start gap-3">
|
||
<div className="mt-0.5 flex size-9 flex-shrink-0 items-center justify-center rounded-full bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">
|
||
<ShieldCheck className="size-5" aria-hidden />
|
||
</div>
|
||
<div>
|
||
<h1 className="text-xl font-semibold">You’re the instance admin</h1>
|
||
<p className="mt-2 text-sm text-muted-foreground">
|
||
Setup is complete. Taking you to onboarding to create your first company…
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div className="mt-5 flex items-center gap-3">
|
||
<Loader2 className="size-4 animate-spin text-muted-foreground" aria-hidden />
|
||
<span className="text-sm text-muted-foreground">Redirecting…</span>
|
||
</div>
|
||
<div className="mt-5">
|
||
<Button asChild variant="outline">
|
||
<a href="/">Continue to dashboard</a>
|
||
</Button>
|
||
</div>
|
||
</StateChrome>
|
||
);
|
||
}
|
||
|
||
function PublicInviteOnly() {
|
||
return (
|
||
<StateChrome>
|
||
<h1 className="text-xl font-semibold">This Paperclip is waiting on its first admin</h1>
|
||
<p className="mt-2 text-sm text-muted-foreground">
|
||
This instance runs in invite‑only mode. The operator must generate a one‑time first‑admin invite URL
|
||
from the host. Once you have the link, open it from this browser to finish setup.
|
||
</p>
|
||
<CliFallback hasActiveInvite />
|
||
<p className="mt-4 text-xs text-muted-foreground">
|
||
Browser‑based claim is intentionally disabled in public mode so anyone on the network can’t
|
||
promote themselves.
|
||
</p>
|
||
</StateChrome>
|
||
);
|
||
}
|
||
|
||
const FIXTURE_BODIES: Record<LabFixtureKey, ReactElement> = {
|
||
"signed-out-private": <SignedOutPrivate />,
|
||
"signed-in-private": <SignedInPrivate />,
|
||
claiming: <ClaimingPrivate />,
|
||
"claim-error": <ClaimErrorPrivate />,
|
||
"claim-success": <ClaimSuccess />,
|
||
"public-invite-only": <PublicInviteOnly />,
|
||
};
|
||
|
||
export function BootstrapSetupUxLab() {
|
||
return (
|
||
<div className="bg-background min-h-screen pb-16">
|
||
<header className="border-b border-border bg-muted/20">
|
||
<div className="mx-auto max-w-3xl px-6 py-6">
|
||
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">UX Lab</p>
|
||
<h1 className="mt-1 text-2xl font-semibold">Bootstrap-pending setup states</h1>
|
||
<p className="mt-2 max-w-2xl text-sm text-muted-foreground">
|
||
Fixtures for the bootstrap-pending screen in <span className="font-mono">CloudAccessGate</span>. Used
|
||
as the UX spec for{" "}
|
||
<a className="underline underline-offset-2" href="/PAP/issues/PAP-10113">
|
||
PAP-10113
|
||
</a>{" "}
|
||
and the implementation reference for{" "}
|
||
<a className="underline underline-offset-2" href="/PAP/issues/PAP-10114">
|
||
PAP-10114
|
||
</a>
|
||
. The browser claim CTA only appears when{" "}
|
||
<span className="font-mono">deploymentMode === "authenticated"</span> and{" "}
|
||
<span className="font-mono">deploymentExposure === "private"</span>.
|
||
</p>
|
||
</div>
|
||
</header>
|
||
<main className="mx-auto max-w-3xl space-y-12 px-6 pt-10">
|
||
{FIXTURE_ORDER.map((key) => (
|
||
<section key={key} aria-labelledby={`lab-${key}`}>
|
||
<h2
|
||
id={`lab-${key}`}
|
||
className="mb-3 text-xs font-medium uppercase tracking-wider text-muted-foreground"
|
||
>
|
||
{FIXTURE_LABELS[key]}
|
||
</h2>
|
||
<div className="rounded-lg border border-dashed border-border/70 bg-muted/10 p-2">
|
||
{FIXTURE_BODIES[key]}
|
||
</div>
|
||
</section>
|
||
))}
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|