feat(ui): Frontend MVP — React 19 + AntD 6 + Vite + StaticFS-Wiring
Scaffold und Core-Infrastruktur 1:1 nach enconf-Pattern (netcell- webpanel/management-ui), reduziert auf EdgeGuard-Scope (kein reseller/ customer-Roles, keine codemirror/extensions). Stack: React 19 + AntD 6 + TS strict + Vite + TanStack-Query + zustand + react-i18next. Layout: AppLayout (Sider+Header+Content), Sidebar (Dashboard/Domains), Header (User-Dropdown + Logout). i18n mit de/en common.json. Pages: Login (POST /auth/login), Setup-Wizard (POST /setup/complete), Dashboard (Health-Polling + Statistics), Domains (volles CRUD via TanStack-Query gegen /domains-API). UpdateBanner-Komponente (/system/package-versions, alle 5 min poll, /system/upgrade trigger) ist von Tag 1 wie vom User gefordert eingebaut. API-Wiring: cmd/edgeguard-api/main.go mountUI() — gin StaticFS für /usr/share/edgeguard/ui/ (overridebar via EDGEGUARD_UI_DIR), echte Files werden direkt geserved, alle nicht-API-Pfade fallen via NoRoute auf index.html für React-Router-SPA. Wenn dist/ fehlt: HTML-Placeholder mit Build-Hinweis. Verifiziert: bun install + npx tsc -b strict (0 errors) + bun run build (12 chunks). End-to-end gegen /tmp/eg-api: / serviert echte React-index.html, /domains SPA-Fallback, /api/v1/* JSON, /assets/* direkt, /api/v1/nonexistent korrekt 404. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
66
management-ui/src/api/client.ts
Normal file
66
management-ui/src/api/client.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import axios, { type AxiosError } from 'axios'
|
||||
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
// edgeguard-api lives at /api/v1 relative to whichever vhost the UI is
|
||||
// being served from (HAProxy proxies /api/* through to 127.0.0.1:9443).
|
||||
// In dev, vite's proxy points /api → http://127.0.0.1:9443.
|
||||
const apiClient = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
// Cookies carry the session — credentials must travel by default
|
||||
// even on same-origin (vite proxy is technically a different origin).
|
||||
withCredentials: true,
|
||||
})
|
||||
|
||||
// Envelope unwrap helper for response.data.data callers. The backend
|
||||
// always wraps payloads as {data, error, message}; this strips the
|
||||
// envelope so callers can `.data.<field>` directly.
|
||||
export function isEnvelope(v: unknown): v is { data: unknown; error: string | null; message: string } {
|
||||
return (
|
||||
typeof v === 'object' &&
|
||||
v !== null &&
|
||||
'data' in v &&
|
||||
'error' in v &&
|
||||
'message' in v
|
||||
)
|
||||
}
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => {
|
||||
const path = window.location.pathname
|
||||
|
||||
// 401 → drop local session, kick to /login (unless we're already
|
||||
// there; setup-mode is a separate gate).
|
||||
if (error.response?.status === 401 && !path.startsWith('/login') && !path.startsWith('/setup')) {
|
||||
useAuthStore.getState().clear()
|
||||
window.location.replace('/login')
|
||||
}
|
||||
|
||||
// 503 setup_required → kick to /setup so the wizard takes over.
|
||||
if (
|
||||
error.response?.status === 503 &&
|
||||
isEnvelope(error.response.data) &&
|
||||
error.response.data.error === 'setup_required' &&
|
||||
!path.startsWith('/setup')
|
||||
) {
|
||||
window.location.replace('/setup')
|
||||
}
|
||||
|
||||
// Translate envelope errors into a plain Error with .message + .status
|
||||
// so callers don't have to inspect axios-shape responses.
|
||||
if (error.response && isEnvelope(error.response.data)) {
|
||||
const env = error.response.data
|
||||
const msg = env.message || env.error || error.message
|
||||
const wrapped = new Error(msg) as Error & { status?: number; response?: typeof error.response }
|
||||
wrapped.status = error.response.status
|
||||
wrapped.response = error.response
|
||||
return Promise.reject(wrapped)
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
export default apiClient
|
||||
Reference in New Issue
Block a user