Zero-terminal first boot. Previously the bootstrap_ceo invite had to be
created via a CLI command (paperclipai auth bootstrap-ceo) and the UI
showed a code block instructing the user to run it. Nexus is meant to
be zero-terminal, so the server now auto-creates the invite on startup
when no instance admin exists and exposes its relative path through
/api/health. BootstrapPendingPage redirects straight to /invite/{token}.
The CLI command is left intact for headless/SSH-only setups.
Invite flow fixes that surfaced during testing:
- InviteLanding's invite query had default React Query refetch
behavior. After a successful bootstrap accept, the invite is marked
accepted server-side, so the refetch returned "not available" and
shadowed the success screen, making it look like the bootstrap had
failed when it actually succeeded. Set staleTime: Infinity +
refetchOnWindowFocus/Mount/Reconnect: false so the first fetch is a
one-shot snapshot.
- Reordered the render checks so result?.kind === "bootstrap" / "join"
are evaluated before the invite-availability error check — defensive
against any stray refetch that still leaks through.
- On bootstrap success, window.location.replace("/") lands the new
admin directly on the board; the "Bootstrap complete" confirmation
screen is now an unreachable safety net.
Vite onnxruntime middleware replaces the earlier public/ dump. The
previous commit put ort-wasm-simd-threaded.{mjs,wasm} in ui/public/ so
VAD's onnxWASMBasePath: "/" would find them. That works at runtime but
trips vite's dep optimizer: it scans onnxruntime-web, resolves the
dynamic import string to the public asset, and errors with "files in
/public should not be imported from source code." Remove the files and
add a vite plugin (configureServer middleware) that serves the two URLs
straight from node_modules/.pnpm/onnxruntime-web@*/. Runtime keeps
working and the files never enter vite's module graph.
Production build caveat: the middleware only runs in dev. When building
a static dist for production, the wasm files will need a different
mechanism (e.g. generateBundle hook). Not addressed here.
Also bundled (load-bearing for LAN browser testing):
- ui/src/lib/queryKeys.ts: add missing 'nexus' group. useNexusMode
referenced queryKeys.nexus.settings since commit 7bb72a5a (Phase
33-02) but the key was never added. Caused a blank screen crash on
any page that mounts Sidebar.
- ctl.sh: read PORT from .env instead of hardcoding 3100, and read it
once at the top so every subcommand honors it. Fixes the Version /
Mode showing '?' in status output after the port move to 6100.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
112 lines
4.1 KiB
TypeScript
112 lines
4.1 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "path";
|
|
import { defineConfig } from "vite";
|
|
import react from "@vitejs/plugin-react";
|
|
import tailwindcss from "@tailwindcss/vite";
|
|
|
|
// [nexus] Dev-only middleware that serves onnxruntime-web's wasm files at
|
|
// /ort-wasm-simd-threaded.{mjs,wasm}. @ricky0123/vad-react sets
|
|
// `onnxWASMBasePath: "/"`, which makes onnxruntime-web issue dynamic imports
|
|
// against the site root at runtime. Putting the files in ui/public/ works
|
|
// at runtime but trips vite's dep optimizer ("files in /public should not
|
|
// be imported from source code") because it scans onnxruntime-web's dynamic
|
|
// import and resolves the string to the public asset. This plugin bypasses
|
|
// public/ entirely — the files never enter vite's module graph, and the URL
|
|
// is served straight from node_modules.
|
|
function serveOnnxRuntimeWasm(): import("vite").Plugin {
|
|
// onnxruntime-web is a transitive dep of @ricky0123/vad-web and not hoisted
|
|
// to top-level node_modules under pnpm. Scan .pnpm/ for the version dir.
|
|
const pnpmRoot = path.resolve(__dirname, "../node_modules/.pnpm");
|
|
const ortEntry = fs
|
|
.readdirSync(pnpmRoot)
|
|
.find((name) => name.startsWith("onnxruntime-web@"));
|
|
const distDir = ortEntry
|
|
? path.join(pnpmRoot, ortEntry, "node_modules/onnxruntime-web/dist")
|
|
: null;
|
|
|
|
return {
|
|
name: "nexus-serve-onnxruntime-wasm",
|
|
configureServer(server) {
|
|
if (!distDir) {
|
|
server.config.logger.warn(
|
|
"[nexus-serve-onnxruntime-wasm] onnxruntime-web not found in .pnpm store — VAD voice input will 404",
|
|
);
|
|
return;
|
|
}
|
|
server.middlewares.use((req, res, next) => {
|
|
const url = (req.url ?? "").split("?")[0];
|
|
const match = url.match(/^\/ort-wasm-simd-threaded\.(mjs|wasm)$/);
|
|
if (!match) return next();
|
|
const filePath = path.join(distDir, `ort-wasm-simd-threaded.${match[1]}`);
|
|
fs.readFile(filePath, (err, content) => {
|
|
if (err) {
|
|
next(err);
|
|
return;
|
|
}
|
|
res.setHeader(
|
|
"Content-Type",
|
|
match[1] === "mjs" ? "text/javascript" : "application/wasm",
|
|
);
|
|
res.end(content);
|
|
});
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
// [nexus] Redirect OnboardingWizard → NexusOnboardingWizard at resolve time.
|
|
// A Vite plugin is used instead of resolve.alias because alias matches against
|
|
// the raw import specifier ("./components/OnboardingWizard") — not the resolved
|
|
// absolute path — so an absolute-path alias key never fires.
|
|
function nexusOnboardingRedirect(): import("vite").Plugin {
|
|
const target = path.resolve(__dirname, "./src/components/NexusOnboardingWizard.tsx");
|
|
return {
|
|
name: "nexus-onboarding-redirect",
|
|
enforce: "pre",
|
|
resolveId(source) {
|
|
if (source.endsWith("/components/OnboardingWizard") && !source.includes("Nexus")) {
|
|
return target;
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
export default defineConfig({
|
|
plugins: [nexusOnboardingRedirect(), serveOnnxRuntimeWasm(), react(), tailwindcss()],
|
|
resolve: {
|
|
alias: {
|
|
"@": path.resolve(__dirname, "./src"),
|
|
lexical: path.resolve(__dirname, "./node_modules/lexical/Lexical.mjs"),
|
|
},
|
|
},
|
|
build: {
|
|
rollupOptions: {
|
|
output: {
|
|
manualChunks: {
|
|
// Note: react/react-dom cannot be split via manualChunks — @vitejs/plugin-react
|
|
// injects the JSX runtime before Rollup processes chunks, so they stay in the entry.
|
|
// The 37 lazy-loaded page chunks are the primary performance lever.
|
|
"vendor-router": ["react-router-dom"],
|
|
"vendor-query": ["@tanstack/react-query"],
|
|
"vendor-markdown": ["react-markdown", "remark-gfm"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
server: {
|
|
port: 5173,
|
|
// COOP/COEP headers are only respected over HTTPS or localhost.
|
|
// Omitted so LAN access (plain HTTP + non-localhost IP) doesn't trigger
|
|
// browser warnings. Re-enable when serving behind TLS.
|
|
// headers: {
|
|
// "Cross-Origin-Opener-Policy": "same-origin",
|
|
// "Cross-Origin-Embedder-Policy": "require-corp",
|
|
// },
|
|
proxy: {
|
|
"/api": {
|
|
target: "http://localhost:6100",
|
|
ws: true,
|
|
},
|
|
},
|
|
},
|
|
});
|