+ )}
{model.playgroundConfig.playgroundVariant === "transcoding" ? (
decodeURIComponent(segment)).join("/");
+ }
+ return id ? decodeURIComponent(id) : "";
+}
+
export default function AppDetailPage() {
- const { id } = useParams<{ id: string }>();
+ const params = useParams<{ id: string | string[] }>();
+ const id = capabilityIdFromParams(params.id);
const { isConnected } = useAuth();
-
- // The unified app — its catalog face and (always, since unification) its
- // deployment manifest under `app.deployment`. One id-based lookup resolves
- // both the org's own apps and the third-party catalog models.
- const app = getAppById(id);
+ // The app detail is powered by live Discovery Service data — the capability
+ // is resolved by id, with loading/error/not-found states handled below.
+ const discovery = useDiscoveryModel(id || undefined);
+ const app = discovery.status === "ready" ? discovery.model : undefined;
// Owner/operator chrome (Settings/manage tab, publish controls) lives in the
// stacked apps PR. In the consumer base the app detail is view-only for
// everyone, so owner mode is gated off here; the stacked PR's revert removes
@@ -657,6 +791,31 @@ export default function AppDetailPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, isOwner]);
+ if (discovery.status === "loading") {
+ return (
+
+
+
+ );
+ }
+
+ if (discovery.status === "error") {
+ return (
+
+
+
Could not load capability from Discovery Service.
+
{discovery.message}
+
+ Back to Explore
+
+
+
+ );
+ }
+
if (!app) {
return (
diff --git a/app/(app)/usage/page.tsx b/app/(app)/usage/page.tsx
index 94a75d1..1c17602 100644
--- a/app/(app)/usage/page.tsx
+++ b/app/(app)/usage/page.tsx
@@ -1,13 +1,9 @@
"use client";
-import { Suspense, useState } from "react";
+import { Suspense } from "react";
import Link from "next/link";
import { BarChart3, Box, ChevronDown } from "lucide-react";
import { useAuth } from "@/components/dashboard/AuthContext";
-import { useEnvironment } from "@/components/dashboard/EnvironmentContext";
-import EnvironmentFilter, {
- ALL_ENVIRONMENTS as ALL,
-} from "@/components/dashboard/EnvironmentFilter";
import DashboardPageHeader from "@/components/dashboard/DashboardPageHeader";
import DashboardPageSkeleton from "@/components/dashboard/DashboardPageSkeleton";
import SignInWall from "@/components/dashboard/SignInWall";
@@ -23,29 +19,26 @@ export default function UsagePage() {
function UsageContent() {
const { isConnected, isLoading } = useAuth();
- const { environments } = useEnvironment();
- const [envFilter, setEnvFilter] = useState(ALL);
// Avoid flashing the wall while auth hydrates.
if (isLoading) return null;
- if (!isConnected) return ;
- const selected = environments.find((e) => e.id === envFilter);
- // Consumption split: production carries the bulk, development the rest.
- const weight =
- envFilter === ALL ? 1 : selected?.kind === "production" ? 0.91 : 0.09;
- const filterName =
- envFilter === ALL ? "all environments" : (selected?.name ?? "all environments");
+ // Workspace-only route — logged-out users see "Usage is workspace-only"
+ // wall in place of the dashboard. The previous behavior (a hard redirect
+ // to /login) was wrong per the v4 prototype: it dropped the
+ // user out of context. The wall keeps them inside the app shell, leaves
+ // the sidebar in its logged-out variant, and offers an explicit
+ // "Explore capabilities" escape hatch.
+ if (!isConnected) return ;
return (
-
Could not load capabilities from Discovery Service.
+
{message}
+
+
+ );
+}
+
function ExplorePageInner() {
+ const exploreState = useExploreModels();
+ const { status, models, reload } = exploreState;
const searchParams = useSearchParams();
const initialCategory = (() => {
const qp = searchParams.get("category");
@@ -422,37 +437,13 @@ function ExplorePageInner() {
const [priceMin, setPriceMin] = useState(0);
const [priceMax, setPriceMax] = useState(100);
- // The org's public deployed apps are listed in Explore alongside the
- // third-party catalog models. Seeded SSR-safely, then refreshed from the
- // localStorage-backed publish state after mount so toggling an app's
- // visibility on its Settings tab is reflected here on next navigation.
- const [pipelineModels, setPipelineModels] = useState(
- SEED_PUBLIC_PIPELINE_APPS,
- );
- useEffect(() => {
- setPipelineModels(publicPipelines());
- }, []);
-
- // APPS now carries the org's own apps too (public + private). Take the
- // third-party catalog from APPS and re-attach only the *public* owned apps so
- // private deployments never leak into Explore and nothing is duplicated.
- const catalogModels = useMemo(
- () => APPS.filter((m) => !PIPELINE_APP_IDS.has(m.id)),
- [],
- );
-
- const allModels = useMemo(
- () => [...catalogModels, ...pipelineModels],
- [catalogModels, pipelineModels],
- );
-
const dataMaxPrice = useMemo(
- () => Math.max(...allModels.map((m) => m.pricing.amount), 0.01),
- [allModels],
+ () => Math.max(...models.map((m) => m.pricing.amount), 0.01),
+ [models],
);
const filtered = useMemo(() => {
- const result = allModels.filter((m) => {
+ const result = models.filter((m) => {
if (availabilityFilter === "warm" && m.status !== "hot") return false;
if (availabilityFilter === "cold" && m.status !== "cold") return false;
if (favoritesOnly && !isStarred(m.id)) return false;
@@ -478,7 +469,28 @@ function ExplorePageInner() {
});
return result;
- }, [allModels, search, category, availabilityFilter, favoritesOnly, isStarred, priceMin, priceMax, dataMaxPrice]);
+ }, [models, search, category, availabilityFilter, favoritesOnly, isStarred, priceMin, priceMax, dataMaxPrice]);
+
+ if (status === "loading" && models.length === 0) {
+ return (
+
+
+
+
+ );
+ }
+
+ if (status === "error") {
+ return (
+
+
+
+
+ );
+ }
const activeFilters = [
...(category
@@ -664,20 +676,9 @@ function ExplorePageInner() {
) : view === "grid" ? (
- {filtered.map((model) => {
- const isPipeline = PIPELINE_APP_IDS.has(model.id);
- // Pipeline cards open the consumer/playground face (/apps/[id]);
- // owners reach the operator console from a "Manage app" affordance
- // there. The catalog is a consume surface, so a card never drops a
- // caller straight into someone's operator view.
- return (
-
- );
- })}
+ {filtered.map((model) => (
+
+ ))}