Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9dd32dd
feat(dashboard): add usage capability display and account usage hooks
eliteprox Jun 1, 2026
15560f6
feat(dashboard): enhance StackedAreaChart and UsageView for daily dat…
eliteprox Jun 2, 2026
bcc5f5a
chore(deps): update @pymthouse/builder-sdk to version 0.3.0
eliteprox Jun 2, 2026
14a1c43
feat(gateway): enhance API routing and signer proxy functionality
eliteprox Jun 8, 2026
d337fb8
refactor(gateway): remove deprecated gateway functionality and clean …
eliteprox Jun 16, 2026
83b4d0a
refactor(api): remove audience configuration from API key exchange
eliteprox Jun 16, 2026
c3b698f
feat(api): include retail data in external user usage fetch
eliteprox Jun 16, 2026
ff43f44
chore(deps): update @pymthouse/builder-sdk to version 0.4.2-rc.1
eliteprox Jun 16, 2026
4d9b3f1
feat(api): enhance account usage API with no-store caching
eliteprox Jun 16, 2026
9b60ee1
chore(deps): update @pymthouse/builder-sdk to version 0.4.3
eliteprox Jun 16, 2026
f2054b4
Merge branch 'main' into feat/pymthouse-integration
eliteprox Jun 18, 2026
7ed3ffe
chore(deps): upgrade @pymthouse/builder-sdk to 0.4.5
eliteprox Jun 22, 2026
98352a7
chore(deps): upgrade @pymthouse/builder-sdk to 0.4.6
eliteprox Jun 23, 2026
9b09d9b
refactor(auth): move device-code login out of pymthouse integration PR
eliteprox Jun 23, 2026
de7e271
refactor(auth): replace DashboardProviders with AuthProvider in layouts
eliteprox Jun 23, 2026
92d405a
feat(runner-gateway): integrate live runner functionality and update …
eliteprox Jun 29, 2026
e0f1823
feat(playground): enhance live runner integration and update environm…
eliteprox Jul 14, 2026
a11f448
feat(auth): update user identification method and enhance API key han…
eliteprox Jul 18, 2026
3c3b28e
chore(deps): update @pymthouse/builder-sdk to version 0.6.1
eliteprox Jul 18, 2026
1fbc30d
feat(api): add account requests endpoint and enhance account usage ha…
eliteprox Jul 18, 2026
bba703c
fix(dashboard): show USD allowance in sidebar usage meter
eliteprox Jul 18, 2026
89d3a9a
fix(dashboard): show consumed/granted dollars in sidebar meter
eliteprox Jul 18, 2026
0eb4246
fix(dashboard): import microsToUsd for sidebar meter display
eliteprox Jul 18, 2026
3a3ac49
fix(discovery): normalize DISCOVERY_SERVICE_URL for Explore and tokens
eliteprox Jul 25, 2026
709aed4
fix(discovery): expect full raw DISCOVERY_SERVICE_URL as-is
eliteprox Jul 25, 2026
8a48b42
chore(deps): bump @pymthouse/builder-sdk to 0.6.3
eliteprox Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,30 @@
# The Graph (optional — falls back to hardcoded values)
THEGRAPH_API_KEY=

# Discovery Service — full raw endpoint (as-is for gateway tokens).
# Explore uses the URL origin for `/v1/discovery/capabilities` etc.
# Aliases: DISCOVERY_URL, LIVEPEER_DISCOVERY_SERVICE_URL
DISCOVERY_SERVICE_URL=https://discovery-service-production-8955.up.railway.app/v1/discovery/raw

# PymtHouse (usage + API keys + signer sessions)
# Production:
PYMTHOUSE_ISSUER_URL=https://pymthouse.com/api/v1/oidc
# Local pymthouse: http://localhost:3001/api/v1/oidc
# Public app client id (OAuth client_id) — used for Builder API path {clientId}
PYMTHOUSE_PUBLIC_CLIENT_ID=
PYMTHOUSE_M2M_CLIENT_ID=
PYMTHOUSE_M2M_CLIENT_SECRET=
# Remote signer DMZ base included as signer_url in API-key exchange responses
# (fallback when the issuer does not return signer_url).
# Production: https://pymthouse-production.up.railway.app
# Local compose: http://127.0.0.1:8080
PYMTHOUSE_SIGNER_URL=https://pymthouse-production.up.railway.app
# Set to 1 for local dev when issuer uses http://127.0.0.1
PYMTHOUSE_ALLOW_INSECURE_HTTP=

# Live-runner discovery (orchestrator /discovery endpoint).
# Local example-apps stack: http://localhost:8935/discovery
# Remote hello-world orch: https://kiloutcorp.link:11111/discovery
RUNNER_DISCOVERY_URL=http://localhost:8935/discovery
# Accept self-signed orch TLS (local/dev only).
# RUNNER_GATEWAY_ALLOW_INSECURE_TLS=1
177 changes: 168 additions & 9 deletions app/(app)/apps/[id]/page.tsx → app/(app)/apps/[...id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,31 @@ import KeyBadge from "@/components/dashboard/KeyBadge";
import CallsTable from "@/components/dashboard/CallsTable";
import StatusDot from "@/components/dashboard/StatusDot";
import {
getAppById,
effectiveVisibility,
setPipelineVisibility,
organizationSlug,
PIPELINE_APP_IDS,
SETTINGS_API_KEYS,
MOCK_RECENT_REQUESTS,
} from "@/lib/dashboard/mock-data";
import { useDiscoveryModel } from "@/lib/dashboard/useDiscoveryModel";
import DashboardPageSkeleton from "@/components/dashboard/DashboardPageSkeleton";
import { getAppIcon } from "@/lib/dashboard/utils";
import PlaygroundForm from "@/components/dashboard/playground/PlaygroundForm";
import JsonInput from "@/components/dashboard/playground/JsonInput";
import PlaygroundOutput from "@/components/dashboard/playground/PlaygroundOutput";
import TranscodingOutput from "@/components/dashboard/playground/TranscodingOutput";
import CodeSnippets from "@/components/dashboard/playground/CodeSnippets";
import WebcamPlayground from "@/components/dashboard/playground/WebcamPlayground";
import {
RunnerGatewayProvider,
useRunnerGatewayContext,
} from "@/components/dashboard/playground/RunnerGatewayContext";
import {
buildLiveRunnerPayload,
extractRunnerResultText,
runnerGatewayPostUrl,
} from "@/lib/dashboard/runner-gateway-client";
import AppAnalytics from "@/components/dashboard/stats/AppAnalytics";
import {
OverviewTab,
Expand Down Expand Up @@ -106,17 +116,33 @@ function modelMatchesRow(catalogId: string, runModel: string): boolean {
// ─── Playground Tab ───

function PlaygroundTab({ model }: { model: App }) {
return (
<RunnerGatewayProvider model={model}>
<PlaygroundTabContent model={model} />
</RunnerGatewayProvider>
);
}

function PlaygroundTabContent({ model }: { model: App }) {
const { user } = useAuth();
const {
canRunLive,
state: runnerGatewayState,
signerJwt,
} = useRunnerGatewayContext();
const [inputMode, setInputMode] = useState<"form" | "json" | "python" | "node" | "http">("form");
const [isRunning, setIsRunning] = useState(false);
const [result, setResult] = useState<string | null>(null);
const [inferenceTime, setInferenceTime] = useState<number | undefined>();
const [lastRunValues, setLastRunValues] = useState<Record<string, unknown> | null>(null);
const [runError, setRunError] = useState<string | null>(null);

const handleRun = useCallback(
const runMock = useCallback(
(values: Record<string, unknown>) => {
setLastRunValues(values);
setIsRunning(true);
setResult(null);
setRunError(null);
const time = 0.3 + Math.random() * 1.5;
setTimeout(() => {
setIsRunning(false);
Expand Down Expand Up @@ -156,6 +182,87 @@ function PlaygroundTab({ model }: { model: App }) {
[model],
);

const runLive = useCallback(
async (values: Record<string, unknown>) => {
if (runnerGatewayState.status !== "ready" || !user?.id?.trim()) {
runMock(values);
return;
}

setLastRunValues(values);
setIsRunning(true);
setResult(null);
setRunError(null);
const started = performance.now();

try {
const runnerPath =
model.playgroundConfig?.runnerPath?.trim() || "chat/completions";
const payload = buildLiveRunnerPayload(model, values);
const url = runnerGatewayPostUrl(
runnerGatewayState.gatewayBaseUrl,
runnerGatewayState.runnerAppId,
runnerPath,
);
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-external-user-id": user.id.trim(),
},
body: JSON.stringify(payload),
});

const contentType = response.headers.get("content-type") ?? "";
if (!response.ok) {
let message = `Gateway error (${response.status})`;
try {
const errBody = (await response.json()) as { error?: string };
if (errBody.error) message = errBody.error;
} catch {
// ignore
}
throw new Error(message);
}

if (contentType.includes("text/event-stream") && response.body) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let streamed = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
streamed += decoder.decode(value, { stream: true });
setResult(streamed);
}
} else {
const data = await response.json();
setResult(extractRunnerResultText(data));
}

setInferenceTime(parseFloat(((performance.now() - started) / 1000).toFixed(1)));
} catch (error) {
const message = error instanceof Error ? error.message : "Run failed";
setRunError(message);
setResult(null);
} finally {
setIsRunning(false);
}
},
[model, runMock, runnerGatewayState, user?.id],
);

const handleRun = useCallback(
(values: Record<string, unknown>) => {
if (canRunLive) {
void runLive(values);
return;
}
runMock(values);
},
[canRunLive, runLive, runMock],
);

// Ctrl+Enter shortcut
useEffect(() => {
const handler = (e: KeyboardEvent) => {
Expand Down Expand Up @@ -231,6 +338,7 @@ function PlaygroundTab({ model }: { model: App }) {
config={model.playgroundConfig}
onRun={handleRun}
isRunning={isRunning}
signerJwt={signerJwt}
/>
)}
{inputMode === "json" && (
Expand All @@ -244,6 +352,10 @@ function PlaygroundTab({ model }: { model: App }) {
inputMode === "node" ||
inputMode === "http") && (
<div className="flex flex-col">
{/* Signer JWT is minted server-side and kept off the visible UI. */}
{signerJwt ? (
<input type="hidden" name="signer-jwt" value={signerJwt} readOnly />
) : null}
<div className="pb-4">
<CodeSnippets model={model} fixedLang={inputMode} />
</div>
Expand Down Expand Up @@ -281,7 +393,22 @@ function PlaygroundTab({ model }: { model: App }) {

{/* Right: Output */}
<div>
<h3 className="mb-4 text-sm font-medium text-fg-faint">Output</h3>
<div className="mb-4 flex items-center justify-between gap-3">
<h3 className="text-sm font-medium text-fg-faint">Output</h3>
{model.runnerAppId && runnerGatewayState.status === "ready" && (
<span className="rounded-full border border-green/30 bg-green/10 px-2 py-0.5 text-[10px] font-medium text-green-bright">
Live runner
</span>
)}
{model.runnerAppId && runnerGatewayState.status === "loading" && (
<span className="text-[10px] text-fg-label">Preparing signer…</span>
)}
</div>
{runError && (
<p className="mb-3 rounded-lg border border-red/30 bg-red/10 px-3 py-2 text-xs text-red-400">
{runError}
</p>
)}
{model.playgroundConfig.playgroundVariant === "transcoding" ? (
<TranscodingOutput
result={result}
Expand Down Expand Up @@ -589,14 +716,21 @@ function JobsTab({

// ─── Main Page ───

function capabilityIdFromParams(id: string | string[] | undefined): string {
if (Array.isArray(id)) {
return id.map((segment) => 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
Expand Down Expand Up @@ -657,6 +791,31 @@ export default function AppDetailPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, isOwner]);

if (discovery.status === "loading") {
return (
<main id="main-content" className="flex flex-1 flex-col bg-dark">
<DashboardPageSkeleton maxWidth="5xl" withTabs kpiCount={0} withChart={false} />
</main>
);
}

if (discovery.status === "error") {
return (
<main id="main-content" className="flex flex-1 flex-col bg-dark">
<div className="flex flex-1 flex-col items-center justify-center px-5 text-center">
<p className="text-sm text-fg-muted">Could not load capability from Discovery Service.</p>
<p className="mt-2 max-w-md font-mono text-xs text-fg-faint">{discovery.message}</p>
<Link
href="/explore"
className="mt-6 text-xs text-green-bright hover:underline focus:outline-none rounded"
>
Back to Explore
</Link>
</div>
</main>
);
}

if (!app) {
return (
<main id="main-content" className="flex flex-1 flex-col bg-dark">
Expand Down
27 changes: 10 additions & 17 deletions app/(app)/usage/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 <SignInWall route="usage" />;

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 <SignInWall route="usage" />;

return (
<main id="main-content" className="flex flex-1 flex-col bg-dark">
<DashboardPageHeader
title="Usage"
icon={BarChart3}
description="Requests, latency, errors, and spend across your API tokens."
description="Signed requests, network cost, and Starter allowance from PymtHouse OpenMeter."
actions={
<>
<EnvironmentFilter value={envFilter} onChange={setEnvFilter} />
<button
type="button"
className="inline-flex h-[26px] items-center gap-1.5 rounded-[4px] border border-transparent px-2.5 text-[12.5px] text-fg-strong transition-colors hover:border-hairline hover:bg-hover hover:text-fg"
Expand All @@ -65,7 +58,7 @@ function UsageContent() {
}
/>
<div className="flex flex-1 flex-col overflow-y-auto">
<UsageView weight={weight} filterName={filterName} />
<UsageView />
</div>
</main>
);
Expand Down
18 changes: 10 additions & 8 deletions app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
"use client";

import { useEffect } from "react";
import { Suspense, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/components/dashboard/AuthContext";
import LoginPage from "@/components/dashboard/LoginPage";

/**
* Signup route — sibling of `/login`. Renders the same
* `LoginPage` component but seeds it with `initialMode="signup"`. The
* footer toggle inside the page is a `<Link>` to `/login`, so
* URL and visible mode stay in sync without query-param trickery.
*/
export default function SignupRoute() {
function SignupRouteInner() {
const { isConnected } = useAuth();
const router = useRouter();

Expand All @@ -25,3 +19,11 @@ export default function SignupRoute() {

return <LoginPage initialMode="signup" />;
}

export default function SignupRoute() {
return (
<Suspense fallback={null}>
<SignupRouteInner />
</Suspense>
);
}
Loading