diff --git a/.gitignore b/.gitignore
index 63c69c4..bb21a5e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
node_modules/
.wrangler/
*.log
+web/dist/
diff --git a/SAMANTHA_GCP_HOSTING.md b/SAMANTHA_GCP_HOSTING.md
new file mode 100644
index 0000000..e9da860
--- /dev/null
+++ b/SAMANTHA_GCP_HOSTING.md
@@ -0,0 +1,47 @@
+# Samantha GCP Hosting Plan
+
+Owner: `bryan@norcalcarbmobile.com`
+Operator agent: Samantha
+Project: `samantha-gumption`
+
+## Split
+
+- `web/` deploys the static Vite build to Firebase Hosting.
+- `api/` deploys `gumption-api` to Cloud Run for provider calls and server-side keys.
+- Firebase Hosting rewrites `/api/**` to Cloud Run in `us-west1`.
+- Vertex AI Gemini uses project-scoped Cloud Run service account auth.
+- Anthropic, OpenAI, and xAI keys belong in Secret Manager and are attached as Cloud Run env vars.
+- SMS/voice progress alerts are sent from `gumption-api`; Bryan's destination phone stays in `ALERT_TO_PHONE`.
+
+## One-time setup
+
+```bash
+gcloud auth login
+gcloud config set account bryan@norcalcarbmobile.com
+gcloud projects create samantha-gumption --name="Gumption by Silverback AI"
+gcloud config set project samantha-gumption
+gcloud services enable run.googleapis.com firebase.googleapis.com aiplatform.googleapis.com secretmanager.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com
+```
+
+## Alert secrets
+
+For "progress does not stop" alerts, add these secrets before enabling live SMS/voice:
+
+```bash
+printf "+15555550199" | gcloud secrets create ALERT_TO_PHONE --data-file=-
+printf "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" | gcloud secrets create TWILIO_ACCOUNT_SID --data-file=-
+printf "twilio-auth-token" | gcloud secrets create TWILIO_AUTH_TOKEN --data-file=-
+printf "+15555550200" | gcloud secrets create TWILIO_FROM --data-file=-
+```
+
+Use your real phone or a receiving Google Voice number for `ALERT_TO_PHONE`. Google Voice can receive or forward alerts, but it is not a supported outbound SMS API; use Twilio for the sending number.
+
+## Deploy
+
+From the repo root:
+
+```powershell
+.\scripts\deploy-gcp.ps1
+```
+
+The script deploys `api/` to Cloud Run, builds `web/`, then deploys Firebase Hosting.
diff --git a/api/.dockerignore b/api/.dockerignore
new file mode 100644
index 0000000..da1fabc
--- /dev/null
+++ b/api/.dockerignore
@@ -0,0 +1,4 @@
+.git
+node_modules
+npm-debug.log
+README.md
diff --git a/api/Dockerfile b/api/Dockerfile
new file mode 100644
index 0000000..987c969
--- /dev/null
+++ b/api/Dockerfile
@@ -0,0 +1,10 @@
+FROM node:22-alpine
+
+WORKDIR /app
+ENV NODE_ENV=production
+
+COPY package.json ./
+COPY server.js ./
+
+EXPOSE 8080
+CMD ["node", "server.js"]
diff --git a/api/README.md b/api/README.md
new file mode 100644
index 0000000..4959f71
--- /dev/null
+++ b/api/README.md
@@ -0,0 +1,30 @@
+# Gumption API
+
+Cloud Run provider proxy for Samantha's Brain Trust app.
+
+## Responsibilities
+
+- Keeps Anthropic, OpenAI, and xAI keys server-side.
+- Uses Vertex AI Gemini from `samantha-gumption` instead of a browser Gemini key.
+- Sends SMS or voice escalation alerts to Bryan when agent progress is blocked.
+- Exposes `/api/health`, `/api/providers`, `/api/chat`, `/api/alerts/status`, and `/api/alerts`.
+- Returns local handoff responses when secrets or Cloud Run metadata are not available.
+
+## Secrets
+
+Create these Secret Manager secrets when each provider is ready:
+
+- `ANTHROPIC_API_KEY`
+- `OPENAI_API_KEY`
+- `XAI_API_KEY`
+- `ALERT_TO_PHONE`
+- `TWILIO_ACCOUNT_SID`
+- `TWILIO_AUTH_TOKEN`
+- `TWILIO_SMS_FROM` or `TWILIO_FROM`
+- `TWILIO_VOICE_FROM` or `TWILIO_FROM`
+
+Attach them to Cloud Run as environment variables when deploying `gumption-api`.
+
+## Google Voice note
+
+Google Voice numbers can receive alerts if you set `ALERT_TO_PHONE` to that number or forward it to your real phone. Google Voice is not a supported outbound SMS API, so the sending side should be a Twilio number unless the Google Voice number is ported to a provider with an API.
diff --git a/api/package.json b/api/package.json
new file mode 100644
index 0000000..371618c
--- /dev/null
+++ b/api/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "gumption-api",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "description": "Cloud Run API proxy for Brain Trust provider calls.",
+ "scripts": {
+ "start": "node server.js",
+ "check": "node --check server.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+}
diff --git a/api/server.js b/api/server.js
new file mode 100644
index 0000000..6242fc8
--- /dev/null
+++ b/api/server.js
@@ -0,0 +1,357 @@
+import { createServer } from "node:http";
+
+const port = Number(process.env.PORT || 8081);
+const projectId = process.env.GCP_PROJECT_ID || process.env.GOOGLE_CLOUD_PROJECT || "samantha-gumption";
+const vertexLocation = process.env.VERTEX_LOCATION || "us-west1";
+
+const providers = {
+ claude: {
+ label: "Claude",
+ env: "ANTHROPIC_API_KEY",
+ configured: () => Boolean(process.env.ANTHROPIC_API_KEY),
+ },
+ chatgpt: {
+ label: "ChatGPT",
+ env: "OPENAI_API_KEY",
+ configured: () => Boolean(process.env.OPENAI_API_KEY),
+ },
+ grok: {
+ label: "SuperGrok",
+ env: "XAI_API_KEY",
+ configured: () => Boolean(process.env.XAI_API_KEY),
+ },
+ gemini: {
+ label: "Gemini",
+ env: "Vertex AI service account",
+ configured: () => Boolean(projectId),
+ },
+ copilot: {
+ label: "Copilot",
+ env: "manual",
+ configured: () => true,
+ },
+};
+
+const alertConfig = {
+ toPhone: process.env.ALERT_TO_PHONE || "",
+ smsFrom: process.env.TWILIO_SMS_FROM || process.env.TWILIO_FROM || "",
+ voiceFrom: process.env.TWILIO_VOICE_FROM || process.env.TWILIO_FROM || "",
+ accountSid: process.env.TWILIO_ACCOUNT_SID || "",
+ authToken: process.env.TWILIO_AUTH_TOKEN || "",
+};
+
+function writeJson(res, status, payload) {
+ res.writeHead(status, {
+ "content-type": "application/json; charset=utf-8",
+ "cache-control": "no-store",
+ "access-control-allow-origin": process.env.CORS_ORIGIN || "*",
+ "access-control-allow-methods": "GET,POST,OPTIONS",
+ "access-control-allow-headers": "content-type,authorization",
+ });
+ res.end(JSON.stringify(payload));
+}
+
+async function readJson(req) {
+ const chunks = [];
+ for await (const chunk of req) {
+ chunks.push(chunk);
+ }
+ if (!chunks.length) return {};
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
+}
+
+function fallbackReply(provider, prompt) {
+ if (provider === "copilot") {
+ return "Copilot is manual handoff only. Prompt copied into the command stream for IDE follow-up.";
+ }
+
+ const config = providers[provider];
+ return `${config.label} proxy received the prompt, but ${config.env} is not configured yet. Add it in Secret Manager and redeploy gumption-api. Prompt: ${prompt.slice(0, 160)}`;
+}
+
+function maskPhone(phone) {
+ if (!phone) return "not configured";
+ const visible = phone.replace(/\D/g, "").slice(-4);
+ return visible ? `***-***-${visible}` : "configured";
+}
+
+function alertStatus() {
+ const twilioReady = Boolean(alertConfig.accountSid && alertConfig.authToken);
+ return {
+ toPhone: maskPhone(alertConfig.toPhone),
+ smsReady: Boolean(twilioReady && alertConfig.toPhone && alertConfig.smsFrom),
+ voiceReady: Boolean(twilioReady && alertConfig.toPhone && alertConfig.voiceFrom),
+ requiredSecrets: [
+ "ALERT_TO_PHONE",
+ "TWILIO_ACCOUNT_SID",
+ "TWILIO_AUTH_TOKEN",
+ "TWILIO_SMS_FROM or TWILIO_FROM",
+ "TWILIO_VOICE_FROM or TWILIO_FROM",
+ ],
+ googleVoiceNote: "Google Voice can receive these alerts as ALERT_TO_PHONE, but it is not a supported outbound SMS API. Use Twilio for sending.",
+ };
+}
+
+function alertSetupReply(channel, message) {
+ return {
+ mode: "setup-required",
+ channel,
+ message: "SMS/voice alert not sent because Twilio alert secrets are not configured in Cloud Run yet.",
+ requestedAlert: message,
+ status: alertStatus(),
+ };
+}
+
+async function twilioRequest(path, params) {
+ const credentials = Buffer.from(`${alertConfig.accountSid}:${alertConfig.authToken}`).toString("base64");
+ const response = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${alertConfig.accountSid}${path}`, {
+ method: "POST",
+ headers: {
+ authorization: `Basic ${credentials}`,
+ "content-type": "application/x-www-form-urlencoded",
+ },
+ body: new URLSearchParams(params),
+ });
+ const payload = await response.json().catch(() => ({}));
+ if (!response.ok) {
+ throw new Error(payload.message || `Twilio returned ${response.status}`);
+ }
+ return payload;
+}
+
+async function sendSmsAlert(message) {
+ if (!alertStatus().smsReady) {
+ return alertSetupReply("sms", message);
+ }
+
+ const payload = await twilioRequest(`/Messages.json`, {
+ To: alertConfig.toPhone,
+ From: alertConfig.smsFrom,
+ Body: message,
+ });
+ return { mode: "sent", channel: "sms", sid: payload.sid, toPhone: maskPhone(alertConfig.toPhone) };
+}
+
+async function sendVoiceAlert(message) {
+ if (!alertStatus().voiceReady) {
+ return alertSetupReply("voice", message);
+ }
+
+ const spoken = message.replace(/[<>&]/g, " ");
+ const payload = await twilioRequest(`/Calls.json`, {
+ To: alertConfig.toPhone,
+ From: alertConfig.voiceFrom,
+ Twiml: `${spoken} `,
+ });
+ return { mode: "sent", channel: "voice", sid: payload.sid, toPhone: maskPhone(alertConfig.toPhone) };
+}
+
+async function routeAlert(body) {
+ const channel = body.channel || "sms";
+ const message = String(body.message || "").trim();
+ if (!message) {
+ return { status: 400, payload: { error: "message is required" } };
+ }
+
+ const alertMessage = `Gumption alert: ${message.slice(0, 420)}`;
+ if (channel === "sms") {
+ return { status: 200, payload: await sendSmsAlert(alertMessage) };
+ }
+ if (channel === "voice") {
+ return { status: 200, payload: await sendVoiceAlert(alertMessage) };
+ }
+ if (channel === "both") {
+ return {
+ status: 200,
+ payload: {
+ mode: "multi",
+ results: [
+ await sendSmsAlert(alertMessage),
+ await sendVoiceAlert(alertMessage),
+ ],
+ },
+ };
+ }
+
+ return { status: 400, payload: { error: `Unknown alert channel: ${channel}` } };
+}
+
+async function openAiCompatible({ apiKey, url, model, prompt }) {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ authorization: `Bearer ${apiKey}`,
+ "content-type": "application/json",
+ },
+ body: JSON.stringify({
+ model,
+ messages: [{ role: "user", content: prompt }],
+ }),
+ });
+ const payload = await response.json();
+ if (!response.ok) {
+ throw new Error(payload.error?.message || `Provider returned ${response.status}`);
+ }
+ return payload.choices?.[0]?.message?.content || "Provider returned an empty response.";
+}
+
+async function anthropic(prompt) {
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
+ method: "POST",
+ headers: {
+ "x-api-key": process.env.ANTHROPIC_API_KEY,
+ "anthropic-version": "2023-06-01",
+ "content-type": "application/json",
+ },
+ body: JSON.stringify({
+ model: process.env.ANTHROPIC_MODEL || "claude-3-5-sonnet-latest",
+ max_tokens: 900,
+ messages: [{ role: "user", content: prompt }],
+ }),
+ });
+ const payload = await response.json();
+ if (!response.ok) {
+ throw new Error(payload.error?.message || `Anthropic returned ${response.status}`);
+ }
+ return payload.content?.map((item) => item.text).filter(Boolean).join("\n") || "Claude returned an empty response.";
+}
+
+async function metadataAccessToken() {
+ const response = await fetch(
+ "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token",
+ { headers: { "metadata-flavor": "Google" } },
+ );
+ const payload = await response.json();
+ if (!response.ok || !payload.access_token) {
+ throw new Error("Unable to obtain Cloud Run service account token for Vertex AI.");
+ }
+ return payload.access_token;
+}
+
+async function vertexGemini(prompt) {
+ const token = await metadataAccessToken();
+ const model = process.env.VERTEX_GEMINI_MODEL || "gemini-1.5-pro";
+ const url = `https://${vertexLocation}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${vertexLocation}/publishers/google/models/${model}:generateContent`;
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ authorization: `Bearer ${token}`,
+ "content-type": "application/json",
+ },
+ body: JSON.stringify({
+ contents: [{ role: "user", parts: [{ text: prompt }] }],
+ }),
+ });
+ const payload = await response.json();
+ if (!response.ok) {
+ throw new Error(payload.error?.message || `Vertex AI returned ${response.status}`);
+ }
+ return payload.candidates?.[0]?.content?.parts?.map((part) => part.text).filter(Boolean).join("\n")
+ || "Gemini returned an empty response.";
+}
+
+async function routeChat(body) {
+ const provider = body.provider || "claude";
+ const prompt = String(body.prompt || "").trim();
+ if (!prompt) {
+ return { status: 400, payload: { error: "prompt is required" } };
+ }
+ if (!providers[provider]) {
+ return { status: 400, payload: { error: `Unknown provider: ${provider}` } };
+ }
+
+ if (!providers[provider].configured() || provider === "copilot") {
+ return { status: 200, payload: { provider, mode: "handoff", reply: fallbackReply(provider, prompt) } };
+ }
+
+ if (provider === "gemini" && !process.env.K_SERVICE) {
+ return {
+ status: 200,
+ payload: {
+ provider,
+ mode: "vertex-ready",
+ reply: "Gemini proxy is ready for Vertex AI. Deploy gumption-api to Cloud Run so Samantha's service account can call Vertex with project-scoped auth.",
+ },
+ };
+ }
+
+ const reply = await {
+ claude: () => anthropic(prompt),
+ chatgpt: () => openAiCompatible({
+ apiKey: process.env.OPENAI_API_KEY,
+ url: "https://api.openai.com/v1/chat/completions",
+ model: process.env.OPENAI_MODEL || "gpt-4o-mini",
+ prompt,
+ }),
+ grok: () => openAiCompatible({
+ apiKey: process.env.XAI_API_KEY,
+ url: "https://api.x.ai/v1/chat/completions",
+ model: process.env.XAI_MODEL || "grok-2-latest",
+ prompt,
+ }),
+ gemini: () => vertexGemini(prompt),
+ }[provider]();
+
+ return { status: 200, payload: { provider, mode: "live", reply } };
+}
+
+const server = createServer(async (req, res) => {
+ const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
+
+ if (req.method === "OPTIONS") {
+ writeJson(res, 204, {});
+ return;
+ }
+
+ try {
+ if (url.pathname === "/api/health") {
+ writeJson(res, 200, {
+ status: "ok",
+ service: "gumption-api",
+ owner: "bryan@norcalcarbmobile.com",
+ operator: "Samantha",
+ projectId,
+ vertexLocation,
+ alerts: alertStatus(),
+ revision: process.env.K_REVISION || "local",
+ });
+ return;
+ }
+
+ if (url.pathname === "/api/providers") {
+ writeJson(res, 200, Object.fromEntries(
+ Object.entries(providers).map(([id, provider]) => [id, {
+ label: provider.label,
+ configured: provider.configured(),
+ secret: provider.env,
+ }]),
+ ));
+ return;
+ }
+
+ if (url.pathname === "/api/alerts/status") {
+ writeJson(res, 200, alertStatus());
+ return;
+ }
+
+ if (url.pathname === "/api/chat" && req.method === "POST") {
+ const result = await routeChat(await readJson(req));
+ writeJson(res, result.status, result.payload);
+ return;
+ }
+
+ if (url.pathname === "/api/alerts" && req.method === "POST") {
+ const result = await routeAlert(await readJson(req));
+ writeJson(res, result.status, result.payload);
+ return;
+ }
+
+ writeJson(res, 404, { error: "not found" });
+ } catch (error) {
+ writeJson(res, 500, { error: error.message || "unexpected api error" });
+ }
+});
+
+server.listen(port, "0.0.0.0", () => {
+ console.log(`gumption-api listening on http://0.0.0.0:${port}`);
+});
diff --git a/scripts/deploy-gcp.ps1 b/scripts/deploy-gcp.ps1
new file mode 100644
index 0000000..d2d3c56
--- /dev/null
+++ b/scripts/deploy-gcp.ps1
@@ -0,0 +1,59 @@
+param(
+ [string]$ProjectId = "samantha-gumption",
+ [string]$Region = "us-west1",
+ [string]$Service = "gumption-api"
+)
+
+$ErrorActionPreference = "Stop"
+Set-StrictMode -Version Latest
+
+$RepoRoot = Split-Path -Parent $PSScriptRoot
+$ApiDir = Join-Path $RepoRoot "api"
+$WebDir = Join-Path $RepoRoot "web"
+
+function Test-GcpSecret {
+ param([string]$Name)
+ & gcloud secrets describe $Name --project $ProjectId *> $null
+ return $LASTEXITCODE -eq 0
+}
+
+$SecretBindings = @()
+foreach ($Name in @("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "XAI_API_KEY", "ALERT_TO_PHONE", "TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", "TWILIO_FROM", "TWILIO_SMS_FROM", "TWILIO_VOICE_FROM")) {
+ if (Test-GcpSecret $Name) {
+ $SecretBindings += ("{0}={0}:latest" -f $Name)
+ }
+}
+
+Write-Host "Deploying $Service to Cloud Run in $ProjectId / $Region..."
+Push-Location $ApiDir
+try {
+ $RunArgs = @(
+ "run", "deploy", $Service,
+ "--source", ".",
+ "--project", $ProjectId,
+ "--region", $Region,
+ "--allow-unauthenticated",
+ "--set-env-vars", "GCP_PROJECT_ID=$ProjectId,VERTEX_LOCATION=$Region"
+ )
+ if ($SecretBindings.Count -gt 0) {
+ $RunArgs += "--update-secrets"
+ $RunArgs += ($SecretBindings -join ",")
+ }
+ & gcloud @RunArgs
+}
+finally {
+ Pop-Location
+}
+
+Write-Host "Building and deploying Firebase Hosting..."
+Push-Location $WebDir
+try {
+ npm install
+ npm run build
+ npx -y firebase-tools@latest deploy --only hosting --project $ProjectId
+}
+finally {
+ Pop-Location
+}
+
+Write-Host "GCP deploy complete."
diff --git a/web/.dockerignore b/web/.dockerignore
new file mode 100644
index 0000000..ce3fb28
--- /dev/null
+++ b/web/.dockerignore
@@ -0,0 +1,7 @@
+.git
+.gitignore
+node_modules
+npm-debug.log
+README.md
+cloudbuild.yaml
+scripts
diff --git a/web/.firebaserc b/web/.firebaserc
new file mode 100644
index 0000000..5004e83
--- /dev/null
+++ b/web/.firebaserc
@@ -0,0 +1,5 @@
+{
+ "projects": {
+ "default": "samantha-gumption"
+ }
+}
diff --git a/web/Dockerfile b/web/Dockerfile
new file mode 100644
index 0000000..7d6e39a
--- /dev/null
+++ b/web/Dockerfile
@@ -0,0 +1,11 @@
+FROM node:22-alpine AS build
+
+WORKDIR /app
+COPY package*.json ./
+RUN npm ci
+COPY . .
+RUN npm run build
+
+FROM nginx:1.27-alpine
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+COPY --from=build /app/dist /usr/share/nginx/html
diff --git a/web/README.md b/web/README.md
new file mode 100644
index 0000000..3cdb384
--- /dev/null
+++ b/web/README.md
@@ -0,0 +1,30 @@
+# Brain Trust Command Center Web
+
+Firebase Hosting static Vite app for Samantha's `samantha-gumption` GCP project.
+
+## What changed
+
+- Preserves the Brain Trust dashboard concept from `https://web-ten-teal-90.vercel.app/`.
+- Builds to `dist/` for Firebase Hosting's CDN-backed static hosting.
+- Rewrites `/api/**` to the `gumption-api` Cloud Run service in `us-west1`.
+- Supports shareable view URLs like `/?view=gcp&demo=launch` for launch reviews.
+- Keeps provider keys out of the browser; AI Assist posts to the Cloud Run proxy.
+
+## Run locally
+
+Start the API from `../api`, then run:
+
+```bash
+npm install
+npm run dev
+```
+
+Open `http://localhost:5173`.
+
+## Deploy
+
+From the repo root, after authenticating to `samantha-gumption`:
+
+```powershell
+.\scripts\deploy-gcp.ps1
+```
diff --git a/web/firebase.json b/web/firebase.json
new file mode 100644
index 0000000..0c5d475
--- /dev/null
+++ b/web/firebase.json
@@ -0,0 +1,34 @@
+{
+ "hosting": {
+ "public": "dist",
+ "ignore": [
+ "firebase.json",
+ "**/.*",
+ "**/node_modules/**"
+ ],
+ "headers": [
+ {
+ "source": "/assets/**",
+ "headers": [
+ {
+ "key": "Cache-Control",
+ "value": "public,max-age=31536000,immutable"
+ }
+ ]
+ }
+ ],
+ "rewrites": [
+ {
+ "source": "/api/**",
+ "run": {
+ "serviceId": "gumption-api",
+ "region": "us-west1"
+ }
+ },
+ {
+ "source": "**",
+ "destination": "/index.html"
+ }
+ ]
+ }
+}
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..222f4ef
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ Brain Trust Command Center
+
+
+
+
+
+
diff --git a/web/nginx.conf b/web/nginx.conf
new file mode 100644
index 0000000..1d2b7b7
--- /dev/null
+++ b/web/nginx.conf
@@ -0,0 +1,10 @@
+server {
+ listen 8080;
+ server_name _;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+}
diff --git a/web/package-lock.json b/web/package-lock.json
new file mode 100644
index 0000000..761aa6f
--- /dev/null
+++ b/web/package-lock.json
@@ -0,0 +1,829 @@
+{
+ "name": "brain-trust-command-center",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "brain-trust-command-center",
+ "version": "1.0.0",
+ "dependencies": {
+ "vite": "^8.0.12"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
+ "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.129.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz",
+ "integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz",
+ "integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz",
+ "integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz",
+ "integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz",
+ "integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz",
+ "integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz",
+ "integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz",
+ "integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz",
+ "integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz",
+ "integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz",
+ "integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz",
+ "integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz",
+ "integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz",
+ "integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz",
+ "integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz",
+ "integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz",
+ "integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==",
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.14",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+ "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz",
+ "integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==",
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.129.0",
+ "@rolldown/pluginutils": "1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0",
+ "@rolldown/binding-darwin-arm64": "1.0.0",
+ "@rolldown/binding-darwin-x64": "1.0.0",
+ "@rolldown/binding-freebsd-x64": "1.0.0",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0",
+ "@rolldown/binding-linux-x64-musl": "1.0.0",
+ "@rolldown/binding-openharmony-arm64": "1.0.0",
+ "@rolldown/binding-wasm32-wasi": "1.0.0",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/vite": {
+ "version": "8.0.12",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz",
+ "integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==",
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.14",
+ "rolldown": "1.0.0",
+ "tinyglobby": "^0.2.16"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.18",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000..a85ec43
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "brain-trust-command-center",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "description": "Firebase Hosting static Brain Trust command center migrated from Vercel.",
+ "scripts": {
+ "dev": "vite --host 0.0.0.0",
+ "build": "vite build",
+ "preview": "vite preview --host 0.0.0.0",
+ "check": "node --check src/main.js && npm run build"
+ },
+ "dependencies": {
+ "vite": "^8.0.12"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+}
diff --git a/web/src/main.js b/web/src/main.js
new file mode 100644
index 0000000..7a622c8
--- /dev/null
+++ b/web/src/main.js
@@ -0,0 +1,683 @@
+import "./styles.css";
+
+const providers = [
+ { id: "claude", label: "Claude", mark: "C" },
+ { id: "chatgpt", label: "ChatGPT", mark: "G" },
+ { id: "grok", label: "SuperGrok", mark: "X" },
+ { id: "gemini", label: "Gemini", mark: "β’" },
+ { id: "copilot", label: "Copilot", mark: "βΆ" },
+];
+
+const agents = [
+ { id: "samantha", name: "Samantha", role: "Command Center / GCP Owner", avatar: "π§ ", status: "online" },
+ { id: "hermes", name: "Hermes", role: "Cold Sales / Outbound", avatar: "πͺ½", status: "online" },
+ { id: "openclaw", name: "OpenClaw", role: "B2B / Solar / Rent-Ruby", avatar: "π", status: "active" },
+ { id: "kesha", name: "Kesha", role: "CRM / SMS / Fleet", avatar: "π±", status: "online" },
+ { id: "belichick", name: "Belichick", role: "Strategy / Review", avatar: "π", status: "active" },
+ { id: "datasync", name: "DataSync", role: "CARB / VIN Pipelines", avatar: "βοΈ", status: "idle" },
+ { id: "finbot", name: "FinBot", role: "Invoices / Reconcile", avatar: "π°", status: "idle" },
+ { id: "nemoclaw", name: "NemoClaw", role: "Infra / Security", avatar: "π‘οΈ", status: "active" },
+];
+
+const channels = [
+ { id: "hq", icon: "π", label: "#hq", helper: "Brain Trust command" },
+ { id: "invoices", icon: "π°", label: "Invoices", helper: "Billing flow" },
+ { id: "tests", icon: "π¬", label: "CARB Tests", helper: "DOORS submissions" },
+ { id: "gcp", icon: "βοΈ", label: "GCP Move", helper: "Cloud Run launch" },
+ { id: "agents", icon: "π€", label: "Agents", helper: "Provider handoff" },
+];
+
+const invoices = [
+ { id: "INV0247", customer: "A+ Clean Truck Check", total: 180, status: "pending", source: "aplus", due: "Today" },
+ { id: "INV0245", customer: "Danny Barbosa", total: 70, status: "pending", source: "aplus", due: "Tomorrow" },
+ { id: "INV0239", customer: "Port City Fleet", total: 760, status: "paid", source: "direct", due: "Paid" },
+ { id: "INV0231", customer: "Sunpath Solar", total: 1240, status: "paid", source: "solar", due: "Paid" },
+ { id: "INV0226", customer: "Bay Area Logistics", total: 410, status: "paid", source: "direct", due: "Paid" },
+];
+
+const tests = [
+ { id: "2113220", customer: "Unknown - needs match", vin: "YE2XC82B1G3048768", result: "fail", invoice: "orphan", action: "Customer follow-up required" },
+ { id: "2113219", customer: "A+ Clean Truck Check", vin: "1FDXF46S12EC58331", result: "pass", invoice: "INV0247", action: "Ready to reconcile" },
+ { id: "2113212", customer: "Port City Fleet", vin: "3AKJHHDR1MSMU7154", result: "pass", invoice: "INV0239", action: "Complete" },
+ { id: "2113198", customer: "Sunpath Solar", vin: "1HTMMAAL7KH123771", result: "pass", invoice: "INV0231", action: "Complete" },
+];
+
+const initialMessages = [
+ {
+ id: "m1",
+ channel: "hq",
+ author: "Samantha",
+ avatar: "π§ ",
+ time: "5/10/2026, 7:06 PM",
+ body: "Brain Trust HQ online. Loaded 80 invoices + 60 CARB tests. Auto-joined where possible.",
+ },
+ {
+ id: "m2",
+ channel: "hq",
+ author: "Belichick",
+ avatar: "π",
+ time: "5/11/2026, 7:06 AM",
+ body: "Strategy note: orphan tests = revenue leakage. Orphan invoices = work owed but not delivered. Both flagged in KPI bar.",
+ },
+ {
+ id: "m3",
+ channel: "invoices",
+ author: "FinBot",
+ avatar: "π°",
+ time: "Today, 8:42 AM",
+ body: "Pending payments today: 2 A+ invoices ($250 total). Both are sub-7-day, no escalation needed.",
+ },
+ {
+ id: "m4",
+ channel: "tests",
+ author: "DataSync",
+ avatar: "βοΈ",
+ time: "Today, 9:10 AM",
+ body: "DOORS export ingested through 5/7. 1 FAIL flagged: Test 2113220. Customer follow-up required.",
+ },
+ {
+ id: "m5",
+ channel: "gcp",
+ author: "NemoClaw",
+ avatar: "π‘οΈ",
+ time: "Today, 10:18 AM",
+ body: "Firebase Hosting + Cloud Run split is now the target: static cockpit on CDN, provider keys behind gumption-api.",
+ },
+];
+
+const params = new URLSearchParams(window.location.search);
+const requestedView = params.get("view");
+const requestedProvider = params.get("provider");
+const demoMode = params.get("demo");
+
+const state = {
+ view: channels.some((channel) => channel.id === requestedView) ? requestedView : "hq",
+ provider: providers.some((provider) => provider.id === requestedProvider) ? requestedProvider : "claude",
+ search: "",
+ prompt: "Draft a handoff for Samantha to finish the Google Cloud Platform (GCP) launch.",
+ alertMessage: "Agent progress is blocked. Please check Brain Trust and unblock Samantha.",
+ messages: loadMessages(),
+ assistantReply: "Provider keys stay server-side in Cloud Run via Secret Manager.",
+ alertReply: "SMS/voice alerts route through Cloud Run so your phone number stays server-side.",
+ toast: params.get("toast") || "",
+};
+
+if (demoMode === "launch") {
+ state.messages = [
+ {
+ id: "demo-launch",
+ channel: "gcp",
+ author: "Samantha",
+ avatar: "π§ ",
+ time: "Demo mode",
+ body: "GCP launch note: deploy Firebase Hosting, verify /api/health through Cloud Run, map production domain, then retire Vercel.",
+ },
+ ...state.messages.filter((message) => message.id !== "demo-launch"),
+ ];
+}
+
+function loadMessages() {
+ try {
+ const saved = JSON.parse(localStorage.getItem("brain_trust_messages_v1") || "[]");
+ return saved.length ? saved : initialMessages;
+ } catch {
+ return initialMessages;
+ }
+}
+
+function saveMessages() {
+ localStorage.setItem("brain_trust_messages_v1", JSON.stringify(state.messages));
+}
+
+function esc(value) {
+ return String(value)
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
+
+function money(value) {
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ maximumFractionDigits: 0,
+ }).format(value);
+}
+
+function setToast(message) {
+ state.toast = message;
+ render();
+ window.clearTimeout(setToast.timer);
+ setToast.timer = window.setTimeout(() => {
+ state.toast = "";
+ render();
+ }, 2400);
+}
+
+function filteredMessages() {
+ const query = state.search.trim().toLowerCase();
+ return state.messages.filter((message) => {
+ const matchesChannel = state.view === "hq" || message.channel === state.view || state.view === "agents";
+ const haystack = `${message.author} ${message.body} ${message.channel}`.toLowerCase();
+ return matchesChannel && (!query || haystack.includes(query));
+ });
+}
+
+function filteredRows(rows) {
+ const query = state.search.trim().toLowerCase();
+ if (!query) return rows;
+ return rows.filter((row) => Object.values(row).join(" ").toLowerCase().includes(query));
+}
+
+function kpis() {
+ const paidTotal = 16206;
+ const pending = invoices.filter((invoice) => invoice.status === "pending");
+ const pendingTotal = pending.reduce((sum, invoice) => sum + invoice.total, 0);
+ const pass = tests.filter((test) => test.result === "pass").length + 24;
+ const fail = tests.filter((test) => test.result === "fail").length;
+ return [
+ { label: "Paid YTD", value: money(paidTotal), color: "var(--green)" },
+ { label: "Pending", value: `${money(pendingTotal)} (${pending.length})`, color: "var(--yellow)" },
+ { label: "A+ Net", value: money(1976), color: "var(--blue)" },
+ { label: "Tests", value: `${pass}P / ${fail}F`, color: "var(--purple)" },
+ { label: "Orphans", value: "6T / 77I", color: "var(--red)" },
+ ];
+}
+
+function renderSidebar() {
+ return `
+
+ `;
+}
+
+function renderTopbar() {
+ return `
+
+ `;
+}
+
+function renderHero() {
+ return `
+
+
+
+
Moved from Vercel target to Google Cloud
+
Brain Trust command, invoices, CARB tests, and GCP launch in one cockpit.
+
+ Enhanced from the live Vercel app with Firebase Hosting for the static cockpit,
+ a Cloud Run provider proxy, Secret Manager-ready keys, and sharper revenue-leakage triage.
+
+
+ Send GCP launch note
+ Log reconciliation sync
+ Export open issues
+
+
+
+
β Cloud Run target ready
+
+
β Firebase Hosting serves the Vite dist build
+
β /api/** rewrites to Cloud Run
+
β Provider keys stay in Secret Manager
+
β Vertex Gemini uses project-scoped auth
+
β‘ Add production domain + IAM after GCP project selection
+
+
+
+
+ `;
+}
+
+function renderKpis() {
+ return `${kpis()
+ .map(
+ (item) => `
+
+ ${item.value}
+ ${item.label}
+
+ `,
+ )
+ .join("")} `;
+}
+
+function renderStream() {
+ const messages = filteredMessages();
+ return `
+
+
+
+
${channels.find((channel) => channel.id === state.view)?.label || "Command Stream"}
+ ${messages.length} visible updates
+
+
Add alert
+
+
+ ${messages
+ .map(
+ (message) => `
+
+ ${message.avatar}
+
+
${esc(message.author)} ${esc(message.time)}
+
${esc(message.body)}
+
+
+ `,
+ )
+ .join("")}
+
+
+ `;
+}
+
+function renderInvoices() {
+ return `
+
+
+
Invoice reconciliation A+ and direct revenue queue
+
+
+ Invoice Customer Total Status Due
+
+ ${filteredRows(invoices)
+ .map(
+ (invoice) => `
+
+ ${invoice.id}
+ ${esc(invoice.customer)}
+ ${money(invoice.total)}
+ ${invoice.status}
+ ${invoice.due}
+
+ `,
+ )
+ .join("")}
+
+
+
+ `;
+}
+
+function renderTests() {
+ return `
+
+
+
CARB test matching DOORS exports joined to invoice records
+
+
+ Test Customer VIN Result Invoice Action
+
+ ${filteredRows(tests)
+ .map(
+ (test) => `
+
+ ${test.id}
+ ${esc(test.customer)}
+ ${test.vin}
+ ${test.result}
+ ${test.invoice}
+ ${esc(test.action)}
+
+ `,
+ )
+ .join("")}
+
+
+
+ `;
+}
+
+function renderGcp() {
+ const steps = [
+ ["Build static web", "ready", "Vite emits dist/ for Firebase Hosting's CDN."],
+ ["Rewrite API calls", "ready", "Firebase Hosting sends /api/** to gumption-api in us-west1."],
+ ["Deploy provider proxy", "ready", "Cloud Run receives AI Assist prompts and reads secrets server-side."],
+ ["Use Vertex Gemini", "ready", "Gemini traffic uses samantha-gumption project auth and GCP credits."],
+ ["SMS/voice alerts", "ready", "Blocked agents can text or call Bryan through the Cloud Run alert proxy."],
+ ["Attach domain", "warn", "Map the final production hostname after Firebase Hosting deploy."],
+ ["Lock down IAM", "warn", "Set invoker policy based on whether the proxy stays public or private."],
+ ];
+ return `
+
+
+
GCP migration board Launch state for Samantha and NemoClaw
+
+
+ ${steps
+ .map(
+ ([title, status, detail]) => `
+
+ ${status}
+ ${title}
+ ${detail}
+
+ `,
+ )
+ .join("")}
+
+
+ `;
+}
+
+function renderAgents() {
+ return `
+
+
+
Agent provider matrix Route each task to the right assistant
+
+
+ ${agents
+ .map(
+ (agent) => `
+
+ ${agent.avatar}
+ ${esc(agent.name)}
+ ${esc(agent.role)}
+
+ `,
+ )
+ .join("")}
+
+
+ `;
+}
+
+function renderQuickCards() {
+ return `
+
+
Todayβs focus Highest-leverage work
+
+
Recover leakage - 6 orphan tests Start with the failed VIN follow-up.
+
Collect A+ pending - $250 Close INV0247 and INV0245 before escalation.
+
Ship GCP - Firebase + Cloud Run Deploy Hosting, verify API, map domain, retire Vercel.
+
SEO blocker - og:image Resolve missing social previews after launch.
+
+
+ `;
+}
+
+function renderPrimaryPanel() {
+ if (state.view === "invoices") return renderInvoices();
+ if (state.view === "tests") return renderTests();
+ if (state.view === "gcp") return renderGcp();
+ if (state.view === "agents") return renderAgents();
+ return renderStream();
+}
+
+function renderAssistant() {
+ const activeProvider = providers.find((provider) => provider.id === state.provider);
+ return `
+
+ `;
+}
+
+function render() {
+ const app = document.getElementById("app");
+ app.innerHTML = `
+
+ ${renderSidebar()}
+
+ ${renderTopbar()}
+ ${renderHero()}
+
+
+ ${renderKpis()}
+ ${renderPrimaryPanel()}
+
+ ${renderQuickCards()}
+
+ ${renderAssistant()}
+
+
+ ${state.toast ? `
${esc(state.toast)}
` : ""}
+
+ `;
+ bindEvents();
+}
+
+function addMessage(channel, author, avatar, body) {
+ state.messages = [
+ {
+ id: `m${Date.now()}`,
+ channel,
+ author,
+ avatar,
+ time: new Date().toLocaleString(),
+ body,
+ },
+ ...state.messages,
+ ];
+ saveMessages();
+}
+
+function exportOpenIssues() {
+ const rows = [
+ ["type", "id", "owner", "status", "next_step"],
+ ...invoices
+ .filter((invoice) => invoice.status === "pending")
+ .map((invoice) => ["invoice", invoice.id, invoice.customer, invoice.status, `Collect ${money(invoice.total)}`]),
+ ...tests
+ .filter((test) => test.result === "fail" || test.invoice === "orphan")
+ .map((test) => ["test", test.id, test.customer, test.result, test.action]),
+ ];
+ const csv = rows.map((row) => row.map((cell) => `"${String(cell).replaceAll('"', '""')}"`).join(",")).join("\n");
+ const blob = new Blob([csv], { type: "text/csv" });
+ const link = document.createElement("a");
+ link.href = URL.createObjectURL(blob);
+ link.download = "brain-trust-open-issues.csv";
+ link.click();
+ URL.revokeObjectURL(link.href);
+}
+
+async function sendPromptToProxy(provider) {
+ const response = await fetch("/api/chat", {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ provider: provider.id,
+ prompt: state.prompt || "Review current command center state.",
+ context: {
+ view: state.view,
+ pendingInvoices: invoices.filter((invoice) => invoice.status === "pending").length,
+ orphanTests: tests.filter((test) => test.invoice === "orphan").length,
+ },
+ }),
+ });
+
+ const payload = await response.json().catch(() => ({}));
+ if (!response.ok) {
+ throw new Error(payload.error || `Proxy returned ${response.status}`);
+ }
+
+ return payload;
+}
+
+async function sendProgressAlert(channel) {
+ const response = await fetch("/api/alerts", {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ channel,
+ message: state.alertMessage,
+ }),
+ });
+
+ const payload = await response.json().catch(() => ({}));
+ if (!response.ok) {
+ throw new Error(payload.error || `Alert proxy returned ${response.status}`);
+ }
+
+ return payload;
+}
+
+function bindEvents() {
+ document.querySelectorAll("[data-view]").forEach((button) => {
+ button.addEventListener("click", () => {
+ state.view = button.dataset.view;
+ render();
+ });
+ });
+
+ document.querySelectorAll("[data-provider]").forEach((button) => {
+ button.addEventListener("click", () => {
+ state.provider = button.dataset.provider;
+ render();
+ });
+ });
+
+ const search = document.getElementById("search");
+ search?.addEventListener("input", (event) => {
+ state.search = event.target.value;
+ render();
+ document.getElementById("search")?.focus();
+ });
+
+ const prompt = document.getElementById("prompt");
+ prompt?.addEventListener("input", (event) => {
+ state.prompt = event.target.value;
+ });
+
+ const alertMessage = document.getElementById("alert-message");
+ alertMessage?.addEventListener("input", (event) => {
+ state.alertMessage = event.target.value;
+ });
+
+ document.querySelectorAll("[data-action]").forEach((button) => {
+ button.addEventListener("click", async () => {
+ const action = button.dataset.action;
+ if (action === "deploy-note") {
+ addMessage("gcp", "Samantha", "π§ ", "GCP launch note: deploy Firebase Hosting, verify /api/health through Cloud Run, map production domain, then retire Vercel.");
+ setToast("GCP launch note posted to #gcp");
+ }
+ if (action === "sync-note") {
+ addMessage("invoices", "FinBot", "π°", "Reconciliation sync queued: 2 pending A+ invoices and 1 failed CARB test need owner review.");
+ setToast("Reconciliation sync logged");
+ }
+ if (action === "seed-alert") {
+ addMessage(state.view === "hq" ? "alerts" : state.view, "NemoClaw", "π‘οΈ", "Alert generated: verify Cloud Run revision and invoice/test orphan queue before launch.");
+ setToast("Alert added");
+ }
+ if (action === "export") {
+ exportOpenIssues();
+ setToast("Open issues CSV exported");
+ }
+ if (action === "copy-prompt") {
+ await navigator.clipboard.writeText(state.prompt);
+ setToast("Prompt copied");
+ }
+ if (action === "send-prompt") {
+ const provider = providers.find((item) => item.id === state.provider);
+ setToast(`Sending to ${provider.label} via Cloud Run proxy...`);
+ try {
+ const result = await sendPromptToProxy(provider);
+ const reply = result.reply || result.message || "Provider proxy accepted the request.";
+ state.assistantReply = reply;
+ addMessage("hq", provider.label, provider.mark, reply);
+ setToast(`Prompt sent to ${provider.label}`);
+ } catch (error) {
+ state.assistantReply = `Proxy unavailable: ${error.message}`;
+ addMessage("hq", "Samantha", "π§ ", `Proxy unavailable for ${provider.label}: ${error.message}`);
+ setToast("Cloud Run proxy needs deployment or secrets");
+ }
+ }
+ if (action === "alert-sms" || action === "alert-voice") {
+ const channel = action === "alert-voice" ? "voice" : "sms";
+ setToast(channel === "voice" ? "Calling Bryan via alert proxy..." : "Texting Bryan via alert proxy...");
+ try {
+ const result = await sendProgressAlert(channel);
+ state.alertReply = result.message || `${channel.toUpperCase()} alert ${result.mode || "queued"} for ${result.toPhone || "Bryan"}.`;
+ addMessage("alerts", "Samantha", "π§ ", state.alertReply);
+ setToast(result.mode === "setup-required" ? "Alert secrets need setup" : `${channel.toUpperCase()} alert sent`);
+ } catch (error) {
+ state.alertReply = `Alert proxy unavailable: ${error.message}`;
+ addMessage("alerts", "Samantha", "π§ ", state.alertReply);
+ setToast("Alert proxy needs deployment");
+ }
+ }
+ });
+ });
+}
+
+render();
diff --git a/web/src/styles.css b/web/src/styles.css
new file mode 100644
index 0000000..e6a103e
--- /dev/null
+++ b/web/src/styles.css
@@ -0,0 +1,647 @@
+:root {
+ color-scheme: dark;
+ --bg: #060a0f;
+ --panel: #0c121a;
+ --panel-strong: #101a25;
+ --card: #121d2a;
+ --line: #223144;
+ --line-bright: #37506f;
+ --text: #c9d1d9;
+ --bright: #f3f8ff;
+ --muted: #76869a;
+ --green: #36d399;
+ --yellow: #f4bf4f;
+ --red: #ff5f70;
+ --blue: #5eb7ff;
+ --purple: #8b5cf6;
+ --pink: #e91e8c;
+ --orange: #f97316;
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body,
+#app {
+ min-height: 100%;
+ margin: 0;
+}
+
+body {
+ background:
+ radial-gradient(circle at top left, rgba(139, 92, 246, 0.22), transparent 34rem),
+ radial-gradient(circle at 80% 12%, rgba(0, 188, 212, 0.14), transparent 28rem),
+ var(--bg);
+ color: var(--text);
+}
+
+button,
+input,
+select,
+textarea {
+ font: inherit;
+}
+
+button {
+ cursor: pointer;
+}
+
+.shell {
+ min-height: 100vh;
+ display: grid;
+ grid-template-columns: 248px minmax(0, 1fr);
+}
+
+.sidebar {
+ position: sticky;
+ top: 0;
+ height: 100vh;
+ padding: 18px;
+ border-right: 1px solid var(--line);
+ background: rgba(6, 10, 15, 0.82);
+ backdrop-filter: blur(18px);
+ overflow-y: auto;
+}
+
+.brand {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ margin-bottom: 18px;
+}
+
+.brand-mark {
+ width: 42px;
+ height: 42px;
+ display: grid;
+ place-items: center;
+ border-radius: 14px;
+ color: white;
+ background: linear-gradient(135deg, var(--purple), #00bcd4);
+ box-shadow: 0 10px 32px rgba(139, 92, 246, 0.35);
+ font-weight: 900;
+}
+
+.eyebrow {
+ color: var(--muted);
+ font-size: 11px;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+}
+
+.brand h1 {
+ margin: 2px 0 0;
+ color: var(--bright);
+ font-size: 17px;
+ line-height: 1.1;
+}
+
+.agent-stack,
+.nav,
+.deploy-card {
+ border: 1px solid var(--line);
+ border-radius: 16px;
+ background: rgba(12, 18, 26, 0.72);
+}
+
+.agent-stack {
+ padding: 12px;
+ margin-bottom: 14px;
+}
+
+.agent {
+ display: grid;
+ grid-template-columns: 30px 1fr auto;
+ gap: 9px;
+ align-items: center;
+ padding: 8px 4px;
+ border-bottom: 1px solid rgba(34, 49, 68, 0.55);
+}
+
+.agent:last-child {
+ border-bottom: 0;
+}
+
+.avatar {
+ width: 30px;
+ height: 30px;
+ display: grid;
+ place-items: center;
+ border-radius: 10px;
+ background: var(--card);
+}
+
+.agent strong {
+ display: block;
+ color: var(--bright);
+ font-size: 12px;
+}
+
+.agent span {
+ display: block;
+ color: var(--muted);
+ font-size: 10px;
+}
+
+.dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--green);
+ box-shadow: 0 0 14px currentColor;
+}
+
+.nav {
+ padding: 8px;
+}
+
+.nav button {
+ width: 100%;
+ display: flex;
+ gap: 9px;
+ align-items: center;
+ margin: 2px 0;
+ padding: 9px 10px;
+ border: 1px solid transparent;
+ border-radius: 11px;
+ color: var(--text);
+ background: transparent;
+ text-align: left;
+}
+
+.nav button.active,
+.nav button:hover {
+ color: var(--bright);
+ border-color: var(--line-bright);
+ background: var(--panel-strong);
+}
+
+.nav button.active {
+ border-color: rgba(94, 183, 255, 0.72);
+ box-shadow: inset 4px 0 0 var(--blue), 0 0 0 1px rgba(94, 183, 255, 0.12);
+}
+
+.deploy-card {
+ margin-top: 14px;
+ padding: 13px;
+}
+
+.deploy-card strong {
+ color: var(--bright);
+ font-size: 13px;
+}
+
+.deploy-card p {
+ margin: 8px 0 10px;
+ color: var(--muted);
+ font-size: 11px;
+ line-height: 1.45;
+}
+
+.progress {
+ height: 8px;
+ border-radius: 99px;
+ overflow: hidden;
+ background: #172130;
+}
+
+.progress span {
+ display: block;
+ height: 100%;
+ width: var(--value);
+ border-radius: inherit;
+ background: linear-gradient(90deg, var(--green), var(--blue), var(--purple));
+}
+
+.main {
+ min-width: 0;
+}
+
+.topbar {
+ position: sticky;
+ top: 0;
+ z-index: 5;
+ display: grid;
+ grid-template-columns: minmax(220px, 1fr) auto;
+ gap: 14px;
+ align-items: center;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--line);
+ background: rgba(6, 10, 15, 0.88);
+ backdrop-filter: blur(18px);
+}
+
+.search {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ max-width: 560px;
+ padding: 8px 10px;
+ border: 1px solid var(--line);
+ border-radius: 13px;
+ background: var(--panel);
+}
+
+.search input {
+ width: 100%;
+ border: 0;
+ outline: 0;
+ color: var(--bright);
+ background: transparent;
+}
+
+.provider-strip {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 7px;
+}
+
+.provider {
+ border: 1px solid var(--line);
+ border-radius: 999px;
+ padding: 6px 9px;
+ color: var(--text);
+ background: var(--panel);
+ font-size: 11px;
+}
+
+.provider.active {
+ border-color: var(--purple);
+ color: white;
+ background: rgba(139, 92, 246, 0.24);
+}
+
+.hero {
+ padding: 18px 18px 0;
+}
+
+.hero-card {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 340px;
+ gap: 18px;
+ padding: 22px;
+ border: 1px solid var(--line);
+ border-radius: 24px;
+ background:
+ linear-gradient(135deg, rgba(18, 29, 42, 0.98), rgba(9, 13, 20, 0.94)),
+ radial-gradient(circle at top right, rgba(139, 92, 246, 0.18), transparent 20rem);
+ box-shadow: 0 22px 80px rgba(0, 0, 0, 0.24);
+}
+
+.hero h2 {
+ margin: 6px 0 8px;
+ color: var(--bright);
+ font-size: clamp(30px, 5vw, 58px);
+ line-height: 0.96;
+ letter-spacing: -0.05em;
+}
+
+.hero p {
+ max-width: 780px;
+ margin: 0;
+ color: var(--muted);
+ font-size: 14px;
+ line-height: 1.6;
+}
+
+.hero-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-top: 18px;
+}
+
+.button {
+ border: 1px solid var(--line-bright);
+ border-radius: 12px;
+ padding: 10px 13px;
+ color: var(--bright);
+ background: var(--panel-strong);
+}
+
+.button.primary {
+ border-color: transparent;
+ background: linear-gradient(135deg, var(--purple), #00bcd4);
+}
+
+.gcp-panel {
+ display: grid;
+ gap: 12px;
+}
+
+.status-pill {
+ justify-self: start;
+ border: 1px solid rgba(54, 211, 153, 0.35);
+ border-radius: 999px;
+ padding: 6px 10px;
+ color: var(--green);
+ background: rgba(54, 211, 153, 0.1);
+ font-size: 12px;
+}
+
+.checklist {
+ display: grid;
+ gap: 8px;
+}
+
+.check {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ padding: 9px;
+ border: 1px solid var(--line);
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.03);
+ font-size: 12px;
+}
+
+.check.done {
+ color: var(--green);
+}
+
+.content {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 360px;
+ gap: 18px;
+ padding: 18px;
+}
+
+.kpis {
+ display: grid;
+ grid-template-columns: repeat(5, minmax(120px, 1fr));
+ gap: 10px;
+ margin-bottom: 18px;
+}
+
+.kpi {
+ padding: 14px;
+ border: 1px solid var(--line);
+ border-radius: 16px;
+ background: rgba(12, 18, 26, 0.72);
+}
+
+.kpi strong {
+ display: block;
+ color: var(--bright);
+ font-size: 23px;
+ line-height: 1;
+}
+
+.kpi span {
+ display: block;
+ margin-top: 7px;
+ color: var(--muted);
+ font-size: 11px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.panel {
+ border: 1px solid var(--line);
+ border-radius: 20px;
+ background: rgba(12, 18, 26, 0.76);
+ overflow: hidden;
+}
+
+.panel-head {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px;
+ border-bottom: 1px solid var(--line);
+}
+
+.panel-head h3 {
+ margin: 0;
+ color: var(--bright);
+ font-size: 15px;
+}
+
+.panel-head span {
+ color: var(--muted);
+ font-size: 11px;
+}
+
+.stream {
+ display: grid;
+ gap: 10px;
+ max-height: 560px;
+ overflow-y: auto;
+ padding: 14px;
+}
+
+.message {
+ display: grid;
+ grid-template-columns: 34px 1fr;
+ gap: 10px;
+ padding: 12px;
+ border: 1px solid rgba(34, 49, 68, 0.72);
+ border-radius: 15px;
+ background: rgba(18, 29, 42, 0.68);
+}
+
+.message h4 {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ margin: 0 0 5px;
+ color: var(--bright);
+ font-size: 13px;
+}
+
+.message time {
+ color: var(--muted);
+ font-size: 10px;
+}
+
+.message p {
+ margin: 0;
+ font-size: 13px;
+ line-height: 1.45;
+}
+
+.quick-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+ padding: 14px;
+}
+
+.quick-card {
+ border: 1px solid var(--line);
+ border-radius: 15px;
+ padding: 13px;
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.quick-card strong {
+ color: var(--bright);
+ font-size: 14px;
+}
+
+.quick-card p {
+ margin: 6px 0 0;
+ color: var(--muted);
+ font-size: 12px;
+ line-height: 1.45;
+}
+
+.table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.table th,
+.table td {
+ padding: 11px 14px;
+ border-bottom: 1px solid var(--line);
+ font-size: 12px;
+ text-align: left;
+}
+
+.table th {
+ color: var(--muted);
+ font-size: 10px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.badge {
+ display: inline-flex;
+ border-radius: 999px;
+ padding: 4px 8px;
+ background: rgba(255, 255, 255, 0.07);
+ font-size: 10px;
+ text-transform: uppercase;
+}
+
+.badge.paid,
+.badge.pass,
+.badge.ready {
+ color: var(--green);
+ background: rgba(54, 211, 153, 0.12);
+}
+
+.badge.pending,
+.badge.warn {
+ color: var(--yellow);
+ background: rgba(244, 191, 79, 0.12);
+}
+
+.badge.fail,
+.badge.blocked {
+ color: var(--red);
+ background: rgba(255, 95, 112, 0.12);
+}
+
+.assistant {
+ position: sticky;
+ top: 86px;
+ align-self: start;
+}
+
+.assistant-body {
+ padding: 14px;
+}
+
+.assistant textarea {
+ width: 100%;
+ min-height: 92px;
+ resize: vertical;
+ border: 1px solid var(--line);
+ border-radius: 14px;
+ outline: none;
+ padding: 11px;
+ color: var(--bright);
+ background: var(--bg);
+}
+
+.assistant-actions {
+ display: flex;
+ gap: 8px;
+ margin-top: 10px;
+}
+
+.assistant-output {
+ margin-top: 12px;
+ padding: 11px;
+ border: 1px solid var(--line);
+ border-radius: 14px;
+ color: var(--text);
+ background: rgba(139, 92, 246, 0.08);
+ font-size: 12px;
+ line-height: 1.5;
+}
+
+.alert-box {
+ margin-top: 14px;
+ padding-top: 14px;
+ border-top: 1px solid var(--line);
+}
+
+.alert-box label {
+ display: block;
+ margin-bottom: 8px;
+ color: var(--bright);
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.toast {
+ position: fixed;
+ right: 18px;
+ bottom: 18px;
+ z-index: 10;
+ max-width: 360px;
+ padding: 13px 15px;
+ border: 1px solid var(--line-bright);
+ border-radius: 16px;
+ color: var(--bright);
+ background: rgba(16, 26, 37, 0.96);
+ box-shadow: 0 18px 60px rgba(0, 0, 0, 0.32);
+}
+
+@media (max-width: 1100px) {
+ .shell {
+ grid-template-columns: 1fr;
+ }
+
+ .sidebar {
+ position: relative;
+ height: auto;
+ }
+
+ .hero-card,
+ .content {
+ grid-template-columns: 1fr;
+ }
+
+ .kpis {
+ grid-template-columns: repeat(2, minmax(120px, 1fr));
+ }
+
+ .assistant {
+ position: relative;
+ top: auto;
+ }
+}
+
+@media (max-width: 720px) {
+ .topbar {
+ grid-template-columns: 1fr;
+ }
+
+ .provider-strip {
+ justify-content: flex-start;
+ }
+
+ .kpis,
+ .quick-grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/web/vite.config.js b/web/vite.config.js
new file mode 100644
index 0000000..396425f
--- /dev/null
+++ b/web/vite.config.js
@@ -0,0 +1,12 @@
+import { defineConfig } from "vite";
+
+export default defineConfig({
+ server: {
+ proxy: {
+ "/api": {
+ target: process.env.GUMPTION_API_URL || "http://127.0.0.1:8081",
+ changeOrigin: true,
+ },
+ },
+ },
+});