diff --git a/apps/ui/app/providers.tsx b/apps/ui/app/providers.tsx
index 3770e302..da9641be 100644
--- a/apps/ui/app/providers.tsx
+++ b/apps/ui/app/providers.tsx
@@ -10,6 +10,15 @@ import { AuthProvider, useAuthStore, getAuthToken } from "@/lib/auth/store"
import { authApi, setAuthTokenGetter } from "@/lib/api/auth"
import { ThemeProvider } from "@/components/theme-provider"
import { ThemeSyncProvider } from "@/components/theme-sync-provider"
+import { DemoBootstrap } from "@/components/demo/demo-bootstrap"
+import { DEMO_MODE } from "@/lib/demo/flag"
+import { installDemoMode } from "@/lib/demo/install"
+
+// Install demo-mode interceptors at module-eval time so they're active before
+// any React effect fires (notably AuthInitializer's getCurrentUser call).
+if (DEMO_MODE && typeof window !== "undefined") {
+ installDemoMode()
+}
function AuthInitializer({ children }: { children: React.ReactNode }) {
const { token, user, setUser, clearAuth } = useAuthStore()
@@ -22,6 +31,7 @@ function AuthInitializer({ children }: { children: React.ReactNode }) {
// Validate token and fetch user on mount if token exists
// Only validate if we have token but no user data
useEffect(() => {
+ if (DEMO_MODE) return
if (token && !user) {
authApi.getCurrentUser()
.then((fetchedUser) => {
@@ -83,13 +93,19 @@ export function Providers({ children }: { children: React.ReactNode }) {
-
-
-
- {children}
-
-
-
+ {DEMO_MODE ? (
+
+ {children}
+
+ ) : (
+
+
+
+ {children}
+
+
+
+ )}
diff --git a/apps/ui/app/sso/callback/page.tsx b/apps/ui/app/sso/callback/page.tsx
index c3d6096a..da5ad811 100644
--- a/apps/ui/app/sso/callback/page.tsx
+++ b/apps/ui/app/sso/callback/page.tsx
@@ -1,10 +1,20 @@
"use client"
-import { useEffect, useRef, useState } from "react"
+export const dynamic = "force-dynamic"
+
+import { Suspense, useEffect, useRef, useState } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { useAuthStore } from "@/lib/auth/store"
export default function SsoCallbackPage() {
+ return (
+
+
+
+ )
+}
+
+function SsoCallbackPageInner() {
const router = useRouter()
const searchParams = useSearchParams()
const { setAuth } = useAuthStore()
diff --git a/apps/ui/components/demo/demo-bootstrap.tsx b/apps/ui/components/demo/demo-bootstrap.tsx
new file mode 100644
index 00000000..8e375750
--- /dev/null
+++ b/apps/ui/components/demo/demo-bootstrap.tsx
@@ -0,0 +1,65 @@
+"use client"
+
+// DemoBootstrap mounts at the top of the providers tree when NEXT_PUBLIC_DEMO_MODE
+// is enabled. Responsibilities:
+// 1. Install the request/WebSocket interceptors before any data hooks fire.
+// 2. Show a small floating banner so visitors know the data is synthetic.
+//
+// Auth: real login flow stays — the user must type admin/admin to enter. The
+// mock /auth/login endpoint validates those credentials and rejects anything
+// else, so visitors get the same experience as the real product.
+
+import { useEffect } from "react"
+import { DEMO_MODE } from "@/lib/demo/flag"
+import { installDemoMode } from "@/lib/demo/install"
+import { Sparkles, RotateCcw } from "lucide-react"
+import { resetState } from "@/lib/demo/state"
+
+export function DemoBootstrap({ children }: { children: React.ReactNode }) {
+ if (!DEMO_MODE) return <>{children}>
+ return {children}
+}
+
+function DemoBootstrapImpl({ children }: { children: React.ReactNode }) {
+ useEffect(() => {
+ installDemoMode()
+ }, [])
+
+ return (
+ <>
+ {children}
+
+ >
+ )
+}
+
+function DemoBanner() {
+ const onReset = () => {
+ resetState()
+ if (typeof window !== "undefined") {
+ // Clear the auth state too so visitors are kicked back to the login
+ // page after a reset — matches the "fresh demo" expectation.
+ try {
+ localStorage.removeItem("auth-storage")
+ } catch {
+ /* ignore */
+ }
+ window.location.href = "/"
+ }
+ }
+ return (
+
+
+ Demo mode — data is simulated
+
+
+ )
+}
diff --git a/apps/ui/lib/demo/flag.ts b/apps/ui/lib/demo/flag.ts
new file mode 100644
index 00000000..41e74f36
--- /dev/null
+++ b/apps/ui/lib/demo/flag.ts
@@ -0,0 +1,11 @@
+// Demo-mode flag — true when the deployment is the public, mocked-data demo.
+// Read at build/runtime via NEXT_PUBLIC_DEMO_MODE.
+//
+// Why a dedicated module: we want a single source of truth that's safe to import
+// from anywhere (server components, client components, lib code) without pulling
+// React or browser-only state.
+
+export const DEMO_MODE: boolean = (() => {
+ const v = process.env.NEXT_PUBLIC_DEMO_MODE
+ return v === "1" || v === "true" || v === "yes"
+})()
diff --git a/apps/ui/lib/demo/install.ts b/apps/ui/lib/demo/install.ts
new file mode 100644
index 00000000..14a27301
--- /dev/null
+++ b/apps/ui/lib/demo/install.ts
@@ -0,0 +1,162 @@
+// Install global demo-mode interceptors. Idempotent: safe to call more than
+// once per page session.
+//
+// What it patches:
+// 1. `apiClient.request` — short-circuits to the mock router.
+// 2. `window.fetch` — for the small set of `fetch(...)` calls inside the app
+// that bypass apiClient (avatar upload, image upload, etc.).
+// 3. `window.WebSocket` — stubs out shell/metrics/log streams with a fake
+// socket that emits canned events.
+
+import { DEMO_MODE } from "./flag"
+import { handleMockRequest } from "./router"
+import { apiClient } from "@/lib/api/http"
+
+const INSTALLED_FLAG = "__nqr_demo_installed__"
+
+export function installDemoMode() {
+ if (!DEMO_MODE) return
+ if (typeof window === "undefined") return
+ if ((window as any)[INSTALLED_FLAG]) return
+ ;(window as any)[INSTALLED_FLAG] = true
+
+ patchApiClient()
+ patchFetch()
+ patchWebSocket()
+
+ // Make sure the auth/token check inside the SPA never explodes — the
+ // bootstrap component below also seeds an auth token. We just guard
+ // against double-init here.
+}
+
+function patchApiClient() {
+ const original = apiClient.request.bind(apiClient)
+ ;(apiClient as any).request = async function patched(endpoint: string, options: RequestInit = {}) {
+ const method = (options.method || "GET").toUpperCase()
+ let body: any = undefined
+ if (typeof options.body === "string") {
+ try { body = JSON.parse(options.body) } catch { body = options.body }
+ } else if (options.body) {
+ body = options.body
+ }
+ // Simulate a small but non-zero latency so loading states still flash.
+ await delay(80 + Math.random() * 160)
+ try {
+ const result = await handleMockRequest({ method, path: endpoint, body })
+ return result as any
+ } catch (e) {
+ throw e
+ }
+ }
+ // keep `original` reachable for debugging
+ ;(apiClient as any)._originalRequest = original
+}
+
+function patchFetch() {
+ const original = window.fetch.bind(window)
+ window.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
+ const url = typeof input === "string" ? input : (input instanceof URL ? input.toString() : input.url)
+ const base = apiClient.baseURL
+ if (url.startsWith(base)) {
+ const path = url.slice(base.length)
+ const method = (init?.method || "GET").toUpperCase()
+ let body: any = undefined
+ if (init?.body && typeof init.body === "string") {
+ try { body = JSON.parse(init.body) } catch { body = init.body }
+ }
+ await delay(120 + Math.random() * 160)
+ const data = await handleMockRequest({ method, path, body })
+ return new Response(JSON.stringify(data ?? {}), { status: 200, headers: { "Content-Type": "application/json" } })
+ }
+ return original(input as any, init)
+ }) as any
+}
+
+function patchWebSocket() {
+ const NativeWS = window.WebSocket
+ class DemoWebSocket {
+ url: string
+ readyState = 0
+ onopen: ((e: any) => void) | null = null
+ onmessage: ((e: any) => void) | null = null
+ onerror: ((e: any) => void) | null = null
+ onclose: ((e: any) => void) | null = null
+ static CONNECTING = 0
+ static OPEN = 1
+ static CLOSING = 2
+ static CLOSED = 3
+ private _interval: any = null
+ private _kind: "metrics" | "logs" | "shell" | "unknown"
+ constructor(url: string) {
+ this.url = url
+ // Classify by URL substring.
+ if (url.includes("/metrics")) this._kind = "metrics"
+ else if (url.includes("/logs")) this._kind = "logs"
+ else if (url.includes("/shell")) this._kind = "shell"
+ else this._kind = "unknown"
+ setTimeout(() => {
+ this.readyState = 1
+ this.onopen?.({})
+ this._start()
+ }, 80)
+ }
+ _start() {
+ const emit = (data: any) => this.onmessage?.({ data: typeof data === "string" ? data : JSON.stringify(data) })
+ if (this._kind === "metrics") {
+ this._interval = setInterval(() => {
+ emit({
+ timestamp: new Date().toISOString(),
+ cpu_usage_percent: 20 + Math.random() * 60,
+ memory_usage_percent: 30 + Math.random() * 50,
+ memory_used_kb: 1024 * 1024,
+ memory_total_kb: 2 * 1024 * 1024,
+ load_average: 0.5 + Math.random(),
+ })
+ }, 2000)
+ } else if (this._kind === "logs") {
+ const samples = [
+ "[info] demo mode: log stream is simulated",
+ "[info] GET /healthz -> 200",
+ "[debug] cache miss key=user:42",
+ "[info] worker accepted job #1284",
+ ]
+ let i = 0
+ this._interval = setInterval(() => {
+ emit({
+ container_id: "demo",
+ timestamp: new Date().toISOString(),
+ stream: i % 4 === 3 ? "stderr" : "stdout",
+ message: samples[i % samples.length],
+ })
+ i++
+ }, 1500)
+ } else if (this._kind === "shell") {
+ emit("\x1b[1;33mDemo mode\x1b[0m: interactive shell is disabled.\r\n")
+ emit("Try the live build of NQR-MicroVM to get a real terminal.\r\n$ ")
+ }
+ }
+ send(_: any) {
+ // ignore in demo mode
+ }
+ close() {
+ this.readyState = 3
+ if (this._interval) clearInterval(this._interval)
+ this.onclose?.({})
+ }
+ addEventListener(name: string, cb: (e: any) => void) {
+ if (name === "open") this.onopen = cb
+ else if (name === "message") this.onmessage = cb
+ else if (name === "error") this.onerror = cb
+ else if (name === "close") this.onclose = cb
+ }
+ removeEventListener() { /* noop */ }
+ }
+ // Replace globally so any new WebSocket(...) uses our shim. Keep the native
+ // class around in case something explicitly references it later.
+ ;(window as any)._NativeWebSocket = NativeWS
+ ;(window as any).WebSocket = DemoWebSocket
+}
+
+function delay(ms: number) {
+ return new Promise((r) => setTimeout(r, ms))
+}
diff --git a/apps/ui/lib/demo/router.ts b/apps/ui/lib/demo/router.ts
new file mode 100644
index 00000000..224e8f59
--- /dev/null
+++ b/apps/ui/lib/demo/router.ts
@@ -0,0 +1,712 @@
+// Mock router for demo mode. Pattern-matches (method, path) tuples against the
+// in-memory store and returns the same shapes the real manager would.
+//
+// Coverage strategy: hand-handle the high-traffic endpoints (auth, eula,
+// license, vms, containers, networks, volumes, images, templates, hosts,
+// functions, users, dashboard, settings, audit, backups, snapshots, storage
+// backends). Unmatched endpoints fall through to a safe default that won't
+// crash list/detail screens (`{ items: [] }`, `{ item: null }`, etc.).
+
+import { DEMO_MODE } from "./flag"
+import { getState, mutateState, newId, nowIso } from "./state"
+import type { Vm, Container, Network, Volume, Template } from "@/lib/types"
+
+export interface MockRequest {
+ method: string
+ path: string // path after the base URL, e.g. "/vms"
+ body?: any
+}
+
+function ok() {
+ return { ok: true }
+}
+
+function notFound(): never {
+ throw new Error(
+ JSON.stringify({
+ error: "Not found",
+ status: 404,
+ suggestion: "Demo data does not include this resource",
+ request_id: "demo",
+ }),
+ )
+}
+
+function asStateVm(req: any): Vm {
+ const tpl = getState().templates.find((t) => t.id === req.template_id) || getState().templates[0]
+ const vcpu = req.vcpu ?? tpl?.spec.vcpu ?? 1
+ const mem = req.mem_mib ?? tpl?.spec.mem_mib ?? 1024
+ const host = getState().hosts[Math.floor(Math.random() * getState().hosts.length)]
+ const ipSuffix = 100 + Math.floor(Math.random() * 100)
+ const id = newId("vm")
+ return {
+ id,
+ name: req.name || `demo-${id.slice(-4)}`,
+ state: "running",
+ host_id: host.id,
+ template_id: req.template_id,
+ host_addr: host.addr,
+ api_sock: "/srv/fc/sock",
+ tap: "tap0",
+ log_path: "/srv/fc/log",
+ http_port: 9000 + Math.floor(Math.random() * 999),
+ fc_unit: "firecracker@demo.service",
+ vcpu,
+ mem_mib: mem,
+ kernel_path: "/srv/images/vmlinux-6.1",
+ rootfs_path: "/srv/images/ubuntu-22.04.ext4",
+ guest_ip: `172.16.0.${ipSuffix}`,
+ tags: req.tags ?? [],
+ cpu_usage_percent: 10 + Math.floor(Math.random() * 60),
+ memory_usage_percent: 20 + Math.floor(Math.random() * 60),
+ created_at: nowIso(),
+ updated_at: nowIso(),
+ }
+}
+
+function fakeMetricsSeries(now = Date.now(), n = 60, base = 50, jitter = 30) {
+ return Array.from({ length: n }, (_, i) => {
+ const t = new Date(now - (n - i) * 60_000).toISOString()
+ return { recorded_at: t, value: Math.max(0, Math.min(100, base + Math.sin(i / 4) * jitter + (Math.random() - 0.5) * 10)) }
+ })
+}
+
+function parseId(path: string, prefix: string): string | null {
+ const m = path.match(new RegExp(`^${prefix}/([^/?]+)`))
+ return m ? m[1] : null
+}
+
+export async function handleMockRequest({ method, path, body }: MockRequest): Promise {
+ // Strip query string for matching, but keep for filters
+ const [rawPath, _query] = path.split("?")
+ const p = rawPath
+
+ // Auth ----------------------------------------------------------------
+ if (p === "/auth/login" && method === "POST") {
+ const username = (body?.username ?? "").toString().trim()
+ const password = (body?.password ?? "").toString()
+ // Demo only accepts admin/admin so the login screen feels real.
+ if (username !== "admin" || password !== "admin") {
+ throw new Error(
+ JSON.stringify({
+ error: "Invalid credentials",
+ status: 401,
+ suggestion: "Use admin / admin to sign in to the demo.",
+ request_id: "demo",
+ }),
+ )
+ }
+ return {
+ token: "demo-token-" + Math.random().toString(36).slice(2),
+ user: {
+ id: "u-demo",
+ username: "admin",
+ role: "admin",
+ email: "admin@nqr-microvm.com",
+ created_at: nowIso(),
+ },
+ }
+ }
+ if (p === "/auth/me" && method === "GET") {
+ return { id: "u-demo", username: "admin", role: "admin", created_at: nowIso(), timezone: "UTC" }
+ }
+ if (p === "/auth/me/avatar" && method === "DELETE") return ok()
+ if (p === "/auth/me/profile" && method === "PATCH") return ok()
+ if (p === "/auth/me/password" && method === "POST") return ok()
+ if (p === "/auth/me/preferences" && method === "GET") {
+ return {
+ preferences: {
+ timezone: "UTC",
+ theme: "dark",
+ date_format: "YYYY-MM-DD",
+ notifications: { email: true, browser: true, desktop: false },
+ vm_defaults: { vcpu: 2, mem_mib: 2048, disk_gb: 10 },
+ auto_refresh: 30,
+ metrics_retention: 7,
+ },
+ }
+ }
+ if (p === "/auth/me/preferences" && method === "PATCH") return ok()
+
+ // EULA + License + SSO providers (gating endpoints)
+ if (p === "/admin/eula/status" || p === "/eula/status") {
+ return { needs_acceptance: false, latest_accepted_version: "1.0.0" }
+ }
+ if (p === "/admin/eula/info" || p === "/eula/info") {
+ return { version: "1.0.0", languages: ["en"] }
+ }
+ if (p === "/admin/eula/accept" && method === "POST") return { success: true }
+ if (p === "/admin/license/status" || p === "/license/status") {
+ return {
+ is_licensed: true,
+ status: "active",
+ is_grace_period: false,
+ customer_name: "NQR-MicroVM Demo",
+ product: "NQR-MicroVM",
+ features: ["vm", "container", "function", "network", "volume", "snapshot", "backup"],
+ expires_at: new Date(Date.now() + 365 * 86400_000).toISOString(),
+ activations: 1,
+ max_activations: 1000,
+ verified_at: nowIso(),
+ license_key: "DEMO-****-****-MODE",
+ }
+ }
+ if (p === "/admin/license/activate" && method === "POST") return { success: true }
+ if (p === "/admin/license/upload" && method === "POST") return { success: true }
+ if (p === "/sso/providers" && method === "GET") return { providers: [] }
+ if (p === "/admin/sso/providers" && method === "GET") return { items: [] }
+
+ // Dashboard ----------------------------------------------------------
+ if (p === "/dashboard/stats" || p === "/stats" || p === "/admin/stats") {
+ const s = getState()
+ return {
+ total_vms: s.vms.length,
+ running_vms: s.vms.filter((v) => v.state === "running").length,
+ total_functions: s.functions.length,
+ invocations_24h: s.functions.reduce((a, f) => a + (f.invocation_count_24h ?? 0), 0),
+ total_containers: s.containers.length,
+ running_containers: s.containers.filter((c) => c.state === "running").length,
+ total_hosts: s.hosts.length,
+ }
+ }
+
+ // System --------------------------------------------------------------
+ if (p === "/admin/db/info" || p === "/db/info") {
+ return { kind: "postgres", host: "demo-db.internal", port: 5432, database: "nqr", version: "16.2", uptime_seconds: 86400 * 30 }
+ }
+ if (p === "/admin/system/stats" || p === "/system/stats") {
+ return { cpu_count: 32, mem_total_mb: 65536, disk_total_gb: 1024, manager_version: "demo", agent_version: "demo" }
+ }
+
+ // Audit Logs ----------------------------------------------------------
+ if (p === "/admin/audit" || p === "/audit-logs") {
+ const items = getState().auditLogs
+ return { items, total: items.length }
+ }
+
+ // VMs -----------------------------------------------------------------
+ if (p === "/vms" && method === "GET") return { items: getState().vms }
+ if (p === "/vms" && method === "POST") {
+ const vm = asStateVm(body)
+ mutateState((s) => s.vms.unshift(vm))
+ return { id: vm.id, item: vm }
+ }
+ const vmId = parseId(p, "/vms")
+ if (vmId) {
+ const isAction = (a: string) => p === `/vms/${vmId}/${a}`
+ if (p === `/vms/${vmId}` && method === "GET") {
+ const item = getState().vms.find((v) => v.id === vmId)
+ if (!item) notFound()
+ return { item }
+ }
+ if (p === `/vms/${vmId}` && method === "PATCH") {
+ mutateState((s) => {
+ const v = s.vms.find((x) => x.id === vmId)
+ if (v && body) {
+ if (body.name) v.name = body.name
+ if (body.tags) v.tags = body.tags
+ v.updated_at = nowIso()
+ }
+ })
+ return ok()
+ }
+ if (p === `/vms/${vmId}` && method === "DELETE") {
+ mutateState((s) => {
+ s.vms = s.vms.filter((v) => v.id !== vmId)
+ })
+ return ok()
+ }
+ if (isAction("start") || isAction("resume")) {
+ mutateState((s) => {
+ const v = s.vms.find((x) => x.id === vmId)
+ if (v) { v.state = "running"; v.updated_at = nowIso(); v.cpu_usage_percent = 20; v.memory_usage_percent = 40 }
+ })
+ return ok()
+ }
+ if (isAction("stop")) {
+ mutateState((s) => {
+ const v = s.vms.find((x) => x.id === vmId)
+ if (v) { v.state = "stopped"; v.updated_at = nowIso(); v.cpu_usage_percent = 0; v.memory_usage_percent = 0; v.guest_ip = "" }
+ })
+ return ok()
+ }
+ if (isAction("pause")) {
+ mutateState((s) => {
+ const v = s.vms.find((x) => x.id === vmId)
+ if (v) { v.state = "paused"; v.updated_at = nowIso(); v.cpu_usage_percent = 0 }
+ })
+ return ok()
+ }
+ if (isAction("flush-metrics") || isAction("ctrl-alt-del") || isAction("initialize")) return ok()
+ if (p === `/vms/${vmId}/shell` && method === "GET") return { username: "demo", password: "demo" }
+ if (p === `/vms/${vmId}/drives` && method === "GET") return { items: getState().drives[vmId] || [] }
+ if (p === `/vms/${vmId}/drives` && method === "POST") {
+ const d = { id: newId("drv"), vm_id: vmId, ...body, created_at: nowIso() }
+ mutateState((s) => { (s.drives[vmId] ||= []).push(d) })
+ return d
+ }
+ if (p === `/vms/${vmId}/nics` && method === "GET") return { items: getState().nics[vmId] || [] }
+ if (p === `/vms/${vmId}/nics` && method === "POST") {
+ const n = { id: newId("nic"), vm_id: vmId, ...body, created_at: nowIso() }
+ mutateState((s) => { (s.nics[vmId] ||= []).push(n) })
+ return n
+ }
+ if (p === `/vms/${vmId}/port-forwards` && method === "GET") return { items: getState().portForwards[vmId] || [] }
+ if (p === `/vms/${vmId}/port-forwards` && method === "POST") {
+ const pf = { id: newId("pf"), vm_id: vmId, ...body, created_at: nowIso() }
+ mutateState((s) => { (s.portForwards[vmId] ||= []).push(pf) })
+ return pf
+ }
+ if (p === `/vms/${vmId}/snapshots` && method === "GET") {
+ return { items: getState().snapshots.filter((s) => s.vm_id === vmId) }
+ }
+ if (p === `/vms/${vmId}/snapshots` && method === "POST") {
+ const v = getState().vms.find((x) => x.id === vmId)
+ const snap = {
+ id: newId("snap"),
+ vm_id: vmId,
+ name: body?.name || `snap-${Date.now()}`,
+ host_addr: v?.host_addr || "10.0.0.11:9090",
+ mem_size_mib: v?.mem_mib || 1024,
+ vcpu: v?.vcpu || 1,
+ kernel_path: v?.kernel_path || "/srv/images/vmlinux-6.1",
+ rootfs_path: `/srv/snapshots/${vmId}-${Date.now()}.ext4`,
+ mem_path: `/srv/snapshots/${vmId}-${Date.now()}.mem`,
+ host_id: v?.host_id,
+ created_at: nowIso(),
+ }
+ mutateState((s) => s.snapshots.unshift(snap as any))
+ return { id: snap.id, item: snap }
+ }
+ }
+
+ // Snapshots top-level
+ if (p.startsWith("/snapshots/") && method === "DELETE") {
+ const id = p.slice("/snapshots/".length)
+ mutateState((s) => { s.snapshots = s.snapshots.filter((x) => x.id !== id) })
+ return ok()
+ }
+
+ // Containers ----------------------------------------------------------
+ if (p === "/containers" && method === "GET") return { items: getState().containers }
+ if (p === "/containers" && method === "POST") {
+ const c: Container = {
+ id: newId("c"),
+ name: body?.name || "demo-container",
+ image: body?.image || "alpine:latest",
+ args: body?.args ?? [],
+ env_vars: body?.env_vars ?? {},
+ volumes: body?.volumes ?? [],
+ port_mappings: body?.port_mappings ?? [],
+ restart_policy: body?.restart_policy ?? "no",
+ state: "running",
+ cpu_limit: body?.cpu_limit,
+ memory_limit_mb: body?.memory_limit_mb,
+ cpu_percent: 5 + Math.floor(Math.random() * 20),
+ memory_used_mb: 50 + Math.floor(Math.random() * 200),
+ uptime_seconds: 1,
+ guest_ip: `172.16.0.${150 + Math.floor(Math.random() * 50)}`,
+ created_at: nowIso(),
+ updated_at: nowIso(),
+ started_at: nowIso(),
+ }
+ mutateState((s) => s.containers.unshift(c))
+ return { id: c.id }
+ }
+ const cId = parseId(p, "/containers")
+ if (cId) {
+ const cAct = (a: string) => p === `/containers/${cId}/${a}`
+ if (p === `/containers/${cId}` && method === "GET") {
+ const item = getState().containers.find((x) => x.id === cId)
+ if (!item) notFound()
+ return { item }
+ }
+ if (p === `/containers/${cId}` && method === "PUT") {
+ mutateState((s) => { const c = s.containers.find((x) => x.id === cId); if (c && body) Object.assign(c, body) })
+ const item = getState().containers.find((x) => x.id === cId)
+ return { item }
+ }
+ if (p === `/containers/${cId}` && method === "DELETE") {
+ mutateState((s) => { s.containers = s.containers.filter((c) => c.id !== cId) })
+ return ok()
+ }
+ if (cAct("start")) { mutateState((s) => { const c = s.containers.find((x) => x.id === cId); if (c) { c.state = "running"; c.started_at = nowIso() } }); return ok() }
+ if (cAct("stop")) { mutateState((s) => { const c = s.containers.find((x) => x.id === cId); if (c) { c.state = "stopped"; c.stopped_at = nowIso(); c.cpu_percent = 0; c.memory_used_mb = 0 } }); return ok() }
+ if (cAct("restart")) { mutateState((s) => { const c = s.containers.find((x) => x.id === cId); if (c) c.state = "running" }); return ok() }
+ if (cAct("pause")) { mutateState((s) => { const c = s.containers.find((x) => x.id === cId); if (c) c.state = "paused" }); return ok() }
+ if (cAct("resume")) { mutateState((s) => { const c = s.containers.find((x) => x.id === cId); if (c) c.state = "running" }); return ok() }
+ if (cAct("exec") && method === "POST") return { stdout: "demo-mode: exec is not available", stderr: "", exit_code: 0 }
+ if (p.startsWith(`/containers/${cId}/logs`)) {
+ const samples = [
+ "[info] starting service",
+ "[info] listening on 0.0.0.0:8080",
+ "[info] accepted connection from 10.0.0.5",
+ "[debug] request /healthz returned 200",
+ "[debug] cache miss for key=user:42",
+ "[info] running gc cycle",
+ ]
+ return {
+ items: samples.map((message, i) => ({
+ container_id: cId,
+ timestamp: new Date(Date.now() - (samples.length - i) * 1500).toISOString(),
+ stream: i % 5 === 0 ? "stderr" : "stdout",
+ message,
+ })),
+ }
+ }
+ if (p === `/containers/${cId}/stats`) {
+ return {
+ items: Array.from({ length: 30 }, (_, i) => ({
+ cpu_percent: 8 + Math.sin(i / 3) * 6 + Math.random() * 3,
+ memory_used_mb: 200 + Math.sin(i / 4) * 50,
+ memory_limit_mb: 1024,
+ network_rx_bytes: i * 1024 * 16,
+ network_tx_bytes: i * 1024 * 12,
+ block_read_bytes: i * 4096,
+ block_write_bytes: i * 4096,
+ pids: 12,
+ recorded_at: new Date(Date.now() - (30 - i) * 60_000).toISOString(),
+ })),
+ }
+ }
+ }
+
+ // Docker Hub mocks ----------------------------------------------------
+ if (p === "/images/dockerhub/search" && method === "POST") {
+ const q = body?.query || ""
+ const items = [
+ { name: "nginx", description: "Official build of NGINX.", star_count: 19000, is_official: true, is_automated: false, pull_count: 1_000_000_000 },
+ { name: "redis", description: "Redis is the world's fastest data platform.", star_count: 12000, is_official: true, is_automated: false, pull_count: 800_000_000 },
+ { name: "postgres", description: "The PostgreSQL object-relational database.", star_count: 13000, is_official: true, is_automated: false, pull_count: 1_200_000_000 },
+ { name: "node", description: "Node.js JavaScript runtime.", star_count: 11000, is_official: true, is_automated: false, pull_count: 900_000_000 },
+ ].filter((x) => !q || x.name.includes(q))
+ return { items }
+ }
+ if (p === "/images/dockerhub/tags" && method === "POST") {
+ return { items: ["latest", "alpine", "1.27", "1.26", "stable"].map((tag) => ({ name: tag, last_updated: nowIso(), images: [] })) }
+ }
+ if (p.startsWith("/images/dockerhub/download")) return { task_id: "demo-task", status: "completed", progress: 100 }
+
+ // Images / Registry ---------------------------------------------------
+ if (p === "/images" && method === "GET") return { items: getState().images }
+ if (p === "/images" && method === "POST") {
+ const img = { id: newId("img"), kind: body?.kind || "rootfs", name: body?.name || "demo-image", host_path: body?.host_path || "/srv/images/demo", sha256: "demo", size: body?.size || 1_000_000, project: body?.project, created_at: nowIso(), updated_at: nowIso() }
+ mutateState((s) => s.images.unshift(img))
+ return { id: img.id }
+ }
+ const imageId = parseId(p, "/images")
+ if (imageId) {
+ if (method === "DELETE") {
+ mutateState((s) => { s.images = s.images.filter((i) => i.id !== imageId) })
+ return ok()
+ }
+ if (method === "GET") {
+ const item = getState().images.find((i) => i.id === imageId)
+ if (item) return { item }
+ }
+ }
+
+ // Templates -----------------------------------------------------------
+ if (p === "/templates" && method === "GET") return { items: getState().templates }
+ if (p === "/templates" && method === "POST") {
+ const t: Template = {
+ id: newId("tpl"),
+ name: body?.name || "new-template",
+ description: body?.description || "",
+ kernel_path: "/srv/images/vmlinux-6.1",
+ mem_mib: body?.spec?.mem_mib || 1024,
+ vcpu: body?.spec?.vcpu || 1,
+ spec: body?.spec || { vcpu: 1, mem_mib: 1024 },
+ created_at: nowIso(),
+ updated_at: nowIso(),
+ }
+ mutateState((s) => s.templates.unshift(t))
+ return { id: t.id }
+ }
+ const tplId = parseId(p, "/templates")
+ if (tplId) {
+ if (method === "DELETE") { mutateState((s) => { s.templates = s.templates.filter((t) => t.id !== tplId) }); return ok() }
+ if (method === "PUT" || method === "PATCH") {
+ mutateState((s) => { const t = s.templates.find((x) => x.id === tplId); if (t && body) Object.assign(t, body) })
+ const item = getState().templates.find((t) => t.id === tplId)
+ return { item }
+ }
+ if (method === "GET") {
+ const item = getState().templates.find((t) => t.id === tplId)
+ if (item) return { item }
+ }
+ if (p === `/templates/${tplId}/instantiate` && method === "POST") {
+ const tpl = getState().templates.find((t) => t.id === tplId)
+ const vm = asStateVm({ name: body?.name || `from-${tpl?.name}`, template_id: tplId, vcpu: tpl?.spec.vcpu, mem_mib: tpl?.spec.mem_mib })
+ mutateState((s) => s.vms.unshift(vm))
+ return { id: vm.id, item: vm }
+ }
+ }
+
+ // Hosts ---------------------------------------------------------------
+ if (p === "/hosts" && method === "GET") return { items: getState().hosts }
+ const hostId = parseId(p, "/hosts")
+ if (hostId) {
+ if (method === "GET") {
+ const item = getState().hosts.find((h) => h.id === hostId)
+ if (item) return { item }
+ }
+ if (method === "DELETE") { mutateState((s) => { s.hosts = s.hosts.filter((h) => h.id !== hostId) }); return ok() }
+ }
+
+ // Networks ------------------------------------------------------------
+ if (p === "/networks" && method === "GET") return { items: getState().networks }
+ if (p === "/networks" && method === "POST") {
+ const n: Network = {
+ id: newId("net"),
+ name: body?.name || "new-network",
+ description: body?.description,
+ type: body?.type || "nat",
+ bridge_name: `br-${body?.name || Math.random().toString(36).slice(2, 6)}`,
+ host_id: body?.host_id,
+ cidr: body?.cidr || "192.168.100.0/24",
+ gateway: body?.gateway,
+ status: "active",
+ managed: true,
+ dhcp_enabled: body?.dhcp_enabled ?? true,
+ dhcp_range_start: body?.dhcp_range_start,
+ dhcp_range_end: body?.dhcp_range_end,
+ vm_count: 0,
+ vlan_id: body?.vlan_id,
+ created_at: nowIso(),
+ updated_at: nowIso(),
+ }
+ mutateState((s) => s.networks.unshift(n))
+ return { item: n }
+ }
+ if (p === "/networks/suggest") {
+ return { bridge_name: "fcbr1", cidr: "172.16.50.0/24", gateway: "172.16.50.1", dhcp_range_start: "172.16.50.10", dhcp_range_end: "172.16.50.200" }
+ }
+ if (p.match(/^\/hosts\/[^/]+\/interfaces$/)) {
+ return {
+ interfaces: [
+ { name: "eth0", mac: "52:54:00:12:34:01", state: "up", addresses: ["10.0.0.11/24"], is_management: true },
+ { name: "eth1", mac: "52:54:00:12:34:02", state: "up", addresses: [], is_management: false },
+ { name: "fcbr0", mac: "52:54:00:12:34:03", state: "up", addresses: ["172.16.0.1/24"], is_management: false, master: "fcbr0" },
+ ],
+ }
+ }
+ const netId = parseId(p, "/networks")
+ if (netId) {
+ if (p === `/networks/${netId}` && method === "GET") {
+ const item = getState().networks.find((n) => n.id === netId)
+ if (item) return { item }
+ }
+ if (p === `/networks/${netId}` && method === "PATCH") {
+ mutateState((s) => { const n = s.networks.find((x) => x.id === netId); if (n && body) Object.assign(n, body) })
+ const item = getState().networks.find((n) => n.id === netId)
+ return { item }
+ }
+ if (p === `/networks/${netId}` && method === "DELETE") {
+ mutateState((s) => { s.networks = s.networks.filter((n) => n.id !== netId) }); return ok()
+ }
+ if (p === `/networks/${netId}/vms`) {
+ const vm_ids = getState().vms.slice(0, Math.min(getState().vms.length, 2)).map((v) => v.id)
+ return { vm_ids }
+ }
+ if (p === `/networks/${netId}/retry` && method === "POST") {
+ const item = getState().networks.find((n) => n.id === netId)
+ return { item }
+ }
+ }
+
+ // Volumes -------------------------------------------------------------
+ if (p === "/volumes" && method === "GET") return { items: getState().volumes }
+ if (p === "/volumes" && method === "POST") {
+ const v: Volume = {
+ id: newId("vol"),
+ name: body?.name || "new-volume",
+ description: body?.description,
+ path: `/srv/volumes/${body?.name || "demo"}.${body?.type || "qcow2"}`,
+ size_bytes: (body?.size_gb || 10) * 1024 * 1024 * 1024,
+ size_gb: body?.size_gb || 10,
+ type: body?.type || "qcow2",
+ status: "available",
+ host_id: body?.host_id || getState().hosts[0].id,
+ host_name: getState().hosts.find((h) => h.id === body?.host_id)?.name,
+ created_at: nowIso(),
+ }
+ mutateState((s) => s.volumes.unshift(v))
+ return { item: v }
+ }
+ const volId = parseId(p, "/volumes")
+ if (volId) {
+ if (p === `/volumes/${volId}` && method === "GET") {
+ const item = getState().volumes.find((v) => v.id === volId)
+ if (item) return { item }
+ }
+ if (p === `/volumes/${volId}` && method === "DELETE") {
+ mutateState((s) => { s.volumes = s.volumes.filter((v) => v.id !== volId) }); return ok()
+ }
+ if (p === `/volumes/${volId}/attach` && method === "POST") {
+ mutateState((s) => {
+ const v = s.volumes.find((x) => x.id === volId)
+ if (v) {
+ v.status = "attached"
+ v.attached_to_vm_id = body?.vm_id
+ v.attached_to_vm_name = s.vms.find((x) => x.id === body?.vm_id)?.name
+ }
+ })
+ return ok()
+ }
+ if (p === `/volumes/${volId}/detach` && method === "POST") {
+ mutateState((s) => {
+ const v = s.volumes.find((x) => x.id === volId)
+ if (v) { v.status = "available"; v.attached_to_vm_id = undefined; v.attached_to_vm_name = undefined }
+ })
+ return ok()
+ }
+ }
+
+ // Functions -----------------------------------------------------------
+ if (p === "/functions" && method === "GET") return getState().functions
+ if (p === "/functions" && method === "POST") {
+ const f = {
+ id: newId("fn"),
+ name: body?.name || "new-function",
+ runtime: body?.runtime || "javascript",
+ handler: body?.handler || "index.handler",
+ timeout_seconds: 30,
+ code: body?.code || "",
+ vcpu: body?.vcpu ?? 1,
+ memory_mb: body?.memory_mb ?? 128,
+ state: "ready" as const,
+ created_at: nowIso(),
+ updated_at: nowIso(),
+ invocation_count_24h: 0,
+ avg_duration_ms: 0,
+ guest_ip: `172.16.0.${180 + Math.floor(Math.random() * 20)}`,
+ port: 9100 + Math.floor(Math.random() * 100),
+ }
+ mutateState((s) => s.functions.unshift(f as any))
+ return f
+ }
+ const fnId = parseId(p, "/functions")
+ if (fnId) {
+ if (p === `/functions/${fnId}` && method === "GET") {
+ const item = getState().functions.find((f) => f.id === fnId)
+ if (item) return item
+ }
+ if (p === `/functions/${fnId}` && method === "PUT") {
+ mutateState((s) => { const f = s.functions.find((x) => x.id === fnId); if (f && body) Object.assign(f, body) })
+ return getState().functions.find((f) => f.id === fnId)
+ }
+ if (p === `/functions/${fnId}` && method === "DELETE") {
+ mutateState((s) => { s.functions = s.functions.filter((f) => f.id !== fnId) }); return ok()
+ }
+ if (p === `/functions/${fnId}/invoke` && method === "POST") {
+ return {
+ request_id: newId("req"),
+ status: "success",
+ duration_ms: 80 + Math.floor(Math.random() * 200),
+ response: { ok: true, echo: body?.event ?? null },
+ logs: ["[info] cold start: 18ms", "[info] handler returned"],
+ }
+ }
+ if (p.startsWith(`/functions/${fnId}/invocations`)) {
+ return { items: Array.from({ length: 12 }, (_, i) => ({
+ id: newId("inv"),
+ function_id: fnId,
+ status: i % 9 === 0 ? "error" : "success",
+ duration_ms: 60 + Math.floor(Math.random() * 400),
+ memory_used_mb: 60 + Math.floor(Math.random() * 120),
+ request_id: newId("req"),
+ event: { hello: "world" },
+ response: { ok: true },
+ logs: ["[info] invocation completed"],
+ invoked_at: new Date(Date.now() - i * 60_000).toISOString(),
+ })) }
+ }
+ }
+ if (p === "/functions/test" && method === "POST") {
+ return { request_id: newId("req"), status: "success", duration_ms: 84, response: { ok: true }, logs: ["[info] test invocation"] }
+ }
+
+ // Users ---------------------------------------------------------------
+ if ((p === "/admin/users" || p === "/users") && method === "GET") return { items: getState().users }
+ if ((p === "/admin/users" || p === "/users") && method === "POST") {
+ const u = { id: newId("u"), username: body?.username || "newuser", role: body?.role || "user", created_at: nowIso() }
+ mutateState((s) => s.users.unshift(u as any))
+ return { id: u.id }
+ }
+ const userId = parseId(p, "/admin/users") || parseId(p, "/users")
+ if (userId && (p.startsWith("/admin/users/") || p.startsWith("/users/"))) {
+ if (method === "GET") {
+ const item = getState().users.find((u) => u.id === userId)
+ if (item) return { item }
+ }
+ if (method === "PATCH" || method === "PUT") {
+ mutateState((s) => { const u = s.users.find((x) => x.id === userId); if (u && body) Object.assign(u, body) })
+ return ok()
+ }
+ if (method === "DELETE") {
+ mutateState((s) => { s.users = s.users.filter((u) => u.id !== userId) }); return ok()
+ }
+ }
+
+ // Storage backends ----------------------------------------------------
+ if ((p === "/admin/storage-backends" || p === "/storage-backends") && method === "GET") {
+ return { items: getState().storageBackends }
+ }
+ if ((p === "/admin/storage-backends" || p === "/storage-backends") && method === "POST") {
+ const sb = { id: newId("sb"), name: body?.name || "new-backend", kind: body?.kind || "local_file", capabilities: {} as any, is_default: !!body?.is_default, created_at: nowIso() }
+ mutateState((s) => s.storageBackends.unshift(sb as any))
+ return { id: sb.id }
+ }
+ // Network scan endpoints (NFS/iSCSI/SMB)
+ if (p.endsWith("/scan") && (p.includes("nfs") || p.includes("iscsi"))) {
+ if (p.includes("iscsi")) return { targets: [{ portal: "10.0.0.50:3260", iqn: "iqn.2024-01.com.example:storage1" }] }
+ return { exports: [{ path: "/exports/nqr", allowed: "10.0.0.0/24" }] }
+ }
+ if (p.endsWith("/health") && p.includes("storage")) {
+ return { reachable: true, status: "ok", used_bytes: 50 * 1024 ** 3, total_bytes: 1024 ** 4 }
+ }
+
+ // Backup targets ------------------------------------------------------
+ if (p === "/admin/backup-targets" || p === "/backup-targets") {
+ if (method === "GET") return { items: getState().backupTargets }
+ if (method === "POST") {
+ const bt = { id: newId("bt"), ...body, created_at: nowIso() }
+ mutateState((s) => s.backupTargets.unshift(bt as any))
+ return bt
+ }
+ }
+ if ((p.startsWith("/admin/backup-targets/") || p.startsWith("/backup-targets/")) && method === "DELETE") {
+ const id = p.split("/").pop()!
+ mutateState((s) => { s.backupTargets = s.backupTargets.filter((b) => b.id !== id) })
+ return ok()
+ }
+
+ // Metrics -------------------------------------------------------------
+ if (p.includes("/metrics")) {
+ if (p.startsWith("/hosts/")) {
+ const hostId = p.split("/")[2]
+ return fakeMetricsSeries().map((m) => ({ host_id: hostId, recorded_at: m.recorded_at, cpu_usage_percent: m.value, memory_used_mb: 30000 + (m.value * 100), memory_total_mb: 65536, disk_used_gb: 400, disk_total_gb: 1024 }))
+ }
+ if (p.startsWith("/vms/")) {
+ const vmId = p.split("/")[2]
+ return fakeMetricsSeries(Date.now(), 60, 40, 25).map((m) => ({ vm_id: vmId, recorded_at: m.recorded_at, cpu_usage_percent: m.value, memory_usage_percent: m.value, memory_used_kb: 1024 * 1024, memory_total_kb: 2 * 1024 * 1024, load_average: m.value / 25 }))
+ }
+ if (p.startsWith("/containers/")) {
+ const cid = p.split("/")[2]
+ return fakeMetricsSeries(Date.now(), 60, 30, 15).map((m) => ({ container_id: cid, recorded_at: m.recorded_at, cpu_percent: m.value, memory_used_mb: m.value * 5, memory_limit_mb: 1024, network_rx_bytes: 1024 * 1024, network_tx_bytes: 1024 * 1024, block_read_bytes: 4096, block_write_bytes: 4096, pids: 8 }))
+ }
+ }
+
+ // Generic safe default: list-looking shape vs single-item -------------
+ if (method === "GET") {
+ if (p.endsWith("s") || p.includes("list")) return { items: [], total: 0 }
+ return { item: null }
+ }
+ if (method === "DELETE") return ok()
+ if (method === "POST" || method === "PUT" || method === "PATCH") return { ok: true, demo: true }
+
+ return null
+}
+
+export { DEMO_MODE }
diff --git a/apps/ui/lib/demo/state.ts b/apps/ui/lib/demo/state.ts
new file mode 100644
index 00000000..3d381063
--- /dev/null
+++ b/apps/ui/lib/demo/state.ts
@@ -0,0 +1,500 @@
+// Seed state + in-memory store for demo mode. All data lives in module scope
+// so it persists across the SPA session; localStorage is used to keep it across
+// reloads.
+
+import type {
+ Vm,
+ Container,
+ Network,
+ Volume,
+ Image,
+ Template,
+ Host,
+ Function as Fn,
+ User,
+ StorageBackend,
+ Snapshot,
+ AuditLog,
+ BackupTarget,
+} from "@/lib/types"
+
+const STORAGE_KEY = "nqr-demo-state-v1"
+
+function iso(daysAgo = 0, hoursAgo = 0): string {
+ const d = new Date()
+ d.setDate(d.getDate() - daysAgo)
+ d.setHours(d.getHours() - hoursAgo)
+ return d.toISOString()
+}
+
+function seedHosts(): Host[] {
+ return [
+ {
+ id: "host-aurora",
+ name: "aurora-01",
+ addr: "10.0.0.11:9090",
+ status: "healthy",
+ capabilities_json: { bridge: "fcbr0", run_dir: "/srv/fc", cpus: 32, total_memory_mb: 131072, total_disk_gb: 2048, used_disk_gb: 612 },
+ total_cpus: 32,
+ total_memory_mb: 131072,
+ total_disk_gb: 2048,
+ used_disk_gb: 612,
+ vm_count: 4,
+ last_seen_at: iso(0, 0),
+ last_metrics_at: iso(0, 0),
+ },
+ {
+ id: "host-borealis",
+ name: "borealis-02",
+ addr: "10.0.0.12:9090",
+ status: "healthy",
+ capabilities_json: { bridge: "fcbr0", run_dir: "/srv/fc", cpus: 64, total_memory_mb: 262144, total_disk_gb: 4096, used_disk_gb: 1320 },
+ total_cpus: 64,
+ total_memory_mb: 262144,
+ total_disk_gb: 4096,
+ used_disk_gb: 1320,
+ vm_count: 7,
+ last_seen_at: iso(0, 0),
+ last_metrics_at: iso(0, 0),
+ },
+ {
+ id: "host-corona",
+ name: "corona-03",
+ addr: "10.0.0.13:9090",
+ status: "degraded",
+ capabilities_json: { bridge: "fcbr0", run_dir: "/srv/fc", cpus: 32, total_memory_mb: 131072, total_disk_gb: 2048, used_disk_gb: 1980 },
+ total_cpus: 32,
+ total_memory_mb: 131072,
+ total_disk_gb: 2048,
+ used_disk_gb: 1980,
+ vm_count: 2,
+ last_seen_at: iso(0, 1),
+ last_metrics_at: iso(0, 1),
+ },
+ ]
+}
+
+function seedImages(): Image[] {
+ return [
+ { id: "img-vmlinux", kind: "kernel", name: "vmlinux-6.1", host_path: "/srv/images/vmlinux-6.1", sha256: "a1b2c3", size: 13631488, created_at: iso(30), updated_at: iso(30) },
+ { id: "img-ubuntu-22", kind: "rootfs", name: "ubuntu-22.04", host_path: "/srv/images/ubuntu-22.04.ext4", sha256: "b2c3d4", size: 1610612736, created_at: iso(28), updated_at: iso(28) },
+ { id: "img-ubuntu-24", kind: "rootfs", name: "ubuntu-24.04", host_path: "/srv/images/ubuntu-24.04.ext4", sha256: "c3d4e5", size: 1879048192, created_at: iso(14), updated_at: iso(14) },
+ { id: "img-alpine", kind: "rootfs", name: "alpine-3.20", host_path: "/srv/images/alpine-3.20.ext4", sha256: "d4e5f6", size: 268435456, created_at: iso(21), updated_at: iso(21) },
+ { id: "img-debian-12", kind: "rootfs", name: "debian-12", host_path: "/srv/images/debian-12.ext4", sha256: "e5f607", size: 1342177280, created_at: iso(20), updated_at: iso(20) },
+ { id: "img-runtime", kind: "rootfs", name: "container-runtime", host_path: "/srv/images/container-runtime.ext4", sha256: "f60718", size: 524288000, project: "system", created_at: iso(60), updated_at: iso(60) },
+ ]
+}
+
+function seedTemplates(): Template[] {
+ return [
+ { id: "tpl-small", name: "small", description: "1 vCPU · 1 GiB", kernel_path: "/srv/images/vmlinux-6.1", mem_mib: 1024, vcpu: 1, spec: { vcpu: 1, mem_mib: 1024, kernel_image_id: "img-vmlinux", rootfs_image_id: "img-ubuntu-22" }, created_at: iso(40), updated_at: iso(40) },
+ { id: "tpl-medium", name: "medium", description: "2 vCPU · 2 GiB", kernel_path: "/srv/images/vmlinux-6.1", mem_mib: 2048, vcpu: 2, spec: { vcpu: 2, mem_mib: 2048, kernel_image_id: "img-vmlinux", rootfs_image_id: "img-ubuntu-22" }, created_at: iso(40), updated_at: iso(40) },
+ { id: "tpl-large", name: "large", description: "4 vCPU · 4 GiB", kernel_path: "/srv/images/vmlinux-6.1", mem_mib: 4096, vcpu: 4, spec: { vcpu: 4, mem_mib: 4096, kernel_image_id: "img-vmlinux", rootfs_image_id: "img-ubuntu-24" }, created_at: iso(40), updated_at: iso(40) },
+ { id: "tpl-xlarge", name: "xlarge", description: "8 vCPU · 16 GiB", kernel_path: "/srv/images/vmlinux-6.1", mem_mib: 16384, vcpu: 8, spec: { vcpu: 8, mem_mib: 16384, kernel_image_id: "img-vmlinux", rootfs_image_id: "img-ubuntu-24" }, created_at: iso(40), updated_at: iso(40) },
+ ]
+}
+
+function vmDefaults(): Partial {
+ return {
+ host_addr: "10.0.0.11:9090",
+ api_sock: "/srv/fc/sock",
+ tap: "tap0",
+ log_path: "/srv/fc/log",
+ fc_unit: "firecracker@1.service",
+ kernel_path: "/srv/images/vmlinux-6.1",
+ rootfs_path: "/srv/images/ubuntu-22.04.ext4",
+ tags: [],
+ }
+}
+
+function seedVms(): Vm[] {
+ const base = vmDefaults()
+ return [
+ {
+ ...(base as Vm),
+ id: "vm-web-01",
+ name: "web-01",
+ state: "running",
+ host_id: "host-aurora",
+ template_id: "tpl-medium",
+ http_port: 8001,
+ vcpu: 2,
+ mem_mib: 2048,
+ guest_ip: "172.16.0.21",
+ tags: ["prod", "web"],
+ cpu_usage_percent: 32,
+ memory_usage_percent: 48,
+ created_at: iso(10),
+ updated_at: iso(0, 2),
+ },
+ {
+ ...(base as Vm),
+ id: "vm-web-02",
+ name: "web-02",
+ state: "running",
+ host_id: "host-aurora",
+ template_id: "tpl-medium",
+ http_port: 8002,
+ vcpu: 2,
+ mem_mib: 2048,
+ guest_ip: "172.16.0.22",
+ tags: ["prod", "web"],
+ cpu_usage_percent: 28,
+ memory_usage_percent: 52,
+ created_at: iso(10),
+ updated_at: iso(0, 2),
+ },
+ {
+ ...(base as Vm),
+ id: "vm-api-01",
+ name: "api-01",
+ state: "running",
+ host_id: "host-borealis",
+ template_id: "tpl-large",
+ http_port: 8101,
+ vcpu: 4,
+ mem_mib: 4096,
+ guest_ip: "172.16.0.31",
+ tags: ["prod", "api"],
+ cpu_usage_percent: 64,
+ memory_usage_percent: 71,
+ created_at: iso(8),
+ updated_at: iso(0, 1),
+ },
+ {
+ ...(base as Vm),
+ id: "vm-worker-01",
+ name: "worker-01",
+ state: "running",
+ host_id: "host-borealis",
+ template_id: "tpl-large",
+ http_port: 8201,
+ vcpu: 4,
+ mem_mib: 4096,
+ guest_ip: "172.16.0.41",
+ tags: ["prod", "worker"],
+ cpu_usage_percent: 81,
+ memory_usage_percent: 62,
+ created_at: iso(5),
+ updated_at: iso(0, 0),
+ },
+ {
+ ...(base as Vm),
+ id: "vm-db-staging",
+ name: "db-staging",
+ state: "stopped",
+ host_id: "host-borealis",
+ template_id: "tpl-xlarge",
+ http_port: 8301,
+ vcpu: 8,
+ mem_mib: 16384,
+ guest_ip: "",
+ tags: ["staging", "db"],
+ cpu_usage_percent: 0,
+ memory_usage_percent: 0,
+ created_at: iso(20),
+ updated_at: iso(2),
+ },
+ {
+ ...(base as Vm),
+ id: "vm-ci-runner",
+ name: "ci-runner",
+ state: "paused",
+ host_id: "host-aurora",
+ template_id: "tpl-small",
+ http_port: 8401,
+ vcpu: 1,
+ mem_mib: 1024,
+ guest_ip: "172.16.0.51",
+ tags: ["dev", "ci"],
+ cpu_usage_percent: 0,
+ memory_usage_percent: 24,
+ created_at: iso(3),
+ updated_at: iso(0, 3),
+ },
+ ]
+}
+
+function seedContainers(): Container[] {
+ return [
+ {
+ id: "c-nginx-01",
+ name: "nginx-edge",
+ image: "nginx:1.27-alpine",
+ args: [],
+ env_vars: { TZ: "UTC" },
+ volumes: [],
+ port_mappings: [{ host: 8080, container: 80, protocol: "tcp" }],
+ restart_policy: "unless-stopped",
+ state: "running",
+ cpu_limit: 1,
+ memory_limit_mb: 512,
+ cpu_percent: 4,
+ memory_used_mb: 84,
+ uptime_seconds: 86400 * 3,
+ guest_ip: "172.16.0.61",
+ created_at: iso(7),
+ updated_at: iso(0, 1),
+ started_at: iso(3),
+ },
+ {
+ id: "c-redis",
+ name: "redis-cache",
+ image: "redis:7-alpine",
+ args: [],
+ env_vars: {},
+ volumes: [],
+ port_mappings: [{ host: 6379, container: 6379, protocol: "tcp" }],
+ restart_policy: "unless-stopped",
+ state: "running",
+ cpu_limit: 1,
+ memory_limit_mb: 1024,
+ cpu_percent: 12,
+ memory_used_mb: 312,
+ uptime_seconds: 86400 * 5,
+ guest_ip: "172.16.0.62",
+ created_at: iso(8),
+ updated_at: iso(0, 1),
+ started_at: iso(5),
+ },
+ {
+ id: "c-postgres",
+ name: "pg-primary",
+ image: "postgres:16",
+ args: [],
+ env_vars: { POSTGRES_PASSWORD: "•••••••", POSTGRES_DB: "app" },
+ volumes: [{ host: "/srv/data/pg", container: "/var/lib/postgresql/data" }],
+ port_mappings: [{ host: 5432, container: 5432, protocol: "tcp" }],
+ restart_policy: "always",
+ state: "running",
+ cpu_limit: 2,
+ memory_limit_mb: 4096,
+ cpu_percent: 18,
+ memory_used_mb: 1842,
+ uptime_seconds: 86400 * 12,
+ guest_ip: "172.16.0.63",
+ created_at: iso(14),
+ updated_at: iso(0, 1),
+ started_at: iso(12),
+ },
+ {
+ id: "c-grafana",
+ name: "grafana",
+ image: "grafana/grafana:latest",
+ args: [],
+ env_vars: {},
+ volumes: [],
+ port_mappings: [{ host: 3000, container: 3000, protocol: "tcp" }],
+ restart_policy: "unless-stopped",
+ state: "stopped",
+ cpu_percent: 0,
+ memory_used_mb: 0,
+ created_at: iso(2),
+ updated_at: iso(1),
+ },
+ ]
+}
+
+function seedNetworks(): Network[] {
+ return [
+ {
+ id: "net-default",
+ name: "default-nat",
+ description: "Default NAT network for VMs",
+ type: "nat",
+ bridge_name: "fcbr0",
+ host_id: "host-aurora",
+ host_name: "aurora-01",
+ cidr: "172.16.0.0/24",
+ gateway: "172.16.0.1",
+ status: "active",
+ managed: true,
+ dhcp_enabled: true,
+ dhcp_range_start: "172.16.0.10",
+ dhcp_range_end: "172.16.0.200",
+ vm_count: 6,
+ created_at: iso(60),
+ updated_at: iso(0),
+ },
+ {
+ id: "net-prod",
+ name: "prod-bridged",
+ description: "Bridged to physical eth0",
+ type: "bridged",
+ bridge_name: "br-prod",
+ host_id: "host-borealis",
+ host_name: "borealis-02",
+ cidr: "10.10.0.0/24",
+ gateway: "10.10.0.1",
+ status: "active",
+ managed: true,
+ dhcp_enabled: false,
+ vm_count: 3,
+ created_at: iso(30),
+ updated_at: iso(2),
+ },
+ {
+ id: "net-isolated",
+ name: "ci-isolated",
+ description: "Isolated CI/CD network",
+ type: "isolated",
+ bridge_name: "br-iso",
+ host_id: "host-aurora",
+ host_name: "aurora-01",
+ cidr: "192.168.10.0/24",
+ gateway: "192.168.10.1",
+ status: "active",
+ managed: true,
+ dhcp_enabled: true,
+ vm_count: 1,
+ created_at: iso(7),
+ updated_at: iso(0),
+ },
+ ]
+}
+
+function seedVolumes(): Volume[] {
+ return [
+ { id: "vol-pg-data", name: "pg-data", description: "Postgres data volume", path: "/srv/volumes/pg-data.qcow2", size_bytes: 53687091200, size_gb: 50, type: "qcow2", status: "attached", host_id: "host-borealis", host_name: "borealis-02", attached_to_vm_id: "vm-api-01", attached_to_vm_name: "api-01", created_at: iso(14) },
+ { id: "vol-logs", name: "shared-logs", description: "Centralized log volume", path: "/srv/volumes/logs.ext4", size_bytes: 21474836480, size_gb: 20, type: "ext4", status: "available", host_id: "host-aurora", host_name: "aurora-01", created_at: iso(7) },
+ { id: "vol-cache", name: "build-cache", description: "CI build cache", path: "/srv/volumes/cache.raw", size_bytes: 107374182400, size_gb: 100, type: "raw", status: "attached", host_id: "host-aurora", host_name: "aurora-01", attached_to_vm_id: "vm-ci-runner", attached_to_vm_name: "ci-runner", created_at: iso(3) },
+ { id: "vol-snapshots", name: "snapshot-store", description: "VM snapshot vault", path: "/srv/volumes/snapshots.qcow2", size_bytes: 214748364800, size_gb: 200, type: "qcow2", status: "available", host_id: "host-corona", host_name: "corona-03", created_at: iso(45) },
+ ]
+}
+
+function seedFunctions(): Fn[] {
+ return [
+ { id: "fn-resize-img", name: "resize-image", runtime: "javascript", handler: "index.handler", timeout_seconds: 30, code: "export const handler = async (e) => ({ ok: true, in: e })", vcpu: 1, memory_mb: 256, state: "ready", invocation_count_24h: 1284, avg_duration_ms: 142, last_invoked_at: iso(0, 0), created_at: iso(14), updated_at: iso(2), guest_ip: "172.16.0.81", port: 9100 },
+ { id: "fn-webhook", name: "github-webhook", runtime: "python", handler: "main.handler", timeout_seconds: 10, code: "def handler(event):\n return { 'ok': True }", vcpu: 1, memory_mb: 128, state: "ready", invocation_count_24h: 386, avg_duration_ms: 68, last_invoked_at: iso(0, 1), created_at: iso(7), updated_at: iso(1), guest_ip: "172.16.0.82", port: 9101 },
+ { id: "fn-classify", name: "classify-event", runtime: "typescript", handler: "index.handler", timeout_seconds: 60, code: "export const handler = async (e) => ({ class: 'A' })", vcpu: 2, memory_mb: 512, state: "ready", invocation_count_24h: 52, avg_duration_ms: 412, last_invoked_at: iso(0, 2), created_at: iso(3), updated_at: iso(0, 4), guest_ip: "172.16.0.83", port: 9102 },
+ ]
+}
+
+function seedUsers(): User[] {
+ return [
+ { id: "u-root", username: "root", role: "admin", created_at: iso(120), last_login_at: iso(0, 0), timezone: "UTC", theme: "dark" },
+ { id: "u-alice", username: "alice", role: "admin", created_at: iso(80), last_login_at: iso(1), timezone: "Europe/Berlin", theme: "dark" },
+ { id: "u-bob", username: "bob", role: "user", created_at: iso(45), last_login_at: iso(0, 3), timezone: "America/New_York", theme: "light" },
+ { id: "u-carol", username: "carol", role: "viewer", created_at: iso(15), last_login_at: iso(2), timezone: "Asia/Singapore", theme: "system" },
+ ]
+}
+
+function seedStorageBackends(): StorageBackend[] {
+ return [
+ { id: "sb-local", name: "Local Disk", kind: "local_file", capabilities: {} as any, is_default: true, created_at: iso(120) },
+ { id: "sb-nfs", name: "NAS-NFS", kind: "nfs", capabilities: {} as any, is_default: false, created_at: iso(60) },
+ { id: "sb-iscsi", name: "Pure-iSCSI", kind: "iscsi", capabilities: {} as any, is_default: false, created_at: iso(30) },
+ ]
+}
+
+function seedBackupTargets(): BackupTarget[] {
+ return [
+ { id: "bt-s3", name: "AWS S3 (us-east-1)", endpoint: "https://s3.us-east-1.amazonaws.com", region: "us-east-1", bucket: "nqr-backups", prefix: "prod/", access_key_id: "AKIA••••••EXAMPLE", gc_hour: 3, created_at: iso(40) },
+ { id: "bt-minio", name: "MinIO (on-prem)", endpoint: "https://minio.internal:9000", bucket: "nqr-backups", prefix: "lab/", access_key_id: "minio-admin", gc_hour: 4, created_at: iso(15) },
+ ]
+}
+
+function seedSnapshots(): Snapshot[] {
+ return [
+ { id: "snap-1", vm_id: "vm-web-01", name: "pre-deploy", host_addr: "10.0.0.11:9090", mem_size_mib: 2048, vcpu: 2, kernel_path: "/srv/images/vmlinux-6.1", rootfs_path: "/srv/snapshots/web-01-1.ext4", mem_path: "/srv/snapshots/web-01-1.mem", host_id: "host-aurora", created_at: iso(2) } as Snapshot,
+ { id: "snap-2", vm_id: "vm-api-01", name: "v1.4-baseline", host_addr: "10.0.0.12:9090", mem_size_mib: 4096, vcpu: 4, kernel_path: "/srv/images/vmlinux-6.1", rootfs_path: "/srv/snapshots/api-01-2.ext4", mem_path: "/srv/snapshots/api-01-2.mem", host_id: "host-borealis", created_at: iso(5) } as Snapshot,
+ ]
+}
+
+function seedAuditLogs(): AuditLog[] {
+ const rows: AuditLog[] = [
+ { id: "al-1", user_id: "u-root", username: "root", action: "vm.start", resource_type: "vm", resource_id: "vm-web-01", details: {}, ip_address: "10.0.0.4", success: true, error_message: null, created_at: iso(0, 1) },
+ { id: "al-2", user_id: "u-alice", username: "alice", action: "vm.create", resource_type: "vm", resource_id: "vm-worker-01", details: { template: "tpl-large" }, ip_address: "10.0.0.6", success: true, error_message: null, created_at: iso(5) },
+ { id: "al-3", user_id: "u-bob", username: "bob", action: "container.deploy", resource_type: "container", resource_id: "c-nginx-01", details: {}, ip_address: "10.0.0.7", success: true, error_message: null, created_at: iso(0, 4) },
+ { id: "al-4", user_id: "u-root", username: "root", action: "user.login", resource_type: "user", resource_id: "u-root", details: {}, ip_address: "10.0.0.4", success: true, error_message: null, created_at: iso(0, 0) },
+ { id: "al-5", user_id: "u-carol", username: "carol", action: "vm.view", resource_type: "vm", resource_id: "vm-api-01", details: {}, ip_address: "10.0.0.9", success: true, error_message: null, created_at: iso(0, 2) },
+ ]
+ return rows
+}
+
+export type DemoStateShape = {
+ vms: Vm[]
+ containers: Container[]
+ networks: Network[]
+ volumes: Volume[]
+ images: Image[]
+ templates: Template[]
+ hosts: Host[]
+ functions: Fn[]
+ users: User[]
+ storageBackends: StorageBackend[]
+ snapshots: Snapshot[]
+ auditLogs: AuditLog[]
+ backupTargets: BackupTarget[]
+ // Per-VM sub-resources are stored by parent id for simplicity.
+ drives: Record
+ nics: Record
+ portForwards: Record
+}
+
+function fresh(): DemoStateShape {
+ return {
+ vms: seedVms(),
+ containers: seedContainers(),
+ networks: seedNetworks(),
+ volumes: seedVolumes(),
+ images: seedImages(),
+ templates: seedTemplates(),
+ hosts: seedHosts(),
+ functions: seedFunctions(),
+ users: seedUsers(),
+ storageBackends: seedStorageBackends(),
+ snapshots: seedSnapshots(),
+ auditLogs: seedAuditLogs(),
+ backupTargets: seedBackupTargets(),
+ drives: {},
+ nics: {},
+ portForwards: {},
+ }
+}
+
+let _state: DemoStateShape | null = null
+
+function load(): DemoStateShape {
+ if (typeof window === "undefined") return fresh()
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY)
+ if (raw) return JSON.parse(raw) as DemoStateShape
+ } catch {
+ // fall through
+ }
+ return fresh()
+}
+
+function persist() {
+ if (typeof window === "undefined" || !_state) return
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(_state))
+ } catch {
+ // quota etc — ignore, demo data is rebuildable
+ }
+}
+
+export function getState(): DemoStateShape {
+ if (!_state) _state = load()
+ return _state
+}
+
+export function mutateState(fn: (s: DemoStateShape) => void) {
+ if (!_state) _state = load()
+ fn(_state)
+ persist()
+}
+
+export function resetState() {
+ _state = fresh()
+ persist()
+}
+
+export function newId(prefix: string): string {
+ return `${prefix}-${Math.random().toString(36).slice(2, 8)}`
+}
+
+export function nowIso(): string {
+ return new Date().toISOString()
+}
diff --git a/apps/ui/next.config.mjs b/apps/ui/next.config.mjs
index 0013134d..3109733e 100644
--- a/apps/ui/next.config.mjs
+++ b/apps/ui/next.config.mjs
@@ -1,6 +1,11 @@
/** @type {import('next').NextConfig} */
+// Standalone output is for self-hosted runtimes (the manager bundles
+// .next/standalone). Vercel uses its own serverless runtime and emits a
+// warning when output: standalone is set, so we disable it there.
+const isVercel = !!process.env.VERCEL
+
const nextConfig = {
- output: "standalone",
+ ...(isVercel ? {} : { output: "standalone" }),
typescript: {
ignoreBuildErrors: true,
},
diff --git a/apps/ui/package.json b/apps/ui/package.json
index 961d67d1..54d9c836 100644
--- a/apps/ui/package.json
+++ b/apps/ui/package.json
@@ -57,7 +57,7 @@
"input-otp": "1.4.1",
"lucide-react": "^0.454.0",
"monaco-editor": "latest",
- "next": "15.5.6",
+ "next": "15.5.18",
"next-mdx-remote": "^6.0.0",
"next-themes": "^0.4.6",
"react": "19.2.0",
diff --git a/apps/ui/pnpm-lock.yaml b/apps/ui/pnpm-lock.yaml
index 701ce25e..69421e7e 100644
--- a/apps/ui/pnpm-lock.yaml
+++ b/apps/ui/pnpm-lock.yaml
@@ -103,7 +103,7 @@ importers:
version: 5.90.2(@tanstack/react-query@5.90.5(react@19.2.0))(react@19.2.0)
'@vercel/analytics':
specifier: 1.3.1
- version: 1.3.1(next@15.5.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)
+ version: 1.3.1(next@15.5.18(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)
'@xterm/addon-fit':
specifier: ^0.10.0
version: 0.10.0(@xterm/xterm@5.5.0)
@@ -144,8 +144,8 @@ importers:
specifier: latest
version: 0.54.0
next:
- specifier: 15.5.6
- version: 15.5.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ specifier: 15.5.18
+ version: 15.5.18(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
next-mdx-remote:
specifier: ^6.0.0
version: 6.0.0(@types/react@19.2.2)(react@19.2.0)
@@ -447,57 +447,57 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- '@next/env@15.5.6':
- resolution: {integrity: sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q==}
+ '@next/env@15.5.18':
+ resolution: {integrity: sha512-hAV85Ckd9QR6RvH04MEKwsfLTksvFpO47j9xwtoIuvuPnlwecpSi+uZTtm8HirVbtlI2Fnz//xpcSTjFdyJk+g==}
- '@next/swc-darwin-arm64@15.5.6':
- resolution: {integrity: sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg==}
+ '@next/swc-darwin-arm64@15.5.18':
+ resolution: {integrity: sha512-w0WvQf1n+txiwns/9pwIQteCJpZTbxzO2SE0FLcwuD4v0WEh1JPOjdyxWL21XwJsdpx8cFRjyzxzCS/siP7HcQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@next/swc-darwin-x64@15.5.6':
- resolution: {integrity: sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA==}
+ '@next/swc-darwin-x64@15.5.18':
+ resolution: {integrity: sha512-znn71QmDuxm+BOaglihMZfvyySMnNljkVIY5Z2TCssBmm+WqL6c19VhtH5ktFkHa8EZ2bnTUpcNcmNSQsg67og==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@next/swc-linux-arm64-gnu@15.5.6':
- resolution: {integrity: sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg==}
+ '@next/swc-linux-arm64-gnu@15.5.18':
+ resolution: {integrity: sha512-yPPe5MNL+igZUa+OsqQJisqSfh6oarIuA1Q0BDxljGJhRQyZeP+WRHh7rs/jZUGMh5aY0YdIjXZG0VohkKkUdw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@next/swc-linux-arm64-musl@15.5.6':
- resolution: {integrity: sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w==}
+ '@next/swc-linux-arm64-musl@15.5.18':
+ resolution: {integrity: sha512-glaCczEWIrHsokFZ3pP08U4BpKxwIdnT+txdOM32OBgpL9Yw4aqx8NejmgtZQZOdstQ5f0L3CasIZudzCuD+nw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@next/swc-linux-x64-gnu@15.5.6':
- resolution: {integrity: sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA==}
+ '@next/swc-linux-x64-gnu@15.5.18':
+ resolution: {integrity: sha512-oUfg2EgJmU3R0OCOWiokGFUTvZiPfXtriXiuF3YNxRoROCdgvTedHIzYoeKH34gsZxS/V7mHbfq2hpAHwhH1/A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@next/swc-linux-x64-musl@15.5.6':
- resolution: {integrity: sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ==}
+ '@next/swc-linux-x64-musl@15.5.18':
+ resolution: {integrity: sha512-JLxSP3KTd9iu/bvUMQxH7RJo9xKSHf55/6RPE4a6FTSZygGn7uvZbCej0AHXydwkggQGSD9UddSjwv6Xz5ESfA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
- '@next/swc-win32-arm64-msvc@15.5.6':
- resolution: {integrity: sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg==}
+ '@next/swc-win32-arm64-msvc@15.5.18':
+ resolution: {integrity: sha512-ir1v7enP52K2HNz3tQQvwF+x7VNxBk1ciiZ18WBPvxf4C59IqdfmHPJYK3vH7rSxpuCVw/8C712wTXNAtEp+NA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@next/swc-win32-x64-msvc@15.5.6':
- resolution: {integrity: sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ==}
+ '@next/swc-win32-x64-msvc@15.5.18':
+ resolution: {integrity: sha512-LIu5me6QTANCd25E7I5uIEfvgQ06RK7tvHAbYo3zCb3VpxQEPvMcSpd87NwUABDT6MbGPdEGR5VRiK4PPTJhQg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -2092,8 +2092,8 @@ packages:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
- next@15.5.6:
- resolution: {integrity: sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==}
+ next@15.5.18:
+ resolution: {integrity: sha512-eKL8zUJkX9Y5lE+RX/2YJoItVdGlIscyVyboeD9wSpp0PaGqjoA4tTpT2qPqz9ax+5IzGESyLSeZ/RCwbSZ2uQ==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@@ -2682,30 +2682,30 @@ snapshots:
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
- '@next/env@15.5.6': {}
+ '@next/env@15.5.18': {}
- '@next/swc-darwin-arm64@15.5.6':
+ '@next/swc-darwin-arm64@15.5.18':
optional: true
- '@next/swc-darwin-x64@15.5.6':
+ '@next/swc-darwin-x64@15.5.18':
optional: true
- '@next/swc-linux-arm64-gnu@15.5.6':
+ '@next/swc-linux-arm64-gnu@15.5.18':
optional: true
- '@next/swc-linux-arm64-musl@15.5.6':
+ '@next/swc-linux-arm64-musl@15.5.18':
optional: true
- '@next/swc-linux-x64-gnu@15.5.6':
+ '@next/swc-linux-x64-gnu@15.5.18':
optional: true
- '@next/swc-linux-x64-musl@15.5.6':
+ '@next/swc-linux-x64-musl@15.5.18':
optional: true
- '@next/swc-win32-arm64-msvc@15.5.6':
+ '@next/swc-win32-arm64-msvc@15.5.18':
optional: true
- '@next/swc-win32-x64-msvc@15.5.6':
+ '@next/swc-win32-x64-msvc@15.5.18':
optional: true
'@radix-ui/number@1.1.0': {}
@@ -3741,11 +3741,11 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
- '@vercel/analytics@1.3.1(next@15.5.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)':
+ '@vercel/analytics@1.3.1(next@15.5.18(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)':
dependencies:
server-only: 0.0.1
optionalDependencies:
- next: 15.5.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ next: 15.5.18(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react: 19.2.0
'@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)':
@@ -4471,9 +4471,9 @@ snapshots:
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
- next@15.5.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ next@15.5.18(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
- '@next/env': 15.5.6
+ '@next/env': 15.5.18
'@swc/helpers': 0.5.15
caniuse-lite: 1.0.30001751
postcss: 8.4.31
@@ -4481,14 +4481,14 @@ snapshots:
react-dom: 19.2.0(react@19.2.0)
styled-jsx: 5.1.6(react@19.2.0)
optionalDependencies:
- '@next/swc-darwin-arm64': 15.5.6
- '@next/swc-darwin-x64': 15.5.6
- '@next/swc-linux-arm64-gnu': 15.5.6
- '@next/swc-linux-arm64-musl': 15.5.6
- '@next/swc-linux-x64-gnu': 15.5.6
- '@next/swc-linux-x64-musl': 15.5.6
- '@next/swc-win32-arm64-msvc': 15.5.6
- '@next/swc-win32-x64-msvc': 15.5.6
+ '@next/swc-darwin-arm64': 15.5.18
+ '@next/swc-darwin-x64': 15.5.18
+ '@next/swc-linux-arm64-gnu': 15.5.18
+ '@next/swc-linux-arm64-musl': 15.5.18
+ '@next/swc-linux-x64-gnu': 15.5.18
+ '@next/swc-linux-x64-musl': 15.5.18
+ '@next/swc-win32-arm64-msvc': 15.5.18
+ '@next/swc-win32-x64-msvc': 15.5.18
sharp: 0.34.4
transitivePeerDependencies:
- '@babel/core'
diff --git a/apps/ui/vercel.json b/apps/ui/vercel.json
new file mode 100644
index 00000000..9f8a4ad6
--- /dev/null
+++ b/apps/ui/vercel.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://openapi.vercel.sh/vercel.json",
+ "framework": "nextjs",
+ "buildCommand": "next build",
+ "installCommand": "pnpm install --no-frozen-lockfile",
+ "outputDirectory": ".next",
+ "regions": ["sin1"]
+}