mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-09-30 20:46:32 +00:00
fix(web): one API resolver, host-derived — delete the localhost fallback
Five different rules decided the API base, and none of them consulted the brand catalog: lib/api.ts env → window.__BOOTNODE_API_URL__ → localStorage → localhost lib/auth.tsx non-local hostname → getBrand().apiUrl, else env → localhost lib/docs-config env → `https://api.${brand.domain}` lib/lux/regions env → "" (same-origin relative) ~18 page files env → "http://localhost:8000", inline, 24 copies The literal was one build-arg away from shipping. `next.config.ts` names Cloudflare Pages as a deploy target and a Pages build has no Dockerfile, so that path produces a bundle pointing every request at localhost:8000. It was harmless only because one ARG default in one Dockerfile happened to be set. `api.${brand.domain}` was already wrong: for the cloud.* white-label hosts it derives api.cloud.pars.network and api.cloud.zoo.network, neither of which resolves. Now there is one resolver, lib/api.ts, and the brand catalog is its authority — host-derived at runtime, the only thing that can be right when one static image serves five hosts. `bootnode_api_url` in localStorage stays as the single escape hatch for pointing a browser at a local backend. The env read, the window.__BOOTNODE_API_URL__ global and every localhost literal are gone. Verified on a clean build: tsc 0 errors, build exits 0, and the shipped bundle contains localhost:8000 in 0 chunks (was 25). Two traps worth recording, both hit while doing this. Inserting an import after "the last line starting with `import `" lands INSIDE a multi-line `import {`. And an import inserted above a `"use client"` directive silently demotes the file to a Server Component — lib/auth.tsx opens with a comment banner, so the directive is on line 6, not line 1. Both compiled past tsc and failed only at the Turbopack build.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -57,7 +58,7 @@ export default function AnalyticsPage() {
|
||||
const headers = getAuthHeaders()
|
||||
|
||||
// Fetch real data from multiple endpoints
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
const apiUrl = getApiUrl()
|
||||
const [nodesRes, chainsRes, keysRes, webhooksRes] = await Promise.all([
|
||||
fetch(`${apiUrl}/v1/nodes/`, { headers }).catch(() => null),
|
||||
fetch(`${apiUrl}/v1/chains`, { headers }).catch(() => null),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -38,7 +39,7 @@ export default function NFTsPage() {
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/v1/nfts/${chain}/owned/${address}`,
|
||||
`${getApiUrl()}/v1/nfts/${chain}/owned/${address}`,
|
||||
{ headers: getAuthHeaders() }
|
||||
)
|
||||
if (res.ok) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -38,7 +39,7 @@ export default function TokensPage() {
|
||||
setToken(null)
|
||||
|
||||
try {
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
const apiUrl = getApiUrl()
|
||||
const res = await fetch(`${apiUrl}/v1/tokens/${chain}/metadata/${contract}`, {
|
||||
headers: getAuthHeaders()
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -44,7 +45,7 @@ export default function ClustersPage() {
|
||||
return {}
|
||||
}
|
||||
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
const apiUrl = getApiUrl()
|
||||
|
||||
async function fetchClusters() {
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import React, { useState, useEffect, useCallback } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -96,7 +97,7 @@ interface NetworkConfig {
|
||||
// Constants & helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
const API_URL = getApiUrl()
|
||||
|
||||
function getAuthHeaders(): Record<string, string> {
|
||||
if (typeof window === "undefined") return {}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -70,7 +71,7 @@ export default function MonitoringPage() {
|
||||
useEffect(() => {
|
||||
async function fetchMonitoring() {
|
||||
try {
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
const apiUrl = getApiUrl()
|
||||
const res = await fetch(`${apiUrl}/v1/monitoring/nodes`, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useParams, useRouter } from "next/navigation"
|
||||
@@ -68,7 +69,7 @@ export default function NodeDetailClient() {
|
||||
async function fetchNode() {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/v1/nodes/${nodeId}`,
|
||||
`${getApiUrl()}/v1/nodes/${nodeId}`,
|
||||
{ headers: getAuthHeaders() }
|
||||
)
|
||||
if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`)
|
||||
@@ -88,7 +89,7 @@ export default function NodeDetailClient() {
|
||||
}, [nodeId])
|
||||
|
||||
async function handleAction(action: "start" | "stop" | "delete") {
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
const apiUrl = getApiUrl()
|
||||
try {
|
||||
if (action === "delete") {
|
||||
if (!confirm("Are you sure you want to delete this node?")) return
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -459,7 +460,7 @@ export default function NodesPage() {
|
||||
|
||||
async function fetchNodes() {
|
||||
try {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/v1/nodes/`, {
|
||||
const res = await fetch(`${getApiUrl()}/v1/nodes/`, {
|
||||
headers: getAuthHeaders()
|
||||
})
|
||||
if (res.ok) {
|
||||
@@ -479,7 +480,7 @@ export default function NodesPage() {
|
||||
setRefreshingMetrics(true)
|
||||
try {
|
||||
// Fetch detailed metrics for each node
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
const apiUrl = getApiUrl()
|
||||
const metricsRes = await fetch(`${apiUrl}/v1/nodes/metrics/all`, {
|
||||
headers: getAuthHeaders()
|
||||
})
|
||||
@@ -516,7 +517,7 @@ export default function NodesPage() {
|
||||
setCreateError(null)
|
||||
|
||||
try {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/v1/nodes/`, {
|
||||
const res = await fetch(`${getApiUrl()}/v1/nodes/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -557,7 +558,7 @@ export default function NodesPage() {
|
||||
}
|
||||
|
||||
async function handleNodeAction(nodeId: string, action: "start" | "stop" | "delete") {
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
const apiUrl = getApiUrl()
|
||||
try {
|
||||
if (action === "delete") {
|
||||
const res = await fetch(`${apiUrl}/v1/nodes/${nodeId}`, {
|
||||
@@ -585,7 +586,7 @@ export default function NodesPage() {
|
||||
async function handleDeleteAll() {
|
||||
if (!confirm(`Are you sure you want to delete ALL ${nodes.length} nodes? This cannot be undone.`)) return
|
||||
setDeletingAll(true)
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
const apiUrl = getApiUrl()
|
||||
try {
|
||||
// Delete all nodes in parallel
|
||||
await Promise.all(nodes.map(node =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -29,7 +30,7 @@ export default function InfrastructurePage() {
|
||||
useEffect(() => {
|
||||
async function fetchStats() {
|
||||
try {
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
const apiUrl = getApiUrl()
|
||||
const res = await fetch(`${apiUrl}/v1/infra/stats`, {
|
||||
headers: getAuthHeaders()
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -63,7 +64,7 @@ export default function StoragePage() {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const headers = getAuthHeaders()
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
const apiUrl = getApiUrl()
|
||||
const [volRes, snapRes, cloneRes] = await Promise.all([
|
||||
fetch(`${apiUrl}/v1/infra/volumes`, { headers }),
|
||||
fetch(`${apiUrl}/v1/infra/snapshots`, { headers }),
|
||||
@@ -88,7 +89,7 @@ export default function StoragePage() {
|
||||
if (!cloneName.trim() || !sourceId) return
|
||||
setCloning(true)
|
||||
try {
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
const apiUrl = getApiUrl()
|
||||
const res = await fetch(`${apiUrl}/v1/infra/clone`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -41,7 +42,7 @@ export default function VolumesPage() {
|
||||
return {}
|
||||
}
|
||||
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
const apiUrl = getApiUrl()
|
||||
|
||||
async function fetchVolumes() {
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -77,7 +78,7 @@ export default function MarketsPage() {
|
||||
async function fetchMarketData() {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/v1/nodes/staking`,
|
||||
`${getApiUrl()}/v1/nodes/staking`,
|
||||
{ headers: getAuthHeaders() }
|
||||
)
|
||||
if (res.ok) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -35,7 +36,7 @@ export default function OrganizationPage() {
|
||||
try {
|
||||
const headers = getAuthHeaders()
|
||||
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
const apiUrl = getApiUrl()
|
||||
const [teamRes, keysRes] = await Promise.all([
|
||||
fetch(`${apiUrl}/v1/team`, { headers }).catch(() => null),
|
||||
fetch(`${apiUrl}/v1/auth/keys`, { headers }).catch(() => null),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -66,7 +67,7 @@ export default function DashboardPage() {
|
||||
const headers = getAuthHeaders()
|
||||
|
||||
// Fetch multiple endpoints in parallel
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
const apiUrl = getApiUrl()
|
||||
const [teamRes, keysRes, webhooksRes, chainsRes, nodesRes] = await Promise.all([
|
||||
fetch(`${apiUrl}/v1/team`, { headers }).catch(() => null),
|
||||
fetch(`${apiUrl}/v1/auth/keys`, { headers }).catch(() => null),
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function SettingsPage() {
|
||||
setProjectName(localStorage.getItem("bootnode_project_name") || "")
|
||||
setProjectDescription(localStorage.getItem("bootnode_project_description") || "")
|
||||
setApiKey(localStorage.getItem("bootnode_api_key") || "")
|
||||
setApiUrl(process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000")
|
||||
setApiUrl(getApiUrl())
|
||||
}, [])
|
||||
|
||||
function handleSaveProject() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
@@ -83,7 +84,7 @@ export default function StakingPage() {
|
||||
} else if (type === "kms") {
|
||||
// Check if KMS (Hanzo KSM) is configured
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/v1/keys/status`,
|
||||
`${getApiUrl()}/v1/keys/status`,
|
||||
{ headers: getAuthHeaders() }
|
||||
)
|
||||
if (res.ok) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -38,7 +39,7 @@ export default function UsagePage() {
|
||||
<CardContent>
|
||||
<Badge variant="default">Connected</Badge>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}
|
||||
{getApiUrl()}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
+19
-8
@@ -1,17 +1,28 @@
|
||||
// Bootnode API Client
|
||||
// Uses auth token from localStorage for authenticated requests
|
||||
import { getBrand } from "@/lib/brand"
|
||||
|
||||
const getApiConfig = () => {
|
||||
let API_URL = "http://localhost:8000"
|
||||
|
||||
// The brand catalog is the authority: it resolves from the HOSTNAME at
|
||||
// runtime, the only thing that can be right when one static image serves
|
||||
// bootno.de, web3.hanzo.ai, cloud.hanzo.network, cloud.pars.network and
|
||||
// cloud.zoo.network. NEXT_PUBLIC_API_URL cannot — it is inlined at build time,
|
||||
// so it is a single global shared by every host. The bootnode-web CR set it to
|
||||
// https://api.web3.hanzo.ai and the shipped bundle used api.bootno.de anyway:
|
||||
// the image is a static export behind the Go hanzoai/static binary, so
|
||||
// container env is never read at all.
|
||||
//
|
||||
// `bootnode_api_url` in localStorage remains the ONE escape hatch, for aiming
|
||||
// a browser at a local backend. The env read, the window.__BOOTNODE_API_URL__
|
||||
// global, and the "http://localhost:8000" literal are gone — that literal
|
||||
// would have shipped to production from any build without the Dockerfile ARG,
|
||||
// and next.config.ts names Cloudflare Pages (which has no Dockerfile) as a
|
||||
// deploy target.
|
||||
if (typeof window !== 'undefined') {
|
||||
API_URL = process.env.NEXT_PUBLIC_API_URL ||
|
||||
(window as any).__BOOTNODE_API_URL__ ||
|
||||
localStorage.getItem("bootnode_api_url") ||
|
||||
API_URL
|
||||
const override = localStorage.getItem("bootnode_api_url")
|
||||
if (override) return { API_URL: override.replace(/\/+$/, '') }
|
||||
}
|
||||
|
||||
return { API_URL }
|
||||
return { API_URL: getBrand().apiUrl.replace(/\/+$/, '') }
|
||||
}
|
||||
|
||||
const getAuthToken = () => {
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@
|
||||
// - Production: Hanzo IAM (hanzo.id, zoo.id, lux.id, pars.id)
|
||||
|
||||
"use client"
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
|
||||
import { createContext, useContext, useEffect, useState, useCallback } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
@@ -66,7 +67,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
const isLocal = h === "localhost" || h === "127.0.0.1" || h === "0.0.0.0" || h.startsWith("192.168.") || h.startsWith("10.")
|
||||
if (!isLocal) return getBrand().apiUrl
|
||||
}
|
||||
return process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
|
||||
return getApiUrl()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -13,7 +13,10 @@ export const docsConfig = {
|
||||
brandDescription: brand.description,
|
||||
|
||||
// API endpoints - detected from env or derived from brand
|
||||
apiUrl: process.env.NEXT_PUBLIC_API_URL || `https://api.${brand.domain}`,
|
||||
// brand.apiUrl, not `api.${brand.domain}`: that derivation yields
|
||||
// api.cloud.pars.network and api.cloud.zoo.network, neither of which
|
||||
// exists. The catalog carries the real per-brand origin.
|
||||
apiUrl: brand.apiUrl,
|
||||
dashboardUrl: process.env.NEXT_PUBLIC_DASHBOARD_URL || `https://${brand.domain}`,
|
||||
wsUrl: process.env.NEXT_PUBLIC_WS_URL || `wss://ws.${brand.domain}`,
|
||||
zapUrl: process.env.NEXT_PUBLIC_ZAP_URL || `zap://api.${brand.domain}:9999`,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getApiUrl } from "@/lib/api"
|
||||
// DigitalOcean Kubernetes regions and Lux network configuration
|
||||
// Used by the fleet management UI for cluster provisioning and monitoring.
|
||||
|
||||
@@ -61,7 +62,8 @@ export const NETWORK_NAMES: LuxNetwork[] = ["mainnet", "testnet", "devnet"] as c
|
||||
// API Client Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "";
|
||||
// One resolver for every caller (lib/api.ts), host-derived at runtime.
|
||||
const API_BASE = getApiUrl();
|
||||
|
||||
function authHeaders(): HeadersInit {
|
||||
const headers: HeadersInit = { "Content-Type": "application/json" };
|
||||
|
||||
Reference in New Issue
Block a user