feat: move resale dashboard into marketplace

This commit is contained in:
david
2026-08-28 10:28:39 +02:00
parent 53607738f8
commit 9463bfae85
14 changed files with 694 additions and 123 deletions

View File

@@ -38,6 +38,9 @@ NETWORK_ID=0
# along over loopback. Production behind TLS should leave this unset / "1".
# COOKIE_SECURE=1
# Internal, redacted data source for the authenticated /dashboard page.
# SURPLUS_DASHBOARD_URL=http://surplus-seller:8080/internal/resale-dashboard
# ─── Optional: PDF Book Summarizer (/book page, /v1/pdf-* routes) ─────────
# Upload a PDF → chunk it → pay suppliers to summarize each chunk (one escrow
# per map + reduce call) → full-book summary. All knobs are optional; defaults

View File

@@ -42,6 +42,9 @@
* API key" page knows where to POST /signup. When
* unset, the SPA falls back to deriving "api." +
* current host.
* SURPLUS_DASHBOARD_URL — Internal URL of the private Surplus dashboard
* snapshot endpoint. When unset, /v1/resale-dashboard
* responds 503 while the rest of the buyer stays live.
* PDF_ENABLED — "1" (default) serves the PDF book summarizer
* (/v1/pdf-* routes). Set "0" to disable the feature
* entirely: no PDF job store is opened and every
@@ -68,6 +71,7 @@ export interface BuyerConfig {
cookieSecure: boolean;
pdfEnabled: boolean;
gatewayPublicUrl: string;
resaleDashboardUrl: string;
}
function requireField(env: Record<string, string | undefined>, name: string): string {
@@ -169,6 +173,20 @@ export function loadConfig(env: Record<string, string | undefined>): BuyerConfig
// the SPA via the boot block (no trailing slash). Empty string means the SPA
// derives the URL from its own host ("api." + location.host).
const gatewayPublicUrl = (env.GATEWAY_PUBLIC_URL ?? "").trim().replace(/\/+$/, "");
const resaleDashboardUrl = (env.SURPLUS_DASHBOARD_URL ?? "")
.trim()
.replace(/\/+$/, "");
if (resaleDashboardUrl !== "") {
let parsed: URL;
try {
parsed = new URL(resaleDashboardUrl);
} catch {
throw new Error("loadConfig: SURPLUS_DASHBOARD_URL must be a valid URL");
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
throw new Error("loadConfig: SURPLUS_DASHBOARD_URL must use http or https");
}
}
return {
privKeyHex,
@@ -186,5 +204,6 @@ export function loadConfig(env: Record<string, string | undefined>): BuyerConfig
cookieSecure,
pdfEnabled,
gatewayPublicUrl,
resaleDashboardUrl,
};
}

View File

@@ -141,6 +141,7 @@ export async function runMain(env: Record<string, string | undefined>): Promise<
sessionSecret: config.sessionSecret,
cookieSecure: config.cookieSecure,
gatewayPublicUrl: config.gatewayPublicUrl,
resaleDashboardUrl: config.resaleDashboardUrl,
jobStore,
pdfCaps,
});

View File

