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 ` +
+ +
+ ${providers + .map( + (provider) => ` + + `, + ) + .join("")} +
+
+ `; +} + +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. +

+
+ + + +
+
+
+ ● 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 +
+ +
+
+ ${messages + .map( + (message) => ` +
+
${message.avatar}
+
+

${esc(message.author)}

+

${esc(message.body)}

+
+
+ `, + ) + .join("")} +
+
+ `; +} + +function renderInvoices() { + return ` +
+
+

Invoice reconciliation

A+ and direct revenue queue
+
+ + + + ${filteredRows(invoices) + .map( + (invoice) => ` + + + + + + + + `, + ) + .join("")} + +
InvoiceCustomerTotalStatusDue
${invoice.id}${esc(invoice.customer)}${money(invoice.total)}${invoice.status}${invoice.due}
+
+ `; +} + +function renderTests() { + return ` +
+
+

CARB test matching

DOORS exports joined to invoice records
+
+ + + + ${filteredRows(tests) + .map( + (test) => ` + + + + + + + + + `, + ) + .join("")} + +
TestCustomerVINResultInvoiceAction
${test.id}${esc(test.customer)}${test.vin}${test.result}${test.invoice}${esc(test.action)}
+
+ `; +} + +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, + }, + }, + }, +});