diff --git a/ctl.sh b/ctl.sh new file mode 100755 index 00000000..18ae7da8 --- /dev/null +++ b/ctl.sh @@ -0,0 +1,228 @@ +#!/usr/bin/env bash +# +# ctl.sh — Nexus dev server control script +# +# Usage: +# ./ctl.sh start Start the dev server (background, logs to .paperclip/dev.log) +# ./ctl.sh stop Gracefully stop the dev server and all child processes +# ./ctl.sh restart Stop then start +# ./ctl.sh status Show whether the server is running +# ./ctl.sh logs Tail the dev server log +# ./ctl.sh fg Start in foreground (interactive, Ctrl-C to stop) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PIDFILE="$SCRIPT_DIR/.paperclip/dev.pid" +LOGFILE="$SCRIPT_DIR/.paperclip/dev.log" + +# Read PORT from .env (default 6100) so every command uses the same port. +PORT=$(grep -s '^PORT=' "$SCRIPT_DIR/.env" | cut -d= -f2) +PORT=${PORT:-6100} + +mkdir -p "$(dirname "$PIDFILE")" + +is_running() { + [[ -f "$PIDFILE" ]] || return 1 + local pid + pid=$(<"$PIDFILE") + kill -0 "$pid" 2>/dev/null +} + +get_pid() { + [[ -f "$PIDFILE" ]] && cat "$PIDFILE" || echo "" +} + +do_start() { + if is_running; then + echo "Already running (pid $(get_pid)). Use '$0 restart' to restart." + exit 0 + fi + + echo "Starting Nexus dev server..." + cd "$SCRIPT_DIR" + # setsid creates a new process group so 'kill -- -PGID' reaches all children + setsid pnpm dev >> "$LOGFILE" 2>&1 & + local parent_pid=$! + echo "$parent_pid" > "$PIDFILE" + + # Wait for health endpoint + local ready=false + for i in $(seq 1 30); do + if curl -sf "http://localhost:$PORT/api/health" > /dev/null 2>&1; then + ready=true + break + fi + sleep 1 + done + + if $ready; then + echo "Server ready (pid $parent_pid) — http://localhost:$PORT" + echo "Logs: $LOGFILE" + else + echo "Server started (pid $parent_pid) but health check not yet responding." + echo "Check logs: tail -f $LOGFILE" + fi +} + +do_stop() { + if ! is_running; then + echo "Not running." + # Clean up orphans anyway + "$SCRIPT_DIR/scripts/kill-dev.sh" 2>/dev/null || true + rm -f "$PIDFILE" + return 0 + fi + + local pid + pid=$(get_pid) + echo "Stopping Nexus dev server (pid $pid)..." + + # Kill the entire process group (negative PID = PGID). + # setsid in do_start makes the parent the session/group leader, + # so this reaches all children: pnpm, tsx, cross-env, node server, etc. + kill -TERM -- -"$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true + + # Wait up to 5s for graceful shutdown + local waited=0 + while kill -0 "$pid" 2>/dev/null && (( waited < 50 )); do + sleep 0.1 + ((waited += 1)) + done + + # Force-kill any stragglers in the group + if kill -0 "$pid" 2>/dev/null; then + echo "Sending SIGKILL to remaining processes..." + kill -KILL -- -"$pid" 2>/dev/null || true + fi + + # Use kill-dev.sh to mop up embedded postgres + "$SCRIPT_DIR/scripts/kill-dev.sh" 2>/dev/null || true + + rm -f "$PIDFILE" + echo "Stopped." +} + +do_status() { + if ! is_running; then + rm -f "$PIDFILE" + echo "Nexus is not running." + return + fi + + local pid + pid=$(get_pid) + + # Uptime from pidfile modification time (written at start) + local uptime started_at + if [[ -f "$PIDFILE" ]]; then + local now pidfile_epoch + now=$(date +%s) + pidfile_epoch=$(stat -c %Y "$PIDFILE" 2>/dev/null || echo "$now") + local diff=$(( now - pidfile_epoch )) + local days=$(( diff / 86400 )) + local hours=$(( (diff % 86400) / 3600 )) + local mins=$(( (diff % 3600) / 60 )) + if (( days > 0 )); then + uptime="${days}d ${hours}h ${mins}m" + elif (( hours > 0 )); then + uptime="${hours}h ${mins}m" + else + uptime="${mins}m" + fi + started_at=$(date -d "@$pidfile_epoch" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "?") + else + uptime="?" + started_at="?" + fi + + # Query health endpoint for deploy info + local health + health=$(curl -sf --max-time 2 "http://localhost:$PORT/api/health" 2>/dev/null || echo "") + + local mode exposure version + mode=$(echo "$health" | grep -oP '"deploymentMode"\s*:\s*"\K[^"]+' || echo "?") + exposure=$(echo "$health" | grep -oP '"deploymentExposure"\s*:\s*"\K[^"]+' || echo "?") + version=$(echo "$health" | grep -oP '"version"\s*:\s*"\K[^"]+' || echo "?") + + # Detect listen host/port from .env or defaults + local host port + host=$(grep -s '^HOST=' "$SCRIPT_DIR/.env" | cut -d= -f2 || echo "127.0.0.1") + host=${host:-127.0.0.1} + port=$PORT + + # LAN IP (first non-loopback IPv4) + local lan_ip + lan_ip=$(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1 || echo "") + + # Detect embedded postgres + local pg_pid pg_port pg_status + pg_pid=$(ps --ppid "$pid" -o pid,args --no-headers -w 2>/dev/null | grep postgres | awk '{print $1}' | head -1 || echo "") + if [[ -z "$pg_pid" ]]; then + # postgres may be deeper in the tree — search all descendants + pg_pid=$(ps -eo pid,args --no-headers 2>/dev/null | grep "[p]ostgres -D" | awk '{print $1}' | head -1 || echo "") + fi + pg_port=$(grep -s 'embeddedPostgresPort' "$HOME/.paperclip/instances/default/config.json" 2>/dev/null | grep -oP '\d+' || echo "54329") + if [[ -n "$pg_pid" ]]; then + pg_status="running (pid $pg_pid, port $pg_port)" + else + pg_status="not detected" + fi + + # Vite HMR port + local hmr_port=$(( port + 10000 )) + + # Process count in group + local proc_count + proc_count=$(ps -g "$pid" --no-headers 2>/dev/null | wc -l || echo "?") + + # Print status + echo "" + echo " Nexus Dev Server" + echo " ──────────────────────────────────────────" + echo " Status running since $started_at ($uptime)" + echo " Version $version" + echo " PID $pid ($proc_count processes)" + echo " Mode $mode ($exposure)" + echo "" + echo " API http://localhost:$port/api" + echo " UI http://localhost:$port" + if [[ "$host" == "0.0.0.0" && -n "$lan_ip" ]]; then + echo " LAN http://$lan_ip:$port" + fi + echo " HMR ws://localhost:$hmr_port" + echo "" + echo " Postgres $pg_status" + echo " Log $LOGFILE" + echo "" +} + +do_logs() { + if [[ -f "$LOGFILE" ]]; then + tail -f "$LOGFILE" + else + echo "No log file at $LOGFILE" + fi +} + +do_fg() { + if is_running; then + echo "Already running in background (pid $(get_pid)). Stop it first with '$0 stop'." + exit 1 + fi + cd "$SCRIPT_DIR" + exec pnpm dev +} + +case "${1:-}" in + start) do_start ;; + stop) do_stop ;; + restart) do_stop; do_start ;; + status) do_status ;; + logs) do_logs ;; + fg) do_fg ;; + *) + echo "Usage: $0 {start|stop|restart|status|logs|fg}" + exit 1 + ;; +esac diff --git a/server/src/app.ts b/server/src/app.ts index e93cc991..fc2a2989 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -90,6 +90,7 @@ export async function createApp( allowedHostnames: string[]; bindHost: string; authReady: boolean; + bootstrapInvitePath?: string | null; companyDeletionEnabled: boolean; instanceId?: string; hostVersion?: string; @@ -177,6 +178,7 @@ export async function createApp( deploymentExposure: opts.deploymentExposure, authReady: opts.authReady, companyDeletionEnabled: opts.companyDeletionEnabled, + bootstrapInvitePath: opts.bootstrapInvitePath, }), ); api.use("/companies", companyRoutes(db, opts.storageService)); diff --git a/server/src/bootstrap-invite.ts b/server/src/bootstrap-invite.ts new file mode 100644 index 00000000..1cca203c --- /dev/null +++ b/server/src/bootstrap-invite.ts @@ -0,0 +1,63 @@ +// [nexus] Auto-create a bootstrap_ceo invite on first boot so the web UI can +// redirect the first user to /invite/{token} without any terminal command. +// Mirrors the logic in cli/src/commands/auth-bootstrap-ceo.ts; the CLI command +// remains available for headless / SSH-only setups. + +import { createHash, randomBytes } from "node:crypto"; +import { and, count, eq, gt, isNull } from "drizzle-orm"; +import type { Db } from "@paperclipai/db"; +import { instanceUserRoles, invites } from "@paperclipai/db"; + +function hashToken(token: string) { + return createHash("sha256").update(token).digest("hex"); +} + +function createInviteToken() { + return `pcp_bootstrap_${randomBytes(24).toString("hex")}`; +} + +/** + * If no instance_admin user exists, create a bootstrap_ceo invite and return + * the relative path the browser should navigate to in order to create the + * first admin account. Returns null if an admin already exists (no-op). + * + * Safe to call on every boot — when an admin already exists it's a single + * count query with no writes. + */ +export async function ensureBootstrapInvite(db: Db): Promise { + const adminCount = await db + .select({ count: count() }) + .from(instanceUserRoles) + .where(eq(instanceUserRoles.role, "instance_admin")) + .then((rows) => Number(rows[0]?.count ?? 0)); + + if (adminCount > 0) return null; + + const now = new Date(); + // Revoke any stale live invites so our fresh token is the only match. + // We can't recover the raw token from the stored hash, so the previous + // invite is unreachable and must be replaced on restart. + await db + .update(invites) + .set({ revokedAt: now, updatedAt: now }) + .where( + and( + eq(invites.inviteType, "bootstrap_ceo"), + isNull(invites.revokedAt), + isNull(invites.acceptedAt), + gt(invites.expiresAt, now), + ), + ); + + const token = createInviteToken(); + const expiresHours = 72; + await db.insert(invites).values({ + inviteType: "bootstrap_ceo", + tokenHash: hashToken(token), + allowedJoinTypes: "human", + expiresAt: new Date(Date.now() + expiresHours * 60 * 60 * 1000), + invitedByUserId: "system", + }); + + return `/invite/${token}`; +} diff --git a/server/src/index.ts b/server/src/index.ts index ec29185a..93991f62 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -26,6 +26,7 @@ import { } from "@paperclipai/db"; import detectPort from "detect-port"; import { createApp } from "./app.js"; +import { ensureBootstrapInvite } from "./bootstrap-invite.js"; import { loadConfig } from "./config.js"; import { logger } from "./middleware/logger.js"; import { setupLiveEventsWebSocketServer } from "./realtime/live-events-ws.js"; @@ -485,6 +486,7 @@ export async function startServer(): Promise { } let authReady = config.deploymentMode === "local_trusted"; + let bootstrapInvitePath: string | null = null; let betterAuthHandler: RequestHandler | undefined; let resolveSession: | ((req: ExpressRequest) => Promise) @@ -546,6 +548,22 @@ export async function startServer(): Promise { resolveSessionFromHeaders = (headers) => resolveBetterAuthSessionFromHeaders(auth, headers); await initializeBoardClaimChallenge(db as any, { deploymentMode: config.deploymentMode }); authReady = true; + + // [nexus] Zero-terminal first boot: if no admin user exists, create a + // bootstrap_ceo invite so the web UI can redirect the first user directly + // to /invite/{token}. The paperclipai auth bootstrap-ceo CLI remains + // available for headless / SSH-only setups. + try { + bootstrapInvitePath = await ensureBootstrapInvite(db as any); + if (bootstrapInvitePath) { + logger.info( + { invitePath: bootstrapInvitePath }, + "auto-created bootstrap invite for zero-terminal first boot", + ); + } + } catch (err) { + logger.error({ err }, "failed to auto-create bootstrap invite on startup"); + } } const listenPort = await detectPort(config.port); @@ -577,6 +595,7 @@ export async function startServer(): Promise { allowedHostnames: config.allowedHostnames, bindHost: config.host, authReady, + bootstrapInvitePath, companyDeletionEnabled: config.companyDeletionEnabled, betterAuthHandler, resolveSession, diff --git a/server/src/routes/health.ts b/server/src/routes/health.ts index 795eb9a5..641d99de 100644 --- a/server/src/routes/health.ts +++ b/server/src/routes/health.ts @@ -14,6 +14,7 @@ export function healthRoutes( deploymentExposure: DeploymentExposure; authReady: boolean; companyDeletionEnabled: boolean; + bootstrapInvitePath?: string | null; } = { deploymentMode: "local_trusted", deploymentExposure: "private", @@ -93,6 +94,12 @@ export function healthRoutes( authReady: opts.authReady, bootstrapStatus, bootstrapInviteActive, + // [nexus] Zero-terminal first boot: auto-generated invite path so the + // UI can redirect the first user to /invite/{token} without touching + // a terminal. Only present while the DB still has no instance_admin. + ...(bootstrapStatus === "bootstrap_pending" && opts.bootstrapInvitePath + ? { bootstrapInvitePath: opts.bootstrapInvitePath } + : {}), features: { companyDeletionEnabled: opts.companyDeletionEnabled, }, diff --git a/ui/public/ort-wasm-simd-threaded.mjs b/ui/public/ort-wasm-simd-threaded.mjs deleted file mode 100644 index 6b5b22c3..00000000 --- a/ui/public/ort-wasm-simd-threaded.mjs +++ /dev/null @@ -1,59 +0,0 @@ -async function ortWasmThreaded(moduleArg={}){var moduleRtn;var h=moduleArg,aa=!!globalThis.window,k=!!globalThis.WorkerGlobalScope,m=globalThis.process?.versions?.node&&"renderer"!=globalThis.process?.type,n=k&&self.name?.startsWith("em-pthread");if(m){const {createRequire:a}=await import("module");var require=a(import.meta.url),ba=require("worker_threads");global.Worker=ba.Worker;n=(k=!ba.ic)&&"em-pthread"==ba.workerData}h.mountExternalData=(a,b)=>{a.startsWith("./")&&(a=a.substring(2));(h.Sb||(h.Sb=new Map)).set(a,b)}; -h.unmountExternalData=()=>{delete h.Sb};var SharedArrayBuffer=globalThis.SharedArrayBuffer??(new WebAssembly.Memory({initial:0,maximum:0,kc:!0})).buffer.constructor,ca="./this.program",da=(a,b)=>{throw b;},ea=import.meta.url,fa="",ha,ia; -if(m){var fs=require("fs");ea.startsWith("file:")&&(fa=require("path").dirname(require("url").fileURLToPath(ea))+"/");ia=a=>{a=ja(a)?new URL(a):a;return fs.readFileSync(a)};ha=async a=>{a=ja(a)?new URL(a):a;return fs.readFileSync(a,void 0)};1{process.exitCode=a;throw b;}}else if(aa||k){try{fa=(new URL(".",ea)).href}catch{}m||(k&&(ia=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer"; -b.send(null);return new Uint8Array(b.response)}),ha=async a=>{if(ja(a))return new Promise((d,c)=>{var e=new XMLHttpRequest;e.open("GET",a,!0);e.responseType="arraybuffer";e.onload=()=>{200==e.status||0==e.status&&e.response?d(e.response):c(e.status)};e.onerror=c;e.send(null)});var b=await fetch(a,{credentials:"same-origin"});if(b.ok)return b.arrayBuffer();throw Error(b.status+" : "+b.url);})}var ka=console.log.bind(console),la=console.error.bind(console); -if(m){var ma=require("util"),na=a=>"object"==typeof a?ma.inspect(a):a;ka=(...a)=>fs.writeSync(1,a.map(na).join(" ")+"\n");la=(...a)=>fs.writeSync(2,a.map(na).join(" ")+"\n")}var oa=ka,p=la,q,r,pa=!1,t,ja=a=>a.startsWith("file://");function v(){x.buffer!=z.buffer&&qa()}var ra,sa; -if(m&&n){var ta=ba.parentPort;ta.on("message",a=>global.onmessage?.({data:a}));Object.assign(globalThis,{self:global,postMessage:a=>ta.postMessage(a)});process.on("uncaughtException",a=>{postMessage({Qb:"uncaughtException",error:a});process.exit(1)})}var ua; -if(n){var va=!1;self.onunhandledrejection=b=>{throw b.reason||b;};function a(b){try{var d=b.data,c=d.Qb;if("load"===c){let e=[];self.onmessage=f=>e.push(f);ua=()=>{postMessage({Qb:"loaded"});for(let f of e)a(f);self.onmessage=a};for(const f of d.$b)if(!h[f]||h[f].proxy)h[f]=(...g)=>{postMessage({Qb:"callHandler",Zb:f,args:g})},"print"==f&&(oa=h[f]),"printErr"==f&&(p=h[f]);x=d.ec;qa();r=d.fc;wa();xa()}else if("run"===c){ya(d.Pb);za(d.Pb,0,0,1,0,0);Aa();Ba(d.Pb);va||=!0;try{Ca(d.cc,d.Ub)}catch(e){if("unwind"!= -e)throw e;}}else"setimmediate"!==d.target&&("checkMailbox"===c?va&&Da():c&&(p(`worker: received unknown command ${c}`),p(d)))}catch(e){throw Ea(),e;}}self.onmessage=a}var z,A,Fa,C,D,Ga,G,H,Ha=!1;function qa(){var a=x.buffer;h.HEAP8=z=new Int8Array(a);Fa=new Int16Array(a);h.HEAPU8=A=new Uint8Array(a);new Uint16Array(a);h.HEAP32=C=new Int32Array(a);h.HEAPU32=D=new Uint32Array(a);Ga=new Float32Array(a);G=new Float64Array(a);H=new BigInt64Array(a);new BigUint64Array(a)} -function Ia(){Ha=!0;n?ua():I.Ua()}function J(a){a="Aborted("+a+")";p(a);pa=!0;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");sa?.(a);throw a;}var Ja;async function Ka(a){if(!q)try{var b=await ha(a);return new Uint8Array(b)}catch{}if(a==Ja&&q)a=new Uint8Array(q);else if(ia)a=ia(a);else throw"both async and sync fetching of the wasm failed";return a} -async function La(a,b){try{var d=await Ka(a);return await WebAssembly.instantiate(d,b)}catch(c){p(`failed to asynchronously prepare wasm: ${c}`),J(c)}}async function Na(a){var b=Ja;if(!q&&!ja(b)&&!m)try{var d=fetch(b,{credentials:"same-origin"});return await WebAssembly.instantiateStreaming(d,a)}catch(c){p(`wasm streaming compile failed: ${c}`),p("falling back to ArrayBuffer instantiation")}return La(b,a)} -function Oa(){Pa={S:Qa,f:Ra,w:Sa,e:Ta,j:Ua,g:Va,T:Wa,b:Xa,G:Ya,ua:Za,k:$a,K:ab,Ka:bb,qa:cb,sa:db,La:eb,Ia:fb,Ba:gb,Ha:hb,Z:ib,ra:jb,oa:kb,Ja:lb,pa:mb,Qa:nb,Ea:ob,ma:pb,va:qb,ja:rb,U:sb,Da:Ba,Na:tb,ya:ub,za:vb,Aa:wb,wa:xb,xa:yb,ka:zb,Sa:Ab,Pa:Bb,W:Cb,V:Db,Oa:Eb,F:Fb,Ma:Gb,na:Hb,u:Ib,H:Jb,R:Kb,la:Lb,da:Mb,Ta:Nb,Fa:Ob,Ga:Pb,ta:Qb,L:Rb,Y:Sb,Ca:Tb,X:Ub,$:Vb,M:Wb,aa:Xb,N:Yb,v:Zb,c:$b,m:ac,n:bc,r:cc,ea:dc,x:ec,o:fc,O:gc,D:hc,I:ic,ba:jc,ca:kc,Q:lc,P:mc,fa:nc,z:oc,E:pc,d:qc,q:rc,i:sc,_:tc,l:uc,p:vc,s:wc,t:xc, -y:yc,ga:zc,B:Ac,J:Bc,C:Cc,ha:Dc,ia:Ec,A:Fc,h:Gc,a:x,Ra:Hc};return{a:Pa}} -async function wa(){function a(c,e){I=c.exports;I=Ic();Jc.push(I.wb);c=I;h._OrtInit=c.Va;h._OrtGetLastError=c.Wa;h._OrtCreateSessionOptions=c.Xa;h._OrtAppendExecutionProvider=c.Ya;h._OrtAddFreeDimensionOverride=c.Za;h._OrtAddSessionConfigEntry=c._a;h._OrtReleaseSessionOptions=c.$a;h._OrtCreateSession=c.ab;h._OrtReleaseSession=c.bb;h._OrtGetInputOutputCount=c.cb;h._OrtGetInputOutputMetadata=c.db;h._OrtFree=c.eb;h._OrtCreateTensor=c.fb;h._OrtGetTensorData=c.gb;h._OrtReleaseTensor=c.hb;h._OrtCreateRunOptions= -c.ib;h._OrtAddRunConfigEntry=c.jb;h._OrtReleaseRunOptions=c.kb;h._OrtCreateBinding=c.lb;h._OrtBindInput=c.mb;h._OrtBindOutput=c.nb;h._OrtClearBoundOutputs=c.ob;h._OrtReleaseBinding=c.pb;h._OrtRunWithBinding=c.qb;h._OrtRun=c.rb;h._OrtEndProfiling=c.sb;Kc=c.tb;Lc=h._free=c.ub;Mc=h._malloc=c.vb;za=c.yb;Ea=c.zb;Nc=c.Ab;Oc=c.Bb;Pc=c.Cb;Qc=c.Db;Rc=c.Eb;K=c.Fb;L=c.Gb;Sc=c.Hb;M=c.Ib;Tc=c.Jb;N=c.Kb;Uc=c.Lb;Vc=c.Mb;Wc=c.Nb;Xc=c.Ob;Yc=c.xb;r=e;return I}var b=Oa();if(h.instantiateWasm)return new Promise(c=>{h.instantiateWasm(b, -(e,f)=>{c(a(e,f))})});if(n){var d=new WebAssembly.Instance(r,Oa());return a(d,r)}Ja??=h.locateFile?h.locateFile?h.locateFile("ort-wasm-simd-threaded.wasm",fa):fa+"ort-wasm-simd-threaded.wasm":(new URL("ort-wasm-simd-threaded.wasm",import.meta.url)).href;return function(c){return a(c.instance,c.module)}(await Na(b))}class Zc{name="ExitStatus";constructor(a){this.message=`Program terminated with exit(${a})`;this.status=a}} -var $c=a=>{a.terminate();a.onmessage=()=>{}},ad=[],O=0,P=null,dd=a=>{0==Q.length&&(bd(),cd(Q[0]));var b=Q.pop();if(!b)return 6;R.push(b);S[a.Pb]=b;b.Pb=a.Pb;var d={Qb:"run",cc:a.bc,Ub:a.Ub,Pb:a.Pb};m&&b.unref();b.postMessage(d,a.Yb);return 0},T=0,U=(a,b,...d)=>{var c=16*d.length,e=N(),f=Tc(c),g=f>>>3,l;for(l of d)"bigint"==typeof l?((v(),H)[g++>>>0]=1n,(v(),H)[g++>>>0]=l):((v(),H)[g++>>>0]=0n,(v(),G)[g++>>>0]=l);a=Nc(a,0,c,f,b);M(e);return a}; -function Hc(a){if(n)return U(0,1,a);t=a;if(!(0{t=a;if(n)throw ed(a),"unwind";Hc(a)},Q=[],R=[],Jc=[],S={};function fd(){for(var a=h.numThreads-1;a--;)bd();ad.push(async()=>{var b=gd();O++;await b;O--;0==O&&P&&(b=P,P=null,b())})}var hd=a=>{var b=a.Pb;delete S[b];Q.push(a);R.splice(R.indexOf(a),1);a.Pb=0;Oc(b)};function Aa(){Jc.forEach(a=>a())} -var cd=a=>new Promise(b=>{a.onmessage=f=>{var g=f.data;f=g.Qb;if(g.Tb&&g.Tb!=Kc()){var l=S[g.Tb];l?l.postMessage(g,g.Yb):p(`Internal error! Worker sent a message "${f}" to target pthread ${g.Tb}, but that thread no longer exists!`)}else if("checkMailbox"===f)Da();else if("spawnThread"===f)dd(g);else if("cleanupThread"===f)jd(()=>{hd(S[g.dc])});else if("loaded"===f)a.loaded=!0,m&&!a.Pb&&a.unref(),b(a);else if("setimmediate"===g.target)a.postMessage(g);else if("uncaughtException"===f)a.onerror(g.error); -else if("callHandler"===f)h[g.Zb](...g.args);else f&&p(`worker sent an unknown command ${f}`)};a.onerror=f=>{p(`${"worker sent an error!"} ${f.filename}:${f.lineno}: ${f.message}`);throw f;};m&&(a.on("message",f=>a.onmessage({data:f})),a.on("error",f=>a.onerror(f)));var d=[],c=[],e;for(e of c)h.propertyIsEnumerable(e)&&d.push(e);a.postMessage({Qb:"load",$b:d,ec:x,fc:r})});async function gd(){if(!n)return Promise.all(Q.map(cd))} -function bd(){var a=new Worker(new URL(import.meta.url),{type:"module",workerData:"em-pthread",name:"em-pthread"});Q.push(a)}function ya(a){var b=(v(),D)[a+52>>>2>>>0];a=(v(),D)[a+56>>>2>>>0];Sc(b,b-a);M(b)}var kd=[],V=a=>{var b=kd[a];b||(kd[a]=b=Yc.get(a));return b},Ca=(a,b)=>{T=0;a=V(a)(b);0>>=0;var b=new nd(a);0==(v(),z)[b.Rb+12>>>0]&&(od(b,!0),md--);pd(b,!1);ld.push(b);return Xc(a)} -var W=0,Sa=()=>{K(0,0);var a=ld.pop();Uc(a.Vb);W=0};function od(a,b){b=b?1:0;(v(),z)[a.Rb+12>>>0]=b}function pd(a,b){b=b?1:0;(v(),z)[a.Rb+13>>>0]=b}class nd{constructor(a){this.Vb=a;this.Rb=a-24}}var qd=a=>{var b=W;if(!b)return L(0),0;var d=new nd(b);(v(),D)[d.Rb+16>>>2>>>0]=b;var c=(v(),D)[d.Rb+4>>>2>>>0];if(!c)return L(0),b;for(var e of a){if(0===e||e===c)break;if(Wc(e,c,d.Rb+16))return L(e),b}L(c);return b};function Ta(){return qd([])}function Ua(a){return qd([a>>>0])} -function Va(a,b,d,c){return qd([a>>>0,b>>>0,d>>>0,c>>>0])}var Wa=()=>{var a=ld.pop();a||J("no exception to throw");var b=a.Vb;0==(v(),z)[a.Rb+13>>>0]&&(ld.push(a),pd(a,!0),od(a,!1),md++);Vc(b);W=b;throw W;};function Xa(a,b,d){a>>>=0;var c=new nd(a);b>>>=0;d>>>=0;(v(),D)[c.Rb+16>>>2>>>0]=0;(v(),D)[c.Rb+4>>>2>>>0]=b;(v(),D)[c.Rb+8>>>2>>>0]=d;Vc(a);W=a;md++;throw W;}var Ya=()=>md;function rd(a,b,d,c){return n?U(2,1,a,b,d,c):Za(a,b,d,c)} -function Za(a,b,d,c){a>>>=0;b>>>=0;d>>>=0;c>>>=0;if(!globalThis.SharedArrayBuffer)return 6;var e=[];if(n&&0===e.length)return rd(a,b,d,c);a={bc:d,Pb:a,Ub:c,Yb:e};return n?(a.Qb="spawnThread",postMessage(a,e),0):dd(a)}function $a(a){W||=a>>>0;throw W;} -var sd=globalThis.TextDecoder&&new TextDecoder,td=(a,b=0,d,c)=>{b>>>=0;var e=b;d=e+d;if(c)c=d;else{for(;a[e]&&!(e>=d);)++e;c=e}if(16d?e+=String.fromCharCode(d):(d-=65536,e+=String.fromCharCode(55296|d>>10,56320| -d&1023))}}else e+=String.fromCharCode(d);return e},ud=(a,b,d)=>(a>>>=0)?td((v(),A),a,b,d):"";function ab(a,b,d){return n?U(3,1,a,b,d):0}function bb(a,b){if(n)return U(4,1,a,b)}function cb(a,b){if(n)return U(5,1,a,b)}function db(a,b,d){if(n)return U(6,1,a,b,d)}function eb(a,b,d){return n?U(7,1,a,b,d):0}function fb(a,b){if(n)return U(8,1,a,b)}function gb(a,b,d){if(n)return U(9,1,a,b,d)}function hb(a,b,d,c){if(n)return U(10,1,a,b,d,c)}function ib(a,b,d,c){if(n)return U(11,1,a,b,d,c)} -function jb(a,b,d,c){if(n)return U(12,1,a,b,d,c)}function kb(a){if(n)return U(13,1,a)}function lb(a,b){if(n)return U(14,1,a,b)}function mb(a,b,d){if(n)return U(15,1,a,b,d)}var nb=()=>J("");function ob(a){za(a>>>0,!k,1,!aa,131072,!1);Aa()} -var jd=a=>{if(!pa)try{if(a(),!(0Number((navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)||[])[2]);function Ba(a){a>>>=0;vd||(Atomics.waitAsync((v(),C),a>>>2,a).value.then(Da),a+=128,Atomics.store((v(),C),a>>>2,1))}var Da=()=>jd(()=>{var a=Kc();a&&(Ba(a),Rc())}); -function pb(a,b){a>>>=0;a==b>>>0?setTimeout(Da):n?postMessage({Tb:a,Qb:"checkMailbox"}):(a=S[a])&&a.postMessage({Qb:"checkMailbox"})}var wd=[];function qb(a,b,d,c,e){b>>>=0;e>>>=0;wd.length=0;d=e>>>3;for(c=e+c>>>3;d>>0]?f=(v(),H)[d++>>>0]:f=(v(),G)[d++>>>0];wd.push(f)}return(b?xd[b]:yd[a])(...wd)}var rb=()=>{T=0};function sb(a){a>>>=0;n?postMessage({Qb:"cleanupThread",dc:a}):hd(S[a])}function tb(a){m&&S[a>>>0].ref()} -function ub(a,b){a=-9007199254740992>a||9007199254740992>>=0;a=new Date(1E3*a);(v(),C)[b>>>2>>>0]=a.getUTCSeconds();(v(),C)[b+4>>>2>>>0]=a.getUTCMinutes();(v(),C)[b+8>>>2>>>0]=a.getUTCHours();(v(),C)[b+12>>>2>>>0]=a.getUTCDate();(v(),C)[b+16>>>2>>>0]=a.getUTCMonth();(v(),C)[b+20>>>2>>>0]=a.getUTCFullYear()-1900;(v(),C)[b+24>>>2>>>0]=a.getUTCDay();a=(a.getTime()-Date.UTC(a.getUTCFullYear(),0,1,0,0,0,0))/864E5|0;(v(),C)[b+28>>>2>>>0]=a} -var zd=a=>0===a%4&&(0!==a%100||0===a%400),Ad=[0,31,60,91,121,152,182,213,244,274,305,335],Bd=[0,31,59,90,120,151,181,212,243,273,304,334]; -function vb(a,b){a=-9007199254740992>a||9007199254740992>>=0;a=new Date(1E3*a);(v(),C)[b>>>2>>>0]=a.getSeconds();(v(),C)[b+4>>>2>>>0]=a.getMinutes();(v(),C)[b+8>>>2>>>0]=a.getHours();(v(),C)[b+12>>>2>>>0]=a.getDate();(v(),C)[b+16>>>2>>>0]=a.getMonth();(v(),C)[b+20>>>2>>>0]=a.getFullYear()-1900;(v(),C)[b+24>>>2>>>0]=a.getDay();var d=(zd(a.getFullYear())?Ad:Bd)[a.getMonth()]+a.getDate()-1|0;(v(),C)[b+28>>>2>>>0]=d;(v(),C)[b+36>>>2>>>0]=-(60*a.getTimezoneOffset());d=(new Date(a.getFullYear(), -6,1)).getTimezoneOffset();var c=(new Date(a.getFullYear(),0,1)).getTimezoneOffset();a=(d!=c&&a.getTimezoneOffset()==Math.min(c,d))|0;(v(),C)[b+32>>>2>>>0]=a} -function wb(a){a>>>=0;var b=new Date((v(),C)[a+20>>>2>>>0]+1900,(v(),C)[a+16>>>2>>>0],(v(),C)[a+12>>>2>>>0],(v(),C)[a+8>>>2>>>0],(v(),C)[a+4>>>2>>>0],(v(),C)[a>>>2>>>0],0),d=(v(),C)[a+32>>>2>>>0],c=b.getTimezoneOffset(),e=(new Date(b.getFullYear(),6,1)).getTimezoneOffset(),f=(new Date(b.getFullYear(),0,1)).getTimezoneOffset(),g=Math.min(f,e);0>d?(v(),C)[a+32>>>2>>>0]=Number(e!=f&&g==c):0>>2>>>0]=b.getDay();d=(zd(b.getFullYear())? -Ad:Bd)[b.getMonth()]+b.getDate()-1|0;(v(),C)[a+28>>>2>>>0]=d;(v(),C)[a>>>2>>>0]=b.getSeconds();(v(),C)[a+4>>>2>>>0]=b.getMinutes();(v(),C)[a+8>>>2>>>0]=b.getHours();(v(),C)[a+12>>>2>>>0]=b.getDate();(v(),C)[a+16>>>2>>>0]=b.getMonth();(v(),C)[a+20>>>2>>>0]=b.getYear();a=b.getTime();return BigInt(isNaN(a)?-1:a/1E3)}function xb(a,b,d,c,e,f,g){return n?U(16,1,a,b,d,c,e,f,g):-52}function yb(a,b,d,c,e,f){if(n)return U(17,1,a,b,d,c,e,f)}var X={},Ib=()=>performance.timeOrigin+performance.now(); -function zb(a,b){if(n)return U(18,1,a,b);X[a]&&(clearTimeout(X[a].id),delete X[a]);if(!b)return 0;var d=setTimeout(()=>{delete X[a];jd(()=>Qc(a,performance.timeOrigin+performance.now()))},b);X[a]={id:d,lc:b};return 0} -var Y=(a,b,d)=>{var c=(v(),A);b>>>=0;if(0=g){if(b>=d)break;c[b++>>>0]=g}else if(2047>=g){if(b+1>=d)break;c[b++>>>0]=192|g>>6;c[b++>>>0]=128|g&63}else if(65535>=g){if(b+2>=d)break;c[b++>>>0]=224|g>>12;c[b++>>>0]=128|g>>6&63;c[b++>>>0]=128|g&63}else{if(b+3>=d)break;c[b++>>>0]=240|g>>18;c[b++>>>0]=128|g>>12&63;c[b++>>>0]=128|g>>6&63;c[b++>>>0]=128|g&63;f++}}c[b>>>0]=0;a=b-e}else a=0;return a}; -function Ab(a,b,d,c){a>>>=0;b>>>=0;d>>>=0;c>>>=0;var e=(new Date).getFullYear(),f=(new Date(e,0,1)).getTimezoneOffset();e=(new Date(e,6,1)).getTimezoneOffset();var g=Math.max(f,e);(v(),D)[a>>>2>>>0]=60*g;(v(),C)[b>>>2>>>0]=Number(f!=e);b=l=>{var u=Math.abs(l);return`UTC${0<=l?"-":"+"}${String(Math.floor(u/60)).padStart(2,"0")}${String(u%60).padStart(2,"0")}`};a=b(f);b=b(e);eDate.now(),Cd=1; -function Bb(a,b,d){d>>>=0;if(!(0<=a&&3>=a))return 28;if(0===a)a=Date.now();else if(Cd)a=performance.timeOrigin+performance.now();else return 52;a=Math.round(1E6*a);(v(),H)[d>>>3>>>0]=BigInt(a);return 0}var Dd=[];function Cb(a,b,d){a>>>=0;b>>>=0;d>>>=0;Dd.length=0;for(var c;c=(v(),A)[b++>>>0];){var e=105!=c;e&=112!=c;d+=e&&d%8?4:0;Dd.push(112==c?(v(),D)[d>>>2>>>0]:106==c?(v(),H)[d>>>3>>>0]:105==c?(v(),C)[d>>>2>>>0]:(v(),G)[d>>>3>>>0]);d+=e?8:4}return xd[a](...Dd)}var Db=()=>{}; -function Fb(a,b){return p(ud(a>>>0,b>>>0))}var Gb=()=>{T+=1;throw"unwind";};function Hb(){return 4294901760}var Jb=()=>m?require("os").cpus().length:navigator.hardwareConcurrency,Z={},Ed=a=>{for(var b=0,d=0;d=c?b++:2047>=c?b+=2:55296<=c&&57343>=c?(b+=4,++d):b+=3}return b},Fd=a=>{var b;return(b=/\bwasm-function\[\d+\]:(0x[0-9a-f]+)/.exec(a))?+b[1]:(b=/:(\d+):\d+(?:\)|$)/.exec(a))?2147483648|+b[1]:0},Gd=a=>{for(var b of a)(a=Fd(b))&&(Z[a]=b)}; -function Mb(){var a=Error().stack.toString().split("\n");"Error"==a[0]&&a.shift();Gd(a);Z.Wb=Fd(a[3]);Z.ac=a;return Z.Wb}function Kb(a){a=Z[a>>>0];if(!a)return 0;var b;if(b=/^\s+at .*\.wasm\.(.*) \(.*\)$/.exec(a))a=b[1];else if(b=/^\s+at (.*) \(.*\)$/.exec(a))a=b[1];else if(b=/^(.+?)@/.exec(a))a=b[1];else return 0;Lc(Kb.Xb??0);b=Ed(a)+1;var d=Mc(b);d&&Y(a,d,b);Kb.Xb=d;return Kb.Xb} -function Lb(a){a>>>=0;var b=(v(),A).length;if(a<=b||4294901760=d;d*=2){var c=b*(1+.2/d);c=Math.min(c,a+100663296);a:{c=(Math.min(4294901760,65536*Math.ceil(Math.max(a,c)/65536))-x.buffer.byteLength+65535)/65536|0;try{x.grow(c);qa();var e=1;break a}catch(f){}e=void 0}if(e)return!0}return!1} -function Nb(a,b,d){a>>>=0;b>>>=0;if(Z.Wb==a)var c=Z.ac;else c=Error().stack.toString().split("\n"),"Error"==c[0]&&c.shift(),Gd(c);for(var e=3;c[e]&&Fd(c[e])!=a;)++e;for(a=0;a>>2>>>0]=Fd(c[a+e]);return a} -var Hd={},Jd=()=>{if(!Id){var a={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8",_:ca||"./this.program"},b;for(b in Hd)void 0===Hd[b]?delete a[b]:a[b]=Hd[b];var d=[];for(b in a)d.push(`${b}=${a[b]}`);Id=d}return Id},Id;function Ob(a,b){if(n)return U(19,1,a,b);a>>>=0;b>>>=0;var d=0,c=0,e;for(e of Jd()){var f=b+d;(v(),D)[a+c>>>2>>>0]=f;d+=Y(e,f,Infinity)+1;c+=4}return 0} -function Pb(a,b){if(n)return U(20,1,a,b);a>>>=0;b>>>=0;var d=Jd();(v(),D)[a>>>2>>>0]=d.length;a=0;for(var c of d)a+=Ed(c)+1;(v(),D)[b>>>2>>>0]=a;return 0}function Rb(a){return n?U(21,1,a):52}function Sb(a,b,d,c){return n?U(22,1,a,b,d,c):52}function Tb(a,b,d,c){return n?U(23,1,a,b,d,c):70}var Kd=[null,[],[]]; -function Ub(a,b,d,c){if(n)return U(24,1,a,b,d,c);b>>>=0;d>>>=0;c>>>=0;for(var e=0,f=0;f>>2>>>0],l=(v(),D)[b+4>>>2>>>0];b+=8;for(var u=0;u>>0],B=Kd[w];0===y||10===y?((1===w?oa:p)(td(B)),B.length=0):B.push(y)}e+=l}(v(),D)[c>>>2>>>0]=e;return 0}function Gc(a){return a>>>0}n||fd();n||(x=new WebAssembly.Memory({initial:256,maximum:65536,shared:!0}),qa());h.wasmBinary&&(q=h.wasmBinary);h.stackSave=()=>N();h.stackRestore=a=>M(a);h.stackAlloc=a=>Tc(a); -h.setValue=function(a,b,d="i8"){d.endsWith("*")&&(d="*");switch(d){case "i1":(v(),z)[a>>>0]=b;break;case "i8":(v(),z)[a>>>0]=b;break;case "i16":(v(),Fa)[a>>>1>>>0]=b;break;case "i32":(v(),C)[a>>>2>>>0]=b;break;case "i64":(v(),H)[a>>>3>>>0]=BigInt(b);break;case "float":(v(),Ga)[a>>>2>>>0]=b;break;case "double":(v(),G)[a>>>3>>>0]=b;break;case "*":(v(),D)[a>>>2>>>0]=b;break;default:J(`invalid type for setValue: ${d}`)}}; -h.getValue=function(a,b="i8"){b.endsWith("*")&&(b="*");switch(b){case "i1":return(v(),z)[a>>>0];case "i8":return(v(),z)[a>>>0];case "i16":return(v(),Fa)[a>>>1>>>0];case "i32":return(v(),C)[a>>>2>>>0];case "i64":return(v(),H)[a>>>3>>>0];case "float":return(v(),Ga)[a>>>2>>>0];case "double":return(v(),G)[a>>>3>>>0];case "*":return(v(),D)[a>>>2>>>0];default:J(`invalid type for getValue: ${b}`)}};h.UTF8ToString=ud;h.stringToUTF8=Y;h.lengthBytesUTF8=Ed; -var yd=[Hc,ed,rd,ab,bb,cb,db,eb,fb,gb,hb,ib,jb,kb,lb,mb,xb,yb,zb,Ob,Pb,Rb,Sb,Tb,Ub],xd={887900:(a,b,d,c,e)=>{if("undefined"==typeof h||!h.Sb)return 1;a=ud(Number(a>>>0));a.startsWith("./")&&(a=a.substring(2));a=h.Sb.get(a);if(!a)return 2;b=Number(b>>>0);d=Number(d>>>0);c=Number(c>>>0);if(b+d>a.byteLength)return 3;try{const f=a.subarray(b,b+d);switch(e){case 0:(v(),A).set(f,c>>>0);break;case 1:h.hc?h.hc(c,f):h.jc(c,f);break;default:return 4}return 0}catch{return 4}},888724:()=>"undefined"!==typeof wasmOffsetConverter}; -function Qa(){return"undefined"!==typeof wasmOffsetConverter}var Kc,Lc,Mc,za,Ea,Nc,Oc,Pc,Qc,Rc,K,L,Sc,M,Tc,N,Uc,Vc,Wc,Xc,Yc,Pa;function bc(a,b,d,c){var e=N();try{return V(a)(b,d,c)}catch(f){M(e);if(f!==f+0)throw f;K(1,0)}}function ac(a,b,d){var c=N();try{return V(a)(b,d)}catch(e){M(c);if(e!==e+0)throw e;K(1,0)}}function sc(a,b,d){var c=N();try{V(a)(b,d)}catch(e){M(c);if(e!==e+0)throw e;K(1,0)}}function $b(a,b){var d=N();try{return V(a)(b)}catch(c){M(d);if(c!==c+0)throw c;K(1,0)}} -function qc(a){var b=N();try{V(a)()}catch(d){M(b);if(d!==d+0)throw d;K(1,0)}}function fc(a,b,d,c,e,f,g){var l=N();try{return V(a)(b,d,c,e,f,g)}catch(u){M(l);if(u!==u+0)throw u;K(1,0)}}function rc(a,b){var d=N();try{V(a)(b)}catch(c){M(d);if(c!==c+0)throw c;K(1,0)}}function wc(a,b,d,c,e,f){var g=N();try{V(a)(b,d,c,e,f)}catch(l){M(g);if(l!==l+0)throw l;K(1,0)}}function uc(a,b,d,c){var e=N();try{V(a)(b,d,c)}catch(f){M(e);if(f!==f+0)throw f;K(1,0)}} -function vc(a,b,d,c,e){var f=N();try{V(a)(b,d,c,e)}catch(g){M(f);if(g!==g+0)throw g;K(1,0)}}function xc(a,b,d,c,e,f,g){var l=N();try{V(a)(b,d,c,e,f,g)}catch(u){M(l);if(u!==u+0)throw u;K(1,0)}}function Ec(a,b,d,c,e,f,g){var l=N();try{V(a)(b,d,c,e,f,g)}catch(u){M(l);if(u!==u+0)throw u;K(1,0)}}function Dc(a,b,d,c,e,f,g,l){var u=N();try{V(a)(b,d,c,e,f,g,l)}catch(w){M(u);if(w!==w+0)throw w;K(1,0)}}function cc(a,b,d,c,e){var f=N();try{return V(a)(b,d,c,e)}catch(g){M(f);if(g!==g+0)throw g;K(1,0)}} -function yc(a,b,d,c,e,f,g,l){var u=N();try{V(a)(b,d,c,e,f,g,l)}catch(w){M(u);if(w!==w+0)throw w;K(1,0)}}function Bc(a,b,d,c,e,f,g,l,u,w,y,B){var E=N();try{V(a)(b,d,c,e,f,g,l,u,w,y,B)}catch(F){M(E);if(F!==F+0)throw F;K(1,0)}}function ec(a,b,d,c,e,f){var g=N();try{return V(a)(b,d,c,e,f)}catch(l){M(g);if(l!==l+0)throw l;K(1,0)}}function oc(a,b,d){var c=N();try{return V(a)(b,d)}catch(e){M(c);if(e!==e+0)throw e;K(1,0);return 0n}} -function zc(a,b,d,c,e,f,g,l,u){var w=N();try{V(a)(b,d,c,e,f,g,l,u)}catch(y){M(w);if(y!==y+0)throw y;K(1,0)}}function Zb(a){var b=N();try{return V(a)()}catch(d){M(b);if(d!==d+0)throw d;K(1,0)}}function lc(a,b,d){var c=N();try{return V(a)(b,d)}catch(e){M(c);if(e!==e+0)throw e;K(1,0)}}function nc(a,b){var d=N();try{return V(a)(b)}catch(c){M(d);if(c!==c+0)throw c;K(1,0);return 0n}}function Fc(a,b,d,c,e){var f=N();try{V(a)(b,d,c,e)}catch(g){M(f);if(g!==g+0)throw g;K(1,0)}} -function mc(a){var b=N();try{return V(a)()}catch(d){M(b);if(d!==d+0)throw d;K(1,0);return 0n}}function ic(a,b,d,c,e,f){var g=N();try{return V(a)(b,d,c,e,f)}catch(l){M(g);if(l!==l+0)throw l;K(1,0)}}function dc(a,b,d,c,e,f){var g=N();try{return V(a)(b,d,c,e,f)}catch(l){M(g);if(l!==l+0)throw l;K(1,0)}}function gc(a,b,d,c,e,f,g,l){var u=N();try{return V(a)(b,d,c,e,f,g,l)}catch(w){M(u);if(w!==w+0)throw w;K(1,0)}} -function pc(a,b,d,c,e){var f=N();try{return V(a)(b,d,c,e)}catch(g){M(f);if(g!==g+0)throw g;K(1,0);return 0n}}function Yb(a,b,d,c){var e=N();try{return V(a)(b,d,c)}catch(f){M(e);if(f!==f+0)throw f;K(1,0)}}function Wb(a,b,d,c){var e=N();try{return V(a)(b,d,c)}catch(f){M(e);if(f!==f+0)throw f;K(1,0)}}function hc(a,b,d,c,e,f,g,l,u,w,y,B){var E=N();try{return V(a)(b,d,c,e,f,g,l,u,w,y,B)}catch(F){M(E);if(F!==F+0)throw F;K(1,0)}} -function Ac(a,b,d,c,e,f,g,l,u,w,y){var B=N();try{V(a)(b,d,c,e,f,g,l,u,w,y)}catch(E){M(B);if(E!==E+0)throw E;K(1,0)}}function Cc(a,b,d,c,e,f,g,l,u,w,y,B,E,F,Ld,Md){var Nd=N();try{V(a)(b,d,c,e,f,g,l,u,w,y,B,E,F,Ld,Md)}catch(Ma){M(Nd);if(Ma!==Ma+0)throw Ma;K(1,0)}}function kc(a,b,d,c){var e=N();try{return V(a)(b,d,c)}catch(f){M(e);if(f!==f+0)throw f;K(1,0)}}function jc(a,b,d,c,e){var f=N();try{return V(a)(b,d,c,e)}catch(g){M(f);if(g!==g+0)throw g;K(1,0)}} -function Xb(a,b,d){var c=N();try{return V(a)(b,d)}catch(e){M(c);if(e!==e+0)throw e;K(1,0)}}function Vb(a,b,d){var c=N();try{return V(a)(b,d)}catch(e){M(c);if(e!==e+0)throw e;K(1,0)}}function tc(a,b,d,c){var e=N();try{V(a)(b,d,c)}catch(f){M(e);if(f!==f+0)throw f;K(1,0)}}function Ic(){var a=I;a=Object.assign({},a);var b=c=>()=>c()>>>0,d=c=>e=>c(e)>>>0;a.tb=b(a.tb);a.vb=d(a.vb);a.Jb=d(a.Jb);a.Kb=b(a.Kb);a.Ob=d(a.Ob);return a} -function xa(){if(0{ra=a;sa=b}); -;return moduleRtn}export default ortWasmThreaded;var isPthread=globalThis.self?.name?.startsWith("em-pthread");var isNode=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";if(isNode)isPthread=(await import("worker_threads")).workerData==="em-pthread";isPthread&&ortWasmThreaded(); diff --git a/ui/public/ort-wasm-simd-threaded.wasm b/ui/public/ort-wasm-simd-threaded.wasm deleted file mode 100644 index f21ee10a..00000000 Binary files a/ui/public/ort-wasm-simd-threaded.wasm and /dev/null differ diff --git a/ui/src/App.tsx b/ui/src/App.tsx index e66574b9..d21eec44 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,7 +1,7 @@ import { Navigate, Outlet, Route, Routes, useLocation, useParams } from "@/lib/router"; import { VOCAB } from "@paperclipai/branding"; import { useQuery } from "@tanstack/react-query"; -import { lazy, Suspense } from "react"; +import { lazy, Suspense, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Layout } from "./components/Layout"; @@ -55,7 +55,34 @@ const PersonalAssistant = lazy(() => import("./pages/PersonalAssistant").then(m const ContentStudio = lazy(() => import("./pages/ContentStudio").then(m => ({ default: m.ContentStudio }))); const ConvertPage = lazy(() => import("./pages/ConvertPage").then(m => ({ default: m.ConvertPage }))); -function BootstrapPendingPage({ hasActiveInvite = false }: { hasActiveInvite?: boolean }) { +function BootstrapPendingPage({ + hasActiveInvite = false, + invitePath, +}: { + hasActiveInvite?: boolean; + invitePath?: string; +}) { + // [nexus] Zero-terminal first boot: the server auto-generates a + // bootstrap_ceo invite on startup when no admin exists and exposes its + // relative path via /api/health. Redirect straight to /invite/{token} + // so the first user never sees a CLI command. + useEffect(() => { + if (invitePath) { + window.location.replace(invitePath); + } + }, [invitePath]); + + if (invitePath) { + return ( +
+ Setting up instance — redirecting to admin account creation… +
+ ); + } + + // Fallback for headless/SSH-only deployments where the server couldn't + // auto-generate an invite (e.g. startup error). The CLI command still + // works in that case. return (
@@ -111,7 +138,12 @@ function CloudAccessGate() { } if (isAuthenticatedMode && healthQuery.data?.bootstrapStatus === "bootstrap_pending") { - return ; + return ( + + ); } if (isAuthenticatedMode && !sessionQuery.data) { diff --git a/ui/src/api/health.ts b/ui/src/api/health.ts index e2725b20..35706c4b 100644 --- a/ui/src/api/health.ts +++ b/ui/src/api/health.ts @@ -20,6 +20,7 @@ export type HealthStatus = { authReady?: boolean; bootstrapStatus?: "ready" | "bootstrap_pending"; bootstrapInviteActive?: boolean; + bootstrapInvitePath?: string; features?: { companyDeletionEnabled?: boolean; }; diff --git a/ui/src/lib/queryKeys.ts b/ui/src/lib/queryKeys.ts index de5d3944..ac1c583a 100644 --- a/ui/src/lib/queryKeys.ts +++ b/ui/src/lib/queryKeys.ts @@ -150,6 +150,9 @@ export const queryKeys = { hardware: { info: ["hardware", "info"] as const, }, + nexus: { + settings: ["nexus", "settings"] as const, + }, plugins: { all: ["plugins"] as const, examples: ["plugins", "examples"] as const, diff --git a/ui/src/pages/InviteLanding.tsx b/ui/src/pages/InviteLanding.tsx index fb60dec9..1a4bf488 100644 --- a/ui/src/pages/InviteLanding.tsx +++ b/ui/src/pages/InviteLanding.tsx @@ -67,6 +67,14 @@ export function InviteLandingPage() { queryFn: () => accessApi.getInvite(token), enabled: token.length > 0, retry: false, + // [nexus] Once we've loaded the invite, never refetch — after the user + // accepts, the server flips the invite to "accepted" and any refetch + // would surface an "Invite not available" error that shadows the success + // screen. The query becomes a one-shot snapshot. + staleTime: Infinity, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, }); const invite = inviteQuery.data; @@ -112,6 +120,13 @@ export function InviteLandingPage() { const asBootstrap = payload && typeof payload === "object" && "bootstrapAccepted" in (payload as Record); setResult({ kind: asBootstrap ? "bootstrap" : "join", payload }); + // [nexus] Zero-terminal first boot: after the first admin is created, + // skip the "Bootstrap complete" confirmation screen and land the user + // directly on the board. Full reload (instead of client navigation) so + // the health query + session both re-resolve against a ready instance. + if (asBootstrap) { + window.location.replace("/"); + } }, onError: (err) => { setError(err instanceof Error ? err.message : "Failed to accept invite"); @@ -126,19 +141,10 @@ export function InviteLandingPage() { return
Loading invite...
; } - if (inviteQuery.error || !invite) { - return ( -
-
-

Invite not available

-

- This invite may be expired, revoked, or already used. -

-
-
- ); - } - + // [nexus] Check the success result BEFORE the invite-availability error: + // after a successful accept, the invite is marked accepted on the server + // and any stray refetch would otherwise shadow the success screen with + // "Invite not available". if (result?.kind === "bootstrap") { return (
@@ -225,6 +231,22 @@ export function InviteLandingPage() { ); } + // No successful result — now it's safe to surface the invite-availability + // error. Reaching this branch with a successful accept is impossible because + // the result checks above already returned. + if (inviteQuery.error || !invite) { + return ( +
+
+

Invite not available

+

+ This invite may be expired, revoked, or already used. +

+
+
+ ); + } + return (
diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 90f2534b..7e4c2298 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -1,17 +1,82 @@ +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: [react(), tailwindcss()], + plugins: [nexusOnboardingRedirect(), serveOnnxRuntimeWasm(), react(), tailwindcss()], resolve: { alias: { "@": path.resolve(__dirname, "./src"), lexical: path.resolve(__dirname, "./node_modules/lexical/Lexical.mjs"), - // [nexus] Replace upstream OnboardingWizard with Nexus single-step version - [path.resolve(__dirname, "src/components/OnboardingWizard")]: - path.resolve(__dirname, "./src/components/NexusOnboardingWizard"), }, }, build: { @@ -30,13 +95,16 @@ export default defineConfig({ }, server: { port: 5173, - headers: { - "Cross-Origin-Opener-Policy": "same-origin", - "Cross-Origin-Embedder-Policy": "require-corp", - }, + // 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:3100", + target: "http://localhost:6100", ws: true, }, },