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:
Debian
2026-05-09 11:16:04 +02:00
parent 914538eed1
commit b507d2a7d5
26 changed files with 1817 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
import { create } from 'zustand'
// Auth state is server-side (HttpOnly cookie). The store mirrors only
// the public claims the UI needs to make routing/UX decisions —
// actor (email), role, expires_at — so a page reload doesn't have to
// roundtrip /auth/me before showing the right layout.
export interface SessionUser {
actor: string
role: string
expires_at: string
}
interface AuthState {
user: SessionUser | null
loading: boolean
set: (u: SessionUser | null) => void
clear: () => void
setLoading: (l: boolean) => void
}
const SESSION_KEY = 'eg_session'
function load(): SessionUser | null {
try {
const raw = sessionStorage.getItem(SESSION_KEY)
if (!raw) return null
return JSON.parse(raw) as SessionUser
} catch {
return null
}
}
function save(u: SessionUser | null): void {
try {
if (u) sessionStorage.setItem(SESSION_KEY, JSON.stringify(u))
else sessionStorage.removeItem(SESSION_KEY)
} catch { /* quota — ignore */ }
}
export const useAuthStore = create<AuthState>()((set) => ({
user: load(),
loading: false,
set: (u) => {
save(u)
set({ user: u })
},
clear: () => {
save(null)
set({ user: null })
},
setLoading: (loading) => set({ loading }),
}))
export function isAuthenticated(): boolean {
return useAuthStore.getState().user !== null
}