fix: resolve DBAL frontend client-side errors and deployment issues

- Fix DBAL overview page: basePath doubled in NavTabs and Link hrefs
- Fix client-side fetch URLs: prepend basePath for /api/status and /api/query
- Remove unused workspace deps (api-clients, core-hooks, redux) from DBAL frontend
- Simplify DBAL Dockerfile to standalone build (no monorepo workspace deps needed)
- Add null guards for health array in ServerStatusPanel
- Fix Prometheus nginx proxy: don't strip prefix (web.external-url handles it)
- Fix caproverforge portal: remove onMouse handlers from Server Component

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 22:17:42 +00:00
parent 9df6e1c64f
commit 8ff699e776
9 changed files with 38 additions and 61 deletions

View File

@@ -345,9 +345,9 @@ http {
}
# Prometheus - Metrics scraping
# --web.external-url=/prometheus/ means it expects the prefix in requests
location /prometheus/ {
set $upstream_prometheus prometheus;
rewrite ^/prometheus/(.*)$ /$1 break;
proxy_pass http://$upstream_prometheus:9090;
proxy_http_version 1.1;
proxy_set_header Host $host;

View File

@@ -749,8 +749,8 @@ services:
# DBAL Frontend - Daemon overview + query console
dbal-frontend:
build:
context: ..
dockerfile: frontends/dbal/Dockerfile
context: ../frontends/dbal
dockerfile: Dockerfile
args:
DBAL_DAEMON_URL: http://dbal:8080
container_name: metabuilder-dbal-frontend

View File

@@ -1,50 +1,35 @@
# Multi-stage build: node_modules from base image, source built fresh
# Context: monorepo root (..)
# Requires: docker build -f deployment/base-images/Dockerfile.node-deps \
# -t metabuilder/base-node-deps:latest .
FROM node:24-alpine AS base
# --- Build stage ---
ARG BASE_REGISTRY=metabuilder
FROM ${BASE_REGISTRY}/base-node-deps:latest AS builder
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json ./
RUN npm install --legacy-peer-deps
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ARG DBAL_DAEMON_URL=http://dbal:8080
ENV DBAL_DAEMON_URL=$DBAL_DAEMON_URL
# ── Shared packages (resolved by transpilePackages) ─────────────────────────
# Only the workspace deps that dbal-ui actually imports.
#
# redux/api-clients → hooks-async → redux-slices (transitive chain)
COPY redux/api-clients/ ./redux/api-clients/
COPY redux/hooks-async/ ./redux/hooks-async/
COPY redux/slices/ ./redux/slices/
COPY redux/core-hooks/ ./redux/core-hooks/
RUN mkdir -p public && npm run build
# ── DBAL frontend (the app itself) ──────────────────────────────────────────
COPY frontends/dbal/ ./frontends/dbal/
# Build workspace library packages
RUN for ws in redux/slices redux/hooks-async redux/api-clients redux/core-hooks; do \
npm run build -w "$ws" --if-present 2>&1 || echo "WARN: $ws build failed (non-critical)"; \
done
# Build the app
RUN cd frontends/dbal && npx next build
# --- Runtime stage ---
FROM node:24-alpine
FROM base AS runner
WORKDIR /app
COPY --from=builder /app/frontends/dbal/.next/standalone ./
COPY --from=builder /app/frontends/dbal/.next/static ./frontends/dbal/.next/static
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
RUN mkdir .next && chown nextjs:nodejs .next
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000 HOSTNAME="0.0.0.0"
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \
CMD wget --quiet --tries=1 --spider http://127.0.0.1:3000/dbal || exit 1
WORKDIR /app/frontends/dbal
CMD ["node", "server.js"]

View File

@@ -4,12 +4,6 @@ const nextConfig: NextConfig = {
reactStrictMode: true,
output: 'standalone',
basePath: '/dbal',
transpilePackages: [
'@metabuilder/api-clients',
'@metabuilder/core-hooks',
'@metabuilder/hooks-async',
'@metabuilder/redux-slices',
],
}
export default nextConfig

View File

@@ -14,13 +14,9 @@
"test:e2e:ui": "playwright test --ui"
},
"dependencies": {
"@metabuilder/api-clients": "*",
"@metabuilder/core-hooks": "*",
"next": "^16.1.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-redux": "^9.2.0",
"redux": "^5.0.1"
"react-dom": "^19.2.4"
},
"overrides": {
"react": "^19.2.4",

View File

@@ -44,10 +44,10 @@ export function DBALDaemonPage() {
A hardened, sandboxed C++ daemon serves all database operations via REST API. It validates every request, enforces ACLs, and executes SQL through safe adapters.
</p>
<div className={styles.buttonGroup}>
<Link href="/dbal" className={styles.btnPrimary}>
<Link href="/" className={styles.btnPrimary}>
Visit daemon docs
</Link>
<Link href="/dbal#status" className={styles.btnOutline}>
<Link href="/#status" className={styles.btnOutline}>
Check status
</Link>
</div>

View File

@@ -4,8 +4,8 @@ import { usePathname } from 'next/navigation'
import styles from './NavTabs.module.scss'
const tabs = [
{ href: '/dbal', label: 'Overview' },
{ href: '/dbal/query', label: 'Query Console' },
{ href: '/', label: 'Overview' },
{ href: '/query', label: 'Query Console' },
]
export function NavTabs() {

View File

@@ -119,7 +119,8 @@ export function QueryConsole() {
}
try {
const res = await fetch('/api/query', {
const basePath = process.env.__NEXT_ROUTER_BASEPATH || '/dbal'
const res = await fetch(`${basePath}/api/query`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ method, path, body: parsedBody }),

View File

@@ -5,7 +5,7 @@ import type { ServerHealth, StatusResponse } from './status'
import styles from './ServerStatusPanel.module.scss'
export function ServerStatusPanel() {
const [health, setHealth] = useState<ServerHealth[]>([])
const [health, setHealth] = useState<ServerHealth[]>(() => [])
const [lastUpdated, setLastUpdated] = useState<string>('')
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
@@ -15,7 +15,8 @@ export function ServerStatusPanel() {
const fetchStatus = async () => {
try {
const response = await fetch('/api/status', { cache: 'no-store' })
const basePath = process.env.__NEXT_ROUTER_BASEPATH || '/dbal'
const response = await fetch(`${basePath}/api/status`, { cache: 'no-store' })
if (!response.ok) {
throw new Error('Status endpoint failed')
}
@@ -48,11 +49,11 @@ export function ServerStatusPanel() {
return 'Status unavailable right now'
}
if (health.length === 0) {
if (!health || health.length === 0) {
return 'Initializing status feed...'
}
const degraded = health.some(item => item.status !== 'online')
const degraded = health?.some(item => item.status !== 'online')
return degraded ? 'Some systems need attention' : 'All systems nominal'
}, [health, error])
@@ -65,7 +66,7 @@ export function ServerStatusPanel() {
</div>
<div className={styles.grid}>
{loading && health.length === 0 ? (
{loading && (!health || health.length === 0) ? (
<div className={styles.placeholder}>Loading status...</div>
) : error ? (
<div className={styles.placeholder}>
@@ -73,7 +74,7 @@ export function ServerStatusPanel() {
<p className={styles.caption}>Try refreshing the page in a few moments.</p>
</div>
) : (
health.map(item => (
(health ?? []).map(item => (
<article key={item.name} className={styles.card}>
<div className={styles.cardRow}>
<div className={styles.cardTitleGroup}>