Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

Give project home a focused production overview

syntaqxcommitted Parentce21448Browse files
6 files+212−3170/6 viewed
+6−6
8383 }
8484
8585 /** The list a commit's mark opens. */
86−export function ChecksList({ checks, className }: { checks: CommitChecks; className?: string }) {
86+export function ChecksList({ checks, className, showHeader = true }: { checks: CommitChecks; className?: string; showHeader?: boolean }) {
8787 return (
8888 <div className={className}>
89− <div className="flex items-start gap-3 border-b border-line px-4 py-3">
89+ {showHeader && <div className="flex items-start gap-3 border-b border-line px-4 py-3">
9090 <span className="mt-0.5">
9191 <CheckStateIcon state={checks.state} size={20} />
9292 </span>
9494 <p className="font-semibold text-fg">{checksHeadline(checks)}</p>
9595 <p className="text-xs text-muted">{checksTally(checks)}</p>
9696 </div>
97− </div>
97+ </div>}
9898 <ul className="max-h-80 divide-y divide-line overflow-y-auto">
9999 {checks.checks.map((item) => (
100100 <CheckRow key={`${item.kind}:${item.id ?? item.name}`} item={item} />
152152 }
153153
154154 /** A readable summary with the same detailed checks as the repository orb. */
155−export function CommitChecksSummary({ checks, sha }: { checks: ChecksSource; sha: string }) {
155+export function CommitChecksSummary({ checks, sha, label }: { checks: ChecksSource; sha: string; label?: string }) {
156156 const summary = (found: Record<string, CommitChecks> | null | undefined) => {
157157 if (found == null) return <p className="text-xs text-muted">Checks could not be loaded.</p>;
158158 const result = found[sha];
161161 <details className="group rounded-lg border border-line">
162162 <summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-2 text-xs hover:bg-raised/40 [&::-webkit-details-marker]:hidden">
163163 <CheckStateIcon state={result.state} />
164− <span className="min-w-0 flex-1"><span className="block font-medium">{checksHeadline(result)}</span><span className="text-muted">{checksTally(result)}</span></span>
164+ <span className="min-w-0 flex-1"><span className="block font-medium">{label ?? checksHeadline(result)}</span><span className="text-muted">{checksTally(result)}</span></span>
165165 <span className="text-accent group-open:hidden">Show checks</span>
166166 <span className="hidden text-accent group-open:inline">Hide checks</span>
167167 </summary>
168− <ChecksList checks={result} className="border-t border-line" />
168+ <ChecksList checks={result} className="border-t border-line" showHeader={false} />
169169 </details>
170170 );
171171 };
+6−3
8282 base,
8383 summary,
8484 className,
85+ excludeEnvironment,
8586 }: {
8687 base: string;
8788 summary: DeploymentEnvironments | null;
89+ excludeEnvironment?: string;
8890 className?: string;
8991 }) {
9092 if (!summary || summary.environments.length === 0) return null;
91− const environments = orderEnvironments(summary.environments);
93+ const environments = orderEnvironments(summary.environments).filter((env) => env.name !== excludeEnvironment);
94+ if (environments.length === 0) return null;
9295 const shown = environments.slice(0, SHOWN);
9396 const more = environments.length - shown.length;
9497 return (
9699 <h2 id="deployments-panel" className="text-sm font-semibold">
97100 <Link to={`${base}/deployments`} className="inline-flex items-center gap-2 hover:text-accent">
98101 <Rocket size={14} className="text-faint" />
99− Deployments
102+ {excludeEnvironment ? "Other environments" : "Deployments"}
100103 <span className="rounded-full bg-line px-1.5 text-[0.6875rem] font-medium tabular-nums text-muted">
101− {summary.total_count.toLocaleString("en-US")}
104+ {(excludeEnvironment ? environments.length : summary.total_count).toLocaleString("en-US")}
102105 </span>
103106 </Link>
104107 </h2>
+3−21
123123
124124 /**
125125 * An app that deploys itself (its workflows, or another pipeline): production at the address it was
126− * given, with the latest reported deployment, or else the default branch's
127− * latest commit. Branch checks are shown separately below the production status.
126+ * given, with the latest reported deployment. Branch checks are shown separately below the production status.
128127 */
129128 export function ElsewhereHead({
130129 project,
131130 base,
132131 canChange,
133− canDeploy,
134132 deployment,
135− commit,
136133 shot,
137−}: Head & {
134+}: Omit<Head, "canDeploy"> & {
138135 deployment?: ExternalDeployment | null;
139− commit: { hash: string; authoredAt: string } | null;
140136 /** Production's picture, beside the rest, as an app on g1t has (components/production-shot.tsx). */
141137 shot?: ReactNode;
142138 }) {
146142 {shot}
147143 <div className="flex min-w-0 grow flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
148144 <div className="min-w-0">
149− <HeadLabel icon={<Rocket />}>Production</HeadLabel>
150145 {url ? (
151146 <>
152147 <BigLink url={url} />
174169 </span>
175170 </>
176171 ) : (
177− <>
178− <span>Deploys itself</span>
179− {commit && (
180− <span className="inline-flex items-center gap-1.5">
181− <span className="text-faint">·</span>
182− <Link to={`${base}/commit/${commit.hash}`} className="inline-flex items-center gap-1 font-mono hover:text-fg">
183− <GitCommitHorizontal size={13} className="text-faint" />
184− {commit.hash.slice(0, 7)}
185− </Link>
186− <TimeAgo at={commit.authoredAt} />
187− </span>
188− )}
189− </>
172+ <span>Deploys itself</span>
190173 )}
191174 </p>
192175 </>
201184 ) : (
202185 <p className="mt-2 text-sm text-muted">Deploys itself.</p>
203186 )}
204− <DeployQuietly base={base} canDeploy={canDeploy} what="it" />
205187 </div>
206188 </div>
207189 </div>
+8−1
11 import assert from "node:assert/strict";
22 import { test } from "node:test";
3−import { projectCheckSections } from "./project-checks.ts";
3+import { hasSeparateAttempt, projectCheckSections } from "./project-checks.ts";
44
55 test("a CLI-only fix keeps latest branch checks separate from the older production commit", () => {
66 assert.deepEqual(projectCheckSections("6049e67", "main", "a8f62b9"), [
1919 assert.deepEqual(projectCheckSections(null, "main", "old"), [{ kind: "production", label: "Checks for deployed commit", sha: "old" }]);
2020 assert.deepEqual(projectCheckSections(null, null, null), []);
2121 });
22+
23+test("a failed same-commit retry does not replace the serving deployment", () => {
24+ assert.equal(hasSeparateAttempt("dep_live", "abc", { id: "dep_retry", sha: "abc", state: "failure" }), true);
25+ assert.equal(hasSeparateAttempt(null, "abc", { id: "dep_retry", sha: "abc", state: "failure" }), true);
26+ assert.equal(hasSeparateAttempt("dep_live", "abc", { id: "dep_live", sha: "abc", state: "success" }), false);
27+ assert.equal(hasSeparateAttempt(null, null, { id: "dep_first", sha: "abc", state: "in_progress" }), false);
28+});
+6−0
55 if (deployed && deployed !== head) sections.push({ kind: "production", label: "Checks for deployed commit", sha: deployed });
66 return sections;
77 }
8+
9+/** A retry of the same SHA is still a separate attempt, including when no deployment ID was returned. */
10+export function hasSeparateAttempt(currentId: string | null | undefined, servingSha: string | null | undefined, latest: { id: string; sha: string; state: string }): boolean {
11+ if (!servingSha) return false;
12+ return currentId ? latest.id !== currentId : latest.sha !== servingSha || latest.state !== "success";
13+}
+183−286
6161 import { useAddresses } from "../../lib/addresses";
6262 import { commitChecksFor } from "../../lib/commit-checks.server";
6363 import { CommitChecksSummary } from "../../components/commit-checks";
64−import { projectCheckSections } from "../../lib/project-checks";
64+import { projectCheckSections, hasSeparateAttempt } from "../../lib/project-checks";
6565 import { StatusIcon, Duration, shortRef, standingWord } from "../../components/actions";
6666 import { resolveRange, rangeLabel } from "../../lib/usage";
6767 import { deliveryDays, duration as deliveryDuration } from "../../lib/workspace-insights";
132132 const service = telemetry;
133133 const telemetryP = service
134134 ? projectP.then((found) =>
135− found?.ok
136− ? forMembers(async () => {
137− const project_id = found.value.id;
138− const [overview, errors] = await Promise.all([
139− soft(service.overview({ project_id, workspace: params.owner, from_us: (now - DAY_MS) * 1000, to_us: now * 1000, step_s: 3600 })),
140− soft(service.errors({ project_id, workspace: params.owner, state: "open" })),
141− ]);
142− return { overview, errors };
143− })
144− : null,
145− )
135+ found?.ok
136+ ? forMembers(async () => {
137+ const project_id = found.value.id;
138+ const [overview, errors] = await Promise.all([
139+ soft(service.overview({ project_id, workspace: params.owner, from_us: (now - DAY_MS) * 1000, to_us: now * 1000, step_s: 3600 })),
140+ soft(service.errors({ project_id, workspace: params.owner, state: "open" })),
141+ ]);
142+ return { overview, errors };
143+ })
144+ : null,
145+ )
146146 : Promise.resolve(null);
147147 const commitP = soft(repos.log(path, viewer, null, 1)).then((log) => log?.ok ? (log.value[0] ?? null) : null);
148148 const range = resolveRange("30d", {}, new Date(now));
284284 builds: builds.slice(0, 30),
285285 previews: live.filter((app) => app.kind === "preview"),
286286 production,
287− serving: current ? { sha: current.sha, at: current.updated_at } : null,
287+ serving: current ? { id: current.id, sha: current.sha, ref: current.ref, at: current.updated_at, creator: current.creator, runUrl: current.run_url } : null,
288288 // The screenshot of production on g1t.page, for members (like its builds).
289289 productionShot: member && liveProduction ? shotVersion(liveProduction) : null,
290290 latestProductionBuild: latestProduction ? { status: latestProduction.status } : null,
471471
472472 /** The overview's sections as they will sit, while they stream in. */
473473 function OverviewSkeleton() {
474− return (
475− <Loading className="space-y-8" aria-label="Loading the overview">
476− {/* Production: its screenshot beside where it is live, then the facts under them. */}
477− <Card asChild aria-hidden="true" className="overflow-hidden rounded-2xl">
478− <section>
479− <div className="flex flex-col gap-5 p-5 sm:flex-row sm:items-start sm:p-6">
480− <Skeleton className="aspect-16/10 w-full shrink-0 rounded-lg sm:w-72" />
481− <div className="min-w-0 grow">
482− <Skeleton className="h-3 w-24" />
483− <Skeleton className="mt-3 h-5 w-72 max-w-full" />
484− <Skeleton className="mt-3 h-3 w-48 max-w-full" />
485− </div>
486− </div>
487− <div className="grid grid-cols-2 gap-px border-t border-line bg-line sm:grid-cols-4">
488− {Array.from({ length: 4 }, (_, index) => (
489− <div key={index} className="bg-surface px-5 py-4">
490− <Skeleton className="h-3 w-16" />
491− <Skeleton className="mt-2.5 h-4 w-28" />
492− </div>
493− ))}
494− </div>
495− </section>
496− </Card>
497− {/* Live now beside the sidebar's cards. */}
498− <div className="grid gap-x-8 gap-y-8 lg:grid-cols-[minmax(0,1fr)_20rem]">
499− <Card>
500− <SkeletonRows rows={4} />
501− </Card>
502− <Card>
503− <SkeletonRows rows={4} />
504− </Card>
474+ return <Loading className="space-y-8" aria-label="Loading the overview">
475+ <Card className="overflow-hidden rounded-2xl">
476+ <div className="border-b border-line p-5"><Skeleton className="h-5 w-48" /></div>
477+ <div className="grid gap-6 p-6 md:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]">
478+ <Skeleton className="aspect-16/10 w-full rounded-lg" />
479+ <SkeletonRows rows={5} />
505480 </div>
506− </Loading>
507− );
481+ </Card>
482+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">{[0, 1, 2].map((key) => <Card key={key} className="p-5"><SkeletonRows rows={4} /></Card>)}</div>
483+ </Loading>;
508484 }
509485
510486 function OverviewFailed() {
512488 <Card asChild className="rounded-2xl p-6 text-sm text-muted">
513489 <p>The overview could not be loaded just now. Reload the page to try again.</p>
514490 </Card>
515− );
516−}
517−
518−/** One of the facts under production: what it is, and where to read more. */
519−function Fact({ label, value, to, hint }: { label: string; value: ReactNode; to?: string; hint?: string }) {
520− const body = (
521− <>
522− <span className="block text-[0.6875rem] text-faint">{label}</span>
523− <span className="mt-0.5 block text-[0.8125rem] font-medium sm:truncate">{value}</span>
524− </>
525491 );
526− const cell = to ? (
527− <Link to={to} prefetch="intent" className="block min-w-0 px-5 py-3 transition-colors hover:bg-raised/50">
528− {body}
529− </Link>
530− ) : (
531− <div className="min-w-0 px-5 py-3">{body}</div>
532− );
533− return hint ? <Hint label={hint}>{cell}</Hint> : cell;
534492 }
535493
536494 function plural(n: number, one: string, many = `${one}s`): string {
537495 return `${n.toLocaleString("en-US")} ${n === 1 ? one : many}`;
538496 }
539497
540−/**
541− * The facts under production, about the project rather than its
542− * repository: four of them, most telling first. Where the card above
543− * already shows production's state, the first is its latest release
544− * instead, so nothing is said twice.
545− */
546−function Facts({ loaderData, base, plan }: { loaderData: Loaded; base: string; plan: string }) {
547− const { productionLatest, release, library, telemetry, open, environments } = loaderData;
548− const headShowsProduction = !library && (plan === "production" || plan === "elsewhere");
549− const releaseFact = release ? (
550− <Fact
551− label="Latest release"
552− to={`${base}/tags`}
553− value={
554− <span className="inline-flex items-center gap-1.5">
555− <span className="truncate font-mono">{release.name}</span>
556− {release.at && (
557− <span className="shrink-0 font-normal text-muted">
558− <TimeAgo at={release.at} />
559− </span>
560− )}
561− </span>
562− }
563− />
564− ) : null;
565− const first =
566− !headShowsProduction && productionLatest ? (
567− <Fact
568− label="Production"
569− to={`${base}/deployments/${productionLatest.id}`}
570− hint={`The newest deployment to ${productionLatest.environment}`}
571− value={
572− <span className="inline-flex items-center gap-1.5">
573− <DeploymentStateIcon state={productionLatest.state} size={14} />
574− {productionLatest.state === "success" ? "Live" : STATE_WORD[productionLatest.state]}
575− <span className="font-normal text-muted">
576− <TimeAgo at={productionLatest.at} />
577− </span>
578− </span>
579− }
580− />
581− ) : releaseFact ? (
582− releaseFact
583− ) : environments && environments.total_count > 0 ? (
584− <Fact label="Deployments" to={`${base}/deployments`} value={<span className="tabular-nums">{environments.total_count.toLocaleString("en-US")}</span>} />
585− ) : (
586− <Fact label="Latest release" to={`${base}/tags`} value={<span className="font-normal text-muted">None yet</span>} />
587− );
588− const branch = <Fact label="Default branch" to={`${base}/code`} value={loaderData.defaultBranch ?? "Unavailable"} />;
589− return (
590− <div className="grid grid-cols-2 divide-line border-t border-line text-sm sm:grid-cols-4 sm:divide-x">
591− {first}
592− {telemetry ? (
593− <>
594− <Fact label="Requests · 24h" to={`${base}/observability`} value={<span className="tabular-nums">{count(telemetry.overview.totals.requests)}</span>} />
595− <Fact
596− label="Errors · 24h"
597− to={`${base}/observability/errors`}
598− hint={`${plural(telemetry.overview.totals.errors, "failed request")} in the last day`}
599− value={
600− <span className="inline-flex items-center gap-1.5 tabular-nums">
601− <span className={telemetry.overview.totals.error_rate >= 0.01 ? "text-danger" : undefined}>{percent(telemetry.overview.totals.error_rate)}</span>
602− <span className="font-normal text-muted">· {telemetry.errors.length.toLocaleString("en-US")} open</span>
603− </span>
604− }
605− />
606− {branch}
607− </>
608− ) : (
609− <>
610− {branch}
611− <Fact label="Open issues" to={`${base}/issues`} value={open.issues} />
612− <Fact label="Open pull requests" to={`${base}/pulls`} value={open.pulls} />
613− </>
614− )}
615− </div>
616− );
617−}
618−
619498 /** One line to the repository, where its files, branches and commits are: Code. */
620499 function CodeLine({ project, base }: { project: Project; base: string }) {
621500 if (project.source.kind !== "hosted") return null;
622− const { repo, defaultBranch, rootDir } = project.source;
501+ const { repo, rootDir } = project.source;
623502 return (
624503 <Link
625504 to={`${base}/code`}
630509 <span className="min-w-0 truncate font-mono text-fg-soft">
631510 {repo.namespace}/{repo.name}
632511 {rootDir ? `/${rootDir}` : ""}
633− </span>
634− <span className="hidden shrink-0 items-center gap-1 font-mono sm:inline-flex">
635− <GitBranch size={12} className="text-faint" />
636− {defaultBranch}
637512 </span>
638513 {project.pushedAt && (
639514 <span className="hidden shrink-0 md:inline">
732607 );
733608 }
734609
610+/** The running deployment owns these fields; a newer attempt never changes its status. */
611+function ProductionSummary({ loaderData: d, base, upload, addImage, elsewhere }: {
612+ loaderData: Loaded; base: string; upload: { url: string; preferred: boolean } | null;
613+ addImage: string | null; elsewhere: boolean;
614+}) {
615+ const current = d.serving;
616+ const sha = current?.sha ?? d.production?.commit;
617+ const url = elsewhere ? d.deployment?.url ?? d.project?.productionUrl : d.settings?.primaryDomain ? `https://${d.settings.primaryDomain}` : d.production?.url;
618+ const at = current?.at ?? d.production?.deployedAt;
619+ const latest = d.productionLatest;
620+ const newerAttempt = latest && hasSeparateAttempt(current?.id, sha, latest);
621+ const screenshot = elsewhere ? d.elsewhereShot : d.productionShot ? `${base}/production.jpg?v=${d.productionShot}` : null;
622+ return (
623+ <>
624+ <div className="grid gap-6 p-5 sm:p-6 md:grid-cols-[minmax(0,2fr)_minmax(0,3fr)] lg:gap-8">
625+ <ProductionShot sources={cardSources(screenshot, upload)} href={url ?? null} label={url ? host(url) : ""} addImage={addImage} className="w-full self-start" />
626+ <dl className="grid min-w-0 content-start grid-cols-2 gap-x-5 gap-y-5 text-sm">
627+ <div className="col-span-2 min-w-0">
628+ <dt className="text-xs text-muted">Deployment</dt>
629+ <dd className="mt-1 break-words font-medium">
630+ {current ? <Link to={`${base}/deployments/${current.id}`} className="hover:text-accent">Current production <ArrowUpRight size={14} className="inline text-faint" /></Link> : <span>Production</span>}
631+ </dd>
632+ </div>
633+ <div className="col-span-2 min-w-0">
634+ <dt className="text-xs text-muted">Domain</dt>
635+ <dd className="mt-1">{url ? <DeployLink href={url} className="inline-flex max-w-full items-center gap-1.5 font-medium hover:text-accent"><span className="truncate">{host(url)}</span><ArrowUpRight size={14} className="shrink-0 text-faint" /></DeployLink> : <span className="text-muted">No address reported</span>}</dd>
636+ {!elsewhere && d.settings?.primaryDomain && d.production && <dd className="mt-1 text-xs text-muted"><DeployLink href={d.production.url} className="break-all hover:text-fg">{host(d.production.url)}</DeployLink></dd>}
637+ </div>
638+ <div><dt className="text-xs text-muted">Status</dt><dd className="mt-1"><StatusDot status="ready" label="Ready" /></dd></div>
639+ <div><dt className="text-xs text-muted">Deployed</dt><dd className="mt-1 text-xs">{at && <TimeAgo at={at} />}{current?.creator && <span className="text-muted"> by {current.creator}</span>}</dd></div>
640+ <div className="col-span-2 min-w-0">
641+ <dt className="text-xs text-muted">Source</dt>
642+ <dd className="mt-1 flex flex-wrap items-center gap-x-4 gap-y-2">
643+ {current?.ref && current.ref !== sha && <Link to={`${base}/tree/${encodeURIComponent(current.ref)}`} className="inline-flex min-w-0 items-center gap-1.5 hover:text-accent"><GitBranch size={14} className="shrink-0 text-faint" /><span className="truncate">{current.ref}</span></Link>}
644+ {sha && <Link to={`${base}/commit/${sha}`} className="inline-flex items-center gap-1.5 font-mono text-xs hover:text-accent"><GitCommitHorizontal size={14} className="text-faint" />{sha.slice(0, 7)}</Link>}
645+ {current?.runUrl && <Link to={current.runUrl} className="text-xs text-muted hover:text-fg">View workflow <ArrowUpRight size={12} className="inline" /></Link>}
646+ </dd>
647+ {sha && sha !== d.commit?.hash && <dd className="mt-3"><CommitChecksSummary checks={d.checks} sha={sha} label="Checks for deployed commit" /></dd>}
648+ </div>
649+ </dl>
650+ </div>
651+ {newerAttempt && <Link to={`${base}/deployments/${latest.id}`} className="flex flex-wrap items-center gap-2 border-t border-line bg-raised/30 px-5 py-3 text-xs hover:bg-raised/60 sm:px-6"><DeploymentStateIcon state={latest.state} size={14} /><span>Latest attempt: {STATE_WORD[latest.state]}</span><span className="font-mono text-muted">{latest.sha.slice(0, 7)}</span><span className="text-muted"><TimeAgo at={latest.at} /></span><span className="ml-auto text-muted">View deployment <ArrowRight size={12} className="inline" /></span></Link>}
652+ </>
653+ );
654+}
655+
656+/** Branch health belongs to the repository head, independently of what is deployed. */
657+function BranchCard({ loaderData: d, base }: { loaderData: Loaded; base: string }) {
658+ const branch = projectCheckSections(d.commit?.hash, d.defaultBranch, null)[0];
659+ return <Card asChild className="min-w-0 p-5"><section aria-labelledby="overview-branch">
660+ <h2 id="overview-branch" className="flex items-center gap-2 text-sm font-semibold"><GitBranch size={14} className="text-faint" />Latest on {d.defaultBranch ?? "default branch"}</h2>
661+ {branch ? <><div className="my-4 flex flex-wrap items-center gap-2 text-xs"><Link to={`${base}/commit/${branch.sha}`} className="font-mono hover:text-accent">{branch.sha.slice(0, 7)}</Link><span className="text-muted">committed {d.commit && <TimeAgo at={d.commit.authoredAt} />}</span></div><CommitChecksSummary checks={d.checks} sha={branch.sha} /></> : <p className="my-4 text-xs text-muted">The latest commit is unavailable.</p>}
662+ <dl className="mt-4 space-y-3 border-t border-line pt-4 text-xs">
663+ <div className="flex justify-between gap-3"><dt className="text-muted">Latest release</dt><dd><Link to={`${base}/tags`} className="font-mono hover:text-accent">{d.release?.name ?? "None yet"}</Link>{d.release?.at && <span className="ml-2 text-muted"><TimeAgo at={d.release.at} /></span>}</dd></div>
664+ <div className="flex justify-between gap-3"><dt className="text-muted">Open pull requests</dt><dd><Link to={`${base}/pulls`} className="tabular-nums hover:text-accent">{d.open.pulls}</Link></dd></div>
665+ </dl>
666+ </section></Card>;
667+}
668+
735669 /** Production controls share a header, whether g1t or a workflow hosts it. */
736670 function ProductionActions({ loaderData, base, elsewhere }: { loaderData: Loaded; base: string; elsewhere: boolean }) {
737671 const { member, settings, production, project, can } = loaderData;
740674 : production ? (settings?.primaryDomain ? `https://${settings.primaryDomain}` : production.url) : null;
741675 return (
742676 <div className="flex flex-wrap items-center gap-2">
743− {member && (
744− <Hint label="Deployment history">
745− <ButtonLink to={`${base}/deployments`} variant="outline" size="icon-sm" aria-label="Deployment history">
746− <Rocket size={15} />
747− </ButtonLink>
748− </Hint>
749− )}
750677 {member && (can.run || can.manage_integrations) && (
751678 <Hint disabled label="Instant rollback is coming soon. It is not available yet.">
752679 <Button disabled variant="outline" size="sm">
769696 <Rocket size={14} /> Turn on deployments
770697 </ButtonLink>
771698 )}
772− {member && can.manage_integrations && (
773− <Hint label="Deployment settings">
774− <ButtonLink to={`${base}/settings/deployments`} variant="outline" size="icon-sm" aria-label="Deployment settings">
775− <Settings2 size={15} />
776− </ButtonLink>
777− </Hint>
778− )}
779699 {url && (
780700 <ButtonLink to={url} variant="accent" size="sm" reloadDocument target="_blank" rel="noopener noreferrer">
781701 Visit <ArrowUpRight size={14} />
800720 const plan = project ? projectPlan(project) : "production";
801721 const canChange = member && loaderData.can.manage_settings;
802722 const deployedSha = loaderData.serving?.sha ?? loaderData.production?.commit;
803− const checkSections = projectCheckSections(loaderData.commit?.hash, defaultBranch, deployedSha);
723+ const productionPlan = plan === "production" || plan === "elsewhere";
724+ const hasProduction = productionPlan && Boolean(deployedSha);
804725 const addresses = useAddresses();
805726 // The production card's picture: an uploaded image, served from the
806727 // usercontent origin, and where someone who may change it adds one.
835756 {(plan === "production" || plan === "elsewhere") && <ProductionActions loaderData={loaderData} base={base} elsewhere={plan === "elsewhere"} />}
836757 </div>
837758 )}
838− {plan === "elsewhere" && project ? (
759+ {hasProduction ? (
760+ <ProductionSummary loaderData={loaderData} base={base} upload={upload} addImage={addImage} elsewhere={plan === "elsewhere"} />
761+ ) : plan === "elsewhere" && project ? (
839762 <ElsewhereHead
840763 project={project}
841764 base={base}
842765 canChange={canChange}
843− canDeploy={member && loaderData.can.manage_integrations}
844766 deployment={loaderData.deployment}
845− commit={loaderData.commit}
846767 shot={
847768 <ProductionShot
848769 sources={cardSources(loaderData.elsewhereShot, upload)}
864785 ) : (
865786 <ProductionHead loaderData={loaderData} base={base} name={name} upload={upload} addImage={addImage} />
866787 )}
867− {checkSections.length > 0 && (
868− <div className="border-t border-line px-5 py-4 sm:px-6">
869− {checkSections.map((section) => section.kind === "branch" ? (
870− <div key={section.kind}>
871− <div className="mb-2 flex flex-wrap items-center gap-2 text-xs text-muted">
872− <GitCommitHorizontal size={14} />
873− <span>{section.label}</span>
874− <Link to={`${base}/commit/${section.sha}`} className="font-mono hover:text-fg">{section.sha.slice(0, 7)}</Link>
875− </div>
876− <CommitChecksSummary checks={loaderData.checks} sha={section.sha} />
877− </div>
878− ) : (
879− <details key={section.kind} className="mt-3 text-xs text-muted">
880− <summary className="cursor-pointer hover:text-fg">{section.label} <span className="font-mono">{section.sha.slice(0, 7)}</span></summary>
881− <p className="my-2">These checks belong to the code deployed to production. Deployment status is shown above.</p>
882− <Link to={`${base}/commit/${section.sha}`} className="mb-2 inline-block text-accent hover:underline">View deployed commit</Link>
883− <CommitChecksSummary checks={loaderData.checks} sha={section.sha} />
884− </details>
885− ))}
886− {deployedSha && <p className="mt-2 text-xs text-muted">Currently serving <Link to={`${base}/commit/${deployedSha}`} className="font-mono text-fg-soft hover:underline">{deployedSha.slice(0, 7)}</Link>{loaderData.serving && <> &middot; deployed <TimeAgo at={loaderData.serving.at} /></>}</p>}
887− </div>
888− )}
889788 {actionData && (
890789 <p className={`border-t border-line px-6 py-2.5 text-sm ${"error" in actionData ? "text-danger" : "text-success"}`}>
891790 {"error" in actionData ? actionData.error : actionData.notice}
892791 </p>
893792 )}
894− <Facts loaderData={loaderData} base={base} plan={plan} />
793+ {productionPlan && <div className="flex flex-wrap items-center justify-between gap-3 border-t border-line px-5 py-3 sm:px-6">
794+ <span className="text-xs text-muted">{plan === "elsewhere" ? "Production is deployed by its own pipeline." : !loaderData.settings?.enabled ? "Deployments are not enabled for this project." : <>Push to <span className="font-mono text-fg-soft">{defaultBranch ?? "the default branch"}</span> to update production.</>}</span>
795+ <div className="flex flex-wrap items-center gap-2">
796+ {member && loaderData.can.manage_integrations && <ButtonLink to={`${base}/settings/deployments`} variant="ghost" size="sm"><Settings2 size={14} />Deployment settings</ButtonLink>}
797+ <ButtonLink to={`${base}/deployments`} variant="outline" size="sm"><Rocket size={14} />Deployments{loaderData.environments && <span className="text-muted">{loaderData.environments.total_count}</span>}</ButtonLink>
798+ </div>
799+ </div>}
895800 {project && <CodeLine project={project} base={base} />}
896801 </section>
897802 </Card>
898803
899− <div className="grid gap-x-8 gap-y-8 lg:grid-cols-[minmax(0,1fr)_20rem]">
900− <div className="min-w-0 space-y-8">
901− {!loaderData.needsLoaded && (
902− <Panel title="Needs you" icon={<Hand size={14} className="text-warn" />}>
903− <Unavailable what="What needs you" />
904− </Panel>
905− )}
906− {loaderData.needsLoaded && needs.length > 0 && (
907− <Panel title="Needs you" icon={<Hand size={14} className="text-warn" />} count={needs.length}>
908− <NeedsList needs={needs} limit={5} />
909− </Panel>
910− )}
804+ <div className={`grid items-start gap-4 md:grid-cols-2 ${loaderData.telemetry || !loaderData.telemetryLoaded ? "xl:grid-cols-3" : ""}`}>
805+ <BranchCard loaderData={loaderData} base={base} />
806+ {!loaderData.telemetryLoaded && <Unavailable what="Observability" compact />}
807+ {loaderData.telemetry && <ObservabilityCard overview={loaderData.telemetry.overview} errors={loaderData.telemetry.errors} base={base} />}
808+ <HealthCard loaderData={loaderData} base={base} plan={plan} />
809+ </div>
911810
912− <Panel
913− title="Live now"
914− icon={<Radio size={14} className={busy ? "text-success" : undefined} />}
915− extra={
916− working ? (
917− <span className="text-xs text-muted">
918− {plural(awake.length, "session")} · {plural(claims.length, "claim")}
919− {idle.length > 0 && ` · ${idle.length} idle`}
920− </span>
921− ) : null
922− }
923− all={{ to: `${base}/work`, label: "Work" }}
924− >
925− {!loaderData.liveLoaded ? (
926− <Unavailable what="Who is working" />
927− ) : working ? (
928− <>
929− <LiveNowList
930− claims={claims}
931− sessions={sessions}
932− titles={titles}
933− pulls={livePulls}
934− defaultBranch={defaultBranch}
935− base={base}
936− now={Date.now()}
937− limit={LIVE_SHOWN}
938− ender={loaderData.ender}
939− />
940− {moreIssues > 0 && (
941− <Link to={`${base}/work`} className="mt-2 inline-block px-1 text-xs text-muted hover:text-fg">
942− {plural(moreIssues, "more issue")} being worked on
943− </Link>
944− )}
945− </>
946− ) : member ? (
947− <StartHere title={`Nobody is working on ${name} right now`} />
948− ) : (
949− <Quiet>
950− Nobody is working on {name} right now. When people and their coding agents claim an issue or start a session, it shows here.
951− </Quiet>
952− )}
953− </Panel>
811+ {previews.length > 0 && (
812+ <Panel title="Preview deployments" icon={<GitBranch size={14} />} count={previews.length}>
813+ <Card asChild divided className="overflow-hidden">
814+ <ul>
815+ {previews.map((app) => (
816+ <li key={app.url} className="flex flex-wrap items-center gap-3 px-5 py-4 text-sm">
817+ <GitBranch size={16} className="shrink-0 text-faint" />
818+ <div className="min-w-0 flex-1">
819+ <div className="flex flex-wrap items-center gap-2">
820+ <span className="truncate font-mono text-[0.8125rem] font-medium">{app.branch}</span>
821+ {app.number != null && <Link to={`${base}/pull/${app.number}`} className="text-xs text-muted hover:text-accent">#{app.number}</Link>}
822+ </div>
823+ <DeployLink href={app.url} className="mt-1 block truncate text-xs text-muted hover:text-accent">{host(app.url)}</DeployLink>
824+ </div>
825+ <span className="shrink-0 text-xs text-faint"><TimeAgo at={app.deployedAt} /></span>
826+ <ButtonLink to={app.url} variant="outline" size="sm" reloadDocument target="_blank" rel="noopener noreferrer">Visit <ArrowUpRight size={14} /></ButtonLink>
827+ </li>
828+ ))}
829+ </ul>
830+ </Card>
831+ </Panel>
832+ )}
954833
955− <DeliveryPanel loaderData={loaderData} base={base} />
956− <WorkflowPanel loaderData={loaderData} base={base} />
834+ {/* Deployments panel (deployments-panel.tsx): each environment's latest deployment. */}
835+ {loaderData.environmentsLoaded ? (
836+ <DeploymentsPanel base={base} summary={loaderData.environments} excludeEnvironment={productionPlan ? loaderData.productionLatest?.environment : undefined} className="rounded-xl border border-line bg-surface p-5" />
837+ ) : (
838+ <Unavailable what="Deployments" compact />
839+ )}
957840
958− {previews.length > 0 && (
959− <Panel title="Previews" icon={<GitBranch size={14} />} count={previews.length} all={{ to: `${base}/deployments`, label: "Deployments" }}>
960− <Card asChild divided className="overflow-hidden">
961− <ul>
962− {previews.map((app) => (
963− <li key={app.url} className="flex items-center gap-3 px-4 py-2.5 text-sm">
964− <span className="min-w-0 grow">
965− <DeployLink href={app.url} className="block truncate font-mono text-[0.8125rem] hover:text-accent">
966− {host(app.url)}
967− </DeployLink>
968− <span className="text-xs text-muted">
969− {app.branch}
970− {app.number != null && (
971− <>
972− {" · "}
973− <Link to={`${base}/pull/${app.number}`} className="hover:underline">
974− #{app.number}
975− </Link>
976− </>
977− )}
978− </span>
979− </span>
980− <span className="shrink-0 text-xs text-faint">
981− <TimeAgo at={app.deployedAt} />
982− </span>
983− </li>
984− ))}
985− </ul>
986− </Card>
987− </Panel>
988− )}
989− </div>
841+ <div className="min-w-0 space-y-8">
842+ {!loaderData.needsLoaded && (
843+ <Panel title="Needs you" icon={<Hand size={14} className="text-warn" />}>
844+ <Unavailable what="What needs you" />
845+ </Panel>
846+ )}
847+ {loaderData.needsLoaded && needs.length > 0 && (
848+ <Panel title="Needs you" icon={<Hand size={14} className="text-warn" />} count={needs.length}>
849+ <NeedsList needs={needs} limit={5} />
850+ </Panel>
851+ )}
990852
991− <aside className="space-y-4 text-sm">
992− {/* Deployments panel (deployments-panel.tsx): each environment's latest deployment. */}
993− {loaderData.environmentsLoaded ? (
994− <DeploymentsPanel base={base} summary={loaderData.environments} className="rounded-xl border border-line bg-surface p-5" />
853+ <Panel
854+ title="Live now"
855+ icon={<Radio size={14} className={busy ? "text-success" : undefined} />}
856+ extra={
857+ working ? (
858+ <span className="text-xs text-muted">
859+ {plural(awake.length, "session")} · {plural(claims.length, "claim")}
860+ {idle.length > 0 && ` · ${idle.length} idle`}
861+ </span>
862+ ) : null
863+ }
864+ all={{ to: `${base}/work`, label: "Work" }}
865+ >
866+ {!loaderData.liveLoaded ? (
867+ <Unavailable what="Who is working" />
868+ ) : working ? (
869+ <>
870+ <LiveNowList
871+ claims={claims}
872+ sessions={sessions}
873+ titles={titles}
874+ pulls={livePulls}
875+ defaultBranch={defaultBranch}
876+ base={base}
877+ now={Date.now()}
878+ limit={LIVE_SHOWN}
879+ ender={loaderData.ender}
880+ />
881+ {moreIssues > 0 && (
882+ <Link to={`${base}/work`} className="mt-2 inline-block px-1 text-xs text-muted hover:text-fg">
883+ {plural(moreIssues, "more issue")} being worked on
884+ </Link>
885+ )}
886+ </>
887+ ) : member ? (
888+ <StartHere title={`Nobody is working on ${name} right now`} />
995889 ) : (
996− <Unavailable what="Deployments" compact />
890+ <Quiet>
891+ Nobody is working on {name} right now. When people and their coding agents claim an issue or start a session, it shows here.
892+ </Quiet>
997893 )}
998− {!loaderData.telemetryLoaded && <Unavailable what="Observability" compact />}
999− {loaderData.telemetry && <ObservabilityCard overview={loaderData.telemetry.overview} errors={loaderData.telemetry.errors} base={base} />}
894+ </Panel>
895+
896+ <div className="grid items-start gap-6 xl:grid-cols-2"><DeliveryPanel loaderData={loaderData} base={base} /><WorkflowPanel loaderData={loaderData} base={base} /></div>
1000897
1001− <HealthCard loaderData={loaderData} base={base} plan={plan} />
1002− </aside>
1003898 </div>
1004899 </div>
1005900 );
1045940 <RequestBars points={overview.points} />
1046941 </div>
1047942 <dl className="mt-3 grid grid-cols-2 gap-3 text-xs">
943+ <div><dt className="text-faint">Requests</dt><dd className="mt-0.5 font-medium tabular-nums">{count(overview.totals.requests)}</dd></div>
944+ <div><dt className="text-faint">Error rate</dt><dd className="mt-0.5 font-medium tabular-nums"><Link to={`${base}/observability/errors`} className={overview.totals.error_rate >= 0.01 ? "text-danger" : "hover:text-accent"}>{percent(overview.totals.error_rate)}</Link><span className="ml-1 text-muted">({count(overview.totals.errors)})</span></dd></div>
1048945 <div>
1049946 <dt className="text-faint">Latency p50</dt>
1050947 <dd className="mt-0.5 font-medium tabular-nums">{duration(overview.totals.latency.p50)}</dd>

Comments

Sign in to comment.