fix(console): one exported apiBase, and no relative /api/ paths

Three dashboard surfaces fetched a RELATIVE /api/v1/... path. That cannot
resolve in a static export — there is no server at that origin — so the
observability panel, the services grid and the AI chat were dead on every
deployed host. It also violates the v1-only rule: the prefix is /v1, never /api.
They now call the host-derived origin at /v1/...

Deleted app/api/health/route.ts, which emitted out/api/health — an /api/ path
SHIPPED INSIDE the artifact.

The deeper fix is the duplication. lib/fleet.ts kept a PRIVATE copy of the
"brand apiUrl, else build-time default" resolver. A second private copy of
exactly that logic, in lib/auth.tsx, shadowed its own import and called itself —
infinite recursion that killed every checkAuth/login/register on any host
classified as local, i.e. all of local development. That copy is already gone;
this removes the other one. apiBase now lives once, exported from lib/api.ts
beside getApiUrl, and fleet.ts imports it.

Left alone deliberately: the /api/v1 strings in app/products/mpc are curl
examples for a CUSTOMER's own host, not calls this app makes.

Console compiles clean.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-26 20:01:02 -07:00
co-authored by Hanzo Dev
parent 7d293212cb
commit 3e7d212986
6 changed files with 20 additions and 17 deletions
-5
View File
@@ -1,5 +0,0 @@
export const dynamic = "force-static"
export function GET() {
return Response.json({ status: "ok" })
}
@@ -1,4 +1,5 @@
"use client"
import { apiBase } from "@/lib/api"
import { useState, useEffect } from "react"
import {
@@ -115,10 +116,10 @@ export default function ObservabilityPage() {
setLoading(true)
try {
const [healthRes, metricsRes, eventsRes, alertsRes] = await Promise.allSettled([
fetch("/api/v1/o11y/health").then((r) => r.ok ? r.json() : null),
fetch("/api/v1/o11y/metrics").then((r) => r.ok ? r.json() : null),
fetch("/api/v1/o11y/events?limit=20").then((r) => r.ok ? r.json() : []),
fetch("/api/v1/o11y/alerts").then((r) => r.ok ? r.json() : []),
fetch(`${apiBase()}/v1/o11y/health`).then((r) => r.ok ? r.json() : null),
fetch(`${apiBase()}/v1/o11y/metrics`).then((r) => r.ok ? r.json() : null),
fetch(`${apiBase()}/v1/o11y/events?limit=20`).then((r) => r.ok ? r.json() : []),
fetch(`${apiBase()}/v1/o11y/alerts`).then((r) => r.ok ? r.json() : []),
])
if (healthRes.status === "fulfilled" && healthRes.value) setHealth(healthRes.value)
@@ -1,4 +1,5 @@
"use client"
import { apiBase } from "@/lib/api"
import { useState, useEffect } from "react"
import {
@@ -109,7 +110,7 @@ export default function ServicesPage() {
const fetchServices = async () => {
try {
setLoading(true)
const res = await fetch("/api/v1/infra/services")
const res = await fetch(`${apiBase()}/v1/infra/services`)
if (res.ok) {
const data = await res.json()
setServices(data.services || [])
+2 -1
View File
@@ -1,4 +1,5 @@
"use client"
import { apiBase } from "@/lib/api"
import { useState, useRef, useEffect, useCallback } from "react"
import {
@@ -83,7 +84,7 @@ export function AiChat({
const tokenKey = `${getBrandKey()}_token`
const apiKeyKey = `${getBrandKey()}_api_key`
const res = await fetch("/api/v1/chat/completions", {
const res = await fetch(`${apiBase()}/v1/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
+10
View File
@@ -36,6 +36,16 @@ const getApiKey = () => {
}
/** Base API URL (no trailing slash, no /v1) */
// apiBase is THE host-derived API origin: the brand's own apiUrl when the
// catalog gives one, else the build-time default. Exported so there is exactly
// ONE copy — a second, private copy of this logic is what made lib/auth.tsx
// infinitely recursive and killed all local development.
export function apiBase(): string {
const brandUrl = getBrand().apiUrl
if (brandUrl) return brandUrl.replace(/\/$/, "")
return getApiUrl()
}
export function getApiUrl(): string {
return getApiConfig().API_URL
}
+1 -6
View File
@@ -4,7 +4,7 @@
// read-only fleet API. The endpoint exposes only public chain data — no auth,
// no secrets — so the lux.cloud landing + fleet dashboard can render it freely.
import { getApiUrl } from "@/lib/api"
import { apiBase, getApiUrl } from "@/lib/api"
import { getBrand } from "@/lib/brand"
/**
@@ -15,11 +15,6 @@ import { getBrand } from "@/lib/brand"
* hostname) is the canonical per-brand API URL — e.g. lux.cloud → api.cloud.lux.network.
* Fall back to getApiUrl() (build-time env) when no brand URL is present.
*/
function apiBase(): string {
const brandUrl = getBrand().apiUrl
if (brandUrl) return brandUrl.replace(/\/$/, "")
return getApiUrl()
}
export interface ChainHeight {
chain: string