Skip to content

Commit fba29a7

Browse files
adamsofferclaude
andauthored
Unify App/Pipeline into one app type; make Overview consumer-visible for every app (#6)
* feat(app-detail): make the Overview tab consumer-visible for every app The Overview tab previously rendered only from a deployment manifest (`Pipeline`), so it appeared only for the org's own deployed apps and never as the default tab on the consumer base — meaning the public apps a consumer browses from Explore had no Overview at all. Render Overview from the catalog `model` (App), which exists for every app, and treat the deployment manifest as optional enrichment: - Headline stats (Calls·7d, p50 latency, uptime, warm orchestrators) come from the model, so they show for any app. Warm-orchestrator count is a liveness/capacity signal for the caller. - Endpoint card shows the call URL for every app; per-route rows appear when the deployment manifest is known. - Deployment-backed apps additionally get the full Deployment card (pipeline id, entrypoint, image, version, GPU, last deployed, deployed by, environments) + the request Schema — shown to any viewer. - Catalog-only apps get a lighter Details card (provider, category, type, pricing) so the tab isn't bare. Overview is now available to everyone and is the default landing tab. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(data): unify App and Pipeline into one app type There were two disjoint types — `App` (static catalog model) and `Pipeline` (a deployed-app manifest) — with separate arrays and separate lookups, even though the product vocabulary says "an app is a deployed pipeline." This makes the data match the vocabulary: one app type, one catalog array, one lookup, and every app carries its pipeline manifest. - `App` keeps all its catalog fields and gains `deployment?: AppDeployment` (the former `Pipeline`-only manifest: pipelineId, entrypoint, image, version, gpu, endpoints, createdBy, environment, kind, status, visibility, warm orchestrators, calls/latency/error metrics). - `Pipeline` is now a transitional alias — `App & { deployment }` — so operator components keep their prop types while reads migrate to `app.deployment.X`. - One `APPS` array = third-party catalog models (each given a derived manifest) + the org's own deployed apps (manifest lifted from the old PIPELINES literal). One `getAppById`; `getModelById` / `getCapabilityById` / `getPipelineById` are thin deprecated aliases. - Helpers (`publicPipelines`, `publicCatalog`, `deploymentsForPipeline`, `PIPELINE_APP_IDS`, visibility) preserve their prior meaning over the unified array — Explore shows the same set, multi-env "Deployed in" pills intact, no private apps leak. - The app-detail Overview now takes a single `app: App` and reads `app.deployment`. Catalog models therefore surface a (synthetic, mock) deployment manifest too, consistent with "every app is a pipeline." Verified: typecheck, lint (0 warnings), build all pass; Explore (20 cards), catalog + owned app detail, and Home all render correctly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bce6057 commit fba29a7

6 files changed

Lines changed: 558 additions & 354 deletions

File tree

app/(app)/apps/[id]/page.tsx

Lines changed: 62 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ import KeyBadge from "@/components/dashboard/KeyBadge";
2222
import CallsTable from "@/components/dashboard/CallsTable";
2323
import StatusDot from "@/components/dashboard/StatusDot";
2424
import {
25-
getCapabilityById,
26-
getPipelineById,
27-
pipelineToExploreApp,
25+
getAppById,
2826
effectiveVisibility,
2927
setPipelineVisibility,
3028
organizationSlug,
@@ -44,7 +42,7 @@ import {
4442
OverviewTab,
4543
SettingsTab,
4644
} from "@/components/dashboard/AppDetailView";
47-
import type { App, PipelineVisibility } from "@/lib/dashboard/types";
45+
import type { App, Pipeline, PipelineVisibility } from "@/lib/dashboard/types";
4846

4947
// ─── Tabs ───
5048
//
@@ -73,9 +71,10 @@ type TabSpec = {
7371
count?: number;
7472
};
7573

76-
// Overview leads when the app is deployment-backed (so there's something to
77-
// show). It's a read-only summary — KPIs + deployment metadata — viewable by
78-
// anyone who can see the app, not just the owner.
74+
// Overview is the app's front page and the default tab for everyone. It's a
75+
// read-only summary — headline stats + how to call it, plus deployment metadata
76+
// when the app is deployment-backed — rendered from the catalog model, so it's
77+
// available for any app, not just the org's own deployments.
7978
const OVERVIEW_TAB: TabSpec = { key: "overview", label: "Overview", icon: Box };
8079

8180
const TABS: TabSpec[] = [
@@ -594,59 +593,57 @@ export default function AppDetailPage() {
594593
const { id } = useParams<{ id: string }>();
595594
const { isConnected } = useAuth();
596595

597-
// The owned deployment (exists for your apps, public or private) and the
598-
// public catalog entry (exists for any listed app). The catalog object is the
599-
// render base; for a private app with no catalog listing we derive it from the
600-
// pipeline so the same template still works.
601-
const pipeline = getPipelineById(id);
596+
// The unified app — its catalog face and (always, since unification) its
597+
// deployment manifest under `app.deployment`. One id-based lookup resolves
598+
// both the org's own apps and the third-party catalog models.
599+
const app = getAppById(id);
602600
// Owner/operator chrome (Settings/manage tab, publish controls) lives in the
603601
// stacked apps PR. In the consumer base the app detail is view-only for
604602
// everyone, so owner mode is gated off here; the stacked PR's revert removes
605603
// this flag to re-enable ownership.
606604
const OWNER_MODE_ENABLED = false;
607605
const isOwner = OWNER_MODE_ENABLED && isConnected && PIPELINE_APP_IDS.has(id);
608-
const model =
609-
getCapabilityById(id) ??
610-
(pipeline ? pipelineToExploreApp(pipeline) : undefined);
611606

612607
// Visibility (publish state) for the owner Settings tab.
613608
const [visibility, setVisibility] = useState<PipelineVisibility>(
614-
pipeline?.visibility ?? "private",
609+
app?.deployment?.visibility ?? "private",
615610
);
616611
useEffect(() => {
617-
if (pipeline) setVisibility(effectiveVisibility(pipeline));
612+
if (app) setVisibility(effectiveVisibility(app));
618613
// eslint-disable-next-line react-hooks/exhaustive-deps
619614
}, [id]);
620615
const toggleVisibility = () => {
621-
if (!pipeline) return;
616+
if (!app) return;
622617
const next: PipelineVisibility =
623618
visibility === "public" ? "private" : "public";
624-
setPipelineVisibility(pipeline.id, next);
619+
setPipelineVisibility(app.id, next);
625620
setVisibility(next);
626621
};
627622

628623
// Runs filtered to this app — drives both the Runs panel and the count chip.
629-
// `model` may be undefined (404 path below); run the hook unconditionally.
624+
// `app` may be undefined (404 path below); run the hook unconditionally.
630625
const filteredRuns = useMemo(() => {
631-
if (!model) return [];
626+
if (!app) return [];
632627
return MOCK_RECENT_REQUESTS.filter((r) =>
633-
modelMatchesRow(model.id, r.model),
628+
modelMatchesRow(app.id, r.model),
634629
);
635-
}, [model]);
630+
}, [app]);
636631

637-
// Tab set: Overview (when deployment-backed) + the consumer tabs are shown to
638-
// everyone; owners additionally get the Settings trail.
632+
// Tab set: Overview + the consumer tabs are shown to everyone (Overview
633+
// renders from the catalog model, so it's there for any app); owners
634+
// additionally get the Settings trail.
639635
const tabs: TabSpec[] = useMemo(() => {
640636
const consumer = TABS.map((t) =>
641637
t.key === "jobs" ? { ...t, count: filteredRuns.length } : t,
642638
);
643-
const base = pipeline ? [OVERVIEW_TAB, ...consumer] : consumer;
639+
const base = [OVERVIEW_TAB, ...consumer];
644640
return isOwner ? [...base, ...OWNER_TABS] : base;
645-
}, [filteredRuns.length, isOwner, pipeline]);
641+
}, [filteredRuns.length, isOwner]);
646642

647-
// Default landing tab: owners land on Overview (the console chrome); everyone
648-
// else on Playground. A `?tab=` param overrides when the viewer has that tab.
649-
const defaultTab: Tab = isOwner ? "overview" : "playground";
643+
// Default landing tab: Overview — the app's front page (what it is, how it
644+
// performs, how to call it). A `?tab=` param overrides when the viewer has
645+
// that tab.
646+
const defaultTab: Tab = "overview";
650647
const [activeTab, setActiveTab] = useState<Tab>(defaultTab);
651648
useEffect(() => {
652649
const requested = new URLSearchParams(window.location.search).get(
@@ -660,7 +657,7 @@ export default function AppDetailPage() {
660657
// eslint-disable-next-line react-hooks/exhaustive-deps
661658
}, [id, isOwner]);
662659

663-
if (!model) {
660+
if (!app) {
664661
return (
665662
<main id="main-content" className="flex flex-1 flex-col bg-dark">
666663
<div className="flex flex-1 flex-col items-center justify-center text-center">
@@ -676,40 +673,41 @@ export default function AppDetailPage() {
676673
);
677674
}
678675

679-
const Icon = getAppIcon(model.category);
676+
const deployment = app.deployment;
677+
const Icon = getAppIcon(app.category);
680678

681679
// One status indicator: deploy state for owners, runtime liveness for
682680
// consumers. One type indicator: Live (streaming) vs Batch (request/response).
683681
const statusTone =
684-
isOwner && pipeline
685-
? pipeline.status === "deployed"
682+
isOwner && deployment
683+
? deployment.status === "deployed"
686684
? "green"
687-
: pipeline.status === "error"
685+
: deployment.status === "error"
688686
? "red"
689-
: pipeline.status === "building"
687+
: deployment.status === "building"
690688
? "amber"
691689
: "blue"
692-
: model.status === "hot"
690+
: app.status === "hot"
693691
? "warm"
694692
: "blue";
695693
const statusLabel =
696-
isOwner && pipeline
697-
? pipeline.status === "deployed"
694+
isOwner && deployment
695+
? deployment.status === "deployed"
698696
? "Deployed"
699-
: pipeline.status === "building"
697+
: deployment.status === "building"
700698
? "Building"
701-
: pipeline.status === "error"
699+
: deployment.status === "error"
702700
? "Error"
703701
: "Stopped"
704-
: model.status === "hot"
702+
: app.status === "hot"
705703
? "warm"
706704
: "cold";
707705
const statusStatic =
708-
isOwner && pipeline
709-
? pipeline.status !== "deployed"
710-
: model.status !== "hot";
706+
isOwner && deployment
707+
? deployment.status !== "deployed"
708+
: app.status !== "hot";
711709
const isLive =
712-
isOwner && pipeline ? pipeline.kind === "live" : Boolean(model.realtime);
710+
isOwner && deployment ? deployment.kind === "live" : Boolean(app.realtime);
713711

714712
return (
715713
<main id="main-content" className="flex flex-1 flex-col bg-dark">
@@ -722,15 +720,15 @@ export default function AppDetailPage() {
722720
aria-label="Breadcrumb"
723721
>
724722
<Link
725-
href={`/orgs/${organizationSlug(model.provider)}`}
723+
href={`/orgs/${organizationSlug(app.provider)}`}
726724
className="text-fg-muted transition-colors hover:text-fg"
727725
>
728-
{model.provider}
726+
{app.provider}
729727
</Link>
730728
<span className="text-fg-disabled" aria-hidden="true">
731729
/
732730
</span>
733-
<span className="truncate font-medium text-fg">{model.name}</span>
731+
<span className="truncate font-medium text-fg">{app.name}</span>
734732
</nav>
735733
</div>
736734

@@ -742,9 +740,9 @@ export default function AppDetailPage() {
742740
<div className="flex items-start gap-4">
743741
{/* Thumbnail — cover image, or a bordered icon tile fallback. */}
744742
<div className="relative h-14 w-14 shrink-0 overflow-hidden rounded-md border border-subtle bg-dark-card">
745-
{model.coverImage ? (
743+
{app.coverImage ? (
746744
<img
747-
src={model.coverImage}
745+
src={app.coverImage}
748746
alt=""
749747
className="h-full w-full object-cover"
750748
/>
@@ -766,7 +764,7 @@ export default function AppDetailPage() {
766764
<div className="min-w-0 flex-1">
767765
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
768766
<h1 className="text-[22px] font-semibold tracking-[-0.02em] text-fg">
769-
{model.name}
767+
{app.name}
770768
</h1>
771769
<span className="inline-flex items-center gap-1.5 text-[12.5px] text-fg-strong">
772770
<StatusDot tone={statusTone} static={statusStatic} />
@@ -808,7 +806,7 @@ export default function AppDetailPage() {
808806
</button>
809807
</div>
810808
<p className="mt-2 max-w-[680px] text-[13.5px] leading-[1.5] text-fg-muted">
811-
{model.description}
809+
{app.description}
812810
</p>
813811
</div>
814812
</div>
@@ -897,23 +895,23 @@ export default function AppDetailPage() {
897895
aria-labelledby={`tab-${activeTab}`}
898896
>
899897
{/* Overview (read-only summary, shown to anyone) + Settings (owner
900-
only) reuse the deployment-console chrome verbatim. */}
901-
{activeTab === "overview" && pipeline && (
902-
<OverviewTab app={pipeline} />
903-
)}
904-
{activeTab === "settings" && pipeline && (
898+
only) reuse the deployment-console chrome verbatim. Overview
899+
renders from the catalog model, so it works for any app; the
900+
deployment manifest, when present, enriches it. */}
901+
{activeTab === "overview" && <OverviewTab app={app} />}
902+
{activeTab === "settings" && app.deployment && (
905903
<SettingsTab
906-
app={pipeline}
904+
app={app as Pipeline}
907905
visibility={visibility}
908906
onToggleVisibility={toggleVisibility}
909907
/>
910908
)}
911-
{activeTab === "playground" && <PlaygroundTab model={model} />}
912-
{activeTab === "api" && <ApiTab model={model} />}
913-
{activeTab === "readme" && <ReadmeTab model={model} />}
914-
{activeTab === "stats" && <StatsTab model={model} />}
909+
{activeTab === "playground" && <PlaygroundTab model={app} />}
910+
{activeTab === "api" && <ApiTab model={app} />}
911+
{activeTab === "readme" && <ReadmeTab model={app} />}
912+
{activeTab === "stats" && <StatsTab model={app} />}
915913
{activeTab === "jobs" && (
916-
<JobsTab model={model} runs={filteredRuns} />
914+
<JobsTab model={app} runs={filteredRuns} />
917915
)}
918916
</div>
919917
</div>

0 commit comments

Comments
 (0)