@@ -114,6 +114,9 @@ export interface AppDeps {
* `${gatewayUrl}/signup`. Public, non-secret value. When empty/omitted the
* SPA derives "api." + its own host. */
gatewayPublicUrl?: string;
/** Private supplier-network endpoint used to populate the resale dashboard.
* The browser only sees the redacted same-origin /v1/resale-dashboard proxy. */
resaleDashboardUrl?: string;
/** PDF book summarizer job registry. When provided, the /v1/pdf-* routes
* are live; when omitted they respond 503 (feature disabled). Built in
* runMain from the same marketplace/chain/wallet/archive deps. */
@@ -317,6 +320,66 @@ export function createApp(deps: AppDeps): Express {
}
: null;
app.get("/v1/resale-dashboard", async (_req: Request, res: Response) => {
const dashboardUrl = (deps.resaleDashboardUrl ?? "").replace(/\/+$/, "");
if (dashboardUrl === "") {
return jsonError(
res,
503,
"service_unavailable",
"buyer-app booted without SURPLUS_DASHBOARD_URL",
);
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 10_000);
try {
const upstream = await (deps.fetchImpl ?? globalThis.fetch)(dashboardUrl, {
headers: { accept: "application/json" },
redirect: "error",
signal: controller.signal,
});
const text = await upstream.text();
if (Buffer.byteLength(text, "utf8") > 1024 * 1024) {
return jsonError(
res,
502,
"dashboard_response_too_large",
"resale dashboard response exceeded 1 MiB",
);
}
if (!upstream.ok) {
return jsonError(
res,
502,
"dashboard_upstream_error",
`resale dashboard upstream returned ${upstream.status}`,
);
}
let payload: unknown;
try {
payload = JSON.parse(text);
} catch {
return jsonError(
res,
502,
"dashboard_invalid_json",
"resale dashboard upstream did not return JSON",
);
}
res.setHeader("Cache-Control", "no-store");
return res.status(200).json(payload);
} catch {
return jsonError(
res,
502,
"dashboard_unreachable",
"resale dashboard upstream is unavailable",
);
} finally {
clearTimeout(timer);
}
});
app.get("/v1/pending-receipts", async (_req: Request, res: Response) => {
if (!resolved) {
return jsonError(

View File

@@ -4,6 +4,7 @@
* Routes (all gated by <RequireAuth>):
* / → <Dashboard />
* /tasks → <TaskHistory />
* /dashboard → <ResaleDashboard /> unified Surplus + Vector metrics
* /pending → <PendingReceipts /> UX-1: buyer-side Accept button
* /wallet → <Wallet />
* /api-keys → <ApiKeys /> self-serve gateway API key generation
@@ -16,6 +17,7 @@ import Wallet from "./pages/Wallet.js";
import PendingReceipts from "./pages/PendingReceipts.js";
import BookSummarizer from "./pages/BookSummarizer.js";
import ApiKeys from "./pages/ApiKeys.js";
import ResaleDashboard from "./pages/ResaleDashboard.js";
import { RequireAuth, useAuth } from "./state/AuthContext.js";
function SignOutButton() {
@@ -37,6 +39,7 @@ export default function App() {
<div className="min-h-screen bg-gray-50 text-gray-900">
<nav className="bg-white border-b border-gray-200 px-6 py-3 flex items-center gap-6">
<Link to="/" className="font-medium hover:underline">Dashboard</Link>
<Link to="/dashboard" className="font-medium hover:underline">Resale</Link>
{/* Book Summarizer tab hidden for now — the /book route and all PDF
summarize code remain intact; restore by re-adding this link:
<Link to="/book" className="font-medium hover:underline">Book Summarizer</Link> */}
@@ -49,6 +52,7 @@ export default function App() {
<main className="p-6">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/dashboard" element={<ResaleDashboard />} />
<Route path="/book" element={<BookSummarizer />} />
<Route path="/tasks" element={<TaskHistory />} />
<Route path="/pending" element={<PendingReceipts />} />

View File

@@ -0,0 +1,411 @@
import { useCallback, useEffect, useState } from "react";
interface DashboardSnapshot {
generatedAt: string;
controller: {
ok: boolean;
phase: string;
lastError: string | null;
};
identity: {
sellerWallet: string;
payoutAddress: string;
};
provider: {
hardLimitUsd: string;
remainingUsd: string;
usedUsd: string;
reserveUsd: string;
estimatedDailyExposureUsd: string;
};
surplus: {
offer: {
id: string | null;
model: string | null;
providerModel: string | null;
status: string;
dailyCapUsd: string | null;
costMultiplier: number | null;
inputUsdPer1m: string | null;
outputUsdPer1m: string | null;
rank: number | null;
available: boolean;
healthy: boolean;
trusted: boolean;
trades24h: number;
volume24hUsd: string;
capRemainingUsd: string | null;
};
earnings: {
totalUsd: string;
pendingUsd: string;
paidUsd: string;
requests: number;
tokens: number;
today: {
earnedUsd: number;
requests: number;
totalTokens: number;
} | null;
topModel: string | null;
payoutHoldReason: string | null;
payoutHoldReleasesAt: string | null;
};
recentSales: Array<{
id: string;
model: string;
createdAt: string | null;
settlementStatus: string;
inputTokens: number;
outputTokens: number;
cacheReadTokens: number;
revenueUsd: string;
transactionHash: string | null;
}>;
};
vector: {
status: "retired";
model: string;
supplierWallet: string;
advertRef: string;
retirementTransaction: string;
retiredOn: string;
historicalAp3xEarned: string;
historicalSettledJobs: number;
historicalUpstreamSpendUsd: string;
};
}
function usd(value: string | number | null): string {
if (value === null || value === "") return "—";
const number = Number(value);
if (!Number.isFinite(number)) return "—";
return `$${number.toFixed(number < 0.01 ? 6 : 4)}`;
}
function count(value: number): string {
return new Intl.NumberFormat().format(value);
}
function timestamp(value: string | null): string {
if (value === null) return "—";
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? "—" : parsed.toLocaleString();
}
function compact(value: string): string {
if (value.length <= 24) return value;
return `${value.slice(0, 12)}…${value.slice(-9)}`;
}
function StatusPill({ label, active }: { label: string; active: boolean }) {
return (
<span
className={`rounded px-2 py-0.5 text-xs font-medium ${
active ? "bg-green-100 text-green-700" : "bg-gray-200 text-gray-600"
}`}
>
{label}
</span>
);
}
function Metric({ label, value, detail }: { label: string; value: string; detail: string }) {
return (
<div className="rounded border border-gray-200 bg-gray-50 p-3">
<p className="text-xs font-medium uppercase tracking-wide text-gray-500">{label}</p>
<p className="mt-1 text-xl font-semibold tracking-tight text-gray-900">{value}</p>
<p className="mt-0.5 text-xs text-gray-500">{detail}</p>
</div>
);
}
function FlowNode({ label, title, detail, mono }: {
label: string;
title: string;
detail: string;
mono?: string | null;
}) {
return (
<div className="min-w-0 flex-1 rounded border border-gray-200 bg-gray-50 p-4">
<p className="text-xs font-medium uppercase tracking-wide text-gray-500">{label}</p>
<p className="mt-1 font-semibold text-gray-900">{title}</p>
<p className="mt-1 text-sm text-gray-600">{detail}</p>
{mono && <p className="mt-2 truncate font-mono text-xs text-gray-400">{mono}</p>}
</div>
);
}
export default function ResaleDashboard() {
const [data, setData] = useState<DashboardSnapshot | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const refresh = useCallback(async () => {
try {
const response = await fetch("/v1/resale-dashboard", { cache: "no-store" });
const body = (await response.json().catch(() => null)) as DashboardSnapshot | {
message?: string;
} | null;
if (!response.ok) {
throw new Error(
body && "message" in body && typeof body.message === "string"
? body.message
: `dashboard request failed (${response.status})`,
);
}
setData(body as DashboardSnapshot);
setError(null);
} catch (cause) {
setError(cause instanceof Error ? cause.message : "dashboard data is unavailable");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void refresh();
const interval = setInterval(() => { void refresh(); }, 15_000);
return () => clearInterval(interval);
}, [refresh]);
if (loading && data === null) {
return <p className="text-sm text-gray-500">Loading resale dashboard…</p>;
}
if (data === null) {
return (
<div className="max-w-3xl rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700">
{error ?? "Dashboard data is unavailable."}
</div>
);
}
const offer = data.surplus.offer;
const earnings = data.surplus.earnings;
const vector = data.vector;
const offerLive = offer.status === "active" && offer.available && offer.healthy;
const advertHash = vector.advertRef.split("#")[0] ?? "";
return (
<div className="mx-auto max-w-7xl space-y-6" data-testid="resale-dashboard">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1">
<h1 className="text-2xl font-semibold">Resale Dashboard</h1>
<p className="max-w-3xl text-sm text-gray-600">
One OpenRouter capacity pool managed by the seller agent. Surplus is the live Base USDC
market. Vector preserves the completed AP3X escrow proof.
</p>
</div>
<div className="flex items-center gap-3">
<StatusPill label={data.controller.phase} active={data.controller.ok} />
<button
type="button"
onClick={() => { void refresh(); }}
className="rounded border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50"
>
Refresh
</button>
</div>
</div>
{error && (
<div className="rounded border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-800">
Last refresh failed: {error}. Showing the last good snapshot.
</div>
)}
{data.controller.lastError && (
<div className="rounded border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
Controller: {data.controller.lastError}
</div>
)}
<section className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
<div className="mb-3 flex items-center justify-between">
<div>
<h2 className="font-semibold">Capacity route</h2>
<p className="text-sm text-gray-500">The same upstream capacity moved from Vector proof to Surplus production.</p>
</div>
<span className="text-xs text-gray-400">Updated {timestamp(data.generatedAt)}</span>
</div>
<div className="flex flex-col items-stretch gap-2 lg:flex-row lg:items-center">
<FlowNode
label="Upstream"
title="OpenRouter"
detail={`${usd(data.provider.remainingUsd)} of ${usd(data.provider.hardLimitUsd)} remaining`}
mono={offer.providerModel}
/>
<span className="self-center text-lg text-gray-400">→</span>
<FlowNode
label="Manager"
title="Seller agent"
detail="Discovers, prices, caps, and reconciles offers"
mono={data.identity.sellerWallet}
/>
<span className="self-center text-lg text-gray-400">→</span>
<FlowNode
label="Live settlement"
title="Surplus / Base USDC"
detail={`${offerLive ? "Available" : "Unavailable"} · ${offer.trusted ? "trusted" : "untrusted"}`}
mono={offer.id}
/>
</div>
</section>
<section className="grid gap-6 xl:grid-cols-2">
<article className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-medium uppercase tracking-wide text-gray-500">Current market</p>
<h2 className="mt-1 text-lg font-semibold">Surplus Intelligence</h2>
<p className="mt-1 text-sm text-gray-600">Discounted inference with USDC settlement on Base.</p>
</div>
<StatusPill label={offer.status} active={offerLive} />
</div>
<div className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-2 2xl:grid-cols-4">
<Metric label="Lifetime revenue" value={usd(earnings.totalUsd)} detail={`${usd(earnings.pendingUsd)} pending`} />
<Metric label="Requests sold" value={count(earnings.requests)} detail={`${count(earnings.tokens)} tokens`} />
<Metric label="Market rank" value={offer.rank === null ? "—" : `#${offer.rank}`} detail={`${offer.trades24h} trades / 24h`} />
<Metric label="Daily seller cap" value={usd(offer.dailyCapUsd)} detail={`${usd(offer.capRemainingUsd)} remaining`} />
</div>
<dl className="mt-4 grid gap-x-6 gap-y-2 border-t border-gray-100 pt-4 text-sm sm:grid-cols-2">
<div><dt className="text-gray-500">Marketplace model</dt><dd className="font-mono text-xs text-gray-900">{offer.model ?? "—"}</dd></div>
<div><dt className="text-gray-500">Provider model</dt><dd className="font-mono text-xs text-gray-900">{offer.providerModel ?? "—"}</dd></div>
<div><dt className="text-gray-500">Input / 1M</dt><dd>{usd(offer.inputUsdPer1m)}</dd></div>
<div><dt className="text-gray-500">Output / 1M</dt><dd>{usd(offer.outputUsdPer1m)}</dd></div>
</dl>
{earnings.payoutHoldReason && (
<div className="mt-4 rounded border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-800">
Payout hold: {earnings.payoutHoldReason}
{earnings.payoutHoldReleasesAt && ` · releases ${timestamp(earnings.payoutHoldReleasesAt)}`}
</div>
)}
</article>
<article className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-medium uppercase tracking-wide text-gray-500">Migration record</p>
<h2 className="mt-1 text-lg font-semibold">Vector marketplace</h2>
<p className="mt-1 text-sm text-gray-600">AP3X escrow route retired after the cross-market proof.</p>
</div>
<StatusPill label="retired" active={false} />
</div>
<div className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-2 2xl:grid-cols-4">
<Metric label="AP3X earned" value={`${vector.historicalAp3xEarned} AP3X`} detail="historical" />
<Metric label="Settled jobs" value={count(vector.historicalSettledJobs)} detail="Vector escrows" />
<Metric label="Upstream spend" value={usd(vector.historicalUpstreamSpendUsd)} detail="proof route" />
<Metric label="Retired on" value={vector.retiredOn} detail="advert closed" />
</div>
<div className="mt-4 space-y-3 border-t border-gray-100 pt-4 text-sm">
<div>
<p className="text-gray-500">Retired advert</p>
<p className="break-all font-mono text-xs text-gray-900">{vector.advertRef}</p>
<a
className="text-xs text-blue-600 hover:underline"
href={`https://vector.apexscan.org/en/transaction/${advertHash}/summary/`}
target="_blank"
rel="noopener noreferrer"
>
Open advert transaction ↗
</a>
</div>
<div>
<p className="text-gray-500">Retirement proof</p>
<p className="break-all font-mono text-xs text-gray-900">{vector.retirementTransaction}</p>
<a
className="text-xs text-blue-600 hover:underline"
href={`https://vector.apexscan.org/en/transaction/${vector.retirementTransaction}/summary/`}
target="_blank"
rel="noopener noreferrer"
>
Open confirmed transaction ↗
</a>
</div>
</div>
</article>
</section>
<section className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
<div>
<p className="text-xs font-medium uppercase tracking-wide text-gray-500">Economics</p>
<h2 className="mt-1 text-lg font-semibold">Capacity and price controls</h2>
<p className="mt-1 text-sm text-gray-600">
The seller cap is discounted revenue. The agent converts it to estimated upstream exposure before opening the route.
</p>
</div>
<div className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<Metric label="OpenRouter remaining" value={usd(data.provider.remainingUsd)} detail={`${usd(data.provider.usedUsd)} used`} />
<Metric label="Protected reserve" value={usd(data.provider.reserveUsd)} detail="fail-closed threshold" />
<Metric label="Daily upstream estimate" value={usd(data.provider.estimatedDailyExposureUsd)} detail="from recovery floor" />
<Metric label="Current multiplier" value={offer.costMultiplier === null ? "—" : `${(offer.costMultiplier * 100).toFixed(3)}%`} detail={`${usd(offer.inputUsdPer1m)} in · ${usd(offer.outputUsdPer1m)} out`} />
</div>
</section>
<section className="space-y-3">
<div className="flex flex-col gap-1 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="text-xs font-medium uppercase tracking-wide text-gray-500">Surplus activity</p>
<h2 className="mt-1 text-lg font-semibold">Recent sales</h2>
</div>
<p className="text-sm text-gray-500">{count(earnings.requests)} lifetime requests · {count(earnings.tokens)} tokens</p>
</div>
<div className="overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm">
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-50 text-left text-xs font-medium uppercase tracking-wide text-gray-500">
<tr>
<th className="px-4 py-3">Time</th>
<th className="px-4 py-3">Status</th>
<th className="px-4 py-3">Model</th>
<th className="px-4 py-3 text-right">Input</th>
<th className="px-4 py-3 text-right">Output</th>
<th className="px-4 py-3 text-right">Cache read</th>
<th className="px-4 py-3 text-right">Revenue</th>
<th className="px-4 py-3">Base settlement</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{data.surplus.recentSales.length === 0 ? (
<tr><td colSpan={8} className="px-4 py-8 text-center text-gray-500">No sales returned.</td></tr>
) : data.surplus.recentSales.map((sale) => (
<tr key={sale.id} className="hover:bg-gray-50">
<td className="whitespace-nowrap px-4 py-3 text-gray-600">{timestamp(sale.createdAt)}</td>
<td className="px-4 py-3">
<span className={`rounded px-2 py-0.5 text-xs font-medium ${
sale.settlementStatus === "confirmed"
? "bg-green-100 text-green-700"
: "bg-amber-100 text-amber-700"
}`}>
{sale.settlementStatus}
</span>
</td>
<td className="whitespace-nowrap px-4 py-3 font-mono text-xs">{sale.model}</td>
<td className="px-4 py-3 text-right tabular-nums">{count(sale.inputTokens)}</td>
<td className="px-4 py-3 text-right tabular-nums">{count(sale.outputTokens)}</td>
<td className="px-4 py-3 text-right tabular-nums">{count(sale.cacheReadTokens)}</td>
<td className="px-4 py-3 text-right tabular-nums">{usd(sale.revenueUsd)}</td>
<td className="px-4 py-3 font-mono text-xs">
{sale.transactionHash ? (
<a
className="text-blue-600 hover:underline"
href={`https://basescan.org/tx/${sale.transactionHash}`}
target="_blank"
rel="noopener noreferrer"
>
{compact(sale.transactionHash)}
</a>
) : "Accruing"}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<p className="text-xs text-gray-400">
Surplus seller {compact(data.identity.sellerWallet)} · payout {compact(data.identity.payoutAddress)}
</p>
</div>
);
}

View File

@@ -34,6 +34,7 @@ services:
# gateway must CORS-allow https://marketplace.vector.apexfusion.org (see
# GATEWAY_CORS_ORIGINS in docker-compose.gateway.yml).
GATEWAY_PUBLIC_URL: https://api.marketplace.vector.apexfusion.org
SURPLUS_DASHBOARD_URL: http://marketplace-mainnet-surplus-seller:8080/internal/resale-dashboard
# NETWORK_ID intentionally not pinned — see supplier compose comment.
volumes:
- buyer-mainnet-archive:/repo/data/archive

View File

@@ -23,19 +23,7 @@ services:
volumes:
- surplus-seller-state:/var/lib/surplus-manager
networks:
- traefik-net
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-net"
- "traefik.http.routers.mp-openrouter-reseller-mainnet.entrypoints=web"
- "traefik.http.routers.mp-openrouter-reseller-mainnet.rule=Host(`mp-suppliers-openrouter-reseller.vector.apexfusion.org`)"
- "traefik.http.routers.mp-openrouter-reseller-mainnet.middlewares=mp-openrouter-reseller-mainnet-https"
- "traefik.http.middlewares.mp-openrouter-reseller-mainnet-https.redirectscheme.scheme=https"
- "traefik.http.middlewares.mp-openrouter-reseller-mainnet-https.redirectscheme.permanent=true"
- "traefik.http.routers.mp-openrouter-reseller-mainnet-tls.entrypoints=websecure"
- "traefik.http.routers.mp-openrouter-reseller-mainnet-tls.rule=Host(`mp-suppliers-openrouter-reseller.vector.apexfusion.org`)"
- "traefik.http.routers.mp-openrouter-reseller-mainnet-tls.tls.certresolver=letsencrypt"
- "traefik.http.services.mp-openrouter-reseller-mainnet.loadbalancer.server.port=8080"
- marketplace-mainnet-net
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz >/dev/null || exit 1"]
interval: 20s
@@ -53,6 +41,6 @@ volumes:
name: marketplace-mainnet-surplus-seller-state
networks:
traefik-net:
marketplace-mainnet-net:
external: true
name: traefik-net
name: marketplace-mainnet-net

View File

@@ -15,7 +15,6 @@ import {
SurplusDashboardService,
type SurplusDashboardSnapshot,
} from "../surplus/dashboard.js";
import { renderSurplusDashboardPage } from "../surplus/dashboardPage.js";
export async function runSurplusSeller(
env: Record<string, string | undefined>,
@@ -81,27 +80,6 @@ async function handleRequest(
response.setHeader("x-content-type-options", "nosniff");
response.setHeader("x-frame-options", "DENY");
response.setHeader("referrer-policy", "no-referrer");
if (request.method === "GET" && path === "/") {
response.statusCode = 302;
response.setHeader("location", "/reseller");
response.end();
return;
}
if (
request.method === "GET" &&
(path === "/reseller" || path === "/reseller/")
) {
response.statusCode = 200;
response.setHeader("content-type", "text/html; charset=utf-8");
response.setHeader(
"content-security-policy",
"default-src 'self'; style-src 'self' 'unsafe-inline'; " +
"script-src 'self' 'unsafe-inline'; connect-src 'self'; " +
"img-src 'self' data:; frame-ancestors 'none'; base-uri 'none'",
);
response.end(renderSurplusDashboardPage());
return;
}
const status = controller.snapshot();
if (request.method === "GET" && path === "/healthz") {
const healthy = controller.healthy();
@@ -116,7 +94,7 @@ async function handleRequest(
response.end(JSON.stringify(status));
return;
}
if (request.method === "GET" && path === "/dashboard/api") {
if (request.method === "GET" && path === "/internal/resale-dashboard") {
try {
const snapshot = await dashboard.getSnapshot();
response.statusCode = 200;

View File

@@ -1,62 +0,0 @@
export function renderSurplusDashboardPage(): string {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="dark">
<title>Inference Resale Control Plane</title>
<style>
:root{--bg:#07110f;--surface:#0c1916;--surface-2:#10231e;--line:#244039;--text:#eef8f3;--muted:#91aaa1;--green:#57e39b;--teal:#5fd8cd;--blue:#79b8ff;--amber:#f3c969;--red:#ff827c;--shadow:0 24px 80px #0007}
*{box-sizing:border-box}html{min-width:320px}body{margin:0;background:radial-gradient(circle at 8% -5%,#17493b 0,transparent 29rem),radial-gradient(circle at 95% 20%,#15344b 0,transparent 34rem),var(--bg);color:var(--text);font:14px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
body:before{content:"";position:fixed;inset:0;pointer-events:none;background-image:linear-gradient(#fff008 1px,transparent 1px),linear-gradient(90deg,#fff008 1px,transparent 1px);background-size:42px 42px;mask-image:linear-gradient(to bottom,#0008,transparent 55%)}
a{color:var(--blue);text-decoration:none}a:hover,a:focus-visible{text-decoration:underline}.shell{width:min(1240px,calc(100% - 32px));margin:auto;padding:38px 0 72px}.top{display:flex;justify-content:space-between;align-items:flex-start;gap:32px;margin-bottom:26px}.eyebrow{color:var(--green);font:700 11px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.16em;text-transform:uppercase}.top h1{margin:10px 0 8px;font-size:clamp(32px,5.5vw,58px);line-height:.98;letter-spacing:-.055em;max-width:780px}.top p{color:var(--muted);margin:0;max-width:660px;font-size:15px}.live-cluster{text-align:right;min-width:210px}.updated{color:var(--muted);font-variant-numeric:tabular-nums;margin-top:8px}.badge{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:999px;padding:7px 11px;background:#081511cc;font-size:12px;font-weight:700}.badge:before{content:"";width:7px;height:7px;border-radius:50%;background:var(--muted);box-shadow:0 0 0 3px #fff1}.badge.live:before{background:var(--green);box-shadow:0 0 18px var(--green)}.badge.retired:before{background:var(--amber)}
.panel{border:1px solid var(--line);border-radius:18px;background:linear-gradient(145deg,#10231ee8,#0a1714f2);box-shadow:var(--shadow)}.flow{display:grid;grid-template-columns:1fr 42px 1.1fr 42px 1.15fr;align-items:stretch;padding:14px;margin-bottom:18px;overflow:hidden}.node{padding:18px;border-radius:13px;background:#091612;border:1px solid #24443a;min-width:0}.node strong{display:block;font-size:18px;margin:5px 0 3px}.node small{color:var(--muted);display:block}.node .sub{font:12px ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.arrow{display:grid;place-items:center;color:var(--teal);font-size:22px}.markets{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:18px}.market-card{padding:22px;position:relative;overflow:hidden}.market-card:after{content:"";position:absolute;width:180px;height:180px;border-radius:50%;right:-90px;top:-110px;background:#57e39b0b}.market-head{display:flex;justify-content:space-between;align-items:flex-start;gap:16px}.market-head h2{margin:3px 0 0;font-size:22px}.market-head p{margin:5px 0 0;color:var(--muted)}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}.truncate{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:16px 0 0}.metric{padding:14px;border:1px solid #254039;border-radius:12px;background:#07130f9c;min-width:0}.label{display:block;color:var(--muted);text-transform:uppercase;letter-spacing:.11em;font-size:10px;font-weight:700}.metric strong{display:block;font-size:clamp(18px,2.2vw,27px);letter-spacing:-.035em;margin-top:5px;font-variant-numeric:tabular-nums}.metric small{color:var(--muted);display:block;margin-top:2px}.good{color:var(--green)}.warn{color:var(--amber)}.retired-copy{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:16px}.fact{padding:12px 14px;border-left:2px solid var(--amber);background:#f3c9690a}.fact strong{display:block;margin-top:3px}
.section-head{display:flex;justify-content:space-between;align-items:end;gap:20px;margin:28px 2px 12px}.section-head h2{margin:0;font-size:22px;letter-spacing:-.025em}.section-head p{margin:0;color:var(--muted)}.table-wrap{overflow:auto}.sales{width:100%;border-collapse:collapse;min-width:820px}.sales th,.sales td{text-align:left;padding:12px 15px;border-bottom:1px solid #213a33}.sales th{position:sticky;top:0;background:#0d1b17;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.12em}.sales tbody tr:hover{background:#ffffff05}.sales tbody tr:last-child td{border-bottom:0}.status-pill{display:inline-flex;border-radius:999px;padding:3px 8px;background:#ffffff0b;border:1px solid #ffffff13;font-size:11px}.status-pill.confirmed{color:var(--green);border-color:#57e39b55}.status-pill.accrued{color:var(--amber);border-color:#f3c96955}.empty{padding:34px!important;text-align:center!important;color:var(--muted)}.alert{display:none;margin-top:14px;padding:12px 14px;border-radius:11px;background:#ff827c10;border:1px solid #ff827c44;color:#ffc4bf}.alert.visible{display:block}.foot{display:flex;justify-content:space-between;gap:24px;color:var(--muted);margin-top:24px;font-size:12px}.foot span:last-child{text-align:right}
@media(max-width:980px){.metrics{grid-template-columns:repeat(2,1fr)}.markets{grid-template-columns:1fr}.flow{grid-template-columns:1fr}.arrow{transform:rotate(90deg);height:34px}.top{flex-direction:column}.live-cluster{text-align:left}.retired-copy{grid-template-columns:1fr 1fr}}
@media(max-width:560px){.shell{width:min(100% - 20px,1240px);padding-top:24px}.metrics,.retired-copy{grid-template-columns:1fr}.market-card{padding:17px}.top h1{font-size:38px}.foot{flex-direction:column}.foot span:last-child{text-align:left}}
</style>
</head>
<body>
<main class="shell">
<header class="top"><div><div class="eyebrow">Autonomous inference resale</div><h1>One capacity pool.<br>Two settlement markets.</h1><p>Live OpenRouter capacity sold through Surplus on Base, with the completed Vector AP3X proof preserved as the migration record.</p></div><div class="live-cluster"><span class="badge" id="controller-badge">Loading controller</span><div class="updated" id="updated">Waiting for current state</div></div></header>
<section class="panel flow" aria-label="Capacity routing flow">
<div class="node"><span class="label">Upstream capacity</span><strong>OpenRouter</strong><small id="flow-allowance">Allowance loading</small><small class="sub" id="flow-provider-model">provider model --</small></div><div class="arrow" aria-hidden="true">→</div>
<div class="node"><span class="label">Autonomous manager</span><strong>Seller agent</strong><small>Discovers · prices · caps · reconciles</small><small class="sub" id="flow-wallet">seller --</small></div><div class="arrow" aria-hidden="true">→</div>
<div class="node"><span class="label">Current settlement</span><strong>Surplus / Base USDC</strong><small id="flow-surplus">Offer loading</small><small class="sub" id="flow-offer">offer --</small></div>
</section>
<section class="markets">
<article class="panel market-card"><div class="market-head"><div><span class="label">Current market</span><h2>Surplus Intelligence</h2><p>Competitive model routing with USDC settlement on Base.</p></div><span class="badge" id="surplus-badge">Loading</span></div>
<div class="metrics"><div class="metric"><span class="label">Lifetime revenue</span><strong id="earned">--</strong><small id="earnings-state">--</small></div><div class="metric"><span class="label">Requests sold</span><strong id="requests">--</strong><small id="tokens">-- tokens</small></div><div class="metric"><span class="label">Market position</span><strong id="rank">--</strong><small id="market-state">--</small></div><div class="metric"><span class="label">Daily seller cap</span><strong id="daily-cap">--</strong><small id="cap-remaining">-- remaining</small></div></div>
<div class="alert" id="payout-hold"></div><div class="alert" id="controller-error"></div></article>
<article class="panel market-card"><div class="market-head"><div><span class="label">Migration record</span><h2>Vector marketplace</h2><p>AP3X escrow route retired after the cross-market proof.</p></div><span class="badge retired">Retired</span></div>
<div class="metrics"><div class="metric"><span class="label">AP3X earned</span><strong id="vector-earned">--</strong><small>historical</small></div><div class="metric"><span class="label">Settled jobs</span><strong id="vector-jobs">--</strong><small>Vector escrows</small></div><div class="metric"><span class="label">Upstream spend</span><strong id="vector-spend">--</strong><small>proof route</small></div><div class="metric"><span class="label">Retired on</span><strong id="vector-date">--</strong><small>advert closed</small></div></div>
<div class="retired-copy"><div class="fact"><span class="label">Retired advert</span><strong class="mono truncate" id="vector-advert">--</strong><a id="advert-link" target="_blank" rel="noopener noreferrer">Open creation transaction ↗</a></div><div class="fact"><span class="label">Retirement proof</span><strong class="mono truncate" id="vector-retirement">--</strong><a id="retirement-link" target="_blank" rel="noopener noreferrer">Open confirmed transaction ↗</a></div></div></article>
</section>
<section class="panel market-card"><div class="market-head"><div><span class="label">Economics</span><h2>Capacity and price controls</h2><p>The seller cap is discounted revenue. The agent converts it to estimated upstream exposure before it opens the route.</p></div><span class="badge live">Bounded</span></div>
<div class="metrics"><div class="metric"><span class="label">OpenRouter remaining</span><strong id="provider-remaining">--</strong><small id="provider-used">-- used</small></div><div class="metric"><span class="label">Protected reserve</span><strong id="provider-reserve">--</strong><small>fail-closed threshold</small></div><div class="metric"><span class="label">Daily upstream estimate</span><strong id="provider-exposure">--</strong><small>from recovery floor</small></div><div class="metric"><span class="label">Current multiplier</span><strong id="multiplier">--</strong><small id="prices">--</small></div></div></section>
<div class="section-head"><div><span class="label">Surplus activity</span><h2>Recent sales</h2></div><p id="sales-note">Loading settlement records</p></div>
<section class="panel table-wrap"><table class="sales"><thead><tr><th>Time</th><th>Status</th><th>Model</th><th>Input</th><th>Output</th><th>Cache read</th><th>Revenue</th><th>Base settlement</th></tr></thead><tbody id="sales-body"><tr><td colspan="8" class="empty">Loading sales</td></tr></tbody></table></section>
<footer class="foot"><span id="identity">Seller identity loading</span><span>Vector proves the agent-owned route. Surplus is the current USDC market.</span></footer>
</main>
<script>
(function(){'use strict';
var $=function(id){return document.getElementById(id)};
var set=function(id,value){$(id).textContent=value==null?'--':String(value)};
var usd=function(value){if(value==null||value==='')return'--';var n=Number(value);return Number.isFinite(n)?'$'+n.toFixed(n<.01?6:4):'--'};
var count=function(value){return new Intl.NumberFormat().format(Number(value)||0)};
var short=function(value){var s=String(value||'');return s.length>18?s.slice(0,10)+'…'+s.slice(-7):s||'--'};
var when=function(value){if(!value)return'--';var d=new Date(value);return Number.isNaN(d.getTime())?'--':d.toLocaleString()};
function statusBadge(id,label,live){var el=$(id);el.textContent=label;el.className='badge '+(live?'live':'');}
function link(id,href){var el=$(id);el.href=href;}
function txLink(row,hash){var td=document.createElement('td');if(typeof hash==='string'&&/^0x[0-9a-fA-F]{64}$/.test(hash)){var a=document.createElement('a');a.href='https://basescan.org/tx/'+hash;a.target='_blank';a.rel='noopener noreferrer';a.textContent=short(hash);td.appendChild(a)}else{td.textContent='Accruing'}row.appendChild(td)}
function cell(row,value,className){var td=document.createElement('td');if(className)td.className=className;td.textContent=value==null?'--':String(value);row.appendChild(td)}
function renderSales(sales){var body=$('sales-body');body.replaceChildren();if(!sales.length){var empty=document.createElement('tr');cell(empty,'No sales returned','empty');empty.firstChild.colSpan=8;body.appendChild(empty);return}sales.forEach(function(sale){var row=document.createElement('tr');cell(row,when(sale.createdAt));var status=document.createElement('td');var pill=document.createElement('span');pill.className='status-pill '+sale.settlementStatus;pill.textContent=sale.settlementStatus;status.appendChild(pill);row.appendChild(status);cell(row,sale.model,'mono');cell(row,count(sale.inputTokens));cell(row,count(sale.outputTokens));cell(row,count(sale.cacheReadTokens));cell(row,usd(sale.revenueUsd));txLink(row,sale.transactionHash);body.appendChild(row)})}
function render(data){var c=data.controller,o=data.surplus.offer,e=data.surplus.earnings,p=data.provider,v=data.vector;statusBadge('controller-badge',c.phase,c.ok);statusBadge('surplus-badge',o.status,o.available&&o.healthy);set('updated','Updated '+when(data.generatedAt));set('flow-allowance',usd(p.remainingUsd)+' of '+usd(p.hardLimitUsd)+' remaining');set('flow-provider-model',o.providerModel);set('flow-wallet',short(data.identity.sellerWallet));set('flow-surplus',(o.available?'Available':'Unavailable')+' · '+(o.healthy?'healthy':'not healthy'));set('flow-offer',o.id);set('earned',usd(e.totalUsd));set('earnings-state',usd(e.pendingUsd)+' pending · '+usd(e.paidUsd)+' paid');set('requests',count(e.requests));set('tokens',count(e.tokens)+' tokens');set('rank',o.rank==null?'--':'#'+o.rank);set('market-state',(o.trusted?'trusted':'untrusted')+' · '+count(o.trades24h)+' trades / 24h');set('daily-cap',usd(o.dailyCapUsd));set('cap-remaining',usd(o.capRemainingUsd)+' remaining');set('provider-remaining',usd(p.remainingUsd));set('provider-used',usd(p.usedUsd)+' used of '+usd(p.hardLimitUsd));set('provider-reserve',usd(p.reserveUsd));set('provider-exposure',usd(p.estimatedDailyExposureUsd));set('multiplier',o.costMultiplier==null?'--':(o.costMultiplier*100).toFixed(3)+'%');set('prices',usd(o.inputUsdPer1m)+' in · '+usd(o.outputUsdPer1m)+' out / 1M');set('vector-earned',v.historicalAp3xEarned+' AP3X');set('vector-jobs',count(v.historicalSettledJobs));set('vector-spend',usd(v.historicalUpstreamSpendUsd));set('vector-date',v.retiredOn);set('vector-advert',v.advertRef);set('vector-retirement',v.retirementTransaction);var advertHash=String(v.advertRef).split('#')[0];link('advert-link','https://vector.apexscan.org/en/transaction/'+advertHash+'/summary/');link('retirement-link','https://vector.apexscan.org/en/transaction/'+v.retirementTransaction+'/summary/');set('identity','Surplus '+short(data.identity.sellerWallet)+' · payout '+short(data.identity.payoutAddress));set('sales-note',count(e.requests)+' lifetime requests · '+count(e.tokens)+' tokens');var hold=$('payout-hold');if(e.payoutHoldReason){hold.className='alert visible';hold.textContent='Payout hold: '+e.payoutHoldReason+(e.payoutHoldReleasesAt?' · releases '+when(e.payoutHoldReleasesAt):'')}else{hold.className='alert'}var error=$('controller-error');if(c.lastError){error.className='alert visible';error.textContent='Controller: '+c.lastError}else{error.className='alert'}renderSales(data.surplus.recentSales)}
async function refresh(){try{var response=await fetch('/dashboard/api',{cache:'no-store'});if(!response.ok)throw new Error('Dashboard API returned '+response.status);render(await response.json())}catch(error){statusBadge('controller-badge','data unavailable',false);set('updated',error instanceof Error?error.message:'Dashboard data unavailable')}}
refresh();setInterval(refresh,15000);
})();
</script>
</body>
</html>`;
}

View File

@@ -0,0 +1,58 @@
import { describe, expect, it, vi } from "vitest";
import request from "supertest";
import { createApp } from "../../buyer/src/server.js";
describe("buyer resale dashboard proxy", () => {
it("serves the private supplier snapshot from a same-origin buyer route", async () => {
const payload = {
generatedAt: "2026-08-28T12:00:00.000Z",
controller: { ok: true, phase: "active" },
surplus: { earnings: { requests: 1_333 } },
vector: { status: "retired" },
};
const fetchImpl = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => {
expect(new Headers(init?.headers).get("accept")).toBe("application/json");
expect(init?.redirect).toBe("error");
return Response.json(payload);
});
const app = createApp({
resaleDashboardUrl:
"http://marketplace-mainnet-surplus-seller:8080/internal/resale-dashboard/",
fetchImpl,
});
const response = await request(app).get("/v1/resale-dashboard");
expect(response.status).toBe(200);
expect(response.body).toEqual(payload);
expect(response.headers["cache-control"]).toBe("no-store");
expect(fetchImpl).toHaveBeenCalledWith(
"http://marketplace-mainnet-surplus-seller:8080/internal/resale-dashboard",
expect.any(Object),
);
});
it("fails without exposing an upstream error body", async () => {
const app = createApp({
resaleDashboardUrl: "http://seller/internal/resale-dashboard",
fetchImpl: async () => new Response("secret upstream detail", { status: 500 }),
});
const response = await request(app).get("/v1/resale-dashboard");
expect(response.status).toBe(502);
expect(response.body.error).toBe("dashboard_upstream_error");
expect(JSON.stringify(response.body)).not.toContain("secret upstream detail");
});
it("returns a bounded feature-disabled response when unconfigured", async () => {
const fetchImpl = vi.fn();
const app = createApp({ fetchImpl });
const response = await request(app).get("/v1/resale-dashboard");
expect(response.status).toBe(503);
expect(response.body.error).toBe("service_unavailable");
expect(fetchImpl).not.toHaveBeenCalled();
});
});

View File

@@ -100,6 +100,24 @@ describe("loadConfig(env)", () => {
it("throws when PDF_ENABLED is not 0 or 1", () => {
expect(() => loadConfig({ ...VALID_ENV, PDF_ENABLED: "false" })).toThrow(/PDF_ENABLED/);
});
it("normalizes the private resale dashboard URL", () => {
const config = loadConfig({
...VALID_ENV,
SURPLUS_DASHBOARD_URL:
"http://surplus-seller:8080/internal/resale-dashboard/",
});
expect(config.resaleDashboardUrl).toBe(
"http://surplus-seller:8080/internal/resale-dashboard",
);
});
it("rejects a resale dashboard URL with an unsupported protocol", () => {
expect(() => loadConfig({
...VALID_ENV,
SURPLUS_DASHBOARD_URL: "file:///tmp/dashboard.json",
})).toThrow(/SURPLUS_DASHBOARD_URL/);
});
});
// ─── createApp tests ──────────────────────────────────────────────────────────

View File

@@ -0,0 +1,107 @@
// @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import ResaleDashboard from "../../buyer/src/ui/pages/ResaleDashboard.js";
const DASHBOARD = {
generatedAt: "2026-08-28T12:00:00.000Z",
controller: { ok: true, phase: "active", lastError: null },
identity: {
sellerWallet: "0x1111111111111111111111111111111111111111",
payoutAddress: "0x1111111111111111111111111111111111111111",
},
provider: {
hardLimitUsd: "20.000000",
remainingUsd: "15.157733",
usedUsd: "4.842267",
reserveUsd: "1.00",
estimatedDailyExposureUsd: "1.020408",
},
surplus: {
offer: {
id: "offer-1",
model: "deepseek-v4-flash",
providerModel: "deepseek/deepseek-v4-flash",
status: "active",
dailyCapUsd: "0.050000",
costMultiplier: 0.05065,
inputUsdPer1m: "0.004027",
outputUsdPer1m: "0.008055",
rank: 11,
available: true,
healthy: true,
trusted: true,
trades24h: 654,
volume24hUsd: "0.112434",
capRemainingUsd: "0.000000",
},
earnings: {
totalUsd: "0.239903",
pendingUsd: "0.219945",
paidUsd: "0.019958",
requests: 1_333,
tokens: 138_612_442,
today: { earnedUsd: 0.053093, requests: 240, totalTokens: 33_913_537 },
topModel: "deepseek-v4-flash",
payoutHoldReason: "new_seller",
payoutHoldReleasesAt: "2026-08-29T11:24:42.552Z",
},
recentSales: [{
id: "sale-1",
model: "deepseek-v4-flash",
createdAt: "2026-08-28T11:00:00.000Z",
settlementStatus: "confirmed",
inputTokens: 10,
outputTokens: 2,
cacheReadTokens: 3,
revenueUsd: "0.000083",
transactionHash: "0x" + "c".repeat(64),
}],
},
vector: {
status: "retired",
model: "deepseek/deepseek-v4-flash",
supplierWallet: "addr1test",
advertRef: `${"a".repeat(64)}#0`,
retirementTransaction: "b".repeat(64),
retiredOn: "2026-08-27",
historicalAp3xEarned: "1.600000",
historicalSettledJobs: 8,
historicalUpstreamSpendUsd: "0.000093",
},
};
afterEach(() => {
vi.unstubAllGlobals();
});
describe("ResaleDashboard", () => {
it("matches the marketplace card system and renders both settlement markets", async () => {
const fetchMock = vi.fn(async () => Response.json(DASHBOARD));
vi.stubGlobal("fetch", fetchMock);
render(<ResaleDashboard />);
expect(await screen.findByRole("heading", { name: "Resale Dashboard" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Surplus Intelligence" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Vector marketplace" })).toBeInTheDocument();
expect(screen.getByText("$0.2399")).toBeInTheDocument();
expect(screen.getByText("1.600000 AP3X")).toBeInTheDocument();
expect(screen.getByText("confirmed")).toBeInTheDocument();
expect(screen.getByTestId("resale-dashboard")).toHaveClass("max-w-7xl");
expect(document.querySelectorAll(".bg-white").length).toBeGreaterThan(3);
expect(screen.getByRole("link", { name: /Open confirmed transaction/ })).toHaveAttribute(
"href",
`https://vector.apexscan.org/en/transaction/${"b".repeat(64)}/summary/`,
);
expect(screen.getByRole("link", { name: /0xcccc/ })).toHaveAttribute(
"href",
`https://basescan.org/tx/0x${"c".repeat(64)}`,
);
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(
"/v1/resale-dashboard",
{ cache: "no-store" },
));
});
});

View File

@@ -5,7 +5,6 @@ import {
SurplusDashboardService,
type SurplusDashboardSnapshot,
} from "../../supplier/src/surplus/dashboard.js";
import { renderSurplusDashboardPage } from "../../supplier/src/surplus/dashboardPage.js";
import { createSurplusServer } from "../../supplier/src/cli/surplus-seller.js";
import type { SurplusControllerStatus } from "../../supplier/src/surplus/controller.js";
@@ -222,38 +221,21 @@ describe("SurplusDashboardService", () => {
expect(getOrderBook).toHaveBeenCalledTimes(1);
});
});
describe("unified dashboard HTTP surface", () => {
it("serves the dashboard, cached API data, and security headers", async () => {
describe("private supplier dashboard data surface", () => {
it("serves redacted data internally without hosting a dashboard page", async () => {
const data = snapshot();
const server = createSurplusServer(
{ snapshot: () => STATUS, healthy: () => true },
{ getSnapshot: async () => data },
);
const page = await request(server).get("/reseller");
expect(page.status).toBe(200);
expect(page.text).toContain("One capacity pool.");
expect(page.text).toContain("Vector marketplace");
expect(page.headers["content-security-policy"]).toContain("frame-ancestors 'none'");
const api = await request(server).get("/dashboard/api");
const api = await request(server).get("/internal/resale-dashboard");
expect(api.status).toBe(200);
expect(api.body).toEqual(data);
expect(JSON.stringify(api.body)).not.toContain("test-secret");
const root = await request(server).get("/");
expect(root.status).toBe(302);
expect(root.headers.location).toBe("/reseller");
expect((await request(server).get("/reseller")).status).toBe(404);
expect((await request(server).get("/")).status).toBe(404);
server.close();
});
it("renders static HTML without embedding credentials", () => {
const html = renderSurplusDashboardPage();
expect(html).toContain("/dashboard/api");
expect(html).toContain("Surplus Intelligence");
expect(html).toContain("Vector marketplace");
expect(html).not.toContain("si_seller_");
expect(html).not.toContain("sk-or-v1-");
});
});