diff --git a/.firebase/hosting.Lm5leHQvc3RhdGlj.cache b/.firebase/hosting.Lm5leHQvc3RhdGlj.cache
deleted file mode 100644
index 2c8b0813..00000000
--- a/.firebase/hosting.Lm5leHQvc3RhdGlj.cache
+++ /dev/null
@@ -1,86 +0,0 @@
-media/8a480f0b521d4e75-s.8e0177b5.woff2,1764292945234,cccaf7bf72117d313a9afc3a475289a19b9fcd0f735c9b2e4ded6cba08a517a0
-media/7178b3e590c64307-s.b97b3418.woff2,1764292945325,2b719e95831c9d92a31ebbe512bbd87cc76501765edb9b6ca4734cc5a2becb94
-media/4fa387ec64143e14-s.c1fdd6c2.woff2,1764292945447,7cdf2599fa32a0a3edc7d4126b5c8f5233d62799ddb46552ca6890389ba7d9c1
-debHSXN92soAOPU1HZgYF/_ssgManifest.js,1764292948075,02dbc1aeab6ef0a6ff2ff9a1643158cf9bb38929945eaa343a3627dee9ba6778
-debHSXN92soAOPU1HZgYF/_clientMiddlewareManifest.json,1764292945829,6731668a37f6a3ed10d77860e21c7ba693c377a061571ffa58564d1d699c1798
-debHSXN92soAOPU1HZgYF/_buildManifest.js,1764292945829,615bff88115b95e28f767491701e61ecffa7e94106127d229aa17a5905c320ed
-chunks/f9cb1844dfa45255.js,1764292945340,01c00f2411e0e3e097e7018d72b70b2b51875d8d225caaf29640ce25a2a544f6
-chunks/f7f1f72136b370ca.js,1764292945327,01a104bc08fcc0604c0ec1a806fca89dcf30e910b371b53deef33ca6265eecde
-chunks/ebe72e5be9ee5ad9.js,1764292945628,daae931739a356f539b4ad0ca419f1eb5b4bb3778cbb61dd7caf61a1d9865f2a
-chunks/f5e1eb514e39cc88.js,1764292944956,e4565dc0670c37ec0f447d2b5b70d452743abe10a95fed3845c1cce4abb48b4c
-chunks/f2800e0af697fdd0.js,1764292945328,6ce375e87966595708eadaf758ada294509c58dfac5ed8c59f70f616721c00c3
-chunks/e4262adb08a1c2bf.js,1764292945289,d4460e79d3dc59fc127276a44923f64f9a58a4265dabfec5271f2558d9fd2bc6
-chunks/turbopack-9ca93d673567d695.js,1764292945039,8192a0d1a0740e6d887b97a7403a2a7f247ef214e8920cab730ff87fed58437b
-chunks/ddebd270303d8e52.js,1764292945615,8c3623313303c5d58f40c3d5a8507a7f26b878129e1a322a4c49cc1abeb27a22
-chunks/e3204003115c48b3.js,1764292945452,4f1d81d5502dc30584b94916d1192cca4ad31abc0c9ec90a41b8a086d2ad36d8
-chunks/f44a1b0e13fe130e.js,1764292945521,bab4ad594f82e40e230885b71ad2e66bbdcc9adf0a06e5dabbb9881b3f090bfd
-chunks/d8fb8fcccb9ed575.js,1764292945641,88dc104238c97d623ed03c0457896bf0e5576f0e555b0521429514968254db72
-chunks/d53fe979bca22d91.js,1764292945292,7e0b70993217191be5b9c1cbcb8eae8e93f77d2ffc8b8c47260dddf45531b0bf
-chunks/e9d7f43cc4a2ffde.js,1764292944977,f3b75db76ea5f0e0897a6643c7eac52ef942c66e06c9e923d8adf0842b6102df
-chunks/c8f249a29afd3371.js,1764292945174,cf90afbb172a0e61b4bb1eba8b903cdf008a5b5dcbb22fbeaad4bdf99a1aa1a7
-chunks/d37715a4848800df.js,1764292945614,3c838423e38372ac0fbf00a2d2a665bd128b7b3bb282fcc3faddbf119821529c
-chunks/cc3b7acd0b8a8ed0.js,1764292945378,63c43444cf37250c0265200f90da944fe7cb30cbf4712ca78320313cc28ae2c0
-chunks/e930ad9e05eaf62d.js,1764292944935,59ebe62acad73a7571001179dfdb02a87205e2b822ca22262e97b8a6582fe5cb
-media/favicon.0b3bf435.ico,1764292945309,04614fc32690cb60b39e472119b7f7aa91d88eaeb8511a7489f8cbe1552e6e59
-chunks/b7d3d522b141a153.js,1764292945341,2f7e530895f432df1e996fe86ed3299a10f7ba8585e1b0d9d206f93cc228bb18
-chunks/b2d11888d122e656.js,1764292945620,f318502788773401c1f1f95699d1d3b62a6e021a53f7e2bff8fdd9a2c6cee3b7
-chunks/b297c493e9d2a547.js,1764292945333,562b883b63a596c2de501948be11263522bf0ba4c16cf2e779c6cd74cad7dcf9
-chunks/b678db9b7a6233a6.js,1764292945451,5930f7e3b147761c765cf03570bf989a4376adc16294f8e7f63041b910145c21
-chunks/aee0a3aab75c6656.js,1764292945645,74ef1a854f40fccfc42849f0eff252e9a9a099dac0fb4afa98b17e3aa773d361
-chunks/a65fa752112154b2.js,1764292945316,695a77ee322b9e0e9597b24b6efd71d653f3651aa579b479f73bf36b93c4211d
-chunks/9c2f2a94801db6cd.js,1764292945267,6bde7c92d8ae55b0bdb2f9cc508671a69314ff01c824d16ffae15b41203123a3
-chunks/8f12ba4a400d0818.js,1764292945640,9d69d3faa9752f8dcc197c2e62ea5c9a843e2431de85570ec152260f0606a64e
-chunks/9d593509176d2bdc.js,1764292945182,58f2e77ebd69f17e0e850f3dff502266bac2f51f6a66e067459ee2344c4c1823
-chunks/8f647170168e8688.js,1764292945436,6d91c1f674b325e03565f00bb0ee8f017c247a836b170067b7271c4184a76368
-chunks/8269db69d7104eaf.js,1764292945289,46b9e7e465ed39ef4b9030e292173ca01e4141f67d7290d9a8df5cb8cfdb94a4
-chunks/a3751053cf95bf66.js,1764292944936,a1864f7575f28ae1495943736f439149ff7ca641c48f352a27b8241e1c5b1895
-chunks/7f6ce89234677f07.js,1764292945596,d028183aa6747e759e6e2e2f94012efeb3c01edd9eab81cffa463ca884daa9fb
-chunks/7a5de61b06aada33.js,1764292945448,4de9756e37dc8195f5d3ab68b7d70a1228b3ef0c14fa376f7d4a41bdfdde1a4b
-chunks/7e6878fd487d3e54.js,1764292945013,f13804b8190486b356af32ba87000b503316d82c8dce6a5bca9e65e39189605d
-chunks/b72884fc3dd51b08.js,1764292944972,83adbd329c23f79df34e0bd3d9d52f2f8f888c6d6a3ff45698a67e90a3b1e485
-media/bbc41e54d2fcbd21-s.799d8ef8.woff2,1764292945043,396955195c54144bce504511dd89d0c74a3f6b453d73823073be1a2cbe00e6de
-chunks/c4e22d55290821bf.js,1764292945037,27669ee06cf5e963c5a0e12977384e4c2894b9befa9bc0d13ccc4ecc3dc2d43b
-media/caa3a2e1cccd8315-s.p.853070df.woff2,1764292945300,d38dd3d36107934ef290b2449c29728caa7bcceeb4750b0a2bec2042fac4c601
-media/797e433ab948586e-s.p.dbea232f.woff2,1764292945245,d4a2afa79a272709433753cffe4f64c13e37ae2fdfa1ded22b38c83f978b78a4
-chunks/8decf5edbe5dd12a.js,1764292945026,a9150cd9cf27455e4190ae18a8db7fca07bc34a3a8b689d1d5cb3524b13c0880
-chunks/ef5b2d67ab809f64.css,1764292945059,90165d549a46ffb7036763cb03adfd897952fc93c25201d746605436ef5ea46b
-chunks/d0f2cbc50b9d061e.js,1764292945736,3c74944275ad985170b8011410d2a5ccc6c6026cf0d4c96951b128dfb8661833
-chunks/767a4a5f6aabf6e2.js,1764292945172,bbbb2624994ac119eb5bc7a692eb7b97d3ffba0d4d8160a406aee89a81c212a8
-chunks/74c6d21fb44e33e9.js,1764292945162,7a886154420672f0f8395bdf535bd17f52063a455ede9987e3b6ea7d5bc91479
-chunks/78680bef0dd9c8e5.js,1764292945077,ec8a5a1356c00f3dbb68f2a7655a6e7ba711212a4f5ced7994a82331eb94c32a
-chunks/67c396666365a0f8.js,1764292945315,63772f51138d6a11e29d5f270f1c2c7de60f1cb1218c0d5a51488272d01f265e
-chunks/6c4b3aa006ad826c.js,1764292945616,523294bac6d6a537fc1dbfe5287ae46897908edc7e56b0fbac214b9ac3d4d6cc
-chunks/6eca49992d798c82.js,1764292944887,12d6b568615b35975b20c73435dcdc7c11a27c92ca224cae23ddd1d5461c3544
-chunks/74e1fd68a7e0896e.js,1764292944962,05e56d1e6e1fca0c5ed8e0957cf019f861c764e7221f33d0ef52116238f814bc
-chunks/5e558e7e27d2aa84.js,1764292945458,5f105cc0e1577cc8ebb66e71f6c5b8e564dd34db3a3542ada5f302e8a23e3b58
-chunks/58f8c6398723d54a.js,1764292945344,8673d8f45265a904abbae8551f71c5a5521c0725b93da73661b93af6a6583991
-chunks/5d0b6a3739039b40.js,1764292944980,bd68dfe5373212a24cb45220c93568159d5341b3198997a613fb6fd193880f98
-chunks/5db6f063644758f9.js,1764292944959,c80f1a857f8d76e5634e745e8bd7d8994e78eb33961668431cb366851cb16d5e
-chunks/523ae041bd709184.js,1764292945042,ef06a0e01cf2cc8cf4883811ef022a392f335d1d38e9a989cf62bae079deea60
-chunks/424c4036add0df26.js,1764292945340,b3e7b7f805682f0b0436474ecded19ccce5b30dcb76a8495d8bee88eede11be0
-chunks/3d3465d604d848a9.js,1764292945339,92abaf6ffd0e336dee0fdfcf6fb6243aac311a212e834594b3e7de146e5d3f6e
-chunks/334c0b45eeee04d3.js,1764292945574,874d6b35af5399e08a2cb7cc1b45cec6a7929205ae6371ef800dd924c20bc293
-chunks/3cf25d104286385c.js,1764292944988,7529dca0fda4234018cdbe2a24db01affdbf5bbe7ca6f97da46d3c1fa97ea8d4
-chunks/3e4ff1ad25a3aee4.js,1764292945339,234650a4582a73748c2f6e6d0ddc8a9aa3eb664cae80464126a35aa95f295615
-chunks/2fd974473265b3b8.js,1764292945207,fc5a8fab93123a055f6f1ca470cbb2ef213e4f075658f13038f265533887cdb8
-chunks/36fc507596e706a4.js,1764292944943,0f382cd8c81102e98bd414ca12d0e1951449a8dcaf734435e973e4e030b6a920
-chunks/211e6519dff5166f.js,1764292945000,388888d4e8a2df019664930f161a592dd7c676134ae9a2760abab30f10a1c12c
-chunks/1f21d91f935fa2f4.js,1764292945414,3bcfd338aac600b69f6c50d060739432e3c8de64fb22ef7db4e2fbc88d35199f
-chunks/483a049d7197220c.js,1764292944995,8f6f500d3b55f867e389a55ffc2a5808a01f75422e2bae4bc07e231d9f70d6f5
-chunks/1f6d845154a92f55.js,1764292945060,b9ac0f84700799143de73d09457b4973bb43f4ee0bf57ff742ef83491eef2aa5
-chunks/1f3d32af4b7e9fce.js,1764292944988,b589322566402033b3b58dd2ddba216b1819f82a8643a08877c4402c89de8cb7
-chunks/1ad9158bace97ad1.js,1764292945642,9ea54650308e293190ed8a7d7ef942e1029cc2cda2d2b03c1759fcd9b175c4e4
-chunks/0924dac1a36a5d4f.js,1764292945341,a823c1c585aea754343d4947d1c35350eec6544b9772486515756ec252992cb8
-chunks/22798aa879c2d479.js,1764292945062,a84d48fd0cb3fa97a0689f059806866fc2fe685e4c13b61b936bb13b7b729dc2
-chunks/01de74e34c8191ad.js,1764292945439,0f86a0b77fcffaa64a1869842351812295bca22dcdccf7a89616a0fe4a812848
-chunks/13c76ac4c576ebea.js,1764292945064,864fb695e806c8fd95eab7ff98fdf24c7fcab284d2eec9a2b030edea5ccd04a1
-chunks/a6dad97d9634a72d.js,1764292945738,bea630d9824beca22855271c757404b58bb7b410c52a5e7d58d69ff26d9ddd0b
-chunks/055808b7b4395593.js,1764292945729,5a06bd47cb2c83a7b4363f3d7b02796224575ffce0dc51fb66157df312ea6239
-chunks/051191fc7c032fe7.js,1764292945459,5d675199d64b330bc32091e0d807a5d807c559ebbeb535d3b81e46d9ac0beba4
-chunks/5ba52f526366ce3d.js,1764292945252,94e2ba60b4d2d276adc47cc684fe3b41b7130b438a22f13fb03240cd3079b9c1
-chunks/0839f6c03dd07402.js,1764292945458,9b607c0411e2db92fea878d5d9450aceae119f0bb4f86eef757b7f012e353ba7
-chunks/02cfabe42ac75354.js,1764292945726,edd9cc50162ed881e6f63569e677d1f330cf09a782c74dd2af3183ac20cf23ed
-chunks/770045fdeaa29947.js,1764292945315,fbff4e91fe383eb226ec79f5d7e557d3f1f798d724a8cd0da9e2606ecab997d5
-chunks/da99455a9bebb11a.js,1764292945437,96396c6b5792f967eb51bfdb0a59b2c10ef6c9cac6bab6ec346832219504f8c5
-chunks/667df385421d23bd.js,1764292945329,0f4ba7a21ad0772c31d49384e853c4f5cde23a277a76e93e14c6ffa8a4b00f1d
-media/icon.69668ad2.png,1764292945757,a9312b012897c18eb945ecc474445181c063a6ff2363fefdd295bca3e7f17a70
diff --git a/.firebase/hosting.cHVibGlj.cache b/.firebase/hosting.cHVibGlj.cache
deleted file mode 100644
index 40698028..00000000
--- a/.firebase/hosting.cHVibGlj.cache
+++ /dev/null
@@ -1,10 +0,0 @@
-window.svg,1762902124964,11deaca6eadbb148caace8a5fe4a67353112de0afc5da83005d4797e403ab4f1
-vobn-favicon.png,1763082657981,9051755f781b64be5155a8ef6b1846afae7ed12a942ce1fca217cde1fe0a4f09
-vibn-sqaure-black-logo.png,1763083818226,2cd39bf33b13110575f3a2b02b4558c5dd157d96506783526d11988a01dbe249
-vibn-logo-circle.png,1763083818571,b24c20ee6505547a3cd03a492681b281bf49c8e95eed78872e87581b5218eee2
-vibn-black-circle-logo.png,1763083818322,a9312b012897c18eb945ecc474445181c063a6ff2363fefdd295bca3e7f17a70
-vibn-2-logo.png,1763081817627,475fcbd3e4fa36dc5c63191220b5090ae90ae36d74d968240af25464829286fa
-vercel.svg,1762902124964,9a61e768442ba3450026d0d69421315044931cbffaf8f6019f856ea82dd91e4e
-next.svg,1762902124964,33c5c6ad1d08bb69d8026289530e377b4d6e2a96f24562e209fd1e1e9ccee64a
-globe.svg,1762902124964,ffe166407c928caa4d1640e2786d3385468043b3b9e6ea2282d4a3e370b3bc23
-file.svg,1762902124964,154a8c2948836a88c695a789045bc44cc74c3d8958d5785a531d26324bc42cb1
diff --git a/.firebaserc b/.firebaserc
deleted file mode 100644
index a0cc1bbc..00000000
--- a/.firebaserc
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "projects": {
- "default": "gen-lang-client-0980079410"
- }
-}
-
diff --git a/app/(onboarding)/onboarding/onboarding-build.tsx b/app/(onboarding)/onboarding/onboarding-build.tsx
new file mode 100644
index 00000000..dfba2982
--- /dev/null
+++ b/app/(onboarding)/onboarding/onboarding-build.tsx
@@ -0,0 +1,447 @@
+import React, { useState, useEffect, useRef, useMemo, useCallback } from "react";
+import { WizardTop, WizardBody, WizardQ, LANE_LABELS } from "./onboarding-primitives";
+// Build + Ready screens. The build screen shows the terminal stream + a live
+// preview stencil; ready is a quiet confirmation page with the workspace URL.
+
+// ── Per-path build plans ───────────────────────────────────────────────────
+function buildPlanFor(path, data) {
+ const common = [
+ { line: "vibn init — reading brief", ms: 600 },
+ { line: "↳ provisioning workspace", ms: 700 },
+ { line: "↳ wiring auth (email + Google)", ms: 800 },
+ { line: "↳ minting database & seed schema", ms: 700 },
+ ];
+
+ if (path === "entrepreneur") {
+ return [
+ ...common,
+ { line: `↳ generating landing page · vibe "${data.vibe || "warm"}"`, ms: 900 },
+ { line: `↳ writing copy aimed at "${(data.audience || "your audience").slice(0, 40)}"`, ms: 800 },
+ { line: "↳ wiring email capture + Stripe payment link", ms: 700 },
+ { line: "↳ scaffolding admin: subscribers, sales, comments", ms: 800 },
+ { line: `↳ tuning launch plan for goal: ${data.goal || "first_customer"}`, ms: 700 },
+ { line: "↳ publishing preview → " + workspaceUrlFor(path, data), ms: 900 },
+ { line: "ready.", ms: 400, ok: true },
+ ];
+ }
+ if (path === "owner") {
+ return [
+ ...common,
+ { line: `↳ modelling ${data.biz || "small business"} for "${data.bizName || "your business"}"`, ms: 800 },
+ { line: `↳ importing your stack (${(data.tools || []).length} tools)`, ms: 800 },
+ { line: `↳ building module: ${labelFor(data.firstThing)}`, ms: 1000 },
+ { line: "↳ generating customer + job records (10 sample)", ms: 700 },
+ { line: "↳ scheduling daily ops view + weekly report", ms: 700 },
+ { line: `↳ wiring savings tracker · est. $${(data.spend || 0)}/mo replaced`, ms: 800 },
+ { line: "↳ publishing preview → " + workspaceUrlFor(path, data), ms: 900 },
+ { line: "ready.", ms: 400, ok: true },
+ ];
+ }
+ return [
+ ...common,
+ { line: `↳ branding workspace for "${data.clientName || "client"}"`, ms: 800 },
+ { line: `↳ scaffolding scope (${(data.scope || []).length} modules)`, ms: 1000 },
+ ...(data.scope || []).slice(0, 4).map((s) => ({ line: ` • ${s}`, ms: 350 })),
+ { line: "↳ generating handoff document + invoice template", ms: 700 },
+ { line: `↳ setting deploy target: ${data.handoff || "subdomain"}`, ms: 700 },
+ { line: "↳ publishing preview → " + workspaceUrlFor(path, data), ms: 900 },
+ { line: "ready.", ms: 400, ok: true },
+ ];
+}
+
+function labelFor(id) {
+ const map = {
+ booking: "Bookings & scheduling",
+ invoicing: "Quotes & invoices",
+ customers: "Customer portal",
+ inventory: "Inventory & orders",
+ team: "Team & dispatch",
+ marketing: "Marketing site",
+ };
+ return map[id] || "your first workflow";
+}
+
+function workspaceUrlFor(path, data) {
+ const seed =
+ (path === "owner" && data.bizName) ||
+ (path === "consultant" && data.clientName) ||
+ (path === "entrepreneur" && (data.audience || data.idea)) ||
+ "your-workspace";
+ const slug = String(seed).toLowerCase()
+ .replace(/[^a-z0-9\s-]/g, "")
+ .trim()
+ .split(/\s+/)
+ .slice(0, 3)
+ .join("-")
+ .slice(0, 28) || "your-workspace";
+ return `${slug}.vibn.app`;
+}
+
+const BUILD_BIZ_LABEL = {
+ service: "Trades / services",
+ retail: "Retail",
+ food: "Food & drink",
+ appointments: "Appointments",
+ events: "Events / hospitality",
+ other: "Small business",
+};
+
+// ── Build screen ───────────────────────────────────────────────────────────
+export function BuildScreen({ path, data, onBack, onClose, onOpen }) {
+ const plan = React.useMemo(() => buildPlanFor(path, data), [path, data]);
+ const [lineIdx, setLineIdx] = React.useState(0);
+ const [done, setDone] = React.useState(false);
+ const logRef = React.useRef(null);
+
+ React.useEffect(() => {
+ if (lineIdx >= plan.length) { setDone(true); return undefined; }
+ const t = setTimeout(() => setLineIdx(lineIdx + 1), plan[lineIdx].ms);
+ return () => clearTimeout(t);
+ }, [lineIdx, plan]);
+
+ React.useEffect(() => {
+ if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
+ }, [lineIdx]);
+
+ const url = workspaceUrlFor(path, data);
+ const lane = LANE_LABELS[path];
+ const previewTitle =
+ path === "owner" ? (data.bizName || "Your business") :
+ path === "consultant" ? (data.clientName || "Your client") :
+ "Your launch page";
+ const previewSub =
+ path === "owner" ? `${BUILD_BIZ_LABEL[data.biz] || "Small business"} · ${labelFor(data.firstThing)}` :
+ path === "consultant" ? (data.industry || "Project") :
+ (data.audience || "An idea worth building").slice(0, 64);
+
+ const pct = done ? 1 : lineIdx / plan.length;
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {/* terminal */}
+
+
+
+
+
+ vibn build — {url}
+ {!done && live }
+
+
+ {plan.slice(0, lineIdx + 1).map((p, i) => {
+ const isCurrent = i === lineIdx && !done;
+ const isOk = p.ok && i <= lineIdx;
+ const cls = isOk ? "l-ok" : isCurrent ? "l-current" : "l-done";
+ return
{p.line}
;
+ })}
+ {!done &&
}
+
+
+
+ {/* preview */}
+
+
+
+
+
+
+
+ https://{url}
+
+
+
+
+ {previewTitle}
+
+
+ {previewSub}
+
+
+
+
+
+
+
+
+
+
+
+ {done ? "build complete" : <>Building {Math.min(lineIdx, plan.length)}/{plan.length} >}
+
+ {done && (
+
+ Open my workspace
+
+ )}
+
+
+
+ >
+ );
+}
+
+// ── Ready screen ───────────────────────────────────────────────────────────
+export function ReadyScreen({ path, data, onClose, onOpenChat }) {
+ const url = workspaceUrlFor(path, data);
+ const summary = summaryFor(path, data);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ Workspace URL
+
+
+ {url}
+
+
+
+ {summary.map((row, i) => (
+
+
+ {row.label}
+
+ {row.value}
+
+ ))}
+
+
+
+
+
+ >
+ );
+}
+
+function summaryFor(path, data) {
+ if (path === "owner") {
+ return [
+ { label: "Business", value: `${data.bizName || "Untitled"} · ${BUILD_BIZ_LABEL[data.biz] || "Small business"}` },
+ { label: "Replacing", value: `${(data.tools || []).length} tools · ~$${data.spend || 0}/mo` },
+ { label: "First fix", value: labelFor(data.firstThing) },
+ { label: "Team", value: `${data.team || 1} · ${(data.customers || 0).toLocaleString()} cust/mo` },
+ ];
+ }
+ if (path === "consultant") {
+ return [
+ { label: "Client", value: `${data.clientName || "Untitled"} · ${data.industry || ""}` },
+ { label: "Scope", value: `${(data.scope || []).length} modules` },
+ { label: "Brief", value: (data.brief || "").slice(0, 60) + ((data.brief || "").length > 60 ? "…" : "") },
+ { label: "Handoff", value: data.handoff || "subdomain" },
+ ];
+ }
+ return [
+ { label: "Building", value: (data.idea || "").slice(0, 64) + ((data.idea || "").length > 64 ? "…" : "") },
+ { label: "Audience", value: (data.audience || "").slice(0, 64) },
+ { label: "Goal", value: data.goal || "first_customer" },
+ { label: "Vibe", value: data.vibe || "warm" },
+ ];
+}
+
+
diff --git a/app/(onboarding)/onboarding/onboarding-consultant.tsx b/app/(onboarding)/onboarding/onboarding-consultant.tsx
new file mode 100644
index 00000000..43546f71
--- /dev/null
+++ b/app/(onboarding)/onboarding/onboarding-consultant.tsx
@@ -0,0 +1,296 @@
+import React, { useState, useEffect, useRef, useMemo, useCallback } from "react";
+import { WizardTop, WizardBody, WizardQ, WizardFooter, Label, LANE_LABELS, PresetGroup, Field } from "./onboarding-primitives";
+// Consultant path — 4 steps for freelancers building for a client.
+
+const CONS_TOTAL = 4;
+const CONS_STEP_NAMES = ["Client", "Brief", "Scope", "Handoff"];
+
+export function ConsClient({ clientName, industry, contact, onChange }) {
+ return (
+ <>
+
+
+ onChange({ clientName: e.target.value })}
+ autoFocus
+ />
+
+
+ onChange({ industry: e.target.value })}
+ />
+
+
+ onChange({ contact: e.target.value })}
+ />
+
+ >
+ );
+}
+
+const BRIEF_TEMPLATES = [
+ { id: "quote_tool", label: "Quote tool",
+ body: "Customers request a quote with a few photos and a project description. The team reviews, sends a polished PDF, customer signs and pays a deposit online." },
+ { id: "booking", label: "Booking system",
+ body: "Customers see real availability, book a service window, and get reminders. The team has a daily view of jobs with addresses and contact info." },
+ { id: "portal", label: "Customer portal",
+ body: "Logged-in customers see past jobs, invoices, documents, and can message the business. The business sees a single page per customer." },
+ { id: "internal", label: "Internal ops",
+ body: "Replace the spreadsheets the team is currently using. CRUD on jobs/customers, simple reports, role-based access, export to accounting." },
+];
+
+function ConsBrief({ brief, onChange }) {
+ return (
+ <>
+
+
+
+ {BRIEF_TEMPLATES.map((t) => (
+ onChange(t.body)}
+ >
+ {t.label}
+
+ ))}
+
+
+
+
+ >
+ );
+}
+
+const SCOPE_GROUPS = [
+ { label: "Foundations", items: ["Auth & accounts", "Roles & permissions", "Database & admin", "Hosting & domain"] },
+ { label: "Customer-facing", items: ["Marketing site", "Booking / scheduling", "Quote requests", "Customer portal", "Self-serve forms"] },
+ { label: "Money", items: ["Stripe / payments", "Invoices & receipts", "Subscriptions", "Refunds & disputes"] },
+ { label: "Ops", items: ["Dashboards & reports", "CSV import / export", "Email + SMS notifs", "Audit log"] },
+];
+
+function ConsScope({ scope, onChange }) {
+ const toggle = (item) => {
+ if (scope.includes(item)) onChange(scope.filter((x) => x !== item));
+ else onChange([...scope, item]);
+ };
+ return (
+ <>
+
+
+ {SCOPE_GROUPS.map((g) => (
+
+
+ {g.label}
+
+
+ {g.items.map((it) => {
+ const active = scope.includes(it);
+ return (
+
toggle(it)}
+ style={{
+ display: "flex", alignItems: "center", gap: 10,
+ textAlign: "left",
+ padding: "6px 4px",
+ color: active ? "var(--fg)" : "var(--fg-dim)",
+ borderRadius: 6,
+ fontSize: 13.5,
+ }}
+ >
+
+ {active && (
+
+
+
+ )}
+
+ {it}
+
+ );
+ })}
+
+
+ ))}
+
+ >
+ );
+}
+
+function ConsHandoff({ data, onChange }) {
+ return (
+ <>
+
+
+ onChange({ brand: e.target.value })}
+ />
+
+
+ onChange({ handoff: v })}
+ columns={1}
+ />
+
+
+
+ $
+ onChange({ rate: e.target.value })}
+ style={{ paddingLeft: 26, paddingRight: 58 }}
+ />
+ / hour
+
+
+ >
+ );
+}
+
+// ── Path wrapper ───────────────────────────────────────────────────────────
+export function ConsultantPath({ data, onUpdate, onBack, onClose, onComplete, onJumpToStep, step }) {
+ const next = () => {
+ if (step < CONS_TOTAL - 1) onJumpToStep(step + 1);
+ else onComplete();
+ };
+ const back = () => {
+ if (step === 0) onBack();
+ else onJumpToStep(step - 1);
+ };
+
+ let body, canNext;
+ if (step === 0) {
+ body = (
+
+ );
+ canNext = (data.clientName || "").trim().length >= 2 && (data.industry || "").trim().length >= 3;
+ } else if (step === 1) {
+ body = onUpdate({ brief: v })} />;
+ canNext = (data.brief || "").trim().length >= 30;
+ } else if (step === 2) {
+ body = onUpdate({ scope: v })} />;
+ canNext = (data.scope || []).length >= 2;
+ } else {
+ body = ;
+ canNext = !!data.handoff;
+ }
+
+ return (
+ <>
+
+
+ {body}
+
+
+ >
+ );
+}
+
+
diff --git a/app/(onboarding)/onboarding/onboarding-entrepreneur.tsx b/app/(onboarding)/onboarding/onboarding-entrepreneur.tsx
new file mode 100644
index 00000000..d38fa34e
--- /dev/null
+++ b/app/(onboarding)/onboarding/onboarding-entrepreneur.tsx
@@ -0,0 +1,276 @@
+import React, { useState, useEffect, useRef, useMemo, useCallback } from "react";
+import { WizardTop, WizardBody, WizardQ, WizardFooter, Label, LANE_LABELS, ChipGroup, PresetGroup, Field } from "./onboarding-primitives";
+// Entrepreneur path — 4 steps. Each step is a focused question.
+
+const ENTREP_TOTAL = 4;
+const ENTREP_STEP_NAMES = ["Idea", "Audience", "Goal", "Look"];
+
+const IDEA_PROMPTS = [
+ "A community for indie game devs to swap playtesters, with weekly demo nights",
+ "An AI tool that turns my handwritten recipe notes into a clean cookbook for my family",
+ "A waitlist + scheduler for my pottery studio — small classes, six people max",
+ "A subscription box service for cold-brew enthusiasts, with monthly tasting cards",
+ "A simple tool that turns my Strava data into framed art prints I can sell",
+];
+
+export function EntrepIdea({ value, onChange }) {
+ const [phIdx, setPhIdx] = React.useState(0);
+ const [phChars, setPhChars] = React.useState(0);
+ const [deleting, setDeleting] = React.useState(false);
+
+ React.useEffect(() => {
+ if (value.length > 0) return undefined;
+ const full = IDEA_PROMPTS[phIdx];
+ const speed = deleting ? 18 : 38;
+ const t = setTimeout(() => {
+ if (!deleting) {
+ if (phChars < full.length) setPhChars(phChars + 1);
+ else setTimeout(() => setDeleting(true), 1500);
+ } else {
+ if (phChars > 0) setPhChars(phChars - 1);
+ else { setDeleting(false); setPhIdx((phIdx + 1) % IDEA_PROMPTS.length); }
+ }
+ }, speed);
+ return () => clearTimeout(t);
+ }, [value, phIdx, phChars, deleting]);
+
+ return (
+ <>
+
+
+
+ {value.length} chars · be specific where it matters
+
+ >
+ );
+}
+
+const AUDIENCE_PRESETS = [
+ "Me and people like me",
+ "A small community I'm part of",
+ "Local people in my city",
+ "Anyone searching for this",
+ "Other small businesses",
+ "Hobbyists in a niche I love",
+];
+
+function EntrepAudience({ value, onChange }) {
+ const isPreset = AUDIENCE_PRESETS.includes(value);
+ return (
+ <>
+
+ onChange(arr[arr.length - 1] || "")}
+ />
+
+ onChange(e.target.value)}
+ />
+
+ >
+ );
+}
+
+const GOALS = [
+ { id: "first_customer", icon: "🎯", label: "First real customer",
+ desc: "Someone I don't know pays me. Even once." },
+ { id: "ten_users", icon: "👥", label: "Ten weekly users",
+ desc: "A signal the thing actually does something useful." },
+ { id: "mrr_1k", icon: "📈", label: "$1k MRR",
+ desc: "Enough to take it seriously." },
+ { id: "side_quit", icon: "🚪", label: "Replace my day job",
+ desc: "The long road. Make this the main thing." },
+ { id: "audience", icon: "📣", label: "Build a tiny audience",
+ desc: "200 emails, a community, something I can talk to." },
+ { id: "ship_it", icon: "🚀", label: "Just ship it",
+ desc: "I want the thing to exist." },
+];
+
+function EntrepGoal({ value, onChange }) {
+ return (
+ <>
+
+ ({
+ id: g.id, label: g.label, desc: g.desc,
+ icon: {g.icon} ,
+ }))}
+ value={value}
+ onChange={onChange}
+ columns={2}
+ />
+ >
+ );
+}
+
+const VIBES = [
+ { id: "warm", name: "Warm coral", swatch: "linear-gradient(135deg, #E27855, #B33B2A)",
+ desc: "Confident, hand-built, warm." },
+ { id: "ink", name: "Ink & paper", swatch: "linear-gradient(135deg, #1d1d1d, #4a4a4a)",
+ desc: "Editorial, serif, quiet." },
+ { id: "sage", name: "Sage matte", swatch: "linear-gradient(135deg, #7BA890, #3F6B57)",
+ desc: "Calm, modern, slightly herbal." },
+ { id: "neon", name: "Neon arcade", swatch: "linear-gradient(135deg, #5B6CFF, #FF3DDB)",
+ desc: "Loud, fun, late-night." },
+ { id: "cream", name: "Cream linen", swatch: "linear-gradient(135deg, #F2E7D5, #C9A977)",
+ desc: "Cozy and beige." },
+ { id: "later", name: "Decide later", swatch: "repeating-linear-gradient(45deg, oklch(0.30 0.010 60), oklch(0.30 0.010 60) 6px, oklch(0.22 0.010 60) 6px, oklch(0.22 0.010 60) 12px)",
+ desc: "Vibn picks one that fits." },
+];
+
+function EntrepVibe({ value, onChange }) {
+ return (
+ <>
+
+
+ {VIBES.map((v) => {
+ const active = value === v.id;
+ return (
+ onChange(v.id)}
+ style={{
+ padding: "10px 10px 10px",
+ borderRadius: 11,
+ border: `1px solid ${active ? "var(--accent)" : "var(--hairline)"}`,
+ background: active ? "oklch(0.20 0.04 35 / 0.4)" : "oklch(0.18 0.009 60 / 0.6)",
+ boxShadow: active ? "0 0 0 3px oklch(0.74 0.175 35 / 0.1)" : "none",
+ textAlign: "left",
+ color: "var(--fg)",
+ display: "flex", flexDirection: "column", gap: 8,
+ transition: "border-color .15s, background .15s",
+ }}
+ >
+
+
+ {v.name}
+
+
+ {v.desc}
+
+
+ );
+ })}
+
+ >
+ );
+}
+
+// ── Path wrapper ───────────────────────────────────────────────────────────
+export function EntrepreneurPath({ data, onUpdate, onBack, onClose, onComplete, onJumpToStep, step }) {
+ const next = () => {
+ if (step < ENTREP_TOTAL - 1) onJumpToStep(step + 1);
+ else onComplete();
+ };
+ const back = () => {
+ if (step === 0) onBack();
+ else onJumpToStep(step - 1);
+ };
+
+ let body, canNext, onSkip = null;
+ if (step === 0) {
+ body = onUpdate({ idea: v })} />;
+ canNext = (data.idea || "").trim().length >= 8;
+ } else if (step === 1) {
+ body = onUpdate({ audience: v })} />;
+ canNext = (data.audience || "").trim().length >= 3;
+ } else if (step === 2) {
+ body = onUpdate({ goal: v })} />;
+ canNext = !!data.goal;
+ } else {
+ body = onUpdate({ vibe: v })} />;
+ canNext = !!data.vibe;
+ onSkip = () => { onUpdate({ vibe: "later" }); next(); };
+ }
+
+ // 5 total: fork(1) + 4 path steps
+ return (
+ <>
+
+
+ {body}
+
+
+ >
+ );
+}
+
+
diff --git a/app/(onboarding)/onboarding/onboarding-fork.tsx b/app/(onboarding)/onboarding/onboarding-fork.tsx
new file mode 100644
index 00000000..1648ca3c
--- /dev/null
+++ b/app/(onboarding)/onboarding/onboarding-fork.tsx
@@ -0,0 +1,136 @@
+import React, { useState, useEffect, useRef, useMemo, useCallback } from "react";
+import { WizardTop, WizardBody, WizardQ, WizardFooter, Label } from "./onboarding-primitives";
+// Step 1: the only branching question — "which describes you?"
+// Quiet radio-style cards. No quotes, no marketing, no glow theatrics.
+
+const FORKS = [
+ {
+ id: "entrepreneur",
+ label: "I'm building my own thing",
+ hint: "Idea → live → first customer. You're the founder.",
+ icon: (
+
+
+
+
+ ),
+ },
+ {
+ id: "owner",
+ label: "I run a business",
+ hint: "Replace the stack of tools you currently rent.",
+ icon: (
+
+
+
+
+ ),
+ },
+ {
+ id: "consultant",
+ label: "I build for clients",
+ hint: "A workspace per client. Bill for the system, not the hours.",
+ icon: (
+
+
+
+
+ ),
+ },
+];
+
+export function ForkScreen({ name, value, onChange, onClose, onNext }) {
+ return (
+ <>
+
+
+
+
+
+ {FORKS.map((f) => {
+ const active = value === f.id;
+ return (
+
onChange(f.id)}
+ onDoubleClick={() => { onChange(f.id); onNext(); }}
+ style={{
+ display: "flex", alignItems: "center", gap: 14,
+ padding: "14px 16px",
+ borderRadius: 12,
+ border: `1px solid ${active ? "var(--accent)" : "var(--hairline)"}`,
+ background: active ? "oklch(0.20 0.04 35 / 0.4)" : "oklch(0.18 0.009 60 / 0.6)",
+ boxShadow: active ? "0 0 0 3px oklch(0.74 0.175 35 / 0.1)" : "none",
+ textAlign: "left",
+ color: "var(--fg)",
+ transition: "border-color .15s, background .15s",
+ cursor: "pointer",
+ }}
+ >
+
+ {f.icon}
+
+
+
+ {f.label}
+
+
+ {f.hint}
+
+
+
+
+ {active && (
+
+
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+ >
+ );
+}
+
+
diff --git a/app/(onboarding)/onboarding/onboarding-owner.tsx b/app/(onboarding)/onboarding/onboarding-owner.tsx
new file mode 100644
index 00000000..54a3d2cb
--- /dev/null
+++ b/app/(onboarding)/onboarding/onboarding-owner.tsx
@@ -0,0 +1,264 @@
+import React, { useState, useEffect, useRef, useMemo, useCallback } from "react";
+import { WizardTop, WizardBody, WizardQ, WizardFooter, Label, LANE_LABELS, ChipGroup, PresetGroup, Slider, Field } from "./onboarding-primitives";
+// Owner path — 4 steps for small-business owners replacing their stack.
+
+const OWNER_TOTAL = 4;
+const OWNER_STEP_NAMES = ["Business", "Stack", "First fix", "Scale"];
+
+const BIZ_KINDS = [
+ { id: "service", icon: "🛠", label: "Trades / home services", desc: "Plumbing, HVAC, landscaping, cleaning" },
+ { id: "retail", icon: "🛍", label: "Retail / shop", desc: "Vintage, boutique, market, online" },
+ { id: "food", icon: "🥐", label: "Food & drink", desc: "Café, bakery, food truck, catering" },
+ { id: "appointments", icon: "💈", label: "Appointment-based", desc: "Salon, studio, clinic, tutoring" },
+ { id: "events", icon: "🎟", label: "Events / hospitality", desc: "Venue, rental, planning" },
+ { id: "other", icon: "✦", label: "Something else", desc: "We'll learn from your answers" },
+];
+
+export function OwnerBiz({ value, name, onChange, onNameChange }) {
+ return (
+ <>
+
+ ({
+ id: b.id, label: b.label, desc: b.desc,
+ icon: {b.icon} ,
+ }))}
+ value={value}
+ onChange={onChange}
+ columns={2}
+ />
+
+ onNameChange(e.target.value)}
+ />
+
+ >
+ );
+}
+
+const STACK_TOOLS = [
+ "Square / POS",
+ "Stripe",
+ "Calendly",
+ "Acuity",
+ "Shopify",
+ "QuickBooks",
+ "Mailchimp",
+ "Instagram",
+ "Google Sheets",
+ "Notion / Airtable",
+ "Wix / Squarespace",
+ "WhatsApp / Slack",
+ "A printed binder",
+ "Head + notepad",
+];
+
+function OwnerStack({ tools, spend, onToolsChange, onSpendChange }) {
+ return (
+ <>
+
+
+
+
+
+ v === 0 ? "$0" : v === 1500 ? "$1.5k+" : `$${v}`}
+ />
+
+
+ {(tools || []).length > 0 && (
+
+
+
+ {tools.length} tool{tools.length === 1 ? "" : "s"}
+ {spend ? <> · ~${spend}/mo > : null}.
+ Replaced by one workspace, owned by you.
+
+
+ )}
+ >
+ );
+}
+
+const OWNER_FIRST_THINGS = [
+ { id: "booking", icon: "📅", label: "Bookings & scheduling", desc: "Customers book themselves." },
+ { id: "invoicing", icon: "🧾", label: "Quotes, invoices, payments", desc: "Send a quote, get paid, no chasing." },
+ { id: "customers", icon: "👥", label: "Customer history & portal", desc: "One place per customer." },
+ { id: "inventory", icon: "📦", label: "Inventory & orders", desc: "Track stock, sales, suppliers." },
+ { id: "team", icon: "🪪", label: "Team & job dispatch", desc: "Assign jobs, log hours." },
+ { id: "marketing", icon: "📣", label: "Website + email + reviews", desc: "A site that converts, list that follows up." },
+];
+
+function OwnerFirstThing({ value, onChange }) {
+ return (
+ <>
+
+ ({
+ id: f.id, label: f.label, desc: f.desc,
+ icon: {f.icon} ,
+ }))}
+ value={value}
+ onChange={onChange}
+ columns={2}
+ />
+ >
+ );
+}
+
+const OWNER_HOW_LONG = [
+ { id: "starting", label: "Just starting" },
+ { id: "1_3", label: "1–3 years" },
+ { id: "3_10", label: "3–10 years" },
+ { id: "10_plus", label: "10+ years" },
+];
+
+function OwnerScale({ customers, team, howLong, onCustomers, onTeam, onHowLong }) {
+ return (
+ <>
+
+
+ v === 0 ? "0" : v >= 2000 ? "2k+" : v.toLocaleString()}
+ />
+
+
+ v >= 50 ? "50+" : `${v}`}
+ />
+
+
+
+ {OWNER_HOW_LONG.map((h) => (
+ onHowLong(h.id)}
+ >
+ {h.label}
+
+ ))}
+
+
+ >
+ );
+}
+
+// ── Path wrapper ───────────────────────────────────────────────────────────
+export function OwnerPath({ data, onUpdate, onBack, onClose, onComplete, onJumpToStep, step }) {
+ const next = () => {
+ if (step < OWNER_TOTAL - 1) onJumpToStep(step + 1);
+ else onComplete();
+ };
+ const back = () => {
+ if (step === 0) onBack();
+ else onJumpToStep(step - 1);
+ };
+
+ let body, canNext;
+ if (step === 0) {
+ body = (
+ onUpdate({ biz: v })}
+ onNameChange={(v) => onUpdate({ bizName: v })}
+ />
+ );
+ canNext = !!data.biz && (data.bizName || "").trim().length >= 2;
+ } else if (step === 1) {
+ body = (
+ onUpdate({ tools: v })}
+ onSpendChange={(v) => onUpdate({ spend: v })}
+ />
+ );
+ canNext = (data.tools || []).length >= 1;
+ } else if (step === 2) {
+ body = onUpdate({ firstThing: v })} />;
+ canNext = !!data.firstThing;
+ } else {
+ body = (
+ onUpdate({ customers: v })}
+ onTeam={(v) => onUpdate({ team: v })}
+ onHowLong={(v) => onUpdate({ howLong: v })}
+ />
+ );
+ canNext = !!data.howLong;
+ }
+
+ return (
+ <>
+
+
+ {body}
+
+
+ >
+ );
+}
+
+
diff --git a/app/(onboarding)/onboarding/onboarding-primitives.tsx b/app/(onboarding)/onboarding/onboarding-primitives.tsx
new file mode 100644
index 00000000..b0a88c60
--- /dev/null
+++ b/app/(onboarding)/onboarding/onboarding-primitives.tsx
@@ -0,0 +1,459 @@
+import React, {
+ useState,
+ useEffect,
+ useRef,
+ useMemo,
+ useCallback,
+} from "react";
+// Shared building blocks for the onboarding flow.
+// All
- {/* Glows */}
-
-
-
-
Welcome back
-
- Sign in and keep building .
-
-
{subtitle}
-
-
- {errorHint && (
-
- {errorHint}
-
- )}
-
- {
- e.currentTarget.style.color = "var(--fg)";
- e.currentTarget.style.borderColor = "var(--hairline-2)";
- e.currentTarget.style.background = "oklch(0.22 0.010 60 / 0.8)";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = "var(--fg-dim)";
- e.currentTarget.style.borderColor = "var(--hairline)";
- e.currentTarget.style.background = "oklch(0.20 0.009 60 / 0.6)";
- }}
- onClick={handleGoogleSignIn}
- disabled={isLoading}
- >
-
-
-
-
-
-
- {isLoading ? "Signing in…" : "Continue with Google"}
-
-
- {showDevLocalSignIn && (
-
-
- Local only: sign in without Google as
-
- {process.env.NEXT_PUBLIC_DEV_LOCAL_AUTH_EMAIL}
-
-
-
-
- )}
-
-
-
+ Sign in with Google
+
);
}
diff --git a/app/styles/onboarding.css b/app/styles/onboarding.css
new file mode 100644
index 00000000..e1ab169a
--- /dev/null
+++ b/app/styles/onboarding.css
@@ -0,0 +1,677 @@
+/* Onboarding shared styles — same tokens as the rest of the site. */
+
+:root {
+ --bg: oklch(0.155 0.008 60);
+ --bg-1: oklch(0.185 0.009 60);
+ --bg-2: oklch(0.225 0.010 60);
+ --hairline: oklch(0.32 0.010 60 / 0.55);
+ --hairline-2: oklch(0.40 0.012 60 / 0.35);
+ --fg: oklch(0.97 0.005 80);
+ --fg-dim: oklch(0.78 0.006 80);
+ --fg-mute: oklch(0.58 0.006 80);
+ --fg-faint: oklch(0.42 0.006 80);
+
+ --accent: oklch(0.74 0.175 35);
+ --accent-soft: oklch(0.74 0.175 35 / 0.18);
+ --accent-glow: oklch(0.74 0.175 35 / 0.35);
+ --accent-fg: #1a0f0a;
+
+ --ok: oklch(0.78 0.16 155);
+
+ --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, sans-serif;
+ --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
+}
+
+* { box-sizing: border-box; }
+html, body { margin: 0; padding: 0; min-height: 100%; }
+body {
+ background: var(--bg);
+ color: var(--fg);
+ font-family: var(--font-sans);
+ line-height: 1.5;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ overflow-x: hidden;
+}
+body::before {
+ content: "";
+ position: fixed; inset: 0;
+ background-image:
+ linear-gradient(to right, oklch(0.30 0.01 60 / 0.10) 1px, transparent 1px),
+ linear-gradient(to bottom, oklch(0.30 0.01 60 / 0.10) 1px, transparent 1px);
+ background-size: 56px 56px;
+ mask-image: radial-gradient(ellipse 80% 80% at 50% 40%, #000 30%, transparent 80%);
+ -webkit-mask-image: radial-gradient(ellipse 80% 80% at 50% 40%, #000 30%, transparent 80%);
+ pointer-events: none;
+ z-index: 0;
+}
+body::after {
+ content: "";
+ position: fixed; inset: 0;
+ pointer-events: none;
+ z-index: 1;
+ opacity: 0.035;
+ mix-blend-mode: overlay;
+ background-image: url("data:image/svg+xml;utf8, ");
+}
+
+a { color: inherit; text-decoration: none; }
+button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
+h1, h2, h3 { margin: 0; font-weight: 500; letter-spacing: -0.02em; line-height: 1.05; }
+p { margin: 0; }
+::selection { background: var(--accent); color: var(--accent-fg); }
+
+.mono { font-family: var(--font-mono); }
+
+/* App shell */
+.app {
+ position: relative;
+ z-index: 2;
+ min-height: 100dvh;
+ display: flex; flex-direction: column;
+}
+
+.app-bar {
+ position: relative; z-index: 5;
+ padding: 20px clamp(20px, 4vw, 48px);
+ display: flex; align-items: center; justify-content: space-between;
+ border-bottom: 1px solid transparent;
+}
+.app-bar-left { display: flex; align-items: center; gap: 24px; }
+.app-step {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ color: var(--fg-faint);
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ display: inline-flex; align-items: center; gap: 8px;
+}
+.app-step::before {
+ content: "";
+ width: 5px; height: 5px; border-radius: 50%;
+ background: var(--accent);
+ box-shadow: 0 0 12px var(--accent-glow);
+}
+.app-bar-right {
+ display: flex; gap: 18px; align-items: center;
+}
+.app-bar-right a, .app-bar-right button {
+ font-size: 13px; color: var(--fg-mute);
+}
+.app-bar-right a:hover, .app-bar-right button:hover { color: var(--fg); }
+
+/* Logo */
+.logo {
+ display: inline-flex; align-items: center; gap: 9px;
+ font-weight: 600; font-size: 17px; letter-spacing: -0.02em;
+ color: var(--fg);
+}
+.logo-mark {
+ width: 26px; height: 26px; border-radius: 50%;
+ background: linear-gradient(135deg, var(--accent) 0%, oklch(0.65 0.20 18) 100%);
+ box-shadow: 0 0 22px var(--accent-glow), inset 0 1px 0 oklch(1 0 0 / 0.25);
+ display: grid; place-items: center;
+ color: var(--accent-fg);
+ flex-shrink: 0;
+}
+.logo-mark svg { display: block; }
+.logo-caret { animation: caret-blink 1.4s steps(2) infinite; }
+@keyframes caret-blink { 50% { opacity: 0.25; } }
+
+/* Main */
+.screen {
+ flex: 1;
+ position: relative;
+ padding: clamp(40px, 7vh, 80px) clamp(20px, 4vw, 48px) clamp(40px, 6vh, 60px);
+ display: flex; flex-direction: column;
+ align-items: center;
+ text-align: center;
+}
+.screen-wide {
+ align-items: stretch;
+ text-align: left;
+}
+.screen-content {
+ position: relative; z-index: 2;
+ width: 100%;
+ max-width: 720px;
+ display: flex; flex-direction: column;
+ align-items: center; text-align: center;
+}
+.screen-content-wide {
+ max-width: 1100px;
+ align-items: stretch; text-align: left;
+}
+
+/* Ambient glows */
+.glow {
+ position: absolute;
+ pointer-events: none;
+ filter: blur(20px);
+ z-index: 0;
+}
+
+/* Typography */
+.eyebrow {
+ display: inline-flex; align-items: center; gap: 10px;
+ font-family: var(--font-mono);
+ font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
+ color: var(--fg-mute);
+}
+.eyebrow::before {
+ content: ""; width: 5px; height: 5px; border-radius: 50%;
+ background: var(--accent); box-shadow: 0 0 12px var(--accent-glow);
+}
+.eyebrow-accent { color: var(--accent); }
+
+.h1 {
+ margin-top: 20px;
+ font-size: clamp(36px, 5.4vw, 64px);
+ font-weight: 500; letter-spacing: -0.03em; line-height: 1.04;
+ text-wrap: balance;
+}
+.h1 em {
+ font-style: normal;
+ color: var(--accent);
+ text-shadow: 0 0 30px var(--accent-glow);
+}
+.sub {
+ margin-top: 18px;
+ font-size: clamp(15px, 1.55vw, 18px);
+ color: var(--fg-mute);
+ line-height: 1.55;
+ text-wrap: balance;
+ max-width: 540px;
+}
+.sub b { color: var(--fg); font-weight: 500; }
+
+.tagline {
+ display: inline-flex; align-items: center; gap: 14px;
+ font-family: var(--font-mono);
+ font-size: 12px;
+ letter-spacing: 0.06em;
+ color: var(--fg-faint);
+ margin-bottom: 8px;
+}
+.tagline::before, .tagline::after {
+ content: ""; width: 28px; height: 1px;
+ background: linear-gradient(90deg, transparent, var(--hairline), transparent);
+}
+
+/* Buttons */
+.btn {
+ display: inline-flex; align-items: center; justify-content: center; gap: 10px;
+ height: 50px; padding: 0 22px;
+ border-radius: 999px;
+ font-weight: 500;
+ font-size: 15px;
+ transition: transform .12s, box-shadow .2s, background .2s, border-color .15s;
+ white-space: nowrap;
+}
+.btn-primary {
+ background: var(--accent);
+ color: var(--accent-fg);
+ box-shadow:
+ 0 0 0 1px oklch(0.84 0.16 35 / 0.5) inset,
+ 0 10px 40px -10px var(--accent-glow),
+ 0 0 40px -8px var(--accent-glow);
+}
+.btn-primary:hover { transform: translateY(-1px); }
+.btn-primary[disabled] { opacity: .55; cursor: not-allowed; transform: none; }
+.btn-primary .arrow { transition: transform .15s; }
+.btn-primary:hover .arrow { transform: translateX(3px); }
+.btn-ghost {
+ background: oklch(0.20 0.009 60 / 0.6);
+ border: 1px solid var(--hairline);
+ color: var(--fg-dim);
+}
+.btn-ghost:hover { color: var(--fg); border-color: var(--hairline-2); background: oklch(0.22 0.010 60 / 0.8); }
+
+.link-quiet {
+ font-size: 13px;
+ color: var(--fg-mute);
+ display: inline-flex; align-items: center; gap: 6px;
+ border-bottom: 1px dashed var(--hairline);
+ padding-bottom: 2px;
+}
+.link-quiet:hover { color: var(--fg); border-color: var(--accent); }
+
+/* Or divider */
+.or-divider {
+ display: flex; align-items: center; gap: 14px;
+ margin: 28px 0 18px;
+ width: 100%; max-width: 360px;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: var(--fg-faint);
+}
+.or-divider::before, .or-divider::after {
+ content: ""; flex: 1; height: 1px; background: var(--hairline);
+}
+
+/* Form */
+.field {
+ width: 100%;
+ display: flex; flex-direction: column; gap: 8px;
+ margin-top: 24px;
+ text-align: left;
+}
+.field-label {
+ font-size: 15px;
+ font-weight: 500;
+ color: var(--fg);
+ letter-spacing: -0.005em;
+}
+.field-hint {
+ font-size: 13px;
+ color: var(--fg-mute);
+ line-height: 1.5;
+}
+.input {
+ width: 100%;
+ padding: 14px 16px;
+ background: oklch(0.16 0.008 60 / 0.8);
+ border: 1px solid var(--hairline);
+ border-radius: 12px;
+ color: var(--fg);
+ font: 15px/1.5 var(--font-sans);
+ outline: none;
+ transition: border-color .15s, background .15s, box-shadow .15s;
+ resize: vertical;
+}
+.input::placeholder { color: var(--fg-faint); }
+.input:focus {
+ border-color: oklch(0.74 0.175 35 / 0.65);
+ background: oklch(0.18 0.009 60 / 0.95);
+ box-shadow: 0 0 0 3px oklch(0.74 0.175 35 / 0.12), 0 0 30px -10px var(--accent-glow);
+}
+.input-textarea { min-height: 110px; resize: vertical; }
+.input-large { padding: 20px 22px; font-size: 17px; border-radius: 16px; }
+
+/* Hero prompt input */
+.prompt {
+ width: 100%;
+ position: relative;
+ margin-top: 24px;
+}
+.prompt-frame {
+ position: relative;
+ border-radius: 22px;
+ padding: 1px;
+ background: linear-gradient(180deg,
+ oklch(0.50 0.06 35 / 0.6),
+ oklch(0.30 0.012 60 / 0.4) 40%,
+ oklch(0.25 0.012 60 / 0.4));
+ box-shadow:
+ 0 30px 80px -20px oklch(0 0 0 / 0.6),
+ 0 0 80px -20px var(--accent-glow);
+}
+.prompt-inner {
+ background: linear-gradient(180deg, oklch(0.19 0.009 60 / 0.92), oklch(0.17 0.008 60 / 0.92));
+ border-radius: 21px;
+ padding: 18px 20px 14px;
+ backdrop-filter: blur(20px);
+ display: flex; flex-direction: column;
+ gap: 12px;
+}
+.prompt-inner textarea {
+ width: 100%;
+ min-height: 92px;
+ background: transparent;
+ border: 0;
+ color: var(--fg);
+ font: 17px/1.5 var(--font-sans);
+ resize: none;
+ outline: none;
+ padding: 4px;
+}
+.prompt-typed {
+ position: absolute;
+ top: 22px; left: 24px; right: 24px;
+ pointer-events: none;
+ color: var(--fg-faint);
+ font: 17px/1.5 var(--font-sans);
+ text-align: left;
+}
+.prompt-typed::after {
+ content: "";
+ display: inline-block;
+ width: 8px; height: 18px;
+ background: var(--accent);
+ vertical-align: -3px;
+ margin-left: 2px;
+ animation: blink 1s steps(2) infinite;
+ box-shadow: 0 0 12px var(--accent-glow);
+}
+@keyframes blink { 50% { opacity: 0; } }
+.prompt-bar {
+ display: flex; align-items: center; justify-content: space-between;
+ padding-top: 8px;
+ border-top: 1px solid var(--hairline);
+}
+.prompt-hint {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ color: var(--fg-faint);
+ letter-spacing: 0.02em;
+}
+
+/* Chip / option grid */
+.chips {
+ display: flex; flex-wrap: wrap; gap: 8px;
+}
+.chip {
+ padding: 9px 14px;
+ border-radius: 999px;
+ border: 1px solid var(--hairline);
+ background: oklch(0.20 0.009 60 / 0.5);
+ color: var(--fg-dim);
+ font-size: 13.5px;
+ transition: border-color .15s, color .15s, background .15s, transform .12s;
+}
+.chip:hover { border-color: var(--hairline-2); color: var(--fg); transform: translateY(-1px); }
+.chip.active {
+ border-color: var(--accent);
+ background: oklch(0.20 0.04 35 / 0.4);
+ color: var(--fg);
+}
+
+/* Preset chips */
+.preset-row {
+ display: flex; gap: 8px; flex-wrap: wrap;
+ margin-top: 4px;
+}
+.preset-chip {
+ padding: 11px 18px;
+ border-radius: 12px;
+ border: 1px solid var(--hairline);
+ background: oklch(0.18 0.009 60 / 0.6);
+ color: var(--fg-dim);
+ font: 500 14.5px var(--font-mono);
+ letter-spacing: -0.005em;
+ transition: all .15s;
+}
+.preset-chip:hover { border-color: var(--hairline-2); color: var(--fg); }
+.preset-chip.active {
+ border-color: var(--accent);
+ background: oklch(0.20 0.04 35 / 0.4);
+ color: var(--fg);
+ box-shadow: 0 0 0 3px oklch(0.74 0.175 35 / 0.1);
+}
+
+/* Trust strip */
+.trust {
+ margin-top: 36px;
+ display: flex; gap: 14px; justify-content: center; align-items: center;
+ flex-wrap: wrap;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ letter-spacing: 0.03em;
+ color: var(--fg-faint);
+}
+.trust .sep { opacity: 0.5; }
+
+/* CTA row */
+.cta-row {
+ margin-top: 36px;
+ display: flex; gap: 14px; align-items: center; flex-wrap: wrap;
+ justify-content: center;
+}
+
+/* Spinner */
+.spinner {
+ width: 16px; height: 16px; border-radius: 50%;
+ border: 2px solid oklch(0 0 0 / 0.2);
+ border-top-color: var(--accent-fg);
+ animation: spin .9s linear infinite;
+ display: inline-block;
+}
+.spinner-line {
+ width: 12px; height: 12px;
+ border-color: var(--hairline);
+ border-top-color: var(--accent);
+}
+@keyframes spin { to { transform: rotate(360deg); } }
+
+/* Surface card */
+.surface {
+ background: linear-gradient(180deg, oklch(0.20 0.009 60 / 0.55), oklch(0.17 0.008 60 / 0.55));
+ border: 1px solid var(--hairline);
+ border-radius: 18px;
+}
+
+/* ── Wizard chrome ───────────────────────────────────────────────────── */
+/* The persistent top strip with progress bar + back + step text + close. */
+.wiz-top {
+ position: sticky; top: 0; z-index: 50;
+ background: oklch(0.155 0.008 60 / 0.85);
+ backdrop-filter: blur(14px) saturate(140%);
+ -webkit-backdrop-filter: blur(14px) saturate(140%);
+ border-bottom: 1px solid var(--hairline);
+}
+.wiz-top-row {
+ height: 54px;
+ padding: 0 clamp(16px, 3vw, 28px);
+ display: flex; align-items: center; gap: 14px;
+}
+.wiz-iconbtn {
+ width: 32px; height: 32px;
+ display: inline-flex; align-items: center; justify-content: center;
+ border-radius: 8px;
+ color: var(--fg-mute);
+ border: 1px solid transparent;
+ transition: color .15s, border-color .15s, background .15s;
+ flex-shrink: 0;
+}
+.wiz-iconbtn:hover {
+ color: var(--fg);
+ background: oklch(0.20 0.009 60 / 0.6);
+ border-color: var(--hairline);
+}
+.wiz-iconbtn[disabled] { opacity: 0; pointer-events: none; }
+
+.wiz-logo {
+ display: inline-flex; align-items: center; gap: 8px;
+ font-weight: 500; font-size: 14px; letter-spacing: -0.01em;
+ color: var(--fg);
+ flex-shrink: 0;
+}
+.wiz-logo .logo-mark { width: 22px; height: 22px; }
+
+.wiz-step {
+ flex: 1;
+ display: flex; align-items: center; gap: 10px;
+ min-width: 0;
+ justify-content: center;
+ font-family: var(--font-mono);
+ font-size: 11.5px;
+ color: var(--fg-mute);
+ letter-spacing: 0.04em;
+ overflow: hidden;
+}
+.wiz-step b { color: var(--fg); font-weight: 500; }
+.wiz-step .dot {
+ width: 4px; height: 4px; border-radius: 50%;
+ background: var(--fg-faint);
+ flex-shrink: 0;
+}
+.wiz-step .lane {
+ color: var(--accent);
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ font-size: 10.5px;
+ display: inline-flex; align-items: center; gap: 6px;
+}
+.wiz-step .lane::before {
+ content: ""; width: 5px; height: 5px; border-radius: 50%;
+ background: var(--accent); box-shadow: 0 0 10px var(--accent-glow);
+}
+
+.wiz-progress {
+ position: relative;
+ height: 2px;
+ background: oklch(0.30 0.010 60 / 0.35);
+}
+.wiz-progress-fill {
+ position: absolute; left: 0; top: 0; bottom: 0;
+ background: var(--accent);
+ box-shadow: 0 0 14px var(--accent-glow);
+ transition: width .35s cubic-bezier(.4,0,.2,1);
+}
+
+@media (max-width: 640px) {
+ .wiz-step .lane { display: none; }
+ .wiz-step .dot:first-of-type { display: none; }
+}
+
+/* ── Wizard body ─────────────────────────────────────────────────────── */
+.wiz-body {
+ flex: 1;
+ position: relative;
+ padding: clamp(40px, 7vh, 88px) clamp(20px, 4vw, 32px) clamp(40px, 6vh, 64px);
+ display: flex; flex-direction: column;
+ align-items: center;
+}
+.wiz-card {
+ width: 100%;
+ max-width: 520px;
+ display: flex; flex-direction: column;
+ gap: 28px;
+}
+.wiz-card.wide { max-width: 760px; }
+.wiz-card.xwide { max-width: 1040px; }
+
+/* Question heading — quiet, one line, no em accents */
+.wiz-q { display: flex; flex-direction: column; gap: 10px; }
+.wiz-q h2 {
+ font-size: clamp(22px, 2.4vw, 28px);
+ font-weight: 500;
+ letter-spacing: -0.018em;
+ line-height: 1.22;
+ color: var(--fg);
+ text-wrap: balance;
+}
+.wiz-q p {
+ font-size: 14.5px;
+ color: var(--fg-mute);
+ line-height: 1.55;
+ max-width: 460px;
+}
+
+/* Footer with back/continue */
+.wiz-foot {
+ display: flex; align-items: center; justify-content: space-between;
+ gap: 14px;
+ margin-top: 8px;
+}
+.wiz-foot-left {
+ display: flex; align-items: center; gap: 10px;
+ font-size: 13px;
+ color: var(--fg-mute);
+}
+.wiz-foot-right {
+ display: flex; align-items: center; gap: 12px;
+}
+.wiz-hint {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ color: var(--fg-faint);
+ letter-spacing: 0.06em;
+}
+.wiz-skip {
+ font-size: 13.5px;
+ color: var(--fg-mute);
+ padding: 8px 12px;
+ border-radius: 8px;
+}
+.wiz-skip:hover { color: var(--fg); background: oklch(0.20 0.009 60 / 0.5); }
+
+.btn-wiz {
+ height: 42px;
+ padding: 0 18px;
+ font-size: 14px;
+ border-radius: 10px;
+}
+
+/* Fields tightened up for wizard context */
+.wiz-field {
+ display: flex; flex-direction: column; gap: 8px;
+}
+.wiz-field-label {
+ font-size: 13.5px;
+ font-weight: 500;
+ color: var(--fg-dim);
+ letter-spacing: -0.005em;
+}
+.wiz-field-hint {
+ font-size: 12.5px;
+ color: var(--fg-mute);
+ line-height: 1.5;
+}
+.wiz-input {
+ width: 100%;
+ padding: 12px 14px;
+ background: oklch(0.16 0.008 60 / 0.8);
+ border: 1px solid var(--hairline);
+ border-radius: 10px;
+ color: var(--fg);
+ font: 14.5px/1.5 var(--font-sans);
+ outline: none;
+ transition: border-color .15s, background .15s, box-shadow .15s;
+}
+.wiz-input::placeholder { color: var(--fg-faint); }
+.wiz-input:focus {
+ border-color: oklch(0.74 0.175 35 / 0.6);
+ background: oklch(0.18 0.009 60 / 0.95);
+ box-shadow: 0 0 0 3px oklch(0.74 0.175 35 / 0.12);
+}
+textarea.wiz-input { min-height: 96px; resize: vertical; }
+
+/* Debug navigator panel */
+.debug {
+ position: fixed; bottom: 16px; right: 16px;
+ z-index: 1000;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ display: flex; flex-direction: column; gap: 6px;
+ align-items: flex-end;
+}
+.debug-toggle {
+ padding: 8px 12px;
+ border-radius: 999px;
+ background: oklch(0.18 0.009 60 / 0.85);
+ border: 1px solid var(--hairline);
+ color: var(--fg-mute);
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ backdrop-filter: blur(12px);
+}
+.debug-toggle:hover { color: var(--fg); border-color: var(--hairline-2); }
+.debug-panel {
+ width: 240px;
+ padding: 12px;
+ background: oklch(0.16 0.008 60 / 0.95);
+ border: 1px solid var(--hairline);
+ border-radius: 12px;
+ backdrop-filter: blur(20px);
+ display: flex; flex-direction: column; gap: 4px;
+ max-height: 60vh; overflow-y: auto;
+}
+.debug-row {
+ display: flex; align-items: center; gap: 8px;
+ padding: 6px 8px;
+ border-radius: 6px;
+ color: var(--fg-mute);
+ cursor: pointer;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ font-size: 10px;
+}
+.debug-row:hover { background: oklch(0.20 0.009 60); color: var(--fg-dim); }
+.debug-row.active {
+ background: oklch(0.74 0.175 35 / 0.18);
+ color: var(--accent);
+}
+.debug-row b { color: inherit; font-weight: 600; }
diff --git a/check-firestore-handoff.sh b/check-firestore-handoff.sh
deleted file mode 100644
index 078394f2..00000000
--- a/check-firestore-handoff.sh
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/bin/bash
-
-# Quick diagnostic to check what's in Firestore for the handoff
-
-PROJECT_ID="$1"
-
-if [ -z "$PROJECT_ID" ]; then
- echo "Usage: ./check-firestore-handoff.sh "
- echo ""
- echo "Get your project ID from the URL:"
- echo " http://localhost:3000/default/project/[PROJECT_ID]/v_ai_chat"
- exit 1
-fi
-
-echo "Checking Firestore for project: $PROJECT_ID"
-echo ""
-echo "Run this in your browser console to check the data:"
-echo ""
-echo "// Copy-paste this into browser console (F12):"
-echo "const { getFirestore, doc, getDoc } = await import('https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js');"
-echo "const db = getFirestore();"
-echo "const snap = await getDoc(doc(db, 'projects', '$PROJECT_ID'));"
-echo "console.log('Project data:', snap.data());"
-echo "console.log('Handoff:', snap.data()?.phaseData?.phaseHandoffs?.collector);"
-echo ""
-echo "Or just paste this shortened version:"
-echo ""
-echo "firebase.firestore().doc('projects/$PROJECT_ID').get().then(d => console.log('Handoff:', d.data()?.phaseData?.phaseHandoffs?.collector));"
-
diff --git a/components/OrchestratorChat.tsx b/components/OrchestratorChat.tsx
deleted file mode 100644
index ee426f4f..00000000
--- a/components/OrchestratorChat.tsx
+++ /dev/null
@@ -1,366 +0,0 @@
-"use client";
-
-import { useState, useRef, useEffect, useCallback } from "react";
-import { Textarea } from "@/components/ui/textarea";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import { ScrollArea } from "@/components/ui/scroll-area";
-import {
- Send,
- Loader2,
- Wrench,
- Bot,
- User,
- RotateCcw,
- ChevronDown,
- ChevronUp,
- Sparkles,
-} from "lucide-react";
-
-// ---------------------------------------------------------------------------
-// Types
-// ---------------------------------------------------------------------------
-
-interface Message {
- role: "user" | "assistant";
- content: string;
- toolCalls?: string[];
- turns?: number;
- model?: string;
- reasoning?: string | null;
- error?: boolean;
-}
-
-interface OrchestratorChatProps {
- projectId: string;
- projectName?: string;
- placeholder?: string;
-}
-
-// ---------------------------------------------------------------------------
-// Friendly labels for tool call names
-// ---------------------------------------------------------------------------
-
-const TOOL_LABELS: Record = {
- spawn_agent: "Dispatched agent",
- get_job_status: "Checked job",
- list_repos: "Listed repos",
- list_all_issues: "Checked issues",
- list_all_apps: "Checked deployments",
- get_app_status: "Checked app status",
- read_repo_file: "Read file",
- deploy_app: "Triggered deploy",
- gitea_create_issue: "Created issue",
- gitea_list_issues: "Listed issues",
- gitea_close_issue: "Closed issue",
- gitea_comment_issue: "Added comment",
- save_memory: "Saved to memory",
-};
-
-function friendlyToolName(raw: string): string {
- return TOOL_LABELS[raw] ?? raw.replace(/_/g, " ");
-}
-
-// ---------------------------------------------------------------------------
-// Suggestion chips shown before the first message
-// ---------------------------------------------------------------------------
-
-const SUGGESTIONS = [
- "What's the current status of this project?",
- "Check if there are any open issues or PRs",
- "What was the last deployment?",
- "Write a quick summary of what's been built so far",
-];
-
-// ---------------------------------------------------------------------------
-// Single message bubble
-// ---------------------------------------------------------------------------
-
-function MessageBubble({ msg }: { msg: Message }) {
- const [showReasoning, setShowReasoning] = useState(false);
- const isUser = msg.role === "user";
-
- return (
-
- {/* Avatar */}
-
- {isUser ? : }
-
-
- {/* Bubble */}
-
-
- {msg.content}
-
-
- {/* Tool calls & meta */}
- {!isUser && (
-
- {msg.toolCalls && msg.toolCalls.length > 0 && (
-
- {/* Deduplicate tool calls before rendering */}
- {[...new Set(msg.toolCalls)].map((t, i) => (
-
-
- {friendlyToolName(t)}
-
- ))}
-
- )}
- {msg.reasoning && (
-
setShowReasoning(v => !v)}
- className="inline-flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground transition-colors"
- >
-
- reasoning
- {showReasoning ? : }
-
- )}
- {msg.model && msg.model !== "unknown" && (
-
- {msg.model.includes("glm") ? "GLM-5" : msg.model.includes("gemini") ? "Gemini" : msg.model}
-
- )}
-
- )}
-
- {/* Reasoning panel */}
- {!isUser && showReasoning && msg.reasoning && (
-
- {msg.reasoning}
-
- )}
-
-
- );
-}
-
-// ---------------------------------------------------------------------------
-// Typing indicator
-// ---------------------------------------------------------------------------
-
-function TypingIndicator() {
- return (
-
-
-
-
-
-
- {[0, 1, 2].map(i => (
-
- ))}
-
-
-
- );
-}
-
-// ---------------------------------------------------------------------------
-// Main component
-// ---------------------------------------------------------------------------
-
-export function OrchestratorChat({
- projectId,
- projectName,
- placeholder = "Ask your AI team anything…",
-}: OrchestratorChatProps) {
- const [messages, setMessages] = useState([]);
- const [input, setInput] = useState("");
- const [loading, setLoading] = useState(false);
- const bottomRef = useRef(null);
- const textareaRef = useRef(null);
- const hasMessages = messages.length > 0;
-
- const scrollToBottom = useCallback(() => {
- bottomRef.current?.scrollIntoView({ behavior: "smooth" });
- }, []);
-
- useEffect(() => {
- scrollToBottom();
- }, [messages, loading]);
-
- const sendMessage = useCallback(
- async (text: string) => {
- const trimmed = text.trim();
- if (!trimmed || loading) return;
-
- setInput("");
- setMessages(prev => [...prev, { role: "user", content: trimmed }]);
- setLoading(true);
-
- try {
- const res = await fetch(`/api/projects/${projectId}/agent-chat`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ message: trimmed }),
- });
-
- const data = await res.json();
-
- if (!res.ok) {
- setMessages(prev => [
- ...prev,
- { role: "assistant", content: data.error ?? "Something went wrong.", error: true },
- ]);
- } else {
- setMessages(prev => [
- ...prev,
- {
- role: "assistant",
- content: data.reply || "(no reply)",
- toolCalls: data.toolCalls,
- turns: data.turns,
- model: data.model,
- reasoning: data.reasoning,
- },
- ]);
- }
- } catch (err) {
- setMessages(prev => [
- ...prev,
- {
- role: "assistant",
- content: err instanceof Error ? err.message : "Network error — is the agent runner online?",
- error: true,
- },
- ]);
- } finally {
- setLoading(false);
- setTimeout(() => textareaRef.current?.focus(), 50);
- }
- },
- [projectId, loading]
- );
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- sendMessage(input);
- }
- };
-
- const clearChat = async () => {
- setMessages([]);
- try {
- await fetch(`/api/projects/${projectId}/agent-chat`, { method: "DELETE" });
- } catch { /* best-effort */ }
- };
-
- return (
-
-
- {/* Header */}
-
-
-
-
- {projectName ? `${projectName} AI` : "Project AI"}
-
-
GLM-5
-
- {hasMessages && (
-
-
- Clear
-
- )}
-
-
- {/* Messages */}
-
- {!hasMessages ? (
- /* Empty state — Lovable-style centered prompt */
-
-
-
What should we build?
-
- Your AI team is ready. Ask them anything about this project.
-
-
-
- {SUGGESTIONS.map(s => (
- sendMessage(s)}
- className="text-xs px-3 py-1.5 rounded-full border border-border bg-muted/40 hover:bg-muted transition-colors text-muted-foreground hover:text-foreground"
- >
- {s}
-
- ))}
-
-
- ) : (
-
-
- {messages.map((msg, i) => (
-
- ))}
- {loading &&
}
-
-
-
- )}
-
-
- {/* Input */}
-
-
-
-
- Enter to send · Shift+Enter for newline
-
-
-
- );
-}
diff --git a/components/justine/JustineWorkspaceProjectsDashboard.tsx b/components/justine/JustineWorkspaceProjectsDashboard.tsx
deleted file mode 100644
index f32c1268..00000000
--- a/components/justine/JustineWorkspaceProjectsDashboard.tsx
+++ /dev/null
@@ -1,586 +0,0 @@
-"use client";
-
-import { useCallback, useEffect, useMemo, useState } from "react";
-import Link from "next/link";
-import { useSession } from "next-auth/react";
-import { Plus_Jakarta_Sans } from "next/font/google";
-import { Loader2, Trash2 } from "lucide-react";
-import { toast } from "sonner";
-import { ProjectCreationModal } from "@/components/project-creation-modal";
-import { isClientDevProjectBypass } from "@/lib/dev-bypass";
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "@/components/ui/alert-dialog";
-
-const justineJakarta = Plus_Jakarta_Sans({
- subsets: ["latin"],
- weight: ["400", "500", "600", "700", "800"],
- variable: "--font-justine-jakarta",
- display: "swap",
-});
-
-interface ProjectWithStats {
- id: string;
- productName: string;
- productVision?: string;
- status?: string;
- updatedAt: string | null;
- stats: { sessions: number; costs: number };
-}
-
-const ICON_BG = ["#6366F1", "#8B5CF6", "#06B6D4", "#EC4899", "#9CA3AF"];
-
-function timeAgo(dateStr?: string | null): string {
- if (!dateStr) return "—";
- const date = new Date(dateStr);
- if (isNaN(date.getTime())) return "—";
- const diff = (Date.now() - date.getTime()) / 1000;
- if (diff < 60) return "just now";
- if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
- if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
- const days = Math.floor(diff / 86400);
- if (days === 1) return "Yesterday";
- if (days < 7) return `${days}d ago`;
- if (days < 30) return `${Math.floor(days / 7)}w ago`;
- return `${Math.floor(days / 30)}mo ago`;
-}
-
-function greetingName(session: { user?: { name?: string | null; email?: string | null } } | null): string {
- const n = session?.user?.name?.trim();
- if (n) return n.split(/\s+/)[0] ?? "there";
- const e = session?.user?.email;
- if (e) return e.split("@")[0] ?? "there";
- return "there";
-}
-
-function greetingPrefix(): string {
- const h = new Date().getHours();
- if (h < 12) return "Good morning";
- if (h < 17) return "Good afternoon";
- return "Good evening";
-}
-
-function StatusPill({ status }: { status?: string }) {
- if (status === "live") {
- return (
-
-
- Live
-
- );
- }
- if (status === "building") {
- return (
-
-
- Building
-
- );
- }
- return Defining ;
-}
-
-export function JustineWorkspaceProjectsDashboard({ workspace }: { workspace: string }) {
- const { data: session, status: authStatus } = useSession();
- const [projects, setProjects] = useState([]);
- const [loading, setLoading] = useState(true);
- const [search, setSearch] = useState("");
- const [showNew, setShowNew] = useState(false);
- const [projectToDelete, setProjectToDelete] = useState(null);
- const [isDeleting, setIsDeleting] = useState(false);
- const [theme, setTheme] = useState<"light" | "dark">("light");
-
- useEffect(() => {
- try {
- const t = localStorage.getItem("jd-dashboard-theme");
- if (t === "dark") setTheme("dark");
- } catch {
- /* ignore */
- }
- }, []);
-
- const toggleTheme = useCallback(() => {
- setTheme((prev) => {
- const next = prev === "light" ? "dark" : "light";
- try {
- localStorage.setItem("jd-dashboard-theme", next);
- } catch {
- /* ignore */
- }
- return next;
- });
- }, []);
-
- const fetchProjects = useCallback(async () => {
- try {
- setLoading(true);
- const res = await fetch("/api/projects");
- if (!res.ok) throw new Error("Failed");
- const data = await res.json();
- setProjects(data.projects ?? []);
- } catch {
- /* silent */
- } finally {
- setLoading(false);
- }
- }, []);
-
- useEffect(() => {
- if (isClientDevProjectBypass()) {
- void fetchProjects();
- return;
- }
- if (authStatus === "authenticated") fetchProjects();
- else if (authStatus === "unauthenticated") setLoading(false);
- }, [authStatus, fetchProjects]);
-
- const filtered = useMemo(() => {
- const q = search.trim().toLowerCase();
- if (!q) return projects;
- return projects.filter((p) => p.productName.toLowerCase().includes(q));
- }, [projects, search]);
-
- const liveN = projects.filter((p) => p.status === "live").length;
- const buildingN = projects.filter((p) => p.status === "building").length;
- const totalCosts = projects.reduce((s, p) => s + (p.stats?.costs ?? 0), 0);
-
- const userInitial =
- session?.user?.name?.[0]?.toUpperCase() ?? session?.user?.email?.[0]?.toUpperCase() ?? "?";
- const displayName = session?.user?.name?.trim() || session?.user?.email?.split("@")[0] || "Account";
-
- const handleDelete = async () => {
- if (!projectToDelete) return;
- setIsDeleting(true);
- try {
- const res = await fetch("/api/projects/delete", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ projectId: projectToDelete.id }),
- });
- if (res.ok) {
- toast.success("Project deleted");
- setProjectToDelete(null);
- fetchProjects();
- } else {
- const err = await res.json();
- toast.error(err.error || "Failed to delete");
- }
- } catch {
- toast.error("An error occurred");
- } finally {
- setIsDeleting(false);
- }
- };
-
- const firstName = greetingName(session);
-
- return (
-
-
-
-
-
- V
-
-
-
- vibn
-
-
-
-
- {theme === "dark" ? "☀️ Light" : "🌙 Dark"}
-
-
-
- {userInitial}
-
-
-
{displayName}
-
Workspace · {workspace}
-
-
-
-
-
-
-
-
-
-
-
-
-
- Projects
-
-
setShowNew(true)}
- style={{
- background: "none",
- border: "none",
- cursor: "pointer",
- fontSize: 18,
- color: "var(--indigo)",
- padding: "0 4px",
- lineHeight: 1,
- fontWeight: 300,
- }}
- title="New project"
- >
- +
-
-
-
-
-
-
-
setSearch(e.target.value)}
- aria-label="Search projects"
- />
-
-
-
-
-
- {loading && (
-
-
-
- )}
- {!loading && filtered.length === 0 && projects.length === 0 && (
-
-
- ✦
-
-
No projects yet
-
- Start building your first product with vibn.
-
-
setShowNew(true)}>
- + Create first project
-
-
- )}
- {!loading && projects.length > 0 && filtered.length === 0 && (
-
No projects match your search.
- )}
- {!loading &&
- filtered.map((p, i) => (
-
-
-
- {(p.productName[0] ?? "P").toUpperCase()}
-
-
-
- {p.productName}
-
-
- {p.productVision ? `${p.productVision.slice(0, 42)}${p.productVision.length > 42 ? "…" : ""}` : "Personal"}
-
-
{timeAgo(p.updatedAt)}
-
-
-
{
- e.preventDefault();
- e.stopPropagation();
- setProjectToDelete(p);
- }}
- >
-
-
-
- ))}
-
-
-
-
- Workspace
-
-
-
↗
-
-
Activity
-
Timeline & runs
-
-
-
-
⚙
-
Settings
-
-
-
- Account
-
-
toast.message("Help — docs coming soon.")}>
- ?
- Help
-
-
-
-
-
-
-
-
-
-
- {greetingPrefix()}, {firstName}.
-
-
- Open a project from the sidebar or start a new one.
-
-
-
- setShowNew(true)}>
- + New project
-
-
-
-
- {!loading && projects.length === 0 && (
-
-
-
- ✦
-
-
- Build your first product
-
-
- Describe your idea, and vibn will architect, design, and help you ship it — no code required.
-
-
setShowNew(true)}>
- + Start a new project
-
-
-
- )}
-
-
Portfolio snapshot
-
-
-
{projects.length}
-
Active projects
-
-
-
- {liveN}
-
-
Live products
-
-
-
- {buildingN}
-
-
Building now
-
-
-
- {totalCosts > 0 ? `$${totalCosts.toFixed(2)}` : "—"}
-
-
- API spend (est.)
-
-
-
-
-
-
-
-
-
{
- setShowNew(open);
- if (!open) fetchProjects();
- }}
- workspace={workspace}
- />
-
- !open && setProjectToDelete(null)}>
-
-
- Delete "{projectToDelete?.productName}"?
-
- This will remove the project record. Sessions will be preserved but unlinked. The Gitea repo will not be deleted automatically.
-
-
-
- Cancel
-
- {isDeleting ? : }
- Delete project
-
-
-
-
-
- );
-}
diff --git a/components/layout/coo-chat.tsx b/components/layout/coo-chat.tsx
deleted file mode 100644
index 2d9a5440..00000000
--- a/components/layout/coo-chat.tsx
+++ /dev/null
@@ -1,287 +0,0 @@
-"use client";
-
-import { useState, useEffect, useRef } from "react";
-
-interface CooMessage {
- id: string;
- role: "user" | "assistant";
- content: string;
- source?: "atlas" | "coo"; // atlas = discovery history, coo = orchestrator response
- streaming?: boolean;
-}
-
-export function CooChat({ projectId }: { projectId: string }) {
- const [messages, setMessages] = useState([]);
- const [input, setInput] = useState("");
- const [loading, setLoading] = useState(false);
- const [historyLoaded, setHistoryLoaded] = useState(false);
- const bottomRef = useRef(null);
- const textareaRef = useRef(null);
-
- // Scroll to bottom whenever messages change
- useEffect(() => {
- bottomRef.current?.scrollIntoView({ behavior: "smooth" });
- }, [messages]);
-
- // Pre-load Atlas discovery history on mount
- useEffect(() => {
- fetch(`/api/projects/${projectId}/atlas-chat`)
- .then(r => r.json())
- .then((data: { messages?: Array<{ role: "user" | "assistant"; content: string }> }) => {
- const atlasMessages: CooMessage[] = (data.messages ?? [])
- .filter(m => m.content?.trim())
- .map((m, i) => ({
- id: `atlas_${i}`,
- role: m.role,
- content: m.content,
- source: "atlas" as const,
- }));
-
- if (atlasMessages.length > 0) {
- // Add a small divider message at the bottom of Atlas history
- setMessages([
- ...atlasMessages,
- {
- id: "coo_divider",
- role: "assistant",
- content: "Discovery complete. I'm your product COO — I have the full context above. What do you need?",
- source: "coo" as const,
- },
- ]);
- } else {
- // No Atlas history — show default COO welcome
- setMessages([{
- id: "welcome",
- role: "assistant",
- content: "Hi. I'm your product COO — I know your codebase, your goals, and what's been built. What do you need?",
- source: "coo" as const,
- }]);
- }
- setHistoryLoaded(true);
- })
- .catch(() => {
- setMessages([{
- id: "welcome",
- role: "assistant",
- content: "Hi. I'm your product COO — I know your codebase, your goals, and what's been built. What do you need?",
- source: "coo" as const,
- }]);
- setHistoryLoaded(true);
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [projectId]);
-
- const send = async () => {
- const text = input.trim();
- if (!text || loading) return;
- setInput("");
-
- const userMsg: CooMessage = { id: Date.now().toString(), role: "user", content: text, source: "coo" };
- const assistantId = (Date.now() + 1).toString();
- const assistantMsg: CooMessage = { id: assistantId, role: "assistant", content: "", source: "coo", streaming: true };
-
- setMessages(prev => [...prev, userMsg, assistantMsg]);
- setLoading(true);
-
- // Build history from COO messages only (skip atlas history for context to orchestrator)
- const history = messages
- .filter(m => m.source === "coo" && m.id !== "coo_divider" && m.content)
- .map(m => ({ role: m.role === "assistant" ? "model" as const : "user" as const, content: m.content }));
-
- try {
- const res = await fetch(`/api/projects/${projectId}/advisor`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ message: text, history }),
- });
-
- if (!res.ok || !res.body) {
- setMessages(prev => prev.map(m => m.id === assistantId
- ? { ...m, content: "Something went wrong. Please try again.", streaming: false }
- : m));
- return;
- }
-
- const reader = res.body.getReader();
- const decoder = new TextDecoder();
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
- const chunk = decoder.decode(value, { stream: true });
- setMessages(prev => prev.map(m => m.id === assistantId
- ? { ...m, content: m.content + chunk }
- : m));
- }
-
- setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, streaming: false } : m));
- } catch {
- setMessages(prev => prev.map(m => m.id === assistantId
- ? { ...m, content: "Connection error. Please try again.", streaming: false }
- : m));
- } finally {
- setLoading(false);
- textareaRef.current?.focus();
- }
- };
-
- if (!historyLoaded) {
- return (
-
-
- {[0, 1, 2].map(i => (
-
- ))}
-
-
- );
- }
-
- return (
-
- {/* Messages */}
-
- {messages.map((msg, idx) => {
- const isAtlas = msg.source === "atlas";
- const isUser = msg.role === "user";
- const isCoo = !isUser && !isAtlas;
-
- // Separator before the divider message
- const prevMsg = messages[idx - 1];
- const showSeparator = msg.id === "coo_divider" && prevMsg?.source === "atlas";
-
- return (
-
- {showSeparator && (
-
-
-
- Discovery · COO
-
-
-
- )}
-
-
- {/* Avatar */}
- {!isUser && (
-
- {isAtlas ? "A" : "◈"}
-
- )}
-
-
- {msg.content}
- {msg.streaming && msg.content === "" && (
-
- {[0, 1, 2].map(i => (
-
- ))}
-
- )}
- {msg.streaming && msg.content !== "" && (
-
- )}
-
-
-
- );
- })}
-
-
-
- {/* Input */}
-
-
-
-
- ↵ send · Shift+↵ newline
-
-
-
-
-
- );
-}
diff --git a/components/layout/project-shell.tsx b/components/layout/project-shell.tsx
deleted file mode 100644
index 14816cf5..00000000
--- a/components/layout/project-shell.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-"use client";
-
-import { usePathname } from "next/navigation";
-import { ReactNode, Suspense } from "react";
-import Link from "next/link";
-import { signOut, useSession } from "next-auth/react";
-import { Toaster } from "sonner";
-
-interface ProjectShellProps {
- children: ReactNode;
- workspace: string;
- projectId: string;
- projectName: string;
- projectDescription?: string;
- projectStatus?: string;
- projectProgress?: number;
- discoveryPhase?: number;
- capturedData?: Record;
- createdAt?: string;
- updatedAt?: string;
- featureCount?: number;
- creationMode?: "fresh" | "chat-import" | "code-import" | "migration";
-}
-
-const SECTIONS = [
- { id: "overview", label: "Vibn", path: "overview" },
- { id: "mvp-setup", label: "Plan", path: "mvp-setup" },
- { id: "tasks", label: "Task", path: "tasks" },
- { id: "build", label: "Build", path: "build" },
- { id: "run", label: "Run", path: "run" },
- { id: "growth", label: "Grow", path: "growth" },
- { id: "assist", label: "Assist", path: "assist" },
- { id: "analytics", label: "Analyze", path: "analytics" },
-] as const;
-
-
-function ProjectShellInner({
- children,
- workspace,
- projectId,
- projectName,
-}: ProjectShellProps) {
- const pathname = usePathname();
- const { data: session } = useSession();
-
- const activeSection =
- pathname?.includes("/overview") ? "overview" :
- pathname?.includes("/mvp-setup") ? "mvp-setup" :
- pathname?.includes("/tasks") ? "tasks" :
- pathname?.includes("/prd") ? "tasks" :
- pathname?.includes("/build") ? "build" :
- pathname?.includes("/run") ? "run" :
- pathname?.includes("/infrastructure") ? "run" :
- pathname?.includes("/growth") ? "growth" :
- pathname?.includes("/assist") ? "assist" :
- pathname?.includes("/analytics") ? "analytics" :
- "overview";
-
- const userInitial = (
- session?.user?.name?.[0] ?? session?.user?.email?.[0] ?? "?"
- ).toUpperCase();
-
- return (
- <>
-
-
- {/* ── Top bar ── */}
-
-
- {/* ── Full-width content ── */}
-
- {children}
-
-
-
-
- >
- );
-}
-
-// Wrap in Suspense because useSearchParams requires it
-export function ProjectShell(props: ProjectShellProps) {
- return (
-
-
-
- );
-}
diff --git a/components/project-association-prompt.tsx b/components/project-association-prompt.tsx
deleted file mode 100644
index b5e581c7..00000000
--- a/components/project-association-prompt.tsx
+++ /dev/null
@@ -1,306 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-import { db, auth } from '@/lib/firebase/config';
-import { collection, query, where, limit, getDocs, orderBy } from 'firebase/firestore';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog';
-import { Button } from '@/components/ui/button';
-import { FolderOpen, Plus, Link as LinkIcon } from 'lucide-react';
-import { toast } from 'sonner';
-import { ProjectCreationModal } from './project-creation-modal';
-
-interface UnassociatedWorkspace {
- workspacePath: string;
- workspaceName: string;
- sessionCount: number;
-}
-
-interface Project {
- id: string;
- name: string;
- productName: string;
- slug: string;
-}
-
-export function ProjectAssociationPrompt({ workspace }: { workspace: string }) {
- // Temporarily disabled - will be re-enabled with better UX
- return null;
-
- const [unassociatedWorkspace, setUnassociatedWorkspace] = useState(null);
- const [projects, setProjects] = useState([]);
- const [showDialog, setShowDialog] = useState(false);
- const [showCreationModal, setShowCreationModal] = useState(false);
- const [loading, setLoading] = useState(false);
- const [dismissedWorkspaces, setDismissedWorkspaces] = useState>(new Set());
- const [hasCheckedThisSession, setHasCheckedThisSession] = useState(false);
-
- // Load dismissed workspaces from localStorage on mount
- useEffect(() => {
- const stored = localStorage.getItem('dismissedWorkspaces');
- if (stored) {
- try {
- setDismissedWorkspaces(new Set(JSON.parse(stored)));
- } catch (e) {
- console.error('Error loading dismissed workspaces:', e);
- }
- }
- }, []);
-
- useEffect(() => {
- let unsubscribe: () => void;
-
- const checkForUnassociatedSessions = async (user: any) => {
- // Check if we've already shown the prompt in this browser session
- const lastPromptTime = sessionStorage.getItem('vibn_last_workspace_prompt');
- const now = Date.now();
- const fiveMinutes = 5 * 60 * 1000;
-
- if (lastPromptTime && (now - parseInt(lastPromptTime)) < fiveMinutes) {
- console.log('⏭️ Already checked recently, skipping');
- return;
- }
-
- try {
- // Mark that we've checked
- sessionStorage.setItem('vibn_last_workspace_prompt', now.toString());
-
- // Check for sessions that need project association
- const sessionsRef = collection(db, 'sessions');
- const q = query(
- sessionsRef,
- where('userId', '==', user.uid),
- where('needsProjectAssociation', '==', true),
- orderBy('createdAt', 'desc'),
- limit(1)
- );
-
- const snapshot = await getDocs(q);
-
- if (!snapshot.empty) {
- const session = snapshot.docs[0].data();
-
- // Check if this workspace was dismissed
- if (dismissedWorkspaces.has(session.workspacePath)) {
- console.log('⏭️ Workspace was dismissed, skipping prompt');
- return;
- }
-
- // Count sessions from this workspace
- const countQuery = query(
- sessionsRef,
- where('userId', '==', user.uid),
- where('workspacePath', '==', session.workspacePath),
- where('needsProjectAssociation', '==', true)
- );
- const countSnapshot = await getDocs(countQuery);
-
- setUnassociatedWorkspace({
- workspacePath: session.workspacePath,
- workspaceName: session.workspaceName || 'Unknown',
- sessionCount: countSnapshot.size,
- });
-
- // Fetch user's projects for linking
- const projectsRef = collection(db, 'projects');
- const projectsQuery = query(
- projectsRef,
- where('userId', '==', user.uid),
- orderBy('createdAt', 'desc')
- );
- const projectsSnapshot = await getDocs(projectsQuery);
-
- const userProjects = projectsSnapshot.docs.map(doc => ({
- id: doc.id,
- name: doc.data().name,
- productName: doc.data().productName,
- slug: doc.data().slug,
- }));
-
- setProjects(userProjects);
- setShowDialog(true);
- }
- } catch (error: any) {
- // Silently handle index building errors - the feature will work once indexes are ready
- if (error?.message?.includes('index')) {
- console.log('⏳ Firestore indexes are still building. Project detection will be available shortly.');
- } else {
- console.error('Error checking for unassociated sessions:', error);
- }
- }
- };
-
- unsubscribe = auth.onAuthStateChanged((user) => {
- if (user) {
- checkForUnassociatedSessions(user);
- }
- });
-
- return () => {
- if (unsubscribe) unsubscribe();
- };
- }, []); // Empty dependency array - only run once on mount
-
- const handleCreateNewProject = () => {
- setShowDialog(false);
- setShowCreationModal(true);
- };
-
- const handleRemindLater = () => {
- if (!unassociatedWorkspace) return;
-
- // Add to dismissed list
- const newDismissed = new Set(dismissedWorkspaces);
- newDismissed.add(unassociatedWorkspace.workspacePath);
- setDismissedWorkspaces(newDismissed);
-
- // Save to localStorage
- localStorage.setItem('dismissedWorkspaces', JSON.stringify(Array.from(newDismissed)));
-
- // Close dialog
- setShowDialog(false);
- setUnassociatedWorkspace(null);
-
- toast.info('💡 We\'ll remind you next time you visit');
- };
-
- const handleLinkToProject = async (projectId: string) => {
- if (!unassociatedWorkspace || !auth.currentUser) return;
-
- setLoading(true);
- try {
- const response = await fetch('/api/sessions/associate-project', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- workspacePath: unassociatedWorkspace.workspacePath,
- projectId,
- userId: auth.currentUser.uid,
- }),
- });
-
- if (response.ok) {
- const data = await response.json();
- toast.success(`✅ Linked ${data.sessionsUpdated} sessions to project!`);
- setShowDialog(false);
- setUnassociatedWorkspace(null);
- } else {
- toast.error('Failed to link sessions to project');
- }
- } catch (error) {
- console.error('Error linking project:', error);
- toast.error('An error occurred while linking');
- } finally {
- setLoading(false);
- }
- };
-
- if (!unassociatedWorkspace) return null;
-
- return (
- <>
-
-
-
-
-
- New Workspace Detected
-
-
- We detected coding activity in a new workspace
-
-
-
- {unassociatedWorkspace && (
-
-
-
📂
-
-
{unassociatedWorkspace?.workspaceName}
-
- {unassociatedWorkspace?.workspacePath}
-
-
-
-
- {unassociatedWorkspace?.sessionCount} coding session{(unassociatedWorkspace?.sessionCount || 0) > 1 ? 's' : ''} tracked
-
-
- )}
-
-
-
What would you like to do?
-
-
-
- Create New Project
-
-
- {projects.length > 0 && (
- <>
-
-
-
-
-
-
- Or link to existing project
-
-
-
-
-
- {projects.map((project) => (
- handleLinkToProject(project.id)}
- disabled={loading}
- >
-
- {project.productName}
-
- ))}
-
- >
- )}
-
-
-
-
- Remind Me Later
-
-
-
-
-
- {/* Project Creation Modal */}
- {
- setShowCreationModal(open);
- if (!open) {
- // Refresh to check for newly created project
- setUnassociatedWorkspace(null);
- }
- }}
- workspace={workspace}
- />
- >
- );
-}
-
diff --git a/components/project-main/BuildMvpJustineV2.tsx b/components/project-main/BuildMvpJustineV2.tsx
deleted file mode 100644
index 4c11c143..00000000
--- a/components/project-main/BuildMvpJustineV2.tsx
+++ /dev/null
@@ -1,1068 +0,0 @@
-"use client";
-
-import { useCallback, useEffect, useRef, useState } from "react";
-import Link from "next/link";
-import { useRouter } from "next/navigation";
-import { JM, JV } from "@/components/project-creation/modal-theme";
-
-const STEPS = [
- { l: "Creating Gitea repository", d: "Setting up version control for your project" },
- { l: "Scaffolding the app", d: "Next.js · TypeScript · Tailwind CSS" },
- { l: "Setting up your database", d: "PostgreSQL + schema based on your product plan" },
- { l: "Building sign up & login", d: "Email + Google + GitHub OAuth" },
- { l: "Wiring payments", d: "Stripe checkout, webhooks, billing portal" },
- { l: "Generating app pages", d: "Dashboard, settings, onboarding, invite flow" },
- { l: "Applying your design", d: "Theme applied across all pages" },
- { l: "Building marketing website", d: "SEO-ready marketing surface" },
- { l: "Setting up email", d: "Welcome, password reset, and marketing templates" },
- { l: "Pushing to Gitea", d: "Full codebase committed and pushed" },
- { l: "Deploying via Coolify", d: "Building Docker image, deploying to your servers" },
- { l: "Running health checks", d: "Verifying pages, auth, and payments are live" },
-] as const;
-
-type PhaseRowProps = {
- done: boolean;
- active: boolean;
- label: string;
- sub?: string;
- onClick?: () => void;
-};
-
-function PhaseRow({ done, active, label, sub, onClick }: PhaseRowProps) {
- return (
- {
- if (onClick && !active) (e.currentTarget as HTMLElement).style.background = "#F5F3FF";
- }}
- onMouseLeave={e => {
- if (!active) (e.currentTarget as HTMLElement).style.background = "transparent";
- }}
- >
-
- {done ? "✓" : active ? "▲" : ""}
-
-
-
{label}
- {sub &&
{sub}
}
-
-
- );
-}
-
-function Confetti() {
- const colors = ["#6366F1", "#818CF8", "#4338CA", "#A5B4FC", "#C7D2FE", "#FCD34D", "#34D399", "#60A5FA"];
- const pieces = Array.from({ length: 90 }, (_, i) => ({
- i,
- color: colors[i % colors.length],
- left: Math.random() * 100,
- delay: Math.random() * 1.2,
- dur: Math.random() * 2.5 + 2,
- size: Math.random() * 9 + 4,
- xDrift: (Math.random() - 0.5) * 200,
- rot: Math.random() * 360,
- br: ["50%", "3px", "0"][Math.floor(Math.random() * 3)],
- }));
- return (
-
-
- {pieces.map(p => (
-
- ))}
-
- );
-}
-
-export interface BuildMvpJustineV2Props {
- workspace: string;
- projectId: string;
- projectName: string;
- giteaRepo?: string;
- /** First webapp surface label + theme if any */
- designFeel?: string;
- designStructure?: string;
- accentLabel?: string;
- accentHex?: string;
- websiteVoice?: string;
- websiteStyle?: string;
- topicsLine?: string;
- pageColumns?: { title: string; pages: string[] }[];
- onSwitchToPreview: () => void;
-}
-
-export function BuildMvpJustineV2({
- workspace,
- projectId,
- projectName,
- giteaRepo,
- designFeel = "Friendly",
- designStructure = "Clean",
- accentLabel = "Indigo",
- accentHex = "#6366F1",
- websiteVoice = "Friendly · Balanced · Warm",
- websiteStyle = "Editorial",
- topicsLine = "The problem · Who it's for · Why now",
- pageColumns = [
- { title: "Public", pages: ["Landing page", "Pricing", "About", "Blog"] },
- { title: "Auth", pages: ["Sign up", "Log in", "Forgot password"] },
- { title: "App", pages: ["Dashboard", "Onboarding", "Settings"] },
- { title: "Payments", pages: ["Checkout", "Success", "Manage subscription"] },
- ],
- onSwitchToPreview,
-}: BuildMvpJustineV2Props) {
- const router = useRouter();
- const [uiPhase, setUiPhase] = useState<"review" | "progress" | "done">("review");
- const [curStep, setCurStep] = useState(0);
- const [building, setBuilding] = useState(false);
- const [showConfetti, setShowConfetti] = useState(false);
- const intervalRef = useRef | null>(null);
-
- const giteaWebBase = process.env.NEXT_PUBLIC_GITEA_WEB_URL ?? "https://git.vibnai.com";
- const giteaHref = giteaRepo ? `${giteaWebBase}/${giteaRepo}` : giteaWebBase;
-
- const clearBuildInterval = useCallback(() => {
- if (intervalRef.current) {
- clearInterval(intervalRef.current);
- intervalRef.current = null;
- }
- }, []);
-
- useEffect(() => () => clearBuildInterval(), [clearBuildInterval]);
-
- const startBuild = () => {
- if (building) return;
- setBuilding(true);
- setTimeout(() => {
- setUiPhase("progress");
- setCurStep(0);
- intervalRef.current = setInterval(() => {
- setCurStep(c => {
- const next = c + 1;
- if (next >= STEPS.length) {
- clearBuildInterval();
- setUiPhase("done");
- setBuilding(false);
- setShowConfetti(true);
- setTimeout(() => setShowConfetti(false), 5000);
- return STEPS.length;
- }
- return next;
- });
- }, 700);
- }, 400);
- };
-
- const renderStepRows = () =>
- STEPS.map((s, i) => {
- const done = i < curStep;
- const active = i === curStep && uiPhase === "progress";
- return (
-
-
- {done && ✓ }
- {active && (
-
- ◎
-
- )}
-
-
-
- {s.l}
-
- {(done || active) && (
-
{s.d}
- )}
-
-
- );
- });
-
- return (
- <>
-
- {showConfetti && }
-
-
-
-
- MVP Setup
-
-
-
router.push(`/${workspace}/project/${projectId}/overview`)}
- />
- router.push(`/${workspace}/project/${projectId}/tasks`)}
- />
- router.push(`/${workspace}/project/${projectId}/design`)}
- />
- router.push(`/${workspace}/project/${projectId}/growth`)}
- />
-
-
-
-
- Save & go to dashboard
-
-
-
-
-
- {uiPhase === "review" && (
-
-
Ready to build
-
- Review everything below. Once you hit Build, AI codes your full product and deploys it.
-
-
-
-
-
- What's being built
-
-
-
- {(
- [
- { icon: "⛓", k: "Sign up & login", v: "Email + social login", br: true, bb: true },
- { icon: "$", k: "Payments", v: "Subscription billing", br: false, bb: true },
- { icon: "✉", k: "Email", v: "Transactional + marketing", br: true, bb: true },
- { icon: "◧", k: "Product style", v: "Clean & focused", br: false, bb: true },
- { icon: "◉", k: "Website style", v: websiteStyle, br: true, bb: false },
- { icon: "≡", k: "Campaign topics", v: topicsLine, br: false, bb: false },
- ] as const
- ).map(cell => (
-
-
- {cell.icon}
-
-
-
- {cell.k}
-
-
{cell.v}
-
-
- ))}
-
-
-
-
-
-
- Pages
-
-
- {pageColumns.reduce((n, c) => n + c.pages.length, 0)} pages total
-
-
-
- {pageColumns.map((col, ci) => (
-
-
- {col.title}
-
-
- {col.pages.map(p => (
-
- {p}
-
-
- ))}
-
-
- ))}
-
-
-
-
-
-
- Your design
-
-
-
-
-
-
-
-
Accent
-
{accentLabel}
-
-
-
-
- ◇
-
-
-
Layout
-
{designStructure}
-
-
-
-
-
-
-
-
- Your website
-
-
-
-
-
- ✦
-
-
-
Voice
-
{websiteVoice}
-
-
-
-
- ⬡
-
-
-
- Website style
-
-
{websiteStyle}
-
-
-
-
- ◉
-
-
-
Topics
-
{topicsLine}
-
-
-
-
-
-
-
-
-
-
- You're ready to build your product
-
-
- Your app will be generated, your backend configured, and everything deployed to your infrastructure —
- fully automated, no code needed.
-
-
-
-
- What happens next
-
- {[
- { icon: "✦", t: "Generate UI & all pages", est: "~30s" },
- { icon: "⛁", t: "Set up database & backend", est: "~45s" },
- { icon: "⛓", t: "Connect auth, payments & email", est: "~30s" },
- { icon: "▲", t: "Deploy your app live", est: "~20s" },
- ].map(row => (
-
-
- {row.icon}
-
-
- {row.t}
-
-
{row.est}
-
- ))}
-
-
- Takes ~2–4 minutes · All steps run in parallel
-
-
- {building ? "Starting…" : "Build my product"}
-
-
- No code needed · You can edit everything after
-
-
-
-
- router.push(`/${workspace}/project/${projectId}/design`)}
- style={{
- background: "none",
- border: "none",
- fontFamily: JM.fontSans,
- fontSize: 12.5,
- color: JM.muted,
- cursor: "pointer",
- padding: "6px 0",
- }}
- >
- ← Go back and tweak choices
-
-
-
- )}
-
- {(uiPhase === "progress" || uiPhase === "done") && (
-
-
- {uiPhase === "done" ? (
- <>
-
🚀
-
- Your MVP is live
-
-
- Deployed to Coolify · Pushed to Gitea · Ready to share
-
- >
- ) : (
- <>
-
- Building your product…
-
-
- Step {curStep} of {STEPS.length}
-
- >
- )}
-
-
- {renderStepRows()}
-
- {uiPhase === "done" && (
-
-
-
- Your next 3 actions
-
- {[
- {
- n: "1",
- t: "Open your live app",
- d: "Share the URL with 5 real people today.",
- },
- {
- n: "2",
- t: "Sign up as a user",
- d: "Go through your own onboarding. Fix anything confusing.",
- },
- {
- n: "3",
- t: "Post your first topic",
- d: "AI has drafted your first content batch. Publish one today.",
- },
- ].map((a, i, arr) => (
-
- ))}
-
-
-
- )}
-
- )}
-
- >
- );
-}
diff --git a/components/project-main/ChatImportMain.tsx b/components/project-main/ChatImportMain.tsx
deleted file mode 100644
index d313b2f3..00000000
--- a/components/project-main/ChatImportMain.tsx
+++ /dev/null
@@ -1,330 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import { useRouter, useParams } from "next/navigation";
-
-interface AnalysisResult {
- decisions: string[];
- ideas: string[];
- openQuestions: string[];
- architecture: string[];
- targetUsers: string[];
-}
-
-interface ChatImportMainProps {
- projectId: string;
- projectName: string;
- sourceData?: { chatText?: string };
- analysisResult?: AnalysisResult;
-}
-
-type Stage = "intake" | "extracting" | "review";
-
-function EditableList({
- label,
- items,
- accent,
- onChange,
-}: {
- label: string;
- items: string[];
- accent: string;
- onChange: (items: string[]) => void;
-}) {
- const handleEdit = (i: number, value: string) => {
- const next = [...items];
- next[i] = value;
- onChange(next);
- };
- const handleDelete = (i: number) => {
- onChange(items.filter((_, idx) => idx !== i));
- };
- const handleAdd = () => {
- onChange([...items, ""]);
- };
-
- return (
-
-
- {label}
-
- {items.length === 0 && (
-
- Nothing captured.
-
- )}
- {items.map((item, i) => (
-
- handleEdit(i, e.target.value)}
- style={{
- flex: 1, padding: "7px 10px", borderRadius: 6,
- border: "1px solid #e0dcd4", background: "#faf8f5",
- fontSize: "0.81rem", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
- color: "#1a1a1a", outline: "none",
- }}
- onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
- onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
- />
- handleDelete(i)}
- style={{ background: "none", border: "none", cursor: "pointer", color: "#c5c0b8", fontSize: "0.85rem", padding: "4px 6px" }}
- onMouseEnter={e => (e.currentTarget.style.color = "#e53e3e")}
- onMouseLeave={e => (e.currentTarget.style.color = "#c5c0b8")}
- >
- ×
-
-
- ))}
-
(e.currentTarget.style.borderColor = "#b5b0a6")}
- onMouseLeave={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
- >
- + Add
-
-
- );
-}
-
-export function ChatImportMain({
- projectId,
- projectName,
- sourceData,
- analysisResult: initialResult,
-}: ChatImportMainProps) {
- const router = useRouter();
- const params = useParams();
- const workspace = params?.workspace as string;
-
- const hasChatText = !!sourceData?.chatText;
- const [stage, setStage] = useState(
- initialResult ? "review" : hasChatText ? "extracting" : "intake"
- );
- const [chatText, setChatText] = useState(sourceData?.chatText ?? "");
- const [error, setError] = useState(null);
- const [result, setResult] = useState(
- initialResult ?? { decisions: [], ideas: [], openQuestions: [], architecture: [], targetUsers: [] }
- );
-
- // Kick off extraction automatically if chatText is ready
- useEffect(() => {
- if (stage === "extracting") {
- runExtraction();
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [stage]);
-
- const runExtraction = async () => {
- setError(null);
- try {
- const res = await fetch(`/api/projects/${projectId}/analyze-chats`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ chatText }),
- });
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Extraction failed");
- setResult(data.analysisResult);
- setStage("review");
- } catch (e) {
- setError(e instanceof Error ? e.message : "Something went wrong");
- setStage("intake");
- }
- };
-
- const handlePRD = () => router.push(`/${workspace}/project/${projectId}/tasks`);
- const handleMVP = () => router.push(`/${workspace}/project/${projectId}/mvp-setup/launch`);
-
- // ── Stage: intake ─────────────────────────────────────────────────────────
- if (stage === "intake") {
- return (
-
-
-
-
- Paste your chat history
-
-
- {projectName} — Atlas will extract decisions, ideas, architecture notes, and more.
-
-
-
- {error && (
-
- {error}
-
- )}
-
-
-
- );
- }
-
- // ── Stage: extracting ─────────────────────────────────────────────────────
- if (stage === "extracting") {
- return (
-
-
-
-
-
- Analysing your chats…
-
-
- Atlas is extracting decisions, ideas, and insights
-
-
-
- );
- }
-
- // ── Stage: review ─────────────────────────────────────────────────────────
- return (
-
-
-
-
- What Atlas found
-
-
- Review and edit the extracted insights for {projectName} . These will seed your PRD or MVP plan.
-
-
-
-
- {/* Left column */}
-
-
- setResult(r => ({ ...r, decisions: items }))}
- />
-
-
- setResult(r => ({ ...r, ideas: items }))}
- />
-
-
- {/* Right column */}
-
-
- setResult(r => ({ ...r, openQuestions: items }))}
- />
-
-
- setResult(r => ({ ...r, architecture: items }))}
- />
-
-
- setResult(r => ({ ...r, targetUsers: items }))}
- />
-
-
-
-
- {/* Decision buttons */}
-
-
-
Ready to move forward?
-
Choose how you want to proceed with {projectName}.
-
-
- (e.currentTarget.style.opacity = "0.88")}
- onMouseLeave={e => (e.currentTarget.style.opacity = "1")}
- >
- Generate PRD →
-
- (e.currentTarget.style.background = "rgba(255,255,255,0.08)")}
- onMouseLeave={e => (e.currentTarget.style.background = "transparent")}
- >
- Plan MVP →
-
-
-
-
-
- );
-}
diff --git a/components/project-main/CodeImportMain.tsx b/components/project-main/CodeImportMain.tsx
deleted file mode 100644
index ee7c3077..00000000
--- a/components/project-main/CodeImportMain.tsx
+++ /dev/null
@@ -1,363 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import { useRouter, useParams } from "next/navigation";
-
-interface ArchRow {
- category: string;
- item: string;
- status: "found" | "partial" | "missing";
- detail?: string;
-}
-
-interface AnalysisResult {
- summary: string;
- rows: ArchRow[];
- suggestedSurfaces: string[];
-}
-
-interface CodeImportMainProps {
- projectId: string;
- projectName: string;
- sourceData?: { repoUrl?: string };
- analysisResult?: AnalysisResult;
- creationStage?: string;
-}
-
-type Stage = "input" | "cloning" | "mapping" | "surfaces";
-
-const STATUS_COLORS = {
- found: { bg: "#f0fdf4", text: "#15803d", label: "Found" },
- partial: { bg: "#fffbeb", text: "#b45309", label: "Partial" },
- missing: { bg: "#fff1f2", text: "#be123c", label: "Missing" },
-};
-
-const CATEGORY_ORDER = [
- "Tech Stack", "Infrastructure", "Database", "API Surface",
- "Frontend", "Auth", "Third-party", "Missing / Gaps",
-];
-
-const PROGRESS_STEPS = [
- { key: "cloning", label: "Cloning repository" },
- { key: "reading", label: "Reading key files" },
- { key: "analyzing", label: "Mapping architecture" },
- { key: "done", label: "Analysis complete" },
-];
-
-export function CodeImportMain({
- projectId,
- projectName,
- sourceData,
- analysisResult: initialResult,
- creationStage,
-}: CodeImportMainProps) {
- const router = useRouter();
- const params = useParams();
- const workspace = params?.workspace as string;
-
- const hasRepo = !!sourceData?.repoUrl;
- const getInitialStage = (): Stage => {
- if (initialResult) return "mapping";
- if (creationStage === "surfaces") return "surfaces";
- if (hasRepo) return "cloning";
- return "input";
- };
-
- const [stage, setStage] = useState(getInitialStage);
- const [repoUrl, setRepoUrl] = useState(sourceData?.repoUrl ?? "");
- const [progressStep, setProgressStep] = useState("cloning");
- const [error, setError] = useState(null);
- const [result, setResult] = useState(initialResult ?? null);
- const [confirmedSurfaces, setConfirmedSurfaces] = useState(
- initialResult?.suggestedSurfaces ?? []
- );
-
- // Kick off analysis when in cloning stage
- useEffect(() => {
- if (stage !== "cloning") return;
- startAnalysis();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [stage]);
-
- // Poll for analysis status when cloning
- useEffect(() => {
- if (stage !== "cloning") return;
- const interval = setInterval(async () => {
- try {
- const res = await fetch(`/api/projects/${projectId}/analysis-status`);
- const data = await res.json();
- setProgressStep(data.stage ?? "cloning");
- if (data.stage === "done" && data.analysisResult) {
- setResult(data.analysisResult);
- setConfirmedSurfaces(data.analysisResult.suggestedSurfaces ?? []);
- clearInterval(interval);
- setStage("mapping");
- }
- } catch { /* keep polling */ }
- }, 2500);
- return () => clearInterval(interval);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [stage]);
-
- const startAnalysis = async () => {
- setError(null);
- try {
- await fetch(`/api/projects/${projectId}/analyze-repo`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ repoUrl }),
- });
- } catch (e) {
- setError(e instanceof Error ? e.message : "Failed to start analysis");
- setStage("input");
- }
- };
-
- const handleConfirmSurfaces = async () => {
- try {
- await fetch(`/api/projects/${projectId}/design-surfaces`, {
- method: "PATCH",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ surfaces: confirmedSurfaces }),
- });
- router.push(`/${workspace}/project/${projectId}/design`);
- } catch { /* navigate anyway */ }
- };
-
- const toggleSurface = (s: string) => {
- setConfirmedSurfaces(prev =>
- prev.includes(s) ? prev.filter(x => x !== s) : [...prev, s]
- );
- };
-
- // ── Stage: input ──────────────────────────────────────────────────────────
- if (stage === "input") {
- const isValid = repoUrl.trim().startsWith("http");
- return (
-
-
-
-
- Import your repository
-
-
- {projectName} — paste a clone URL to map your existing stack.
-
-
- {error && (
-
- {error}
-
- )}
-
- Repository URL (HTTPS)
-
-
setRepoUrl(e.target.value)}
- placeholder="https://github.com/yourorg/your-repo"
- style={{
- width: "100%", padding: "12px 14px", marginBottom: 16,
- borderRadius: 8, border: "1px solid #e0dcd4",
- background: "#faf8f5", fontSize: "0.9rem",
- fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", color: "#1a1a1a",
- outline: "none", boxSizing: "border-box",
- }}
- onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
- onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
- onKeyDown={e => { if (e.key === "Enter" && isValid) setStage("cloning"); }}
- autoFocus
- />
-
- Atlas will clone and map your stack — tech, database, auth, APIs, and what's missing for a complete go-to-market build.
-
-
{ if (isValid) setStage("cloning"); }}
- disabled={!isValid}
- style={{
- width: "100%", padding: "13px", borderRadius: 8, border: "none",
- background: isValid ? "#1a1a1a" : "#e0dcd4",
- color: isValid ? "#fff" : "#b5b0a6",
- fontSize: "0.9rem", fontWeight: 600, fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
- cursor: isValid ? "pointer" : "not-allowed",
- }}
- >
- Map this repo →
-
-
-
- );
- }
-
- // ── Stage: cloning ────────────────────────────────────────────────────────
- if (stage === "cloning") {
- const currentIdx = PROGRESS_STEPS.findIndex(s => s.key === progressStep);
- return (
-
-
-
-
-
- Mapping your codebase
-
-
- {repoUrl || sourceData?.repoUrl || "Repository"}
-
-
- {PROGRESS_STEPS.map((step, i) => {
- const done = i < currentIdx;
- const active = i === currentIdx;
- return (
-
-
- {done ? "✓" : active ? : ""}
-
-
- {step.label}
-
-
- );
- })}
-
-
-
- );
- }
-
- // ── Stage: mapping ────────────────────────────────────────────────────────
- if (stage === "mapping" && result) {
- const byCategory: Record = {};
- for (const row of result.rows) {
- const cat = row.category || "Other";
- if (!byCategory[cat]) byCategory[cat] = [];
- byCategory[cat].push(row);
- }
- const categories = [
- ...CATEGORY_ORDER.filter(c => byCategory[c]),
- ...Object.keys(byCategory).filter(c => !CATEGORY_ORDER.includes(c)),
- ];
-
- return (
-
-
-
-
- Architecture map
-
-
- {projectName} — {result.summary}
-
-
-
-
- {categories.map((cat, catIdx) => (
-
- {catIdx > 0 &&
}
-
- {cat}
-
- {byCategory[cat].map((row, i) => {
- const sc = STATUS_COLORS[row.status];
- return (
-
-
{row.item}
- {row.detail &&
{row.detail}
}
-
- {sc.label}
-
-
- );
- })}
-
- ))}
-
-
-
setStage("surfaces")}
- style={{
- width: "100%", padding: "13px", borderRadius: 8, border: "none",
- background: "#1a1a1a", color: "#fff",
- fontSize: "0.9rem", fontWeight: 600, fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", cursor: "pointer",
- }}
- onMouseEnter={e => (e.currentTarget.style.opacity = "0.88")}
- onMouseLeave={e => (e.currentTarget.style.opacity = "1")}
- >
- Choose what to build next →
-
-
-
- );
- }
-
- // ── Stage: surfaces ───────────────────────────────────────────────────────
- const SURFACE_OPTIONS = [
- { id: "marketing", label: "Marketing Site", icon: "◎", desc: "Landing page, pricing, blog" },
- { id: "web-app", label: "Web App", icon: "⬡", desc: "Core SaaS product with auth" },
- { id: "admin", label: "Admin Panel", icon: "◫", desc: "Ops dashboard, content management" },
- { id: "api", label: "API Layer", icon: "⌁", desc: "REST/GraphQL endpoints" },
- ];
-
- return (
-
-
-
-
- What should Atlas build?
-
-
- Based on the gap analysis, Atlas suggests the surfaces below. Confirm or adjust.
-
-
-
- {SURFACE_OPTIONS.map(s => {
- const selected = confirmedSurfaces.includes(s.id);
- return (
-
toggleSurface(s.id)}
- style={{
- padding: "18px", borderRadius: 10, textAlign: "left",
- border: `2px solid ${selected ? "#1a1a1a" : "#e8e4dc"}`,
- background: selected ? "#1a1a1a08" : "#fff",
- cursor: "pointer", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
- transition: "all 0.12s",
- }}
- onMouseEnter={e => { if (!selected) e.currentTarget.style.borderColor = "#d0ccc4"; }}
- onMouseLeave={e => { if (!selected) e.currentTarget.style.borderColor = "#e8e4dc"; }}
- >
- {s.icon}
- {s.label}
- {s.desc}
-
- );
- })}
-
-
0 ? "#1a1a1a" : "#e0dcd4",
- color: confirmedSurfaces.length > 0 ? "#fff" : "#b5b0a6",
- fontSize: "0.9rem", fontWeight: 600, fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
- cursor: confirmedSurfaces.length > 0 ? "pointer" : "not-allowed",
- }}
- >
- Go to Design →
-
-
-
- );
-}
diff --git a/components/project-main/FreshIdeaMain.tsx b/components/project-main/FreshIdeaMain.tsx
deleted file mode 100644
index b2dbb0dc..00000000
--- a/components/project-main/FreshIdeaMain.tsx
+++ /dev/null
@@ -1,705 +0,0 @@
-"use client";
-
-import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
-import { AtlasChat } from "@/components/AtlasChat";
-import { useRouter, useParams } from "next/navigation";
-import Link from "next/link";
-import { ArrowUpDown, Filter, LayoutPanelLeft, Search } from "lucide-react";
-import { JM, JV } from "@/components/project-creation/modal-theme";
-import {
- type ChatContextRef,
- contextRefKey,
-} from "@/lib/chat-context-refs";
-
-const DISCOVERY_PHASES = [
- "big_picture",
- "users_personas",
- "features_scope",
- "business_model",
- "screens_data",
- "risks_questions",
-] as const;
-
-const PHASE_DISPLAY: Record = {
- big_picture: "Big picture",
- users_personas: "Users & personas",
- features_scope: "Features & scope",
- business_model: "Business model",
- screens_data: "Screens & data",
- risks_questions: "Risks & questions",
-};
-
-// Maps discovery phases → the PRD sections they populate
-const PRD_SECTIONS: { label: string; phase: string | null }[] = [
- { label: "Executive Summary", phase: "big_picture" },
- { label: "Problem Statement", phase: "big_picture" },
- { label: "Vision & Success Metrics", phase: "big_picture" },
- { label: "Users & Personas", phase: "users_personas" },
- { label: "User Flows", phase: "users_personas" },
- { label: "Feature Requirements", phase: "features_scope" },
- { label: "Screen Specs", phase: "features_scope" },
- { label: "Business Model", phase: "business_model" },
- { label: "Integrations & Dependencies", phase: "screens_data" },
- { label: "Non-Functional Reqs", phase: "features_scope" },
- { label: "Risks & Mitigations", phase: "risks_questions" },
- { label: "Open Questions", phase: "risks_questions" },
-];
-
-type SidebarTab = "tasks" | "phases";
-type GroupBy = "none" | "phase" | "status";
-
-function sectionDone(
- phase: string | null,
- savedPhaseIds: Set,
- allDone: boolean
-): boolean {
- return phase === null ? allDone : savedPhaseIds.has(phase);
-}
-
-interface FreshIdeaMainProps {
- projectId: string;
- projectName: string;
-}
-
-export function FreshIdeaMain({ projectId, projectName }: FreshIdeaMainProps) {
- const router = useRouter();
- const params = useParams();
- const workspace = params?.workspace as string;
-
- const [savedPhaseIds, setSavedPhaseIds] = useState>(new Set());
- const [allDone, setAllDone] = useState(false);
- const [prdLoading, setPrdLoading] = useState(false);
- const [dismissed, setDismissed] = useState(false);
- const [hasPrd, setHasPrd] = useState(false);
- const [sidebarTab, setSidebarTab] = useState("tasks");
- const [sectionSearch, setSectionSearch] = useState("");
- const [phaseScope, setPhaseScope] = useState("all");
- const [groupBy, setGroupBy] = useState("none");
- const [pendingOnly, setPendingOnly] = useState(false);
- const [sortAlpha, setSortAlpha] = useState(false);
- const [chatContextRefs, setChatContextRefs] = useState([]);
-
- const addSectionToChat = useCallback((label: string, phase: string | null) => {
- setChatContextRefs(prev => {
- const next: ChatContextRef = { kind: "section", label, phaseId: phase };
- const k = contextRefKey(next);
- if (prev.some(r => contextRefKey(r) === k)) return prev;
- return [...prev, next];
- });
- }, []);
-
- const addPhaseToChat = useCallback((phaseId: string, label: string) => {
- setChatContextRefs(prev => {
- const next: ChatContextRef = { kind: "phase", phaseId, label };
- const k = contextRefKey(next);
- if (prev.some(r => contextRefKey(r) === k)) return prev;
- return [...prev, next];
- });
- }, []);
-
- const removeChatContextRef = useCallback((key: string) => {
- setChatContextRefs(prev => prev.filter(r => contextRefKey(r) !== key));
- }, []);
-
- useEffect(() => {
- // Check if PRD already exists on the project
- fetch(`/api/projects/${projectId}`)
- .then(r => r.json())
- .then(d => { if (d.project?.prd) setHasPrd(true); })
- .catch(() => {});
-
- const poll = () => {
- fetch(`/api/projects/${projectId}/save-phase`)
- .then(r => r.json())
- .then(d => {
- const ids = new Set((d.phases ?? []).map((p: { phase: string }) => p.phase));
- setSavedPhaseIds(ids);
- const done = DISCOVERY_PHASES.every(id => ids.has(id));
- setAllDone(done);
- })
- .catch(() => {});
- };
- poll();
- const interval = setInterval(poll, 8_000);
- return () => clearInterval(interval);
- }, [projectId]);
-
- const handleGeneratePRD = async () => {
- if (prdLoading) return;
- setPrdLoading(true);
- try {
- router.push(`/${workspace}/project/${projectId}/tasks`);
- } finally {
- setPrdLoading(false);
- }
- };
-
- const handleMVP = () => {
- router.push(`/${workspace}/project/${projectId}/mvp-setup/launch`);
- };
-
- // PRD exists — show a thin notice bar at the top, then keep the chat fully accessible
-
- const completedSections = PRD_SECTIONS.filter(({ phase }) =>
- phase === null ? allDone : savedPhaseIds.has(phase)
- ).length;
- const totalSections = PRD_SECTIONS.length;
-
- const filteredSections = useMemo(() => {
- const q = sectionSearch.trim().toLowerCase();
- let rows = PRD_SECTIONS.map((s, index) => ({ ...s, index }));
- if (q) {
- rows = rows.filter(r => r.label.toLowerCase().includes(q));
- }
- if (phaseScope !== "all") {
- rows = rows.filter(r => r.phase === phaseScope);
- }
- if (pendingOnly) {
- rows = rows.filter(r => !sectionDone(r.phase, savedPhaseIds, allDone));
- }
- if (sortAlpha) {
- rows = [...rows].sort((a, b) => a.label.localeCompare(b.label));
- } else {
- rows = [...rows].sort((a, b) => a.index - b.index);
- }
- return rows;
- }, [sectionSearch, phaseScope, pendingOnly, sortAlpha, savedPhaseIds, allDone]);
-
- const effectiveGroupBy: GroupBy = sidebarTab === "phases" ? "phase" : groupBy;
-
- return (
-
-
- {/* ── Left: Atlas chat (Justine describe column) ── */}
-
-
- {hasPrd && (
-
-
- ✦ PRD saved — keep refining here or open the full document.
-
-
- View PRD →
-
-
- )}
-
- {allDone && !dismissed && !hasPrd && (
-
-
-
- ✦ Discovery complete — what's next?
-
-
- All 6 phases captured. Generate your PRD or open the MVP plan flow.
-
-
-
-
- {prdLoading ? "Navigating…" : "Generate PRD →"}
-
-
- Plan MVP →
-
- setDismissed(true)}
- style={{
- background: "none", border: "none", cursor: "pointer",
- color: "rgba(255,255,255,0.55)", fontSize: 18, padding: "4px 6px",
- }}
- title="Dismiss"
- >
- ×
-
-
-
- )}
-
-
-
-
- {/* ── Right: Teams-style task rail (requirements = PRD sections as tasks) ── */}
-
- {/* Tab bar */}
-
-
-
-
- {([
- { id: "tasks" as const, label: "Tasks" },
- { id: "phases" as const, label: "Phases" },
- ]).map(t => {
- const active = sidebarTab === t.id;
- return (
- setSidebarTab(t.id)}
- style={{
- padding: "10px 12px 8px",
- border: "none",
- background: "none",
- cursor: "pointer",
- fontSize: 13,
- fontWeight: active ? 600 : 500,
- color: active ? JM.ink : JM.muted,
- fontFamily: JM.fontSans,
- borderBottom: active ? `2px solid ${JM.indigo}` : "2px solid transparent",
- marginBottom: -1,
- }}
- >
- {t.label}
-
- );
- })}
-
-
- {/* Search + tools */}
-
-
-
setSectionSearch(e.target.value)}
- placeholder="Search sections…"
- aria-label="Search sections"
- style={{
- flex: 1, minWidth: 0,
- border: "none", background: "transparent",
- fontSize: 12, fontFamily: JM.fontSans,
- color: JM.ink, outline: "none",
- }}
- />
-
setSortAlpha(s => !s)}
- style={{
- border: "none", background: sortAlpha ? JV.violetTint : "transparent",
- borderRadius: 6, padding: 6, cursor: "pointer", color: JM.mid,
- }}
- >
-
-
-
setPendingOnly(p => !p)}
- style={{
- border: "none", background: pendingOnly ? JV.violetTint : "transparent",
- borderRadius: 6, padding: 6, cursor: "pointer", color: JM.mid,
- }}
- >
-
-
-
-
- {/* Scope + group (Tasks tab only shows group pills; Phases tab locks grouping) */}
-
-
setPhaseScope(e.target.value)}
- aria-label="Filter by discovery phase"
- style={{
- width: "100%",
- padding: "8px 10px",
- borderRadius: 8,
- border: `1px solid ${JM.border}`,
- background: "#fff",
- fontSize: 12,
- fontFamily: JM.fontSans,
- color: JM.ink,
- marginBottom: 8,
- cursor: "pointer",
- }}
- >
- All sections
- {DISCOVERY_PHASES.map(p => (
- {PHASE_DISPLAY[p]}
- ))}
-
- {sidebarTab === "tasks" && (
-
-
- Group by
-
- {([
- { id: "none" as const, label: "None" },
- { id: "phase" as const, label: "Phase" },
- { id: "status" as const, label: "Status" },
- ]).map(opt => {
- const on = groupBy === opt.id;
- return (
- setGroupBy(opt.id)}
- style={{
- padding: "4px 10px",
- borderRadius: 999,
- border: `1px solid ${on ? JM.indigo : JM.border}`,
- background: on ? JV.violetTint : "#fff",
- fontSize: 11,
- fontWeight: on ? 600 : 500,
- color: on ? JM.indigo : JM.mid,
- fontFamily: JM.fontSans,
- cursor: "pointer",
- }}
- >
- {opt.label}
-
- );
- })}
-
- )}
- {sidebarTab === "phases" && (
-
- Grouped by discovery phase
-
- )}
-
-
- {/* Progress summary */}
-
-
-
- {completedSections} of {totalSections} sections · Requirements task
-
-
- Click a section row or phase header to attach it to your next message.
-
-
-
- {/* Task list */}
-
- {(() => {
- const rows = filteredSections;
- if (rows.length === 0) {
- return (
-
- No sections match your search or filters.
-
- );
- }
-
- const renderRow = (label: string, phase: string | null, key: string) => {
- const isDone = sectionDone(phase, savedPhaseIds, allDone);
- const phaseSlug = phase ? phase.replace(/_/g, "-") : "prd";
- const phaseLine = phase ? PHASE_DISPLAY[phase] ?? phase : "PRD";
- return (
-
addSectionToChat(label, phase)}
- style={{
- padding: "10px 12px",
- borderBottom: `1px solid rgba(229,231,235,0.85)`,
- borderTop: "none",
- borderLeft: "none",
- borderRight: "none",
- display: "flex", gap: 10, alignItems: "flex-start",
- background: isDone ? "rgba(237,233,254,0.55)" : "transparent",
- width: "100%",
- textAlign: "left",
- cursor: "pointer",
- font: "inherit",
- }}
- >
-
- {isDone ? "✓" : ""}
-
-
-
- {label}
-
-
-
- {phaseSlug}
-
-
- {isDone ? "Done" : "Pending"}
-
-
-
- Discovery · {phaseLine}
- {!isDone ? " · complete in chat" : ""}
-
-
-
- );
- };
-
- if (effectiveGroupBy === "none") {
- return rows.map(r => renderRow(r.label, r.phase, `${r.label}-${r.index}`));
- }
-
- if (effectiveGroupBy === "phase") {
- const byPhase = new Map
();
- for (const r of rows) {
- const pk = r.phase ?? "null";
- if (!byPhase.has(pk)) byPhase.set(pk, []);
- byPhase.get(pk)!.push(r);
- }
- const order = [...DISCOVERY_PHASES, "null"];
- return order.flatMap(pk => {
- const list = byPhase.get(pk);
- if (!list?.length) return [];
- const header = pk === "null" ? "Final" : PHASE_DISPLAY[pk] ?? pk;
- const phaseClickable = pk !== "null";
- return [
- phaseClickable ? (
- addPhaseToChat(pk, header)}
- style={{
- display: "block",
- width: "100%",
- padding: "8px 12px 6px",
- fontSize: 10,
- fontWeight: 700,
- letterSpacing: "0.06em",
- textTransform: "uppercase",
- color: JM.muted,
- fontFamily: JM.fontSans,
- background: "#EDE9FE",
- border: "none",
- borderBottom: `1px solid ${JM.border}`,
- cursor: "pointer",
- textAlign: "left",
- }}
- >
- {header}
-
- ) : (
-
- {header}
-
- ),
- ...list.map(r => renderRow(r.label, r.phase, `${r.label}-${r.index}`)),
- ];
- });
- }
-
- const doneRows = rows.filter(r => sectionDone(r.phase, savedPhaseIds, allDone));
- const todoRows = rows.filter(r => !sectionDone(r.phase, savedPhaseIds, allDone));
- const statusBlocks: ReactNode[] = [];
- if (todoRows.length > 0) {
- statusBlocks.push(
-
- To do
-
- );
- todoRows.forEach(r => {
- statusBlocks.push(renderRow(r.label, r.phase, `todo-${r.label}-${r.index}`));
- });
- }
- if (doneRows.length > 0) {
- statusBlocks.push(
-
- Done
-
- );
- doneRows.forEach(r => {
- statusBlocks.push(renderRow(r.label, r.phase, `done-${r.label}-${r.index}`));
- });
- }
- return statusBlocks;
- })()}
-
-
- {allDone && (
-
-
- Open Tasks →
-
-
- )}
-
-
- );
-}
diff --git a/components/project-main/MigrateMain.tsx b/components/project-main/MigrateMain.tsx
deleted file mode 100644
index b5e3a857..00000000
--- a/components/project-main/MigrateMain.tsx
+++ /dev/null
@@ -1,353 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import { useRouter, useParams } from "next/navigation";
-
-interface MigrateMainProps {
- projectId: string;
- projectName: string;
- sourceData?: { repoUrl?: string; liveUrl?: string; hosting?: string };
- analysisResult?: Record;
- migrationPlan?: string;
- creationStage?: string;
-}
-
-type Stage = "input" | "auditing" | "review" | "planning" | "plan";
-
-const HOSTING_OPTIONS = [
- { value: "", label: "Select hosting provider" },
- { value: "vercel", label: "Vercel" },
- { value: "aws", label: "AWS" },
- { value: "heroku", label: "Heroku" },
- { value: "digitalocean", label: "DigitalOcean" },
- { value: "gcp", label: "Google Cloud Platform" },
- { value: "azure", label: "Microsoft Azure" },
- { value: "railway", label: "Railway" },
- { value: "render", label: "Render" },
- { value: "netlify", label: "Netlify" },
- { value: "self-hosted", label: "Self-hosted / VPS" },
- { value: "other", label: "Other" },
-];
-
-function MarkdownRenderer({ md }: { md: string }) {
- const lines = md.split('\n');
- return (
-
- {lines.map((line, i) => {
- if (line.startsWith('## ')) return
{line.slice(3)} ;
- if (line.startsWith('### ')) return
{line.slice(4)} ;
- if (line.startsWith('# ')) return
{line.slice(2)} ;
- if (line.match(/^- \[ \] /)) return (
-
-
- {line.slice(6)}
-
- );
- if (line.match(/^- \[x\] /i)) return (
-
-
- {line.slice(6)}
-
- );
- if (line.startsWith('- ') || line.startsWith('* ')) return
• {line.slice(2)}
;
- if (line.startsWith('---')) return
;
- if (!line.trim()) return
;
- // Bold inline
- const parts = line.split(/(\*\*.*?\*\*)/g).map((seg, j) =>
- seg.startsWith("**") && seg.endsWith("**")
- ?
{seg.slice(2, -2)}
- :
{seg}
- );
- return
{parts}
;
- })}
-
- );
-}
-
-export function MigrateMain({
- projectId,
- projectName,
- sourceData,
- analysisResult: initialAnalysis,
- migrationPlan: initialPlan,
- creationStage,
-}: MigrateMainProps) {
- const router = useRouter();
- const params = useParams();
- const workspace = params?.workspace as string;
-
- const getInitialStage = (): Stage => {
- if (initialPlan) return "plan";
- if (creationStage === "planning") return "planning";
- if (creationStage === "review" || initialAnalysis) return "review";
- if (sourceData?.repoUrl || sourceData?.liveUrl) return "auditing";
- return "input";
- };
-
- const [stage, setStage] = useState(getInitialStage);
- const [repoUrl, setRepoUrl] = useState(sourceData?.repoUrl ?? "");
- const [liveUrl, setLiveUrl] = useState(sourceData?.liveUrl ?? "");
- const [hosting, setHosting] = useState(sourceData?.hosting ?? "");
- const [analysisResult, setAnalysisResult] = useState | null>(initialAnalysis ?? null);
- const [migrationPlan, setMigrationPlan] = useState(initialPlan ?? "");
- const [progressStep, setProgressStep] = useState("cloning");
- const [error, setError] = useState(null);
-
- // Poll during audit
- useEffect(() => {
- if (stage !== "auditing") return;
- const interval = setInterval(async () => {
- try {
- const res = await fetch(`/api/projects/${projectId}/analysis-status`);
- const data = await res.json();
- setProgressStep(data.stage ?? "cloning");
- if (data.stage === "done" && data.analysisResult) {
- setAnalysisResult(data.analysisResult);
- clearInterval(interval);
- setStage("review");
- }
- } catch { /* keep polling */ }
- }, 2500);
- return () => clearInterval(interval);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [stage]);
-
- const startAudit = async () => {
- setError(null);
- setStage("auditing");
- if (repoUrl) {
- try {
- await fetch(`/api/projects/${projectId}/analyze-repo`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ repoUrl, liveUrl, hosting }),
- });
- } catch (e) {
- setError(e instanceof Error ? e.message : "Failed to start audit");
- setStage("input");
- }
- } else {
- // No repo — just use live URL fingerprinting via generate-migration-plan directly
- setStage("review");
- setAnalysisResult({ summary: `Live product at ${liveUrl}`, rows: [], suggestedSurfaces: [] });
- }
- };
-
- const startPlanning = async () => {
- setStage("planning");
- setError(null);
- try {
- const res = await fetch(`/api/projects/${projectId}/generate-migration-plan`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ analysisResult, sourceData: { repoUrl, liveUrl, hosting } }),
- });
- const data = await res.json();
- if (!res.ok) throw new Error(data.error || "Planning failed");
- setMigrationPlan(data.migrationPlan);
- setStage("plan");
- } catch (e) {
- setError(e instanceof Error ? e.message : "Planning failed");
- setStage("review");
- }
- };
-
- // ── Stage: input ──────────────────────────────────────────────────────────
- if (stage === "input") {
- const canProceed = repoUrl.trim().startsWith("http") || liveUrl.trim().startsWith("http");
- return (
-
-
-
-
- Tell us about your product
-
-
- {projectName} — Atlas will audit your current setup and build a safe migration plan.
-
-
- {error && (
-
- {error}
-
- )}
-
- Repository URL (recommended)
-
-
setRepoUrl(e.target.value)}
- placeholder="https://github.com/yourorg/your-repo"
- style={{ width: "100%", padding: "11px 14px", marginBottom: 16, borderRadius: 8, border: "1px solid #e0dcd4", background: "#faf8f5", fontSize: "0.9rem", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", color: "#1a1a1a", outline: "none", boxSizing: "border-box" }}
- onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
- onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")} autoFocus
- />
-
- Live URL (optional)
-
-
setLiveUrl(e.target.value)}
- placeholder="https://yourproduct.com"
- style={{ width: "100%", padding: "11px 14px", marginBottom: 16, borderRadius: 8, border: "1px solid #e0dcd4", background: "#faf8f5", fontSize: "0.9rem", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", color: "#1a1a1a", outline: "none", boxSizing: "border-box" }}
- onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
- onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
- />
-
- Current hosting provider
-
-
setHosting(e.target.value)}
- style={{ width: "100%", padding: "11px 14px", marginBottom: 20, borderRadius: 8, border: "1px solid #e0dcd4", background: "#faf8f5", fontSize: "0.88rem", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", color: hosting ? "#1a1a1a" : "#a09a90", outline: "none", boxSizing: "border-box", appearance: "none" }}
- >
- {HOSTING_OPTIONS.map(o => {o.label} )}
-
-
- Non-destructive. Your existing product stays live throughout. Atlas duplicates, never deletes.
-
-
- Start audit →
-
-
-
- );
- }
-
- // ── Stage: auditing ───────────────────────────────────────────────────────
- if (stage === "auditing") {
- const steps = [
- { key: "cloning", label: "Cloning repository" },
- { key: "reading", label: "Reading configuration" },
- { key: "analyzing", label: "Auditing infrastructure" },
- { key: "done", label: "Audit complete" },
- ];
- const currentIdx = steps.findIndex(s => s.key === progressStep);
- return (
-
-
-
-
-
Auditing your product
-
This is non-destructive — your live product is untouched
-
- {steps.map((step, i) => {
- const done = i < currentIdx;
- const active = i === currentIdx;
- return (
-
-
- {done ? "✓" : active ? : ""}
-
-
{step.label}
-
- );
- })}
-
-
-
- );
- }
-
- // ── Stage: review ─────────────────────────────────────────────────────────
- if (stage === "review") {
- const rows = (analysisResult?.rows as Array<{ category: string; item: string; status: string; detail?: string }>) ?? [];
- const summary = (analysisResult?.summary as string) ?? '';
- return (
-
-
-
-
Audit complete
-
{summary || `${projectName} — review your current infrastructure below.`}
-
-
- {rows.length > 0 && (
-
- {rows.map((row, i) => {
- const colorMap = { found: { bg: "#f0fdf4", text: "#15803d", label: "Found" }, partial: { bg: "#fffbeb", text: "#b45309", label: "Partial" }, missing: { bg: "#fff1f2", text: "#be123c", label: "Missing" } };
- const sc = colorMap[row.status as keyof typeof colorMap] ?? colorMap.found;
- return (
-
0 ? "1px solid #f6f4f0" : "none" }}>
-
{row.category}
-
{row.item}
- {row.detail &&
{row.detail}
}
-
{sc.label}
-
- );
- })}
-
- )}
-
- {error && (
-
- {error}
-
- )}
-
-
-
-
Ready to build the migration plan?
-
Atlas will generate a phased migration doc with Mirror, Validate, Cutover, and Decommission phases.
-
-
(e.currentTarget.style.opacity = "0.88")}
- onMouseLeave={e => (e.currentTarget.style.opacity = "1")}
- >
- Generate plan →
-
-
-
-
- );
- }
-
- // ── Stage: planning ───────────────────────────────────────────────────────
- if (stage === "planning") {
- return (
-
-
-
-
Generating migration plan…
-
Atlas is designing a safe, phased migration strategy
-
-
- );
- }
-
- // ── Stage: plan ───────────────────────────────────────────────────────────
- return (
-
- {/* Non-destructive banner */}
-
-
🛡️
-
- Non-destructive migration —
- your existing product stays live throughout every phase. Atlas duplicates, never deletes.
-
-
-
-
-
-
-
Migration Plan
-
{projectName} — four phased migration with rollback plan
-
-
-
-
-
- router.push(`/${workspace}/project/${projectId}/design`)}
- style={{ padding: "11px 22px", borderRadius: 8, border: "none", background: "#1a1a1a", color: "#fff", fontSize: "0.85rem", fontWeight: 600, fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", cursor: "pointer" }}
- >
- Go to Design →
-
- window.print()}
- style={{ padding: "11px 22px", borderRadius: 8, border: "1px solid #e0dcd4", background: "#fff", color: "#6b6560", fontSize: "0.85rem", fontWeight: 500, fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", cursor: "pointer" }}
- >
- Print / Export
-
-
-
-
-
- );
-}
diff --git a/components/project-main/MvpSetupDescribeView.tsx b/components/project-main/MvpSetupDescribeView.tsx
deleted file mode 100644
index 18777004..00000000
--- a/components/project-main/MvpSetupDescribeView.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-"use client";
-
-import { Suspense, useCallback, useEffect, useState } from "react";
-import { JM, JV } from "@/components/project-creation/modal-theme";
-import { AtlasChat } from "@/components/AtlasChat";
-import {
- BuildLivePlanPanel,
- addSectionContextRef,
-} from "@/components/project-main/BuildLivePlanPanel";
-import {
- type ChatContextRef,
- contextRefKey,
-} from "@/lib/chat-context-refs";
-
-export function MvpSetupDescribeView({ projectId, workspace }: { projectId: string; workspace: string }) {
- const [chatContextRefs, setChatContextRefs] = useState([]);
- const [tab, setTab] = useState<"chat" | "plan">("chat");
- const [narrow, setNarrow] = useState(false);
-
- useEffect(() => {
- const mq = window.matchMedia("(max-width: 900px)");
- const apply = () => setNarrow(mq.matches);
- apply();
- mq.addEventListener("change", apply);
- return () => mq.removeEventListener("change", apply);
- }, []);
-
- const removeChatContextRef = useCallback((key: string) => {
- setChatContextRefs(prev => prev.filter(r => contextRefKey(r) !== key));
- }, []);
-
- const addPlanSectionToChat = useCallback((label: string, phaseId: string | null) => {
- setChatContextRefs(prev => addSectionContextRef(prev, label, phaseId));
- }, []);
-
- return (
-
-
-
- Describe
-
-
- Tell Vibn about your idea — your plan fills in on the right as you go.
-
-
-
- {narrow && (
-
- {(["chat", "plan"] as const).map(id => (
- setTab(id)}
- style={{
- flex: 1,
- padding: "11px 8px",
- border: "none",
- background: "transparent",
- fontSize: 13,
- fontWeight: tab === id ? 600 : 500,
- color: tab === id ? JM.indigo : JM.muted,
- borderBottom: tab === id ? `2px solid ${JM.indigo}` : "2px solid transparent",
- cursor: "pointer",
- fontFamily: JM.fontSans,
- }}
- >
- {id === "chat" ? "Chat" : "Your plan"}
-
- ))}
-
- )}
-
-
-
- );
-}
diff --git a/components/project-main/MvpSetupLayoutClient.tsx b/components/project-main/MvpSetupLayoutClient.tsx
deleted file mode 100644
index 26d62dec..00000000
--- a/components/project-main/MvpSetupLayoutClient.tsx
+++ /dev/null
@@ -1,174 +0,0 @@
-"use client";
-
-import type { ReactNode } from "react";
-import Link from "next/link";
-import { usePathname } from "next/navigation";
-import { JM } from "@/components/project-creation/modal-theme";
-
-const BUILD_LEFT_BG = "#faf8f5";
-const BUILD_LEFT_BORDER = "#e8e4dc";
-
-export function MvpSetupLayoutClient({
- workspace,
- projectId,
- children,
-}: {
- workspace: string;
- projectId: string;
- children: ReactNode;
-}) {
- const pathname = usePathname() ?? "";
- const base = `/${workspace}/project/${projectId}/mvp-setup`;
-
- const steps = [
- { href: `${base}/describe`, label: "Describe", sub: "Your idea", suffix: "/describe" },
- { href: `${base}/architect`, label: "Architect", sub: "Discovery", suffix: "/architect" },
- { href: `${base}/design`, label: "Design", sub: "Look & feel", suffix: "/design" },
- { href: `${base}/website`, label: "Website", sub: "Grow", suffix: "/website" },
- { href: `${base}/launch`, label: "Build MVP", sub: "Review & launch", suffix: "/launch" },
- ] as const;
-
- return (
-
-
-
-
-
- Steps
-
-
- {steps.map(step => {
- const active = pathname.includes(`${base}${step.suffix}`);
- return (
-
-
- {active ? "▲" : "○"}
-
-
-
{step.label}
-
{step.sub}
-
-
- );
- })}
-
-
-
-
- Save & go to dashboard
-
-
- Open Build workspace →
-
-
-
-
-
- {children}
-
-
- );
-}
diff --git a/components/project-main/MvpSetupStepPlaceholder.tsx b/components/project-main/MvpSetupStepPlaceholder.tsx
deleted file mode 100644
index d4ca2e85..00000000
--- a/components/project-main/MvpSetupStepPlaceholder.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-"use client";
-
-import Link from "next/link";
-import { JM } from "@/components/project-creation/modal-theme";
-
-export function MvpSetupStepPlaceholder({
- title,
- subtitle,
- body,
- primaryHref,
- primaryLabel,
- nextHref,
- nextLabel,
-}: {
- title: string;
- subtitle: string;
- body: string;
- primaryHref: string;
- primaryLabel: string;
- nextHref: string;
- nextLabel: string;
-}) {
- return (
-
-
-
- {title}
-
-
{subtitle}
-
{body}
-
- {primaryLabel}
-
-
- {nextLabel} →
-
-
-
- );
-}
diff --git a/components/project-main/ProjectInfraPanel.tsx b/components/project-main/ProjectInfraPanel.tsx
deleted file mode 100644
index 4fdc5126..00000000
--- a/components/project-main/ProjectInfraPanel.tsx
+++ /dev/null
@@ -1,358 +0,0 @@
-"use client";
-
-import { Suspense, useState, useEffect } from "react";
-import { useParams, useSearchParams, useRouter } from "next/navigation";
-import { JM } from "@/components/project-creation/modal-theme";
-
-export type ProjectInfraRouteBase = "run" | "infrastructure";
-
-export interface ProjectInfraPanelProps {
- routeBase: ProjectInfraRouteBase;
- /** Uppercase rail heading (e.g. Run vs Infrastructure) */
- navGroupLabel: string;
-}
-
-// ── Types ─────────────────────────────────────────────────────────────────────
-
-interface InfraApp {
- name: string;
- domain?: string | null;
- coolifyServiceUuid?: string | null;
-}
-
-interface ProjectData {
- giteaRepo?: string;
- giteaRepoUrl?: string;
- apps?: InfraApp[];
-}
-
-// ── Tab definitions ───────────────────────────────────────────────────────────
-
-const TABS = [
- { id: "builds", label: "Builds", icon: "⬡" },
- { id: "databases", label: "Databases", icon: "◫" },
- { id: "services", label: "Services", icon: "◎" },
- { id: "environment", label: "Environment", icon: "≡" },
- { id: "domains", label: "Domains", icon: "◬" },
- { id: "logs", label: "Logs", icon: "≈" },
-] as const;
-
-type TabId = typeof TABS[number]["id"];
-
-// ── Shared empty state ────────────────────────────────────────────────────────
-
-function ComingSoonPanel({ icon, title, description }: { icon: string; title: string; description: string }) {
- return (
-
-
- {icon}
-
-
-
{title}
-
{description}
-
-
- Coming soon
-
-
- );
-}
-
-// ── Builds tab ────────────────────────────────────────────────────────────────
-
-function BuildsTab({ project }: { project: ProjectData | null }) {
- const apps = project?.apps ?? [];
- if (apps.length === 0) {
- return (
-
- );
- }
- return (
-
-
- Deployed Apps
-
-
- {apps.map(app => (
-
-
-
⬡
-
-
{app.name}
- {app.domain && (
-
{app.domain}
- )}
-
-
-
-
- Running
-
-
- ))}
-
-
- );
-}
-
-// ── Databases tab ─────────────────────────────────────────────────────────────
-
-function DatabasesTab() {
- return (
-
- );
-}
-
-// ── Services tab ──────────────────────────────────────────────────────────────
-
-function ServicesTab() {
- return (
-
- );
-}
-
-// ── Environment tab ───────────────────────────────────────────────────────────
-
-function EnvironmentTab() {
- return (
-
-
- Environment Variables & Secrets
-
-
-
- Key Value
-
- {["DATABASE_URL", "NEXTAUTH_SECRET", "GITEA_API_TOKEN"].map(k => (
-
- {k}
- ••••••••
- Edit
-
- ))}
-
-
- + Add variable
-
-
-
-
- Variables are encrypted at rest and auto-injected into deployed containers. Secrets are never exposed in logs.
-
-
- );
-}
-
-// ── Domains tab ───────────────────────────────────────────────────────────────
-
-function DomainsTab({ project }: { project: ProjectData | null }) {
- const apps = (project?.apps ?? []).filter(a => a.domain);
- return (
-
-
- Domains & SSL
-
- {apps.length > 0 ? (
-
- {apps.map(app => (
-
-
-
- {app.domain}
-
-
{app.name}
-
-
-
- SSL active
-
-
- ))}
-
- ) : (
-
-
No custom domains configured
-
Deploy an app first, then point a domain here.
-
- )}
-
- + Add domain
-
-
- );
-}
-
-// ── Logs tab ──────────────────────────────────────────────────────────────────
-
-function LogsTab({ project }: { project: ProjectData | null }) {
- const apps = project?.apps ?? [];
- if (apps.length === 0) {
- return (
-
- );
- }
- return (
-
-
- Runtime Logs
-
-
-
{"# Logs will stream here once connected to Coolify"}
-
{"→ Select a service to tail its log output"}
-
-
- );
-}
-
-// ── Inner ───────────────────────────────────────────────────────────────────
-
-function ProjectInfraPanelInner({ routeBase, navGroupLabel }: ProjectInfraPanelProps) {
- const params = useParams();
- const searchParams = useSearchParams();
- const router = useRouter();
- const projectId = params.projectId as string;
- const workspace = params.workspace as string;
-
- const activeTab = (searchParams.get("tab") ?? "builds") as TabId;
- const [project, setProject] = useState(null);
-
- useEffect(() => {
- fetch(`/api/projects/${projectId}/apps`)
- .then(r => r.json())
- .then(d => setProject({ apps: d.apps ?? [], giteaRepo: d.giteaRepo, giteaRepoUrl: d.giteaRepoUrl }))
- .catch(() => {});
- }, [projectId]);
-
- const setTab = (id: TabId) => {
- router.push(`/${workspace}/project/${projectId}/${routeBase}?tab=${id}`, { scroll: false });
- };
-
- return (
-
-
-
-
- {navGroupLabel}
-
- {TABS.map(tab => {
- const active = activeTab === tab.id;
- return (
-
setTab(tab.id)}
- style={{
- display: "flex", alignItems: "center", gap: 9,
- padding: "7px 10px", borderRadius: 6,
- background: active ? "#f0ece4" : "transparent",
- border: "none", cursor: "pointer", width: "100%", textAlign: "left",
- color: active ? "#1a1a1a" : "#6b6560",
- fontSize: "0.8rem", fontWeight: active ? 600 : 450,
- transition: "background 0.1s",
- fontFamily: JM.fontSans,
- }}
- onMouseEnter={e => { if (!active) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }}
- onMouseLeave={e => { if (!active) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
- >
- {tab.icon}
- {tab.label}
-
- );
- })}
-
-
-
- {activeTab === "builds" && }
- {activeTab === "databases" && }
- {activeTab === "services" && }
- {activeTab === "environment" && }
- {activeTab === "domains" && }
- {activeTab === "logs" && }
-
-
- );
-}
-
-export function ProjectInfraPanel(props: ProjectInfraPanelProps) {
- return (
- Loading…}>
-
-
- );
-}
diff --git a/components/project/project-header-urls.tsx b/components/project/project-header-urls.tsx
deleted file mode 100644
index e466117f..00000000
--- a/components/project/project-header-urls.tsx
+++ /dev/null
@@ -1,300 +0,0 @@
-"use client";
-
-/**
- * Project header URL chips — surfaces the user's "front door" URLs
- * next to the status pill so they're one click away from any tab.
- *
- * - Live chips → every Coolify endpoint with an attached fqdn
- * - Prev. chips → every running dev-server preview
- *
- * When there are more than MAX_VISIBLE total links, extras collapse
- * into a "+N" pill that opens a popover with the full list as
- * clickable links (was a `title=` tooltip before — popover is
- * discoverable on touch and keyboard, tooltip wasn't).
- *
- * Polls anatomy every 30s for URL chips (stage pill polls faster while deploying).
- */
-
-import { ExternalLink, Globe, Play, Zap } from "lucide-react";
-import { useEffect, useRef, useState } from "react";
-import { dashboardBridgeScriptUrl } from "@/lib/dashboard-bridge-url";
-import { useAnatomy } from "./use-anatomy";
-
-const MAX_VISIBLE = 3;
-
-const START_PREVIEW_PROMPT_BASE =
- "Start the dev server for the user-facing app in this project on port 3000 and share the preview URL so I can see what it looks like. If multiple services exist (frontend + API + worker), pick the user-facing one. If a server is already running, just share the URL.";
-
-interface Props {
- projectId: string;
-}
-
-export function ProjectHeaderUrls({ projectId }: Props) {
- /** Rare churn — stage pill polls when deploys are active; 30s is plenty for new preview URLs */
- const { anatomy } = useAnatomy(projectId, { pollMs: 30000 });
- const [overflowOpen, setOverflowOpen] = useState(false);
- const overflowRef = useRef(null);
-
- // Close popover on outside click / Escape — both expected by users
- // who don't realize it's modal-ish.
- useEffect(() => {
- if (!overflowOpen) return;
- function onDoc(e: MouseEvent) {
- if (overflowRef.current && !overflowRef.current.contains(e.target as Node)) {
- setOverflowOpen(false);
- }
- }
- function onKey(e: KeyboardEvent) {
- if (e.key === "Escape") setOverflowOpen(false);
- }
- document.addEventListener("mousedown", onDoc);
- document.addEventListener("keydown", onKey);
- return () => {
- document.removeEventListener("mousedown", onDoc);
- document.removeEventListener("keydown", onKey);
- };
- }, [overflowOpen]);
-
- if (!anatomy) return null;
-
- const liveLinks = anatomy.hosting.live
- .filter((l) => !!l.fqdn)
- .map((l) => ({
- key: l.uuid,
- kind: "live" as const,
- label: l.name,
- url: ensureScheme(l.fqdn!),
- host: stripScheme(l.fqdn!),
- }));
-
- const previewLinks = anatomy.hosting.previews
- .filter((p) => p.state === "running" && p.url)
- .map((p) => ({
- key: p.id,
- kind: "preview" as const,
- label: `${p.name}:${p.port}`,
- url: p.url,
- host: hostOf(p.url),
- }));
-
- const allLinks = [...liveLinks, ...previewLinks];
-
- // Empty header is dishonest UX — the user has no idea whether they
- // SHOULD have a preview or whether nothing has been built yet. Surface
- // a one-click "Start preview" affordance instead. Clicking it opens
- // the chat panel (if collapsed) and fires the canned prompt; the
- // moment dev_server_start lands in anatomy (poll cadence: 4s) this
- // button is replaced by the real preview chip.
- if (allLinks.length === 0) {
- return (
- {
- const bridgeUrl = dashboardBridgeScriptUrl();
- const prompt =
- START_PREVIEW_PROMPT_BASE +
- ` After startup, add the preview picker script once (e.g. next/script in the root layout): ${bridgeUrl}`;
- window.dispatchEvent(
- new CustomEvent("vibn:chat-prompt", {
- detail: { prompt, scopeProjectId: projectId },
- }),
- );
- }}
- title="Start the dev server and share a preview URL in chat"
- style={startPreviewBtn}
- onMouseEnter={(e) => {
- e.currentTarget.style.background = "#f6f2ec";
- e.currentTarget.style.borderColor = "#d9d2c5";
- e.currentTarget.style.color = "#1a1a1a";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.background = "#fff";
- e.currentTarget.style.borderColor = "#e8e4dc";
- e.currentTarget.style.color = "#6b665e";
- }}
- >
-
- Preview
-
- );
- }
-
- const visible = allLinks.slice(0, MAX_VISIBLE);
- const hidden = allLinks.slice(MAX_VISIBLE);
-
- return (
-
- {visible.map((l) => (
-
- {l.kind === "live"
- ?
- : }
- {l.label}
-
-
- ))}
- {hidden.length > 0 && (
-
-
setOverflowOpen((v) => !v)}
- aria-expanded={overflowOpen}
- aria-haspopup="true"
- style={overflowPill}
- >
- +{hidden.length}
-
- {overflowOpen && (
-
- )}
-
- )}
-
- );
-}
-
-// ──────────────────────────────────────────────────
-
-function ensureScheme(host: string): string {
- if (/^https?:\/\//i.test(host)) return host;
- return `https://${host}`;
-}
-function stripScheme(host: string): string {
- return host.replace(/^https?:\/\//i, "").replace(/\/$/, "");
-}
-function hostOf(url: string): string {
- try { return new URL(url).host; } catch { return url; }
-}
-
-const wrap: React.CSSProperties = {
- display: "flex", gap: 6, alignItems: "center",
- flexWrap: "wrap",
-};
-
-const chipBase: React.CSSProperties = {
- display: "inline-flex", alignItems: "center", gap: 6,
- padding: "4px 10px", borderRadius: 4,
- fontSize: "0.72rem", fontWeight: 500,
- textDecoration: "none",
- whiteSpace: "nowrap", maxWidth: 220,
- border: "1px solid",
- fontFamily: '"Outfit", "Inter", ui-sans-serif, sans-serif',
- transition: "background 0.15s, border-color 0.15s",
-};
-const liveChip: React.CSSProperties = {
- ...chipBase,
- color: "#1a1a1a", borderColor: "#e8e4dc", background: "#fff",
-};
-const previewChip: React.CSSProperties = {
- ...chipBase,
- color: "#3d5afe", borderColor: "#3d5afe33", background: "#3d5afe08",
-};
-const startPreviewBtn: React.CSSProperties = {
- ...chipBase,
- color: "#6b665e",
- borderColor: "#e8e4dc",
- background: "#fff",
- cursor: "pointer",
- font: "inherit",
- fontSize: "0.72rem",
- fontWeight: 500,
- transition: "background 0.15s, border-color 0.15s, color 0.15s",
-};
-const chipLabel: React.CSSProperties = {
- overflow: "hidden", textOverflow: "ellipsis",
- maxWidth: 180,
-};
-const overflowPill: React.CSSProperties = {
- ...chipBase,
- borderColor: "#e8e4dc",
- color: "#6b665e",
- background: "#f8f5f0",
- cursor: "pointer",
- font: "inherit",
- fontSize: "0.72rem",
- fontWeight: 500,
-};
-
-const popoverStyle: React.CSSProperties = {
- position: "absolute",
- top: "calc(100% + 6px)",
- right: 0,
- minWidth: 240,
- maxWidth: 360,
- padding: 4,
- background: "#fff",
- border: "1px solid #e8e4dc",
- borderRadius: 6,
- boxShadow: "0 6px 20px rgba(0,0,0,0.08)",
- zIndex: 50,
- display: "flex",
- flexDirection: "column",
- gap: 2,
- fontFamily: '"Outfit", "Inter", ui-sans-serif, sans-serif',
-};
-const popoverHeader: React.CSSProperties = {
- padding: "6px 10px 4px",
- fontSize: "0.65rem",
- textTransform: "uppercase",
- letterSpacing: "0.04em",
- color: "#a09a90",
- fontWeight: 600,
-};
-const popoverItem: React.CSSProperties = {
- display: "flex",
- alignItems: "center",
- gap: 8,
- padding: "8px 10px",
- borderRadius: 4,
- textDecoration: "none",
- color: "#1a1a1a",
- fontSize: "0.78rem",
- cursor: "pointer",
-};
-const popoverItemText: React.CSSProperties = {
- flex: 1,
- minWidth: 0,
- overflow: "hidden",
-};
-const popoverItemLabel: React.CSSProperties = {
- fontWeight: 500,
- whiteSpace: "nowrap",
- overflow: "hidden",
- textOverflow: "ellipsis",
-};
-const popoverItemHost: React.CSSProperties = {
- fontSize: "0.7rem",
- color: "#a09a90",
- whiteSpace: "nowrap",
- overflow: "hidden",
- textOverflow: "ellipsis",
-};
diff --git a/components/project/project-stage-pill.tsx b/components/project/project-stage-pill.tsx
deleted file mode 100644
index 3454c522..00000000
--- a/components/project/project-stage-pill.tsx
+++ /dev/null
@@ -1,343 +0,0 @@
-"use client";
-
-/**
- * Project header status pill — surfaces what Coolify is actually doing.
- *
- * Priority (highest urgency wins):
- * 1. Build failed — most recent finished deploy errored
- * 2. Deploying — at least one in-flight deployment (queued / in_progress)
- * 3. Down — apps exist but none running
- * 4. Live — at least one app/service running healthy
- * 5. Empty — no apps deployed yet (replaces the old false "Live"
- * fallback when data.status="active")
- *
- * while deploys or unhealthy states need attention (~8s); stays quiet when live or empty.
- * On hover the pill shows a tooltip with the breakdown of why we're
- * in the current state ("vibn-frontend is deploying", "twenty-live last
- * deploy failed 3m ago", etc.) — no more guessing.
- */
-
-import { useMemo, useEffect, useState } from "react";
-import { Loader2, ExternalLink } from "lucide-react";
-import { useAnatomy, type Anatomy } from "./use-anatomy";
-
-interface ProjectStagePillProps {
- projectId: string;
- /** Stage value pulled from fs_projects.data.status — only used while
- * the first anatomy fetch is in flight, so the user sees something
- * immediately instead of an empty header. */
- fallbackStage: "discovery" | "architecture" | "building" | "active";
-}
-
-type PillState =
- | { kind: "build_failed"; reason: string }
- | { kind: "deploying"; reason: string }
- | { kind: "down"; reason: string }
- | { kind: "live"; reason: string }
- | { kind: "empty"; reason: string };
-
-export function ProjectStagePill({
- projectId,
- fallbackStage,
-}: ProjectStagePillProps) {
- const [anatomyPollMs, setAnatomyPollMs] = useState(0);
- const { anatomy, loading } = useAnatomy(projectId, { pollMs: anatomyPollMs });
-
- useEffect(() => {
- if (!anatomy) {
- // Don't call setState here if not needed
- if (anatomyPollMs !== 0) setAnatomyPollMs(0);
- return;
- }
- const s = derivePillState(anatomy);
- const targetPollMs = s.kind === "live" || s.kind === "empty" ? 0 : 8000;
- if (anatomyPollMs !== targetPollMs) {
- setAnatomyPollMs(targetPollMs);
- }
- }, [anatomy, anatomyPollMs]);
-
- const state = useMemo(() => {
- if (!anatomy) return null;
- return derivePillState(anatomy);
- }, [anatomy]);
-
- if (loading && !anatomy) {
- const f = FALLBACK_PRESETS[fallbackStage];
- return (
-
- );
- }
- if (!state) {
- const f = FALLBACK_PRESETS[fallbackStage];
- return (
-
- );
- }
-
- const visual = VISUALS[state.kind];
-
- // Deep-link target for the "Logs" affordance. Coolify v4 redirects
- // `/project/` to that project's first environment, putting
- // the user one click from any application's deployment logs. We
- // don't store the environment UUID in anatomy (yet), so this is
- // the closest we can get without an extra Coolify call. When
- // anatomy exposes `lastBuildDeploymentUuid` we can deep-link
- // straight to `/applications/.../deployment/`.
- const coolifyBase = process.env.NEXT_PUBLIC_COOLIFY_URL ?? "";
- const coolifyProjectUuid = anatomy?.project?.coolifyProjectUuid;
- const coolifyDeepLink =
- coolifyBase && coolifyProjectUuid
- ? `${coolifyBase.replace(/\/$/, "")}/project/${coolifyProjectUuid}`
- : coolifyBase || null;
-
- // Show the "Logs" affordance whenever there's something interesting
- // happening Coolify-side: a build is in flight, the last build
- // failed, or apps are down. Hide on `live` and `empty` to avoid
- // visual noise when nothing's wrong.
- const showLogsLink =
- coolifyDeepLink &&
- (state.kind === "build_failed" ||
- state.kind === "deploying" ||
- state.kind === "down");
-
- const logsLinkColor =
- state.kind === "build_failed" || state.kind === "down"
- ? "#c5392b"
- : "#3d5afe";
-
- return (
-
-
- {showLogsLink && (
-
- Logs
-
- )}
-
- );
-}
-
-// Coolify reports container status as `` or `:`,
-// e.g. "running:healthy", "starting:unknown", "exited:unhealthy".
-// Phase taxonomy:
-// running → up
-// starting → transient (booting / health-check pending)
-// restarting → transient
-// created / paused → transient (rare in our flow)
-// exited / dead → down
-// We classify each app, then aggregate to a pill state.
-type AppPhase = "up" | "transient" | "down" | "unknown";
-function classifyAppStatus(raw?: string): AppPhase {
- const s = (raw ?? "").toLowerCase().trim();
- if (!s || s === "unknown") return "unknown";
- if (/^(running|healthy)/.test(s)) return "up";
- if (/healthy/.test(s) && !/unhealthy/.test(s)) return "up";
- if (
- /^(starting|restarting|created|paused|deploying|building|in_progress|queued)/.test(
- s,
- )
- )
- return "transient";
- if (/^(exited|dead|failed|stopped|unhealthy|error)/.test(s)) return "down";
- // Default to transient for anything unrecognised — Coolify occasionally
- // emits novel phases during upgrades; better to wait than mis-flag red.
- return "transient";
-}
-
-// Pure function. Exported-style intent only — keeps logic testable.
-function derivePillState(a: Anatomy): PillState {
- const live = a.hosting?.live ?? [];
-
- if (live.length === 0) {
- return {
- kind: "empty",
- reason: "No apps deployed yet. Use the chat to spin one up.",
- };
- }
-
- // 1. Active build in flight — highest priority signal.
- const deploying = live.filter((l) => l.inFlightBuild);
- if (deploying.length > 0) {
- const names = deploying.map((l) => l.name).join(", ");
- const stage = deploying[0].inFlightBuild?.status ?? "in progress";
- return {
- kind: "deploying",
- reason: `Deploying ${names}\nCoolify status: ${stage}`,
- };
- }
-
- // 2. Container is currently booting (starting / restarting). Surface
- // as "Deploying" since to the user this is the same wait state.
- const transient = live.filter(
- (l) => classifyAppStatus(l.status) === "transient",
- );
- if (transient.length > 0) {
- const lines = transient.map((l) => `${l.name}: ${l.status}`);
- return {
- kind: "deploying",
- reason: `Containers starting:\n${lines.join("\n")}`,
- };
- }
-
- // 3. Last finished build errored — call attention regardless of
- // whether the previous container is still serving.
- const failed = live.filter(
- (l) => l.lastBuild && /fail|error|cancel/i.test(l.lastBuild.status),
- );
- if (failed.length > 0) {
- const lines = failed.map(
- (l) =>
- `${l.name}: ${l.lastBuild?.status}` +
- (l.lastBuild?.finishedAt
- ? ` · ${relTime(l.lastBuild.finishedAt)}`
- : ""),
- );
- return {
- kind: "build_failed",
- reason: `Last deploy failed:\n${lines.join("\n")}`,
- };
- }
-
- const phases = live.map((l) => classifyAppStatus(l.status));
- const upCount = phases.filter((p) => p === "up").length;
- const downCount = phases.filter((p) => p === "down").length;
-
- if (upCount === live.length) {
- return {
- kind: "live",
- reason: `All ${live.length} ${live.length === 1 ? "service is" : "services are"} running.`,
- };
- }
- if (upCount > 0) {
- return {
- kind: "live",
- reason: `${upCount}/${live.length} services running.`,
- };
- }
- if (downCount > 0) {
- const sample = live
- .slice(0, 3)
- .map((l) => `${l.name}: ${l.status}`)
- .join("\n");
- return { kind: "down", reason: `Apps are not running.\n${sample}` };
- }
-
- // All "unknown" — Coolify hasn't reported state yet (fresh project,
- // API hiccup). Treat as transient rather than red.
- return {
- kind: "deploying",
- reason: "Waiting on Coolify to report container state…",
- };
-}
-
-// ──────────────────────────────────────────────────
-
-const VISUALS: Record<
- PillState["kind"],
- { label: string; color: string; bg: string }
-> = {
- build_failed: { label: "Build failed", color: "#c5392b", bg: "#c5392b14" },
- deploying: { label: "Deploying", color: "#3d5afe", bg: "#3d5afe10" },
- down: { label: "Down", color: "#c5392b", bg: "#c5392b14" },
- live: { label: "Live", color: "#2e7d32", bg: "#2e7d3210" },
- empty: { label: "Empty", color: "#7c7770", bg: "#a09a9014" },
-};
-
-const FALLBACK_PRESETS: Record<
- "discovery" | "architecture" | "building" | "active",
- { label: string; color: string; bg: string }
-> = {
- discovery: { label: "Defining", color: "#9a7b3a", bg: "#d4a04a14" },
- architecture: { label: "Planning", color: "#3d5afe", bg: "#3d5afe10" },
- building: { label: "Building", color: "#3d5afe", bg: "#3d5afe10" },
- active: { label: "Live", color: "#2e7d32", bg: "#2e7d3210" },
-};
-
-function Pill({
- label,
- color,
- bg,
- title,
- spinning,
-}: {
- label: string;
- color: string;
- bg: string;
- title?: string;
- spinning?: boolean;
-}) {
- return (
-
- {spinning ? (
-
- ) : (
-
- )}
- {label}
-
- );
-}
-
-function relTime(iso: string): string {
- const ms = Date.now() - new Date(iso).getTime();
- if (Number.isNaN(ms)) return "";
- const min = Math.floor(ms / 60_000);
- if (min < 1) return "just now";
- if (min < 60) return `${min}m ago`;
- const hr = Math.floor(min / 60);
- if (hr < 24) return `${hr}h ago`;
- return `${Math.floor(hr / 24)}d ago`;
-}
diff --git a/components/project/project-tab-bar.tsx b/components/project/project-tab-bar.tsx
deleted file mode 100644
index dd71e75a..00000000
--- a/components/project/project-tab-bar.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-"use client";
-
-/**
- * Project tab bar — Product · Infrastructure · Hosting.
- *
- * Lives at the top of the cream main area, right below the project
- * header. The active tab is determined by the URL pathname so back /
- * forward / refresh always highlight the right one.
- */
-
-import Link from "next/link";
-import { usePathname } from "next/navigation";
-import { Box, Cloud, Server, NotebookPen } from "lucide-react";
-
-const TABS = [
- { id: "plan", label: "Plan", icon: NotebookPen, blurb: "Vision, ideas, tasks, and decisions for this project." },
- { id: "product", label: "Product", icon: Box, blurb: "Custom code, design, and content built for this vision." },
- { id: "infrastructure", label: "Infrastructure", icon: Server, blurb: "Swappable services this product depends on." },
- { id: "hosting", label: "Hosting", icon: Cloud, blurb: "Where it runs and how people reach it." },
-] as const;
-
-export function ProjectTabBar({
- workspace,
- projectId,
-}: {
- workspace: string;
- projectId: string;
-}) {
- const pathname = usePathname() ?? "";
- const activeTab =
- TABS.find(t => pathname.includes(`/project/${projectId}/${t.id}`))?.id ??
- "plan";
-
- return (
-
- {TABS.map(tab => {
- const isActive = tab.id === activeTab;
- const Icon = tab.icon;
- return (
-
-
- {tab.label}
-
- );
- })}
-
- );
-}
-
-const tabBar: React.CSSProperties = {
- display: "flex",
- gap: 4,
- marginTop: 22,
- marginBottom: -1,
-};
-
-const tabLink: React.CSSProperties = {
- display: "inline-flex",
- alignItems: "center",
- gap: 8,
- padding: "10px 14px",
- fontSize: "0.82rem",
- textDecoration: "none",
- borderBottom: "2px solid transparent",
- transition: "color 0.15s, border-color 0.15s",
- fontFamily: '"Outfit", "Inter", ui-sans-serif, sans-serif',
-};
diff --git a/components/project/section-scaffold.tsx b/components/project/section-scaffold.tsx
deleted file mode 100644
index 24edb9fe..00000000
--- a/components/project/section-scaffold.tsx
+++ /dev/null
@@ -1,277 +0,0 @@
-/**
- * Shared layout for the Product / Infrastructure / Hosting tabs.
- *
- * The tab bar in the page header already names the section, so the
- * page itself is just two columns:
- * - left: a "what lives here" grid of sub-areas
- * - right: live status panels (counts, empty states, CTAs)
- */
-
-import { ReactNode } from "react";
-
-export interface SubArea {
- label: string;
- hint: string;
- /** When provided, the tile renders as a button; pair with `active`. */
- onClick?: () => void;
- /** Visually mark this tile as the current selection. */
- active?: boolean;
-}
-
-interface SectionScaffoldProps {
- subAreas: SubArea[];
- rightPanel: ReactNode;
- /** Defaults to "What lives here". Pass e.g. "Codebases" for the Product tab. */
- subAreasHeading?: string;
- /** Optional heading above the right panel — keeps both columns
- * vertically aligned. If omitted, an invisible spacer is rendered
- * with the same height so panels still line up with tiles. */
- rightHeading?: string;
-}
-
-export function SectionScaffold({
- subAreas,
- rightPanel,
- subAreasHeading = "What lives here",
- rightHeading,
-}: SectionScaffoldProps) {
- return (
-
-
-
- {subAreasHeading}
-
- {subAreas.map(area => {
- const interactive = typeof area.onClick === "function";
- const style: React.CSSProperties = {
- ...subItem,
- cursor: interactive ? "pointer" : "default",
- borderColor: area.active ? INK.ink : INK.borderSoft,
- boxShadow: area.active ? "0 0 0 1px " + INK.ink : "none",
- transition: "border-color 0.12s, box-shadow 0.12s, background 0.12s",
- background: area.active ? "#fffdf8" : INK.cardBg,
- };
- const content = (
- <>
-
-
-
{area.label}
-
{area.hint}
-
- >
- );
- return interactive ? (
-
-
- {content}
-
-
- ) : (
-
- {content}
-
- );
- })}
-
-
-
-
-
- {rightHeading ?? "\u00A0"}
-
-
- {rightPanel}
-
-
-
-
- );
-}
-
-export function StatusPanel({
- title,
- children,
- cta,
-}: {
- title?: string;
- children: ReactNode;
- cta?: ReactNode;
-}) {
- return (
-
- {(title || cta) && (
-
- {title && {title} }
- {cta}
-
- )}
-
- {children}
-
-
- );
-}
-
-export function EmptyState({
- message,
- hint,
-}: {
- message: string;
- hint?: string;
-}) {
- return (
-
-
{message}
- {hint &&
{hint}
}
-
- );
-}
-
-const INK = {
- ink: "#1a1a1a",
- mid: "#5f5e5a",
- muted: "#a09a90",
- stone: "#b5b0a6",
- border: "#e8e4dc",
- borderSoft: "#efebe1",
- cardBg: "#fff",
- fontSans: '"Outfit", "Inter", ui-sans-serif, sans-serif',
-} as const;
-
-const pageWrap: React.CSSProperties = {
- padding: "28px 48px 48px",
- fontFamily: INK.fontSans,
- color: INK.ink,
-};
-
-const grid: React.CSSProperties = {
- display: "grid",
- gridTemplateColumns: "minmax(220px, 280px) minmax(0, 1fr)",
- gap: 28,
- maxWidth: 1280,
- margin: "0 auto",
- alignItems: "stretch",
-};
-
-const leftCol: React.CSSProperties = {
- minWidth: 0,
- display: "flex",
- flexDirection: "column",
-};
-
-const rightCol: React.CSSProperties = {
- minWidth: 0,
- display: "flex",
- flexDirection: "column",
-};
-
-const subHeading: React.CSSProperties = {
- fontSize: "0.72rem",
- fontWeight: 600,
- letterSpacing: "0.12em",
- textTransform: "uppercase",
- color: INK.muted,
- margin: "0 0 14px",
-};
-
-const subList: React.CSSProperties = {
- listStyle: "none",
- padding: 0,
- margin: 0,
- display: "flex",
- flexDirection: "column",
- gap: 8,
-};
-
-const subItem: React.CSSProperties = {
- display: "flex",
- gap: 10,
- alignItems: "flex-start",
- padding: "12px 14px",
- background: INK.cardBg,
- border: `1px solid ${INK.borderSoft}`,
- borderRadius: 8,
-};
-
-const subItemDot: React.CSSProperties = {
- width: 6,
- height: 6,
- borderRadius: "50%",
- background: INK.stone,
- marginTop: 7,
- flexShrink: 0,
-};
-
-const subItemLabel: React.CSSProperties = {
- fontSize: "0.85rem",
- fontWeight: 600,
- color: INK.ink,
- marginBottom: 2,
-};
-
-const subItemHint: React.CSSProperties = {
- fontSize: "0.75rem",
- color: INK.mid,
- lineHeight: 1.4,
-};
-
-const panel: React.CSSProperties = {
- background: INK.cardBg,
- border: `1px solid ${INK.border}`,
- borderRadius: 10,
- padding: 18,
- marginBottom: 16,
- display: "flex",
- flexDirection: "column",
- flex: 1,
- minHeight: 0,
-};
-
-const panelHeader: React.CSSProperties = {
- display: "flex",
- alignItems: "center",
- justifyContent: "space-between",
- marginBottom: 14,
- gap: 12,
-};
-
-const panelTitle: React.CSSProperties = {
- fontSize: "0.78rem",
- fontWeight: 600,
- letterSpacing: "0.06em",
- textTransform: "uppercase",
- color: INK.ink,
-};
-
-const emptyWrap: React.CSSProperties = {
- padding: "20px 0 4px",
- textAlign: "center",
-};
-
-const emptyMsg: React.CSSProperties = {
- fontSize: "0.85rem",
- color: INK.mid,
- marginBottom: 4,
-};
-
-const emptyHint: React.CSSProperties = {
- fontSize: "0.74rem",
- color: INK.muted,
- fontStyle: "italic",
-};
diff --git a/components/project/use-anatomy.ts b/components/project/use-anatomy.ts
index 233a5de5..fd619c1c 100644
--- a/components/project/use-anatomy.ts
+++ b/components/project/use-anatomy.ts
@@ -155,13 +155,14 @@ export function useAnatomy(
projectId: string,
options: UseAnatomyOptions = {},
): UseAnatomyResult {
+ const pollMs = options.pollMs && options.pollMs > 0 ? options.pollMs : 0;
+
const { data, error, isLoading, mutate } = useSWR(
projectId ? `/api/projects/${projectId}/anatomy` : null,
fetcher,
{
- refreshInterval:
- options.pollMs && options.pollMs > 0 ? options.pollMs : 0,
- dedupingInterval: 2000,
+ refreshInterval: pollMs,
+ dedupingInterval: 5000,
revalidateOnFocus: false,
revalidateOnReconnect: false,
},
diff --git a/components/ui/collapsible-sidebar.tsx b/components/ui/collapsible-sidebar.tsx
deleted file mode 100644
index a203275b..00000000
--- a/components/ui/collapsible-sidebar.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-"use client";
-
-import React, { useState } from 'react';
-import { ChevronLeft, ChevronRight } from 'lucide-react';
-import { cn } from '@/lib/utils';
-
-interface CollapsibleSidebarProps {
- children: React.ReactNode;
- className?: string;
- initialExpanded?: boolean;
-}
-
-export function CollapsibleSidebar({ children, className, initialExpanded = true }: CollapsibleSidebarProps) {
- const [isExpanded, setIsExpanded] = useState(initialExpanded);
-
- return (
- {
- if (!isExpanded) setIsExpanded(true);
- }}
- >
- {/* Toggle Button */}
- {isExpanded && (
-
{
- e.stopPropagation();
- setIsExpanded(false);
- }}
- className="absolute top-2 right-2 z-20 p-1 hover:bg-background/50 rounded-sm transition-colors"
- title="Collapse sidebar"
- >
-
-
- )}
-
- {/* Content Container */}
-
- {children}
-
-
- );
-}
diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx
deleted file mode 100644
index 0ed82381..00000000
--- a/components/ui/dropdown-menu.tsx
+++ /dev/null
@@ -1,201 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
-import { Check, ChevronRight, Circle } from "lucide-react"
-
-import { cn } from "@/lib/utils"
-
-const DropdownMenu = DropdownMenuPrimitive.Root
-
-const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
-
-const DropdownMenuGroup = DropdownMenuPrimitive.Group
-
-const DropdownMenuPortal = DropdownMenuPrimitive.Portal
-
-const DropdownMenuSub = DropdownMenuPrimitive.Sub
-
-const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
-
-const DropdownMenuSubTrigger = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef & {
- inset?: boolean
- }
->(({ className, inset, children, ...props }, ref) => (
-
- {children}
-
-
-))
-DropdownMenuSubTrigger.displayName =
- DropdownMenuPrimitive.SubTrigger.displayName
-
-const DropdownMenuSubContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-DropdownMenuSubContent.displayName =
- DropdownMenuPrimitive.SubContent.displayName
-
-const DropdownMenuContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, sideOffset = 4, ...props }, ref) => (
-
-
-
-))
-DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
-
-const DropdownMenuItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef & {
- inset?: boolean
- }
->(({ className, inset, ...props }, ref) => (
- svg]:size-4 [&>svg]:shrink-0",
- inset && "pl-8",
- className
- )}
- {...props}
- />
-))
-DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
-
-const DropdownMenuCheckboxItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, checked, ...props }, ref) => (
-
-
-
-
-
-
- {children}
-
-))
-DropdownMenuCheckboxItem.displayName =
- DropdownMenuPrimitive.CheckboxItem.displayName
-
-const DropdownMenuRadioItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
-
-
-
-
-
-
- {children}
-
-))
-DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
-
-const DropdownMenuLabel = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef & {
- inset?: boolean
- }
->(({ className, inset, ...props }, ref) => (
-
-))
-DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
-
-const DropdownMenuSeparator = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
-
-const DropdownMenuShortcut = ({
- className,
- ...props
-}: React.HTMLAttributes) => {
- return (
-
- )
-}
-DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
-
-export {
- DropdownMenu,
- DropdownMenuTrigger,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuCheckboxItem,
- DropdownMenuRadioItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuShortcut,
- DropdownMenuGroup,
- DropdownMenuPortal,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuRadioGroup,
-}
-
diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx
deleted file mode 100644
index 32ea0ef7..00000000
--- a/components/ui/skeleton.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { cn } from "@/lib/utils"
-
-function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-export { Skeleton }
diff --git a/components/ui/tree-view.tsx b/components/ui/tree-view.tsx
deleted file mode 100644
index 462f37fe..00000000
--- a/components/ui/tree-view.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { ChevronRight, ChevronDown, Circle, CheckCircle2, Clock } from "lucide-react";
-import { cn } from "@/lib/utils";
-
-export interface TreeNode {
- id: string;
- label: string;
- status?: "built" | "in_progress" | "missing";
- children?: TreeNode[];
- metadata?: {
- sessionsCount?: number;
- commitsCount?: number;
- cost?: number;
- };
-}
-
-interface TreeViewProps {
- data: TreeNode[];
- selectedId?: string | null;
- onSelect?: (node: TreeNode) => void;
- className?: string;
-}
-
-interface TreeNodeItemProps {
- node: TreeNode;
- level: number;
- selectedId?: string | null;
- onSelect?: (node: TreeNode) => void;
-}
-
-function TreeNodeItem({ node, level, selectedId, onSelect }: TreeNodeItemProps) {
- const [isExpanded, setIsExpanded] = useState(level === 0); // Auto-expand top level
- const hasChildren = node.children && node.children.length > 0;
- const isSelected = selectedId === node.id;
-
- const getStatusIcon = () => {
- if (!node.status) return null;
-
- switch (node.status) {
- case "built":
- return ;
- case "in_progress":
- return ;
- case "missing":
- return ;
- }
- };
-
- const getStatusColor = () => {
- if (!node.status) return "";
-
- switch (node.status) {
- case "built":
- return "bg-secondary hover:bg-muted border-l-2 border-l-primary";
- case "in_progress":
- return "bg-muted/40 hover:bg-muted border-l-2 border-l-border";
- case "missing":
- return "hover:bg-muted/30 border-l-2 border-l-transparent";
- }
- };
-
- return (
-
-
0 && "text-sm"
- )}
- style={{ paddingLeft: `${level * 12 + 8}px` }}
- onClick={() => {
- if (hasChildren) {
- setIsExpanded(!isExpanded);
- }
- if (onSelect) {
- onSelect(node);
- }
- }}
- >
- {hasChildren ? (
-
{
- e.stopPropagation();
- setIsExpanded(!isExpanded);
- }}
- >
- {isExpanded ? (
-
- ) : (
-
- )}
-
- ) : (
-
- {getStatusIcon()}
-
- )}
-
-
1 && "text-muted-foreground"
- )}>
- {node.label}
-
-
- {node.metadata && (
-
- {node.metadata.sessionsCount && node.metadata.sessionsCount > 0 && (
-
- {node.metadata.sessionsCount}s
-
- )}
- {node.metadata.commitsCount && node.metadata.commitsCount > 0 && (
-
- {node.metadata.commitsCount}c
-
- )}
-
- )}
-
-
- {hasChildren && isExpanded && (
-
- {node.children!.map((child) => (
-
- ))}
-
- )}
-
- );
-}
-
-export function TreeView({ data, selectedId, onSelect, className }: TreeViewProps) {
- return (
-
- {data.map((node) => (
-
- ))}
-
- );
-}
-
-
-
diff --git a/ARCHITECTURE.md b/docs/ARCHITECTURE.md
similarity index 100%
rename from ARCHITECTURE.md
rename to docs/ARCHITECTURE.md
diff --git a/MCP_README.md b/docs/MCP_README.md
similarity index 100%
rename from MCP_README.md
rename to docs/MCP_README.md
diff --git a/README.md b/docs/README.md
similarity index 100%
rename from README.md
rename to docs/README.md
diff --git a/SETUP.md b/docs/SETUP.md
similarity index 100%
rename from SETUP.md
rename to docs/SETUP.md
diff --git a/.test-questions b/docs/archive/.test-questions
similarity index 100%
rename from .test-questions
rename to docs/archive/.test-questions
diff --git a/AI_WELCOME_MESSAGE_FIX.md b/docs/archive/AI_WELCOME_MESSAGE_FIX.md
similarity index 100%
rename from AI_WELCOME_MESSAGE_FIX.md
rename to docs/archive/AI_WELCOME_MESSAGE_FIX.md
diff --git a/ALLOYDB_INTEGRATION_COMPLETE.md b/docs/archive/ALLOYDB_INTEGRATION_COMPLETE.md
similarity index 100%
rename from ALLOYDB_INTEGRATION_COMPLETE.md
rename to docs/archive/ALLOYDB_INTEGRATION_COMPLETE.md
diff --git a/BACKEND_EXTRACTION_FIXES.md b/docs/archive/BACKEND_EXTRACTION_FIXES.md
similarity index 100%
rename from BACKEND_EXTRACTION_FIXES.md
rename to docs/archive/BACKEND_EXTRACTION_FIXES.md
diff --git a/BROKEN_FLOW_ANALYSIS.md b/docs/archive/BROKEN_FLOW_ANALYSIS.md
similarity index 100%
rename from BROKEN_FLOW_ANALYSIS.md
rename to docs/archive/BROKEN_FLOW_ANALYSIS.md
diff --git a/CHATGPT_IMPORT_GUIDE.md b/docs/archive/CHATGPT_IMPORT_GUIDE.md
similarity index 100%
rename from CHATGPT_IMPORT_GUIDE.md
rename to docs/archive/CHATGPT_IMPORT_GUIDE.md
diff --git a/CHECKLIST_FIXES_COMPLETE.md b/docs/archive/CHECKLIST_FIXES_COMPLETE.md
similarity index 100%
rename from CHECKLIST_FIXES_COMPLETE.md
rename to docs/archive/CHECKLIST_FIXES_COMPLETE.md
diff --git a/COLLECTOR_EXTRACTOR_REFACTOR.md b/docs/archive/COLLECTOR_EXTRACTOR_REFACTOR.md
similarity index 100%
rename from COLLECTOR_EXTRACTOR_REFACTOR.md
rename to docs/archive/COLLECTOR_EXTRACTOR_REFACTOR.md
diff --git a/COLLECTOR_HANDOFF_PERSISTENCE.md b/docs/archive/COLLECTOR_HANDOFF_PERSISTENCE.md
similarity index 100%
rename from COLLECTOR_HANDOFF_PERSISTENCE.md
rename to docs/archive/COLLECTOR_HANDOFF_PERSISTENCE.md
diff --git a/COLLECTOR_TO_EXTRACTION_FLOW.md b/docs/archive/COLLECTOR_TO_EXTRACTION_FLOW.md
similarity index 100%
rename from COLLECTOR_TO_EXTRACTION_FLOW.md
rename to docs/archive/COLLECTOR_TO_EXTRACTION_FLOW.md
diff --git a/DATABASE-INTEGRATION.md b/docs/archive/DATABASE-INTEGRATION.md
similarity index 100%
rename from DATABASE-INTEGRATION.md
rename to docs/archive/DATABASE-INTEGRATION.md
diff --git a/E2E_TEST_INSTRUCTIONS.md b/docs/archive/E2E_TEST_INSTRUCTIONS.md
similarity index 100%
rename from E2E_TEST_INSTRUCTIONS.md
rename to docs/archive/E2E_TEST_INSTRUCTIONS.md
diff --git a/ENDPOINT_TEST_RESULTS.md b/docs/archive/ENDPOINT_TEST_RESULTS.md
similarity index 100%
rename from ENDPOINT_TEST_RESULTS.md
rename to docs/archive/ENDPOINT_TEST_RESULTS.md
diff --git a/EXTENSION_INTEGRATION.md b/docs/archive/EXTENSION_INTEGRATION.md
similarity index 100%
rename from EXTENSION_INTEGRATION.md
rename to docs/archive/EXTENSION_INTEGRATION.md
diff --git a/EXTENSION_SETUP_SUMMARY.md b/docs/archive/EXTENSION_SETUP_SUMMARY.md
similarity index 100%
rename from EXTENSION_SETUP_SUMMARY.md
rename to docs/archive/EXTENSION_SETUP_SUMMARY.md
diff --git a/FIREBASE_DEPLOYMENT.md b/docs/archive/FIREBASE_DEPLOYMENT.md
similarity index 100%
rename from FIREBASE_DEPLOYMENT.md
rename to docs/archive/FIREBASE_DEPLOYMENT.md
diff --git a/FIREBASE_SETUP.md b/docs/archive/FIREBASE_SETUP.md
similarity index 100%
rename from FIREBASE_SETUP.md
rename to docs/archive/FIREBASE_SETUP.md
diff --git a/FRONTEND_MAP.md b/docs/archive/FRONTEND_MAP.md
similarity index 100%
rename from FRONTEND_MAP.md
rename to docs/archive/FRONTEND_MAP.md
diff --git a/GEMINI_3_SUCCESS.md b/docs/archive/GEMINI_3_SUCCESS.md
similarity index 100%
rename from GEMINI_3_SUCCESS.md
rename to docs/archive/GEMINI_3_SUCCESS.md
diff --git a/GEMINI_SETUP.md b/docs/archive/GEMINI_SETUP.md
similarity index 100%
rename from GEMINI_SETUP.md
rename to docs/archive/GEMINI_SETUP.md
diff --git a/HANDOFF_CONTRACT_VERIFIED.md b/docs/archive/HANDOFF_CONTRACT_VERIFIED.md
similarity index 100%
rename from HANDOFF_CONTRACT_VERIFIED.md
rename to docs/archive/HANDOFF_CONTRACT_VERIFIED.md
diff --git a/LAYOUT-ARCHITECTURE.md b/docs/archive/LAYOUT-ARCHITECTURE.md
similarity index 100%
rename from LAYOUT-ARCHITECTURE.md
rename to docs/archive/LAYOUT-ARCHITECTURE.md
diff --git a/MCP_API_KEYS_GUIDE.md b/docs/archive/MCP_API_KEYS_GUIDE.md
similarity index 100%
rename from MCP_API_KEYS_GUIDE.md
rename to docs/archive/MCP_API_KEYS_GUIDE.md
diff --git a/MCP_SETUP.md b/docs/archive/MCP_SETUP.md
similarity index 100%
rename from MCP_SETUP.md
rename to docs/archive/MCP_SETUP.md
diff --git a/MCP_SUMMARY.md b/docs/archive/MCP_SUMMARY.md
similarity index 100%
rename from MCP_SUMMARY.md
rename to docs/archive/MCP_SUMMARY.md
diff --git a/NAVIGATION_STRUCTURE.md b/docs/archive/NAVIGATION_STRUCTURE.md
similarity index 100%
rename from NAVIGATION_STRUCTURE.md
rename to docs/archive/NAVIGATION_STRUCTURE.md
diff --git a/PROJECT_CREATION_FIX.md b/docs/archive/PROJECT_CREATION_FIX.md
similarity index 100%
rename from PROJECT_CREATION_FIX.md
rename to docs/archive/PROJECT_CREATION_FIX.md
diff --git a/PROMPT_REFACTOR_COMPLETE.md b/docs/archive/PROMPT_REFACTOR_COMPLETE.md
similarity index 100%
rename from PROMPT_REFACTOR_COMPLETE.md
rename to docs/archive/PROMPT_REFACTOR_COMPLETE.md
diff --git a/QA_FIXES_APPLIED.md b/docs/archive/QA_FIXES_APPLIED.md
similarity index 100%
rename from QA_FIXES_APPLIED.md
rename to docs/archive/QA_FIXES_APPLIED.md
diff --git a/QA_ISSUES_FOUND.md b/docs/archive/QA_ISSUES_FOUND.md
similarity index 100%
rename from QA_ISSUES_FOUND.md
rename to docs/archive/QA_ISSUES_FOUND.md
diff --git a/QUICK_E2E_START.md b/docs/archive/QUICK_E2E_START.md
similarity index 100%
rename from QUICK_E2E_START.md
rename to docs/archive/QUICK_E2E_START.md
diff --git a/QUICK_START_THINKING_MODE.md b/docs/archive/QUICK_START_THINKING_MODE.md
similarity index 100%
rename from QUICK_START_THINKING_MODE.md
rename to docs/archive/QUICK_START_THINKING_MODE.md
diff --git a/SUCCESS-SUMMARY.md b/docs/archive/SUCCESS-SUMMARY.md
similarity index 100%
rename from SUCCESS-SUMMARY.md
rename to docs/archive/SUCCESS-SUMMARY.md
diff --git a/TABLE_STAKES_IMPLEMENTATION.md b/docs/archive/TABLE_STAKES_IMPLEMENTATION.md
similarity index 100%
rename from TABLE_STAKES_IMPLEMENTATION.md
rename to docs/archive/TABLE_STAKES_IMPLEMENTATION.md
diff --git a/TEST_SESSION_API.md b/docs/archive/TEST_SESSION_API.md
similarity index 100%
rename from TEST_SESSION_API.md
rename to docs/archive/TEST_SESSION_API.md
diff --git a/THINKING_MODE_ENABLED.md b/docs/archive/THINKING_MODE_ENABLED.md
similarity index 100%
rename from THINKING_MODE_ENABLED.md
rename to docs/archive/THINKING_MODE_ENABLED.md
diff --git a/THINKING_MODE_STATUS.md b/docs/archive/THINKING_MODE_STATUS.md
similarity index 100%
rename from THINKING_MODE_STATUS.md
rename to docs/archive/THINKING_MODE_STATUS.md
diff --git a/TODO_CHATGPT_IMPORT.md b/docs/archive/TODO_CHATGPT_IMPORT.md
similarity index 100%
rename from TODO_CHATGPT_IMPORT.md
rename to docs/archive/TODO_CHATGPT_IMPORT.md
diff --git a/UPLOAD_CHUNKING_REMOVED.md b/docs/archive/UPLOAD_CHUNKING_REMOVED.md
similarity index 100%
rename from UPLOAD_CHUNKING_REMOVED.md
rename to docs/archive/UPLOAD_CHUNKING_REMOVED.md
diff --git a/V0-INTEGRATION.md b/docs/archive/V0-INTEGRATION.md
similarity index 100%
rename from V0-INTEGRATION.md
rename to docs/archive/V0-INTEGRATION.md
diff --git a/V0-SETUP.md b/docs/archive/V0-SETUP.md
similarity index 100%
rename from V0-SETUP.md
rename to docs/archive/V0-SETUP.md
diff --git a/VERTEX_AI_MIGRATION.md b/docs/archive/VERTEX_AI_MIGRATION.md
similarity index 100%
rename from VERTEX_AI_MIGRATION.md
rename to docs/archive/VERTEX_AI_MIGRATION.md
diff --git a/VERTEX_AI_MIGRATION_COMPLETE.md b/docs/archive/VERTEX_AI_MIGRATION_COMPLETE.md
similarity index 100%
rename from VERTEX_AI_MIGRATION_COMPLETE.md
rename to docs/archive/VERTEX_AI_MIGRATION_COMPLETE.md
diff --git a/mark.md b/docs/archive/mark.md
similarity index 100%
rename from mark.md
rename to docs/archive/mark.md
diff --git a/check-db-via-api.js b/docs/scripts/check-db-via-api.js
similarity index 100%
rename from check-db-via-api.js
rename to docs/scripts/check-db-via-api.js
diff --git a/check-documents.js b/docs/scripts/check-documents.js
similarity index 100%
rename from check-documents.js
rename to docs/scripts/check-documents.js
diff --git a/check-phase.js b/docs/scripts/check-phase.js
similarity index 100%
rename from check-phase.js
rename to docs/scripts/check-phase.js
diff --git a/docs/scripts/fix_auth_redirect.js b/docs/scripts/fix_auth_redirect.js
new file mode 100644
index 00000000..75261e10
--- /dev/null
+++ b/docs/scripts/fix_auth_redirect.js
@@ -0,0 +1,13 @@
+const fs = require('fs');
+
+const file = 'app/auth/page.tsx';
+let code = fs.readFileSync(file, 'utf8');
+
+// Currently it routes to `/${workspace}/projects`
+// We will route them to `/onboarding`
+code = code.replace(
+ 'router.push(`/${workspace}/projects`);',
+ 'router.push(`/onboarding`);'
+);
+
+fs.writeFileSync(file, code);
diff --git a/docs/scripts/fix_click.js b/docs/scripts/fix_click.js
new file mode 100644
index 00000000..b96116a2
--- /dev/null
+++ b/docs/scripts/fix_click.js
@@ -0,0 +1,10 @@
+const fs = require('fs');
+const file = 'app/(onboarding)/onboarding/page.tsx';
+let code = fs.readFileSync(file, 'utf8');
+
+code = code.replace(
+ 'const btn = document.querySelector(".btn-primary:not([disabled])") as HTMLElement;\n if (btn)(".btn-primary:not([disabled])");\n if (btn) btn.click();',
+ 'const btn = document.querySelector(".btn-primary:not([disabled])") as HTMLElement;\n if (btn) btn.click();'
+);
+
+fs.writeFileSync(file, code);
diff --git a/fix_mcp.js b/docs/scripts/fix_mcp.js
similarity index 100%
rename from fix_mcp.js
rename to docs/scripts/fix_mcp.js
diff --git a/docs/scripts/fix_primitives_errors.js b/docs/scripts/fix_primitives_errors.js
new file mode 100644
index 00000000..fdf872a5
--- /dev/null
+++ b/docs/scripts/fix_primitives_errors.js
@@ -0,0 +1,22 @@
+const fs = require('fs');
+const path = require('path');
+
+const dir = 'app/(onboarding)/onboarding';
+
+// Replace Arrow in onboarding-primitives.tsx
+let primCode = fs.readFileSync(path.join(dir, 'onboarding-primitives.tsx'), 'utf8');
+primCode = primCode.replace(
+ / /g,
+ ' '
+);
+fs.writeFileSync(path.join(dir, 'onboarding-primitives.tsx'), primCode);
+
+// Add Field to imports
+const filesToFix = ['onboarding-consultant.tsx', 'onboarding-entrepreneur.tsx', 'onboarding-owner.tsx'];
+for (const f of filesToFix) {
+ let code = fs.readFileSync(path.join(dir, f), 'utf8');
+ if (!code.includes('Field,') && !code.includes(', Field')) {
+ code = code.replace(/import \{ ([^}]+) \} from "\.\/onboarding-primitives";/, 'import { $1, Field } from "./onboarding-primitives";');
+ }
+ fs.writeFileSync(path.join(dir, f), code);
+}
diff --git a/docs/scripts/fix_primitives_errors2.js b/docs/scripts/fix_primitives_errors2.js
new file mode 100644
index 00000000..000a81e0
--- /dev/null
+++ b/docs/scripts/fix_primitives_errors2.js
@@ -0,0 +1,12 @@
+const fs = require('fs');
+const path = require('path');
+
+const dir = 'app/(onboarding)/onboarding';
+let code = fs.readFileSync(path.join(dir, 'onboarding-build.tsx'), 'utf8');
+
+code = code.replace(
+ / /g,
+ ' '
+);
+
+fs.writeFileSync(path.join(dir, 'onboarding-build.tsx'), code);
diff --git a/docs/scripts/fix_window_assign.js b/docs/scripts/fix_window_assign.js
new file mode 100644
index 00000000..5956cd6e
--- /dev/null
+++ b/docs/scripts/fix_window_assign.js
@@ -0,0 +1,16 @@
+const fs = require('fs');
+const path = require('path');
+
+const dir = 'app/(onboarding)/onboarding';
+const files = fs.readdirSync(dir).filter(f => f.endsWith('.tsx'));
+
+for (const file of files) {
+ const filePath = path.join(dir, file);
+ let code = fs.readFileSync(filePath, 'utf8');
+
+ // Remove Object.assign(window, {...}) lines since these are now proper React imports
+ code = code.replace(/Object\.assign\(window,\s*\{[\s\S]*?\}\);/g, '');
+
+ fs.writeFileSync(filePath, code);
+}
+console.log("Removed global window assignments from child components.");
diff --git a/docs/scripts/format_onboarding.js b/docs/scripts/format_onboarding.js
new file mode 100644
index 00000000..88ed49d9
--- /dev/null
+++ b/docs/scripts/format_onboarding.js
@@ -0,0 +1,15 @@
+const fs = require('fs');
+const file = 'app/(onboarding)/onboarding/page.tsx';
+let code = fs.readFileSync(file, 'utf8');
+
+// Prepend "use client" and imports
+const header = `"use client";\n\nimport React, { useState, useEffect, useMemo, Fragment } from "react";\nimport "./onboarding.css";\n\n`;
+
+// Add export default to the function
+code = code.replace('function OnboardingApp() {', 'export default function OnboardingApp() {');
+
+// Remove ReactDOM.createRoot
+code = code.replace('ReactDOM.createRoot(document.getElementById("root")).render( );', '');
+
+fs.writeFileSync(file, header + code);
+console.log("Reformatted page.tsx");
diff --git a/index.js b/docs/scripts/index.js
similarity index 100%
rename from index.js
rename to docs/scripts/index.js
diff --git a/mcp-server.js b/docs/scripts/mcp-server.js
similarity index 100%
rename from mcp-server.js
rename to docs/scripts/mcp-server.js
diff --git a/mcp-zed-bridge.js b/docs/scripts/mcp-zed-bridge.js
similarity index 100%
rename from mcp-zed-bridge.js
rename to docs/scripts/mcp-zed-bridge.js
diff --git a/docs/scripts/patch_local.js b/docs/scripts/patch_local.js
new file mode 100644
index 00000000..2ea64dee
--- /dev/null
+++ b/docs/scripts/patch_local.js
@@ -0,0 +1,11 @@
+const fs = require('fs');
+
+const file = 'app/(onboarding)/onboarding/page.tsx';
+let code = fs.readFileSync(file, 'utf8');
+
+code = code.replace(
+ 'try { return localStorage.getItem("vibn:firstName") || ""; } catch { return ""; }',
+ 'try { return typeof window !== "undefined" ? localStorage.getItem("vibn:firstName") || "" : ""; } catch { return ""; }'
+);
+
+fs.writeFileSync(file, code);
diff --git a/docs/scripts/patch_window.js b/docs/scripts/patch_window.js
new file mode 100644
index 00000000..e7d00247
--- /dev/null
+++ b/docs/scripts/patch_window.js
@@ -0,0 +1,15 @@
+const fs = require('fs');
+
+const file = 'app/(onboarding)/onboarding/page.tsx';
+let code = fs.readFileSync(file, 'utf8');
+
+code = code.replace(
+ 'const close = () => { window.location.href = "index.html"; };',
+ 'const close = () => { if (typeof window !== "undefined") window.location.href = "/"; };'
+);
+code = code.replace(
+ 'const openChat = () => { window.location.href = "index.html"; };',
+ 'const openChat = () => { if (typeof window !== "undefined") window.location.href = "/"; };'
+);
+
+fs.writeFileSync(file, code);
diff --git a/setup-e2e-test.sh b/docs/scripts/setup-e2e-test.sh
similarity index 100%
rename from setup-e2e-test.sh
rename to docs/scripts/setup-e2e-test.sh
diff --git a/test-actual-user-flow.sh b/docs/scripts/test-actual-user-flow.sh
similarity index 100%
rename from test-actual-user-flow.sh
rename to docs/scripts/test-actual-user-flow.sh
diff --git a/test-backend-extraction.sh b/docs/scripts/test-backend-extraction.sh
similarity index 100%
rename from test-backend-extraction.sh
rename to docs/scripts/test-backend-extraction.sh
diff --git a/test-e2e-collector.sh b/docs/scripts/test-e2e-collector.sh
similarity index 100%
rename from test-e2e-collector.sh
rename to docs/scripts/test-e2e-collector.sh
diff --git a/test-gemini-3-global.js b/docs/scripts/test-gemini-3-global.js
similarity index 100%
rename from test-gemini-3-global.js
rename to docs/scripts/test-gemini-3-global.js
diff --git a/test-gemini-3-simple.js b/docs/scripts/test-gemini-3-simple.js
similarity index 100%
rename from test-gemini-3-simple.js
rename to docs/scripts/test-gemini-3-simple.js
diff --git a/test-handoff-persistence.sh b/docs/scripts/test-handoff-persistence.sh
similarity index 100%
rename from test-handoff-persistence.sh
rename to docs/scripts/test-handoff-persistence.sh
diff --git a/test-simplified-flow.sh b/docs/scripts/test-simplified-flow.sh
similarity index 100%
rename from test-simplified-flow.sh
rename to docs/scripts/test-simplified-flow.sh
diff --git a/test-table-stakes.sh b/docs/scripts/test-table-stakes.sh
similarity index 100%
rename from test-table-stakes.sh
rename to docs/scripts/test-table-stakes.sh
diff --git a/test-vision-flow.sh b/docs/scripts/test-vision-flow.sh
similarity index 100%
rename from test-vision-flow.sh
rename to docs/scripts/test-vision-flow.sh
diff --git a/test-vision-simple.sh b/docs/scripts/test-vision-simple.sh
similarity index 100%
rename from test-vision-simple.sh
rename to docs/scripts/test-vision-simple.sh
diff --git a/test_auth.js b/docs/scripts/test_auth.js
similarity index 100%
rename from test_auth.js
rename to docs/scripts/test_auth.js
diff --git a/test_auth2.js b/docs/scripts/test_auth2.js
similarity index 100%
rename from test_auth2.js
rename to docs/scripts/test_auth2.js
diff --git a/test_mcp_fetch.js b/docs/scripts/test_mcp_fetch.js
similarity index 100%
rename from test_mcp_fetch.js
rename to docs/scripts/test_mcp_fetch.js
diff --git a/trigger-extraction.js b/docs/scripts/trigger-extraction.js
similarity index 100%
rename from trigger-extraction.js
rename to docs/scripts/trigger-extraction.js
diff --git a/verify-handoff-simple.sh b/docs/scripts/verify-handoff-simple.sh
similarity index 100%
rename from verify-handoff-simple.sh
rename to docs/scripts/verify-handoff-simple.sh
diff --git a/verify-handoff.sh b/docs/scripts/verify-handoff.sh
similarity index 100%
rename from verify-handoff.sh
rename to docs/scripts/verify-handoff.sh
diff --git a/firebase-debug.log b/firebase-debug.log
deleted file mode 100644
index 65787172..00000000
--- a/firebase-debug.log
+++ /dev/null
@@ -1,477 +0,0 @@
-[debug] [2025-11-28T01:28:54.264Z] ----------------------------------------------------------------------
-[debug] [2025-11-28T01:28:54.266Z] Command: /opt/homebrew/Cellar/node/24.1.0/bin/node /opt/homebrew/bin/firebase deploy --only functions,hosting
-[debug] [2025-11-28T01:28:54.266Z] CLI Version: 14.25.1
-[debug] [2025-11-28T01:28:54.266Z] Platform: darwin
-[debug] [2025-11-28T01:28:54.266Z] Node Version: v24.1.0
-[debug] [2025-11-28T01:28:54.266Z] Time: Thu Nov 27 2025 17:28:54 GMT-0800 (Pacific Standard Time)
-[debug] [2025-11-28T01:28:54.266Z] ----------------------------------------------------------------------
-[debug]
-[debug] [2025-11-28T01:28:54.344Z] Field "/functions" in "firebase.json" is possibly invalid: must be object
-[debug] [2025-11-28T01:28:54.345Z] Field "/functions" in "firebase.json" is possibly invalid: must be object
-[debug] [2025-11-28T01:28:54.345Z] Object "/functions/0" in "firebase.json" has unknown property: {"additionalProperty":"memory"}
-[debug] [2025-11-28T01:28:54.345Z] Object "/functions/0" in "firebase.json" is missing required property: {"missingProperty":"remoteSource"}
-[debug] [2025-11-28T01:28:54.345Z] Field "/functions/0" in "firebase.json" is possibly invalid: must match a schema in anyOf
-[debug] [2025-11-28T01:28:54.345Z] Field "/functions" in "firebase.json" is possibly invalid: must match a schema in anyOf
-[debug] [2025-11-28T01:28:54.347Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
-[debug] [2025-11-28T01:28:54.347Z] > authorizing via signed-in user (mark@getacquired.com)
-[debug] [2025-11-28T01:28:54.347Z] [iam] checking project gen-lang-client-0980079410 for permissions ["cloudfunctions.functions.create","cloudfunctions.functions.delete","cloudfunctions.functions.get","cloudfunctions.functions.list","cloudfunctions.functions.update","cloudfunctions.operations.get","firebase.projects.get","firebasehosting.sites.update"]
-[debug] [2025-11-28T01:28:54.348Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:28:54.348Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:28:54.348Z] >>> [apiv2][query] POST https://cloudresourcemanager.googleapis.com/v1/projects/gen-lang-client-0980079410:testIamPermissions [none]
-[debug] [2025-11-28T01:28:54.348Z] >>> [apiv2][(partial)header] POST https://cloudresourcemanager.googleapis.com/v1/projects/gen-lang-client-0980079410:testIamPermissions x-goog-quota-user=projects/gen-lang-client-0980079410
-[debug] [2025-11-28T01:28:54.348Z] >>> [apiv2][body] POST https://cloudresourcemanager.googleapis.com/v1/projects/gen-lang-client-0980079410:testIamPermissions {"permissions":["cloudfunctions.functions.create","cloudfunctions.functions.delete","cloudfunctions.functions.get","cloudfunctions.functions.list","cloudfunctions.functions.update","cloudfunctions.operations.get","firebase.projects.get","firebasehosting.sites.update"]}
-[debug] [2025-11-28T01:28:54.464Z] <<< [apiv2][status] POST https://cloudresourcemanager.googleapis.com/v1/projects/gen-lang-client-0980079410:testIamPermissions 200
-[debug] [2025-11-28T01:28:54.465Z] <<< [apiv2][body] POST https://cloudresourcemanager.googleapis.com/v1/projects/gen-lang-client-0980079410:testIamPermissions {"permissions":["cloudfunctions.functions.create","cloudfunctions.functions.delete","cloudfunctions.functions.get","cloudfunctions.functions.list","cloudfunctions.functions.update","cloudfunctions.operations.get","firebase.projects.get","firebasehosting.sites.update"]}
-[debug] [2025-11-28T01:28:54.465Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:28:54.465Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:28:54.465Z] >>> [apiv2][query] POST https://iam.googleapis.com/v1/projects/gen-lang-client-0980079410/serviceAccounts/gen-lang-client-0980079410@appspot.gserviceaccount.com:testIamPermissions [none]
-[debug] [2025-11-28T01:28:54.465Z] >>> [apiv2][body] POST https://iam.googleapis.com/v1/projects/gen-lang-client-0980079410/serviceAccounts/gen-lang-client-0980079410@appspot.gserviceaccount.com:testIamPermissions {"permissions":["iam.serviceAccounts.actAs"]}
-[debug] [2025-11-28T01:28:54.621Z] <<< [apiv2][status] POST https://iam.googleapis.com/v1/projects/gen-lang-client-0980079410/serviceAccounts/gen-lang-client-0980079410@appspot.gserviceaccount.com:testIamPermissions 200
-[debug] [2025-11-28T01:28:54.621Z] <<< [apiv2][body] POST https://iam.googleapis.com/v1/projects/gen-lang-client-0980079410/serviceAccounts/gen-lang-client-0980079410@appspot.gserviceaccount.com:testIamPermissions {"permissions":["iam.serviceAccounts.actAs"]}
-[debug] [2025-11-28T01:28:54.622Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:28:54.622Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:28:54.623Z] >>> [apiv2][query] GET https://firebase.googleapis.com/v1beta1/projects/gen-lang-client-0980079410 [none]
-[debug] [2025-11-28T01:28:54.967Z] <<< [apiv2][status] GET https://firebase.googleapis.com/v1beta1/projects/gen-lang-client-0980079410 200
-[debug] [2025-11-28T01:28:54.968Z] <<< [apiv2][body] GET https://firebase.googleapis.com/v1beta1/projects/gen-lang-client-0980079410 {"projectId":"gen-lang-client-0980079410","projectNumber":"487105246327","displayName":"vibn","name":"projects/gen-lang-client-0980079410","resources":{"hostingSite":"gen-lang-client-0980079410"},"state":"ACTIVE","etag":"1_4308cf09-f9b5-4401-8026-11f6add88c9b"}
-[info]
-[info] === Deploying to 'gen-lang-client-0980079410'...
-[info]
-[info] i deploying functions, hosting
-[debug] [2025-11-28T01:28:54.972Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:28:54.973Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:28:54.973Z] >>> [apiv2][query] GET https://cloudresourcemanager.googleapis.com/v1/projects/gen-lang-client-0980079410 [none]
-[debug] [2025-11-28T01:28:55.083Z] <<< [apiv2][status] GET https://cloudresourcemanager.googleapis.com/v1/projects/gen-lang-client-0980079410 200
-[debug] [2025-11-28T01:28:55.083Z] <<< [apiv2][body] GET https://cloudresourcemanager.googleapis.com/v1/projects/gen-lang-client-0980079410 {"projectNumber":"487105246327","projectId":"gen-lang-client-0980079410","lifecycleState":"ACTIVE","name":"vibn","labels":{"firebase":"enabled","firebase-core":"disabled"},"createTime":"2025-11-11T01:24:25.937309Z","parent":{"type":"organization","id":"296340871869"}}
-[info] i functions: preparing codebase default for deployment
-[info] i functions: ensuring required API cloudfunctions.googleapis.com is enabled...
-[info] i functions: ensuring required API cloudbuild.googleapis.com is enabled...
-[info] i artifactregistry: ensuring required API artifactregistry.googleapis.com is enabled...
-[debug] [2025-11-28T01:28:55.085Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:28:55.085Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:28:55.085Z] >>> [apiv2][query] GET https://firebase.googleapis.com/v1beta1/projects/gen-lang-client-0980079410/adminSdkConfig [none]
-[debug] [2025-11-28T01:28:55.573Z] <<< [apiv2][status] GET https://firebase.googleapis.com/v1beta1/projects/gen-lang-client-0980079410/adminSdkConfig 200
-[debug] [2025-11-28T01:28:55.573Z] <<< [apiv2][body] GET https://firebase.googleapis.com/v1beta1/projects/gen-lang-client-0980079410/adminSdkConfig {"projectId":"gen-lang-client-0980079410","storageBucket":"gen-lang-client-0980079410.firebasestorage.app"}
-[debug] [2025-11-28T01:28:55.574Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:28:55.574Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:28:55.574Z] >>> [apiv2][query] GET https://runtimeconfig.googleapis.com/v1beta1/projects/gen-lang-client-0980079410/configs [none]
-[debug] [2025-11-28T01:28:55.741Z] <<< [apiv2][status] GET https://runtimeconfig.googleapis.com/v1beta1/projects/gen-lang-client-0980079410/configs 200
-[debug] [2025-11-28T01:28:55.741Z] <<< [apiv2][body] GET https://runtimeconfig.googleapis.com/v1beta1/projects/gen-lang-client-0980079410/configs {}
-[debug] [2025-11-28T01:28:55.743Z] Validating nodejs source
-[debug] [2025-11-28T01:28:56.218Z] > [functions] package.json contents: {
- "name": "vibn-frontend",
- "version": "0.1.0",
- "private": true,
- "scripts": {
- "dev": "next dev",
- "build": "next build",
- "start": "next start",
- "lint": "eslint",
- "test:db": "tsx scripts/test-alloydb.ts",
- "migrate:postgres": "tsx scripts/migrate-from-postgres.ts",
- "migrate:reassign": "tsx scripts/reassign-migrated-data.ts",
- "firebase:emulators": "firebase emulators:start",
- "firebase:deploy:rules": "firebase deploy --only firestore:rules,storage",
- "firebase:deploy:indexes": "firebase deploy --only firestore:indexes",
- "firebase:deploy:app": "npm run build && firebase deploy --only functions,hosting",
- "firebase:deploy:all": "npm run build && firebase deploy",
- "mcp:server": "node mcp-server.js"
- },
- "dependencies": {
- "@google-cloud/vertexai": "^1.10.0",
- "@google/genai": "^1.30.0",
- "@google/generative-ai": "^0.24.1",
- "@modelcontextprotocol/sdk": "^1.22.0",
- "@radix-ui/react-alert-dialog": "^1.1.15",
- "@radix-ui/react-dialog": "^1.1.15",
- "@radix-ui/react-dropdown-menu": "^2.1.16",
- "@radix-ui/react-label": "^2.1.8",
- "@radix-ui/react-scroll-area": "^1.2.10",
- "@radix-ui/react-select": "^2.2.6",
- "@radix-ui/react-separator": "^1.1.8",
- "@radix-ui/react-slot": "^1.2.4",
- "@radix-ui/react-tabs": "^1.1.13",
- "@types/pg": "^8.15.6",
- "@types/uuid": "^10.0.0",
- "@v0-sdk/react": "^0.4.0",
- "class-variance-authority": "^0.7.1",
- "clsx": "^2.1.1",
- "dotenv": "^17.2.3",
- "firebase": "^12.5.0",
- "google-auth-library": "^10.5.0",
- "lucide-react": "^0.553.0",
- "next": "16.0.1",
- "next-themes": "^0.4.6",
- "pg": "^8.16.3",
- "react": "19.2.0",
- "react-dom": "19.2.0",
- "sonner": "^2.0.7",
- "tailwind-merge": "^3.4.0",
- "tsx": "^4.20.6",
- "uuid": "^13.0.0",
- "v0-sdk": "^0.14.0",
- "zod": "^3.23.8"
- },
- "devDependencies": {
- "@tailwindcss/postcss": "^4",
- "@types/node": "^20",
- "@types/react": "^19",
- "@types/react-dom": "^19",
- "eslint": "^9",
- "eslint-config-next": "16.0.1",
- "firebase-admin": "^13.6.0",
- "firebase-functions": "^7.0.0",
- "tailwindcss": "^4",
- "tw-animate-css": "^1.4.0",
- "typescript": "^5"
- }
-}
-[debug] [2025-11-28T01:28:56.218Z] Building nodejs source
-[info] i functions: Loading and analyzing source code for codebase default to determine what to deploy
-[debug] [2025-11-28T01:28:56.218Z] Could not find functions.yaml. Must use http discovery
-[debug] [2025-11-28T01:28:56.224Z] Found firebase-functions binary at '/Users/markhenderson/ai-proxy/vibn-frontend/node_modules/.bin/firebase-functions'
-[info] Serving at port 8788
-
-[debug] [2025-11-28T01:28:56.611Z] Got response from /__/functions.yaml {"endpoints":{"nextjsFunc":{"availableMemoryMb":2048,"timeoutSeconds":300,"minInstances":0,"maxInstances":10,"ingressSettings":null,"concurrency":null,"serviceAccountEmail":null,"vpc":null,"platform":"gcfv2","region":["us-central1"],"labels":{},"httpsTrigger":{},"entryPoint":"nextjsFunc"}},"specVersion":"v1alpha1","requiredAPIs":[],"extensions":{}}
-[error] ⚠ Warning: Next.js inferred your workspace root, but it may not be correct.
- We detected multiple lockfiles and selected the directory of /Users/markhenderson/package-lock.json as the root directory.
- To silence this warning, set `turbopack.root` in your Next.js config, or consider removing one of the lockfiles if it's not needed.
- See https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopack#root-directory for more information.
- Detected additional lockfiles:
- * /Users/markhenderson/ai-proxy/vibn-frontend/package-lock.json
- * /Users/markhenderson/ai-proxy/package-lock.json
-
-
-[info] [Firebase Admin] Initializing...
-
-[info] [Firebase Admin] Project ID: gen-lang-client-0980079410
-[Firebase Admin] Client Email: firebase-adminsdk-fbsvc@gen-lang-client-0980079410.iam.gserviceaccount.com
-[Firebase Admin] Private Key length: 1704
-
-[info] [Firebase Admin] Initialized successfully!
-
-[info] The user provided project/location will take precedence over the API key from the environment variables.
-
-[info] i extensions: ensuring required API firebaseextensions.googleapis.com is enabled...
-[debug] [2025-11-28T01:29:00.771Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
-[debug] [2025-11-28T01:29:00.771Z] > authorizing via signed-in user (mark@getacquired.com)
-[debug] [2025-11-28T01:29:00.771Z] [iam] checking project gen-lang-client-0980079410 for permissions ["firebase.projects.get","firebaseextensions.instances.list"]
-[debug] [2025-11-28T01:29:00.772Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:00.772Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:00.772Z] >>> [apiv2][query] POST https://cloudresourcemanager.googleapis.com/v1/projects/gen-lang-client-0980079410:testIamPermissions [none]
-[debug] [2025-11-28T01:29:00.772Z] >>> [apiv2][(partial)header] POST https://cloudresourcemanager.googleapis.com/v1/projects/gen-lang-client-0980079410:testIamPermissions x-goog-quota-user=projects/gen-lang-client-0980079410
-[debug] [2025-11-28T01:29:00.772Z] >>> [apiv2][body] POST https://cloudresourcemanager.googleapis.com/v1/projects/gen-lang-client-0980079410:testIamPermissions {"permissions":["firebase.projects.get","firebaseextensions.instances.list"]}
-[debug] [2025-11-28T01:29:00.906Z] <<< [apiv2][status] POST https://cloudresourcemanager.googleapis.com/v1/projects/gen-lang-client-0980079410:testIamPermissions 200
-[debug] [2025-11-28T01:29:00.907Z] <<< [apiv2][body] POST https://cloudresourcemanager.googleapis.com/v1/projects/gen-lang-client-0980079410:testIamPermissions {"permissions":["firebase.projects.get","firebaseextensions.instances.list"]}
-[debug] [2025-11-28T01:29:00.907Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:00.907Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:00.907Z] >>> [apiv2][query] GET https://firebaseextensions.googleapis.com/v1beta/projects/gen-lang-client-0980079410/instances pageSize=100&pageToken=
-[debug] [2025-11-28T01:29:01.212Z] <<< [apiv2][status] GET https://firebaseextensions.googleapis.com/v1beta/projects/gen-lang-client-0980079410/instances 200
-[debug] [2025-11-28T01:29:01.213Z] <<< [apiv2][body] GET https://firebaseextensions.googleapis.com/v1beta/projects/gen-lang-client-0980079410/instances {}
-[info] i functions: preparing . directory for uploading...
-[info] i functions: packaged /Users/markhenderson/ai-proxy/vibn-frontend (384.54 KB) for uploading
-[debug] [2025-11-28T01:29:01.320Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:01.320Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:01.320Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v1/projects/gen-lang-client-0980079410/locations/-/functions [none]
-[debug] [2025-11-28T01:29:01.921Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v1/projects/gen-lang-client-0980079410/locations/-/functions 200
-[debug] [2025-11-28T01:29:01.921Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v1/projects/gen-lang-client-0980079410/locations/-/functions {}
-[debug] [2025-11-28T01:29:01.922Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:01.922Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:01.922Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/-/functions filter=environment%3D%22GEN_2%22
-[debug] [2025-11-28T01:29:02.647Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/-/functions 200
-[debug] [2025-11-28T01:29:02.647Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/-/functions {"functions":[{"name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"build":"projects/487105246327/locations/us-central1/builds/9bccb651-3854-4d9b-85d4-a32c21bf1cca","runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764292973496057"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","updateTime":"2025-11-28T01:25:10.231966771Z","labels":{"deployment-tool":"cli-firebase","firebase-functions-hash":"8bfdf6f8734d27065a245954fd6a230e6fc8ca23"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","createTime":"2025-11-13T19:00:05.102139537Z","satisfiesPzi":true}]}
-[info] i functions: ensuring required API run.googleapis.com is enabled...
-[info] i functions: ensuring required API eventarc.googleapis.com is enabled...
-[info] i functions: ensuring required API pubsub.googleapis.com is enabled...
-[info] i functions: ensuring required API storage.googleapis.com is enabled...
-[info] i functions: generating the service identity for pubsub.googleapis.com...
-[debug] [2025-11-28T01:29:02.654Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:02.654Z] Checked if tokens are valid: true, expires at: 1764296401599
-[info] i functions: generating the service identity for eventarc.googleapis.com...
-[debug] [2025-11-28T01:29:02.654Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:02.654Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:02.655Z] >>> [apiv2][query] POST https://serviceusage.googleapis.com/v1beta1/projects/487105246327/services/pubsub.googleapis.com:generateServiceIdentity [none]
-[debug] [2025-11-28T01:29:02.655Z] >>> [apiv2][(partial)header] POST https://serviceusage.googleapis.com/v1beta1/projects/487105246327/services/pubsub.googleapis.com:generateServiceIdentity x-goog-quota-user=projects/487105246327
-[debug] [2025-11-28T01:29:02.655Z] >>> [apiv2][body] POST https://serviceusage.googleapis.com/v1beta1/projects/487105246327/services/pubsub.googleapis.com:generateServiceIdentity {}
-[debug] [2025-11-28T01:29:02.656Z] >>> [apiv2][query] POST https://serviceusage.googleapis.com/v1beta1/projects/487105246327/services/eventarc.googleapis.com:generateServiceIdentity [none]
-[debug] [2025-11-28T01:29:02.657Z] >>> [apiv2][(partial)header] POST https://serviceusage.googleapis.com/v1beta1/projects/487105246327/services/eventarc.googleapis.com:generateServiceIdentity x-goog-quota-user=projects/487105246327
-[debug] [2025-11-28T01:29:02.657Z] >>> [apiv2][body] POST https://serviceusage.googleapis.com/v1beta1/projects/487105246327/services/eventarc.googleapis.com:generateServiceIdentity {}
-[debug] [2025-11-28T01:29:03.137Z] <<< [apiv2][status] POST https://serviceusage.googleapis.com/v1beta1/projects/487105246327/services/eventarc.googleapis.com:generateServiceIdentity 200
-[debug] [2025-11-28T01:29:03.137Z] <<< [apiv2][body] POST https://serviceusage.googleapis.com/v1beta1/projects/487105246327/services/eventarc.googleapis.com:generateServiceIdentity {"name":"operations/finished.DONE_OPERATION","done":true,"response":{"@type":"type.googleapis.com/google.api.serviceusage.v1beta1.ServiceIdentity","email":"service-487105246327@gcp-sa-eventarc.iam.gserviceaccount.com","uniqueId":"111346241227052034870"}}
-[debug] [2025-11-28T01:29:03.139Z] <<< [apiv2][status] POST https://serviceusage.googleapis.com/v1beta1/projects/487105246327/services/pubsub.googleapis.com:generateServiceIdentity 200
-[debug] [2025-11-28T01:29:03.140Z] <<< [apiv2][body] POST https://serviceusage.googleapis.com/v1beta1/projects/487105246327/services/pubsub.googleapis.com:generateServiceIdentity {"name":"operations/finished.DONE_OPERATION","done":true,"response":{"@type":"type.googleapis.com/google.api.serviceusage.v1beta1.ServiceIdentity","email":"service-487105246327@gcp-sa-pubsub.iam.gserviceaccount.com","uniqueId":"110520361113696520928"}}
-[debug] [2025-11-28T01:29:03.148Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:03.148Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:03.148Z] >>> [apiv2][query] GET https://cloudresourcemanager.googleapis.com/v1/projects/gen-lang-client-0980079410 [none]
-[debug] [2025-11-28T01:29:03.239Z] <<< [apiv2][status] GET https://cloudresourcemanager.googleapis.com/v1/projects/gen-lang-client-0980079410 200
-[debug] [2025-11-28T01:29:03.239Z] <<< [apiv2][body] GET https://cloudresourcemanager.googleapis.com/v1/projects/gen-lang-client-0980079410 {"projectNumber":"487105246327","projectId":"gen-lang-client-0980079410","lifecycleState":"ACTIVE","name":"vibn","labels":{"firebase":"enabled","firebase-core":"disabled"},"createTime":"2025-11-11T01:24:25.937309Z","parent":{"type":"organization","id":"296340871869"}}
-[debug] [2025-11-28T01:29:03.240Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:03.240Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:03.240Z] >>> [apiv2][query] GET https://compute.googleapis.com/compute/v1/projects/487105246327 [none]
-[debug] [2025-11-28T01:29:03.624Z] <<< [apiv2][status] GET https://compute.googleapis.com/compute/v1/projects/487105246327 200
-[debug] [2025-11-28T01:29:03.624Z] <<< [apiv2][body] GET https://compute.googleapis.com/compute/v1/projects/487105246327 {"kind":"compute#project","id":"8233455852862230880","creationTimestamp":"2025-11-17T10:55:12.177-08:00","name":"gen-lang-client-0980079410","commonInstanceMetadata":{"kind":"compute#metadata","fingerprint":"JAjcU1yufZ8="},"quotas":[{"metric":"SNAPSHOTS","limit":5000,"usage":0},{"metric":"NETWORKS","limit":15,"usage":1},{"metric":"FIREWALLS","limit":200,"usage":4},{"metric":"IMAGES","limit":2000,"usage":0},{"metric":"STATIC_ADDRESSES","limit":21,"usage":0},{"metric":"ROUTES","limit":250,"usage":0},{"metric":"FORWARDING_RULES","limit":45,"usage":0},{"metric":"TARGET_POOLS","limit":150,"usage":0},{"metric":"HEALTH_CHECKS","limit":150,"usage":0},{"metric":"IN_USE_ADDRESSES","limit":69,"usage":0},{"metric":"TARGET_INSTANCES","limit":150,"usage":0},{"metric":"TARGET_HTTP_PROXIES","limit":30,"usage":0},{"metric":"URL_MAPS","limit":30,"usage":0},{"metric":"BACKEND_SERVICES","limit":75,"usage":0},{"metric":"INSTANCE_TEMPLATES","limit":300,"usage":0},{"metric":"TARGET_VPN_GATEWAYS","limit":15,"usage":0},{"metric":"VPN_TUNNELS","limit":30,"usage":0},{"metric":"BACKEND_BUCKETS","limit":9,"usage":0},{"metric":"ROUTERS","limit":10,"usage":0},{"metric":"TARGET_SSL_PROXIES","limit":30,"usage":0},{"metric":"TARGET_HTTPS_PROXIES","limit":30,"usage":0},{"metric":"SSL_CERTIFICATES","limit":30,"usage":0},{"metric":"SUBNETWORKS","limit":175,"usage":0},{"metric":"TARGET_TCP_PROXIES","limit":30,"usage":0},{"metric":"SECURITY_POLICIES","limit":10,"usage":0},{"metric":"SECURITY_POLICY_RULES","limit":100,"usage":0},{"metric":"XPN_SERVICE_PROJECTS","limit":1000,"usage":0},{"metric":"PACKET_MIRRORINGS","limit":45,"usage":0},{"metric":"NETWORK_ENDPOINT_GROUPS","limit":1000,"usage":0},{"metric":"INTERCONNECTS","limit":6,"usage":0},{"metric":"SSL_POLICIES","limit":30,"usage":0},{"metric":"GLOBAL_INTERNAL_ADDRESSES","limit":5000,"usage":1},{"metric":"VPN_GATEWAYS","limit":15,"usage":0},{"metric":"MACHINE_IMAGES","limit":2000,"usage":0},{"metric":"SECURITY_POLICY_CEVAL_RULES","limit":20,"usage":0},{"metric":"EXTERNAL_VPN_GATEWAYS","limit":15,"usage":0},{"metric":"PUBLIC_ADVERTISED_PREFIXES","limit":1,"usage":0},{"metric":"PUBLIC_DELEGATED_PREFIXES","limit":10,"usage":0},{"metric":"STATIC_BYOIP_ADDRESSES","limit":1024,"usage":0},{"metric":"NETWORK_FIREWALL_POLICIES","limit":30,"usage":0},{"metric":"INTERNAL_TRAFFIC_DIRECTOR_FORWARDING_RULES","limit":45,"usage":0},{"metric":"GLOBAL_EXTERNAL_MANAGED_FORWARDING_RULES","limit":45,"usage":0},{"metric":"GLOBAL_INTERNAL_MANAGED_BACKEND_SERVICES","limit":75,"usage":0},{"metric":"GLOBAL_EXTERNAL_MANAGED_BACKEND_SERVICES","limit":75,"usage":0},{"metric":"GLOBAL_EXTERNAL_PROXY_LB_BACKEND_SERVICES","limit":75,"usage":0},{"metric":"GLOBAL_INTERNAL_TRAFFIC_DIRECTOR_BACKEND_SERVICES","limit":500,"usage":0}],"selfLink":"https://www.googleapis.com/compute/v1/projects/gen-lang-client-0980079410","defaultServiceAccount":"487105246327-compute@developer.gserviceaccount.com","xpnProjectStatus":"UNSPECIFIED_XPN_PROJECT_STATUS","defaultNetworkTier":"PREMIUM","vmDnsSetting":"ZONAL_ONLY","cloudArmorTier":"CA_STANDARD"}
-[debug] [2025-11-28T01:29:03.628Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:03.628Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:03.628Z] >>> [apiv2][query] POST https://firebasehosting.googleapis.com/v1beta1/projects/-/sites/gen-lang-client-0980079410/versions [none]
-[debug] [2025-11-28T01:29:03.628Z] >>> [apiv2][body] POST https://firebasehosting.googleapis.com/v1beta1/projects/-/sites/gen-lang-client-0980079410/versions {"status":"CREATED","labels":{"deployment-tool":"cli-firebase"}}
-[debug] [2025-11-28T01:29:04.323Z] <<< [apiv2][status] POST https://firebasehosting.googleapis.com/v1beta1/projects/-/sites/gen-lang-client-0980079410/versions 200
-[debug] [2025-11-28T01:29:04.323Z] <<< [apiv2][body] POST https://firebasehosting.googleapis.com/v1beta1/projects/-/sites/gen-lang-client-0980079410/versions {"name":"projects/487105246327/sites/gen-lang-client-0980079410/versions/77783b350e2eaaa2","status":"CREATED","config":{},"labels":{"deployment-tool":"cli-firebase"}}
-[debug] [2025-11-28T01:29:04.324Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:04.324Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:04.324Z] >>> [apiv2][query] GET https://cloudbilling.googleapis.com/v1/projects/gen-lang-client-0980079410/billingInfo [none]
-[debug] [2025-11-28T01:29:04.567Z] <<< [apiv2][status] GET https://cloudbilling.googleapis.com/v1/projects/gen-lang-client-0980079410/billingInfo 200
-[debug] [2025-11-28T01:29:04.567Z] <<< [apiv2][body] GET https://cloudbilling.googleapis.com/v1/projects/gen-lang-client-0980079410/billingInfo {"name":"projects/gen-lang-client-0980079410/billingInfo","projectId":"gen-lang-client-0980079410","billingAccountName":"billingAccounts/01B04F-1C4798-D03704","billingEnabled":true}
-[debug] [2025-11-28T01:29:04.570Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:04.570Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:04.570Z] >>> [apiv2][query] POST https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/functions:generateUploadUrl [none]
-[debug] [2025-11-28T01:29:04.964Z] <<< [apiv2][status] POST https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/functions:generateUploadUrl 200
-[debug] [2025-11-28T01:29:04.964Z] <<< [apiv2][body] POST https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/functions:generateUploadUrl {"uploadUrl":"https://storage.googleapis.com/gcf-v2-uploads-487105246327.us-central1.cloudfunctions.appspot.com/99eda618-9756-469d-b198-fe8caf72855a.zip?GoogleAccessId=service-487105246327@gcf-admin-robot.iam.gserviceaccount.com&Expires=1764295144&Signature=IZr%2FYH28Bjzp4TcOezKgBtvk4apJovzpUR3%2BNay3WSdaos48rt9MBH2XJzBjGjaKw0W5hKYFaG%2FSb2HqdkxXxw5%2FoTXS8202bC4UnkjNJfy8qh52MTiS3ljIhawgiXwJGxDUE7uVeBQUdzEeZ0S0zK0r9f80ISFbzCjzrBpC4tvHnCBe1jwu8sKG5HQF9wrt753uVogbvoVPDTxHuFvjjNaWZFjdoiof22H5Bh4djkJd7ZW%2BfnFUSvz3z2DfNsU5wwJVnJV7cGig70PkXG1e2gS%2F3HS7FEA%2F6f%2FrAk0xZqF3I0SCCE9MZ3lOvqfI4X8Gx98tlPQtta1mcAFQmEmBLw%3D%3D","storageSource":{"bucket":"gcf-v2-uploads-487105246327.us-central1.cloudfunctions.appspot.com","object":"99eda618-9756-469d-b198-fe8caf72855a.zip"}}
-[debug] [2025-11-28T01:29:04.966Z] >>> [apiv2][query] PUT https://storage.googleapis.com/gcf-v2-uploads-487105246327.us-central1.cloudfunctions.appspot.com/99eda618-9756-469d-b198-fe8caf72855a.zip GoogleAccessId=service-487105246327%40gcf-admin-robot.iam.gserviceaccount.com&Expires=1764295144&Signature=IZr%2FYH28Bjzp4TcOezKgBtvk4apJovzpUR3%2BNay3WSdaos48rt9MBH2XJzBjGjaKw0W5hKYFaG%2FSb2HqdkxXxw5%2FoTXS8202bC4UnkjNJfy8qh52MTiS3ljIhawgiXwJGxDUE7uVeBQUdzEeZ0S0zK0r9f80ISFbzCjzrBpC4tvHnCBe1jwu8sKG5HQF9wrt753uVogbvoVPDTxHuFvjjNaWZFjdoiof22H5Bh4djkJd7ZW%2BfnFUSvz3z2DfNsU5wwJVnJV7cGig70PkXG1e2gS%2F3HS7FEA%2F6f%2FrAk0xZqF3I0SCCE9MZ3lOvqfI4X8Gx98tlPQtta1mcAFQmEmBLw%3D%3D
-[debug] [2025-11-28T01:29:04.966Z] >>> [apiv2][body] PUT https://storage.googleapis.com/gcf-v2-uploads-487105246327.us-central1.cloudfunctions.appspot.com/99eda618-9756-469d-b198-fe8caf72855a.zip [stream]
-[debug] [2025-11-28T01:29:05.396Z] <<< [apiv2][status] PUT https://storage.googleapis.com/gcf-v2-uploads-487105246327.us-central1.cloudfunctions.appspot.com/99eda618-9756-469d-b198-fe8caf72855a.zip 200
-[debug] [2025-11-28T01:29:05.396Z] <<< [apiv2][body] PUT https://storage.googleapis.com/gcf-v2-uploads-487105246327.us-central1.cloudfunctions.appspot.com/99eda618-9756-469d-b198-fe8caf72855a.zip [omitted]
-[info] ✔ functions: . source uploaded successfully
-[info] i hosting[gen-lang-client-0980079410]: beginning deploy...
-[info] i hosting[gen-lang-client-0980079410]: found 10 files in public
-[debug] [2025-11-28T01:29:05.418Z] [hosting] uploading with 200 concurrency
-[debug] [2025-11-28T01:29:05.421Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:05.421Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:05.421Z] [hosting] hash cache [cHVibGlj] stored for 10 files
-[debug] [2025-11-28T01:29:05.422Z] [hosting][hash queue][FINAL] {"max":0,"min":0,"avg":0,"active":0,"complete":10,"success":10,"errored":0,"retried":0,"total":10,"elapsed":2}
-[debug] [2025-11-28T01:29:05.422Z] >>> [apiv2][query] POST https://firebasehosting.googleapis.com/v1beta1/projects/487105246327/sites/gen-lang-client-0980079410/versions/77783b350e2eaaa2:populateFiles [none]
-[debug] [2025-11-28T01:29:05.422Z] >>> [apiv2][body] POST https://firebasehosting.googleapis.com/v1beta1/projects/487105246327/sites/gen-lang-client-0980079410/versions/77783b350e2eaaa2:populateFiles {"files":{"/window.svg":"11deaca6eadbb148caace8a5fe4a67353112de0afc5da83005d4797e403ab4f1","/vobn-favicon.png":"9051755f781b64be5155a8ef6b1846afae7ed12a942ce1fca217cde1fe0a4f09","/vibn-sqaure-black-logo.png":"2cd39bf33b13110575f3a2b02b4558c5dd157d96506783526d11988a01dbe249","/vibn-logo-circle.png":"b24c20ee6505547a3cd03a492681b281bf49c8e95eed78872e87581b5218eee2","/vibn-black-circle-logo.png":"a9312b012897c18eb945ecc474445181c063a6ff2363fefdd295bca3e7f17a70","/vibn-2-logo.png":"475fcbd3e4fa36dc5c63191220b5090ae90ae36d74d968240af25464829286fa","/vercel.svg":"9a61e768442ba3450026d0d69421315044931cbffaf8f6019f856ea82dd91e4e","/next.svg":"33c5c6ad1d08bb69d8026289530e377b4d6e2a96f24562e209fd1e1e9ccee64a","/globe.svg":"ffe166407c928caa4d1640e2786d3385468043b3b9e6ea2282d4a3e370b3bc23","/file.svg":"154a8c2948836a88c695a789045bc44cc74c3d8958d5785a531d26324bc42cb1"}}
-[debug] [2025-11-28T01:29:05.584Z] <<< [apiv2][status] POST https://firebasehosting.googleapis.com/v1beta1/projects/487105246327/sites/gen-lang-client-0980079410/versions/77783b350e2eaaa2:populateFiles 200
-[debug] [2025-11-28T01:29:05.584Z] <<< [apiv2][body] POST https://firebasehosting.googleapis.com/v1beta1/projects/487105246327/sites/gen-lang-client-0980079410/versions/77783b350e2eaaa2:populateFiles {"uploadUrl":"https://upload-firebasehosting.googleapis.com/upload/sites/gen-lang-client-0980079410/versions/77783b350e2eaaa2/files"}
-[debug] [2025-11-28T01:29:05.584Z] [hosting][populate queue][FINAL] {"max":164,"min":164,"avg":164,"active":0,"complete":1,"success":1,"errored":0,"retried":0,"total":1,"elapsed":164}
-[debug] [2025-11-28T01:29:05.585Z] [hosting] uploads queued: 0
-[debug] [2025-11-28T01:29:05.585Z] [hosting][upload queue][FINAL] {"max":0,"min":9999999999,"avg":0,"active":0,"complete":0,"success":0,"errored":0,"retried":0,"total":0,"elapsed":1764293345585}
-[info] i hosting: upload complete
-[info] ✔ hosting[gen-lang-client-0980079410]: file upload complete
-[debug] [2025-11-28T01:29:05.585Z] [hosting] deploy completed after 183ms
-[info] i functions: updating Node.js 22 (2nd Gen) function nextjsFunc(us-central1)...
-[debug] [2025-11-28T01:29:05.589Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:05.589Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:05.590Z] >>> [apiv2][query] PATCH https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc updateMask=name%2CbuildConfig.runtime%2CbuildConfig.entryPoint%2CbuildConfig.source.storageSource.bucket%2CbuildConfig.source.storageSource.object%2CbuildConfig.environmentVariables%2CbuildConfig.sourceToken%2CserviceConfig.environmentVariables%2CserviceConfig.ingressSettings%2CserviceConfig.timeoutSeconds%2CserviceConfig.serviceAccountEmail%2CserviceConfig.availableMemory%2CserviceConfig.minInstanceCount%2CserviceConfig.maxInstanceCount%2CserviceConfig.maxInstanceRequestConcurrency%2CserviceConfig.availableCpu%2CserviceConfig.vpcConnector%2CserviceConfig.vpcConnectorEgressSettings%2Clabels
-[debug] [2025-11-28T01:29:05.590Z] >>> [apiv2][body] PATCH https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc {"name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-uploads-487105246327.us-central1.cloudfunctions.appspot.com","object":"99eda618-9756-469d-b198-fe8caf72855a.zip"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""}},"serviceConfig":{"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"ingressSettings":null,"timeoutSeconds":300,"serviceAccountEmail":null,"availableMemory":"2Gi","minInstanceCount":0,"maxInstanceCount":10,"maxInstanceRequestConcurrency":80,"availableCpu":"1","vpcConnector":null,"vpcConnectorEgressSettings":null},"labels":{"deployment-tool":"cli-firebase","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"}}
-[debug] [2025-11-28T01:29:06.463Z] <<< [apiv2][status] PATCH https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc 200
-[debug] [2025-11-28T01:29:06.464Z] <<< [apiv2][body] PATCH https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2"},"done":false}
-[debug] [2025-11-28T01:29:06.465Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:06.466Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:06.466Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:29:06.780Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:29:06.781Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","state":"NOT_STARTED"},{"name":"SERVICE","state":"NOT_STARTED"}],"operationType":"UPDATE_FUNCTION"},"done":false}
-[debug] [2025-11-28T01:29:07.284Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:29:07.284Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:07.285Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:07.285Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:29:07.610Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:29:07.611Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build in progress","state":"IN_PROGRESS","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","state":"NOT_STARTED"}],"operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:29:08.612Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:29:08.613Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:08.613Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:08.613Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:29:08.889Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:29:08.889Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build in progress","state":"IN_PROGRESS","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","state":"NOT_STARTED"}],"operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:29:10.890Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:29:10.890Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:10.891Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:10.891Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:29:11.250Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:29:11.250Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build in progress","state":"IN_PROGRESS","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","state":"NOT_STARTED"}],"operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:29:15.251Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:29:15.252Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:15.252Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:15.252Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:29:15.504Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:29:15.504Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build in progress","state":"IN_PROGRESS","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","state":"NOT_STARTED"}],"operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:29:23.506Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:29:23.506Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:23.507Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:23.507Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:29:23.859Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:29:23.860Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build in progress","state":"IN_PROGRESS","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","state":"NOT_STARTED"}],"operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:29:33.862Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:29:33.863Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:33.863Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:33.864Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:29:34.140Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:29:34.141Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build in progress","state":"IN_PROGRESS","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","state":"NOT_STARTED"}],"operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:29:44.149Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:29:44.152Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:44.152Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:44.152Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:29:44.431Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:29:44.431Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build in progress","state":"IN_PROGRESS","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","state":"NOT_STARTED"}],"operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:29:54.434Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:29:54.445Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:54.445Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:29:54.447Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:29:54.816Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:29:54.816Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build in progress","state":"IN_PROGRESS","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","state":"NOT_STARTED"}],"operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:30:04.818Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:30:04.819Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:30:04.819Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:30:04.819Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:30:05.133Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:30:05.133Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build in progress","state":"IN_PROGRESS","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","state":"NOT_STARTED"}],"operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:30:15.139Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:30:15.158Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:30:15.159Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:30:15.159Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:30:15.519Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:30:15.519Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build in progress","state":"IN_PROGRESS","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","state":"NOT_STARTED"}],"operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:30:25.521Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:30:25.522Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:30:25.523Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:30:25.523Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:30:25.858Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:30:25.859Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build in progress","state":"IN_PROGRESS","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","state":"NOT_STARTED"}],"operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:30:35.861Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:30:35.864Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:30:35.864Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:30:35.865Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:30:36.231Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:30:36.231Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build in progress","state":"IN_PROGRESS","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","state":"NOT_STARTED"}],"operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:30:46.238Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:30:46.255Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:30:46.255Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:30:46.258Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:30:46.589Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:30:46.592Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build in progress","state":"IN_PROGRESS","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","state":"NOT_STARTED"}],"operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:30:56.601Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:30:56.601Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:30:56.601Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:30:56.601Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:30:56.943Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:30:56.943Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build finished","state":"COMPLETE","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","message":"Updating Cloud Run service","state":"IN_PROGRESS","resource":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","resourceUri":"https://console.cloud.google.com/run/detail/us-central1/nextjsfunc?project=gen-lang-client-0980079410","stateMessages":[{"severity":"INFO","type":"CloudRunServiceNewRevisionTrafficInfo","message":"A new revision will be deployed serving with 100% traffic."}]}],"sourceToken":"Cldwcm9qZWN0cy80ODcxMDUyNDYzMjcvbG9jYXRpb25zL3VzLWNlbnRyYWwxL2J1aWxkcy8wNGVhYmJmNi1hY2Y0LTRjNDMtODMzZS02Zjg0Yjg3NWFhYTAShgF1cy1jZW50cmFsMS1kb2NrZXIucGtnLmRldi9nZW4tbGFuZy1jbGllbnQtMDk4MDA3OTQxMC9nY2YtYXJ0aWZhY3RzL2dlbi0tbGFuZy0tY2xpZW50LS0wOTgwMDc5NDEwX191cy0tY2VudHJhbDFfX25leHRqc19mdW5jOnZlcnNpb25fMRj36PDNlg4iTnByb2plY3RzL2dlbi1sYW5nLWNsaWVudC0wOTgwMDc5NDEwL2xvY2F0aW9ucy91cy1jZW50cmFsMS9mdW5jdGlvbnMvbmV4dGpzRnVuYyoMCM32o8kGENCRkK8BMghub2RlanMyMjp2CiNnY3IuaW8vZ2FlLXJ1bnRpbWVzL25vZGVqczIyOnN0YWJsZRJPdXMtY2VudHJhbDEtZG9ja2VyLnBrZy5kZXYvc2VydmVybGVzcy1ydW50aW1lcy9nb29nbGUtMjItZnVsbC9ydW50aW1lcy9ub2RlanMyMkAB","operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:30:56.943Z] Got source token Cldwcm9qZWN0cy80ODcxMDUyNDYzMjcvbG9jYXRpb25zL3VzLWNlbnRyYWwxL2J1aWxkcy8wNGVhYmJmNi1hY2Y0LTRjNDMtODMzZS02Zjg0Yjg3NWFhYTAShgF1cy1jZW50cmFsMS1kb2NrZXIucGtnLmRldi9nZW4tbGFuZy1jbGllbnQtMDk4MDA3OTQxMC9nY2YtYXJ0aWZhY3RzL2dlbi0tbGFuZy0tY2xpZW50LS0wOTgwMDc5NDEwX191cy0tY2VudHJhbDFfX25leHRqc19mdW5jOnZlcnNpb25fMRj36PDNlg4iTnByb2plY3RzL2dlbi1sYW5nLWNsaWVudC0wOTgwMDc5NDEwL2xvY2F0aW9ucy91cy1jZW50cmFsMS9mdW5jdGlvbnMvbmV4dGpzRnVuYyoMCM32o8kGENCRkK8BMghub2RlanMyMjp2CiNnY3IuaW8vZ2FlLXJ1bnRpbWVzL25vZGVqczIyOnN0YWJsZRJPdXMtY2VudHJhbDEtZG9ja2VyLnBrZy5kZXYvc2VydmVybGVzcy1ydW50aW1lcy9nb29nbGUtMjItZnVsbC9ydW50aW1lcy9ub2RlanMyMkAB for region us-central1
-[debug] [2025-11-28T01:31:06.948Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:31:06.949Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:31:06.949Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:31:06.951Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:31:07.259Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:31:07.259Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build finished","state":"COMPLETE","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","message":"Updating Cloud Run service","state":"IN_PROGRESS","resource":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","resourceUri":"https://console.cloud.google.com/run/detail/us-central1/nextjsfunc?project=gen-lang-client-0980079410","stateMessages":[{"severity":"INFO","type":"CloudRunServiceNewRevisionTrafficInfo","message":"A new revision will be deployed serving with 100% traffic."}]}],"sourceToken":"Cldwcm9qZWN0cy80ODcxMDUyNDYzMjcvbG9jYXRpb25zL3VzLWNlbnRyYWwxL2J1aWxkcy8wNGVhYmJmNi1hY2Y0LTRjNDMtODMzZS02Zjg0Yjg3NWFhYTAShgF1cy1jZW50cmFsMS1kb2NrZXIucGtnLmRldi9nZW4tbGFuZy1jbGllbnQtMDk4MDA3OTQxMC9nY2YtYXJ0aWZhY3RzL2dlbi0tbGFuZy0tY2xpZW50LS0wOTgwMDc5NDEwX191cy0tY2VudHJhbDFfX25leHRqc19mdW5jOnZlcnNpb25fMRj36PDNlg4iTnByb2plY3RzL2dlbi1sYW5nLWNsaWVudC0wOTgwMDc5NDEwL2xvY2F0aW9ucy91cy1jZW50cmFsMS9mdW5jdGlvbnMvbmV4dGpzRnVuYyoMCM32o8kGENCRkK8BMghub2RlanMyMjp2CiNnY3IuaW8vZ2FlLXJ1bnRpbWVzL25vZGVqczIyOnN0YWJsZRJPdXMtY2VudHJhbDEtZG9ja2VyLnBrZy5kZXYvc2VydmVybGVzcy1ydW50aW1lcy9nb29nbGUtMjItZnVsbC9ydW50aW1lcy9ub2RlanMyMkAB","operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:31:07.259Z] Got source token Cldwcm9qZWN0cy80ODcxMDUyNDYzMjcvbG9jYXRpb25zL3VzLWNlbnRyYWwxL2J1aWxkcy8wNGVhYmJmNi1hY2Y0LTRjNDMtODMzZS02Zjg0Yjg3NWFhYTAShgF1cy1jZW50cmFsMS1kb2NrZXIucGtnLmRldi9nZW4tbGFuZy1jbGllbnQtMDk4MDA3OTQxMC9nY2YtYXJ0aWZhY3RzL2dlbi0tbGFuZy0tY2xpZW50LS0wOTgwMDc5NDEwX191cy0tY2VudHJhbDFfX25leHRqc19mdW5jOnZlcnNpb25fMRj36PDNlg4iTnByb2plY3RzL2dlbi1sYW5nLWNsaWVudC0wOTgwMDc5NDEwL2xvY2F0aW9ucy91cy1jZW50cmFsMS9mdW5jdGlvbnMvbmV4dGpzRnVuYyoMCM32o8kGENCRkK8BMghub2RlanMyMjp2CiNnY3IuaW8vZ2FlLXJ1bnRpbWVzL25vZGVqczIyOnN0YWJsZRJPdXMtY2VudHJhbDEtZG9ja2VyLnBrZy5kZXYvc2VydmVybGVzcy1ydW50aW1lcy9nb29nbGUtMjItZnVsbC9ydW50aW1lcy9ub2RlanMyMkAB for region us-central1
-[debug] [2025-11-28T01:31:17.261Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:31:17.261Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:31:17.261Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:31:17.261Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:31:17.604Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:31:17.604Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build finished","state":"COMPLETE","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","message":"Updating Cloud Run service","state":"IN_PROGRESS","resource":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","resourceUri":"https://console.cloud.google.com/run/detail/us-central1/nextjsfunc?project=gen-lang-client-0980079410","stateMessages":[{"severity":"INFO","type":"CloudRunServiceNewRevisionTrafficInfo","message":"A new revision will be deployed serving with 100% traffic."}]}],"sourceToken":"Cldwcm9qZWN0cy80ODcxMDUyNDYzMjcvbG9jYXRpb25zL3VzLWNlbnRyYWwxL2J1aWxkcy8wNGVhYmJmNi1hY2Y0LTRjNDMtODMzZS02Zjg0Yjg3NWFhYTAShgF1cy1jZW50cmFsMS1kb2NrZXIucGtnLmRldi9nZW4tbGFuZy1jbGllbnQtMDk4MDA3OTQxMC9nY2YtYXJ0aWZhY3RzL2dlbi0tbGFuZy0tY2xpZW50LS0wOTgwMDc5NDEwX191cy0tY2VudHJhbDFfX25leHRqc19mdW5jOnZlcnNpb25fMRj36PDNlg4iTnByb2plY3RzL2dlbi1sYW5nLWNsaWVudC0wOTgwMDc5NDEwL2xvY2F0aW9ucy91cy1jZW50cmFsMS9mdW5jdGlvbnMvbmV4dGpzRnVuYyoMCM32o8kGENCRkK8BMghub2RlanMyMjp2CiNnY3IuaW8vZ2FlLXJ1bnRpbWVzL25vZGVqczIyOnN0YWJsZRJPdXMtY2VudHJhbDEtZG9ja2VyLnBrZy5kZXYvc2VydmVybGVzcy1ydW50aW1lcy9nb29nbGUtMjItZnVsbC9ydW50aW1lcy9ub2RlanMyMkAB","operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:31:17.604Z] Got source token Cldwcm9qZWN0cy80ODcxMDUyNDYzMjcvbG9jYXRpb25zL3VzLWNlbnRyYWwxL2J1aWxkcy8wNGVhYmJmNi1hY2Y0LTRjNDMtODMzZS02Zjg0Yjg3NWFhYTAShgF1cy1jZW50cmFsMS1kb2NrZXIucGtnLmRldi9nZW4tbGFuZy1jbGllbnQtMDk4MDA3OTQxMC9nY2YtYXJ0aWZhY3RzL2dlbi0tbGFuZy0tY2xpZW50LS0wOTgwMDc5NDEwX191cy0tY2VudHJhbDFfX25leHRqc19mdW5jOnZlcnNpb25fMRj36PDNlg4iTnByb2plY3RzL2dlbi1sYW5nLWNsaWVudC0wOTgwMDc5NDEwL2xvY2F0aW9ucy91cy1jZW50cmFsMS9mdW5jdGlvbnMvbmV4dGpzRnVuYyoMCM32o8kGENCRkK8BMghub2RlanMyMjp2CiNnY3IuaW8vZ2FlLXJ1bnRpbWVzL25vZGVqczIyOnN0YWJsZRJPdXMtY2VudHJhbDEtZG9ja2VyLnBrZy5kZXYvc2VydmVybGVzcy1ydW50aW1lcy9nb29nbGUtMjItZnVsbC9ydW50aW1lcy9ub2RlanMyMkAB for region us-central1
-[debug] [2025-11-28T01:31:27.605Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:31:27.608Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:31:27.608Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:31:27.609Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:31:27.919Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:31:27.919Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build finished","state":"COMPLETE","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","message":"Updating Cloud Run service","state":"IN_PROGRESS","resource":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","resourceUri":"https://console.cloud.google.com/run/detail/us-central1/nextjsfunc?project=gen-lang-client-0980079410","stateMessages":[{"severity":"INFO","type":"CloudRunServiceNewRevisionTrafficInfo","message":"A new revision will be deployed serving with 100% traffic."}]}],"sourceToken":"Cldwcm9qZWN0cy80ODcxMDUyNDYzMjcvbG9jYXRpb25zL3VzLWNlbnRyYWwxL2J1aWxkcy8wNGVhYmJmNi1hY2Y0LTRjNDMtODMzZS02Zjg0Yjg3NWFhYTAShgF1cy1jZW50cmFsMS1kb2NrZXIucGtnLmRldi9nZW4tbGFuZy1jbGllbnQtMDk4MDA3OTQxMC9nY2YtYXJ0aWZhY3RzL2dlbi0tbGFuZy0tY2xpZW50LS0wOTgwMDc5NDEwX191cy0tY2VudHJhbDFfX25leHRqc19mdW5jOnZlcnNpb25fMRj36PDNlg4iTnByb2plY3RzL2dlbi1sYW5nLWNsaWVudC0wOTgwMDc5NDEwL2xvY2F0aW9ucy91cy1jZW50cmFsMS9mdW5jdGlvbnMvbmV4dGpzRnVuYyoMCM32o8kGENCRkK8BMghub2RlanMyMjp2CiNnY3IuaW8vZ2FlLXJ1bnRpbWVzL25vZGVqczIyOnN0YWJsZRJPdXMtY2VudHJhbDEtZG9ja2VyLnBrZy5kZXYvc2VydmVybGVzcy1ydW50aW1lcy9nb29nbGUtMjItZnVsbC9ydW50aW1lcy9ub2RlanMyMkAB","operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:31:27.919Z] Got source token Cldwcm9qZWN0cy80ODcxMDUyNDYzMjcvbG9jYXRpb25zL3VzLWNlbnRyYWwxL2J1aWxkcy8wNGVhYmJmNi1hY2Y0LTRjNDMtODMzZS02Zjg0Yjg3NWFhYTAShgF1cy1jZW50cmFsMS1kb2NrZXIucGtnLmRldi9nZW4tbGFuZy1jbGllbnQtMDk4MDA3OTQxMC9nY2YtYXJ0aWZhY3RzL2dlbi0tbGFuZy0tY2xpZW50LS0wOTgwMDc5NDEwX191cy0tY2VudHJhbDFfX25leHRqc19mdW5jOnZlcnNpb25fMRj36PDNlg4iTnByb2plY3RzL2dlbi1sYW5nLWNsaWVudC0wOTgwMDc5NDEwL2xvY2F0aW9ucy91cy1jZW50cmFsMS9mdW5jdGlvbnMvbmV4dGpzRnVuYyoMCM32o8kGENCRkK8BMghub2RlanMyMjp2CiNnY3IuaW8vZ2FlLXJ1bnRpbWVzL25vZGVqczIyOnN0YWJsZRJPdXMtY2VudHJhbDEtZG9ja2VyLnBrZy5kZXYvc2VydmVybGVzcy1ydW50aW1lcy9nb29nbGUtMjItZnVsbC9ydW50aW1lcy9ub2RlanMyMkAB for region us-central1
-[debug] [2025-11-28T01:31:37.921Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:31:37.921Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:31:37.922Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:31:37.922Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:31:38.297Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:31:38.297Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build finished","state":"COMPLETE","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","message":"Updating Cloud Run service","state":"IN_PROGRESS","resource":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","resourceUri":"https://console.cloud.google.com/run/detail/us-central1/nextjsfunc?project=gen-lang-client-0980079410","stateMessages":[{"severity":"INFO","type":"CloudRunServiceNewRevisionTrafficInfo","message":"A new revision will be deployed serving with 100% traffic."}]}],"sourceToken":"Cldwcm9qZWN0cy80ODcxMDUyNDYzMjcvbG9jYXRpb25zL3VzLWNlbnRyYWwxL2J1aWxkcy8wNGVhYmJmNi1hY2Y0LTRjNDMtODMzZS02Zjg0Yjg3NWFhYTAShgF1cy1jZW50cmFsMS1kb2NrZXIucGtnLmRldi9nZW4tbGFuZy1jbGllbnQtMDk4MDA3OTQxMC9nY2YtYXJ0aWZhY3RzL2dlbi0tbGFuZy0tY2xpZW50LS0wOTgwMDc5NDEwX191cy0tY2VudHJhbDFfX25leHRqc19mdW5jOnZlcnNpb25fMRj36PDNlg4iTnByb2plY3RzL2dlbi1sYW5nLWNsaWVudC0wOTgwMDc5NDEwL2xvY2F0aW9ucy91cy1jZW50cmFsMS9mdW5jdGlvbnMvbmV4dGpzRnVuYyoMCM32o8kGENCRkK8BMghub2RlanMyMjp2CiNnY3IuaW8vZ2FlLXJ1bnRpbWVzL25vZGVqczIyOnN0YWJsZRJPdXMtY2VudHJhbDEtZG9ja2VyLnBrZy5kZXYvc2VydmVybGVzcy1ydW50aW1lcy9nb29nbGUtMjItZnVsbC9ydW50aW1lcy9ub2RlanMyMkAB","operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:31:38.297Z] Got source token Cldwcm9qZWN0cy80ODcxMDUyNDYzMjcvbG9jYXRpb25zL3VzLWNlbnRyYWwxL2J1aWxkcy8wNGVhYmJmNi1hY2Y0LTRjNDMtODMzZS02Zjg0Yjg3NWFhYTAShgF1cy1jZW50cmFsMS1kb2NrZXIucGtnLmRldi9nZW4tbGFuZy1jbGllbnQtMDk4MDA3OTQxMC9nY2YtYXJ0aWZhY3RzL2dlbi0tbGFuZy0tY2xpZW50LS0wOTgwMDc5NDEwX191cy0tY2VudHJhbDFfX25leHRqc19mdW5jOnZlcnNpb25fMRj36PDNlg4iTnByb2plY3RzL2dlbi1sYW5nLWNsaWVudC0wOTgwMDc5NDEwL2xvY2F0aW9ucy91cy1jZW50cmFsMS9mdW5jdGlvbnMvbmV4dGpzRnVuYyoMCM32o8kGENCRkK8BMghub2RlanMyMjp2CiNnY3IuaW8vZ2FlLXJ1bnRpbWVzL25vZGVqczIyOnN0YWJsZRJPdXMtY2VudHJhbDEtZG9ja2VyLnBrZy5kZXYvc2VydmVybGVzcy1ydW50aW1lcy9nb29nbGUtMjItZnVsbC9ydW50aW1lcy9ub2RlanMyMkAB for region us-central1
-[debug] [2025-11-28T01:31:48.299Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:31:48.299Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:31:48.299Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:31:48.300Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:31:48.617Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:31:48.617Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build finished","state":"COMPLETE","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","message":"Updating Cloud Run service","state":"IN_PROGRESS","resource":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","resourceUri":"https://console.cloud.google.com/run/detail/us-central1/nextjsfunc?project=gen-lang-client-0980079410","stateMessages":[{"severity":"INFO","type":"CloudRunServiceNewRevisionTrafficInfo","message":"A new revision will be deployed serving with 100% traffic."}]}],"sourceToken":"Cldwcm9qZWN0cy80ODcxMDUyNDYzMjcvbG9jYXRpb25zL3VzLWNlbnRyYWwxL2J1aWxkcy8wNGVhYmJmNi1hY2Y0LTRjNDMtODMzZS02Zjg0Yjg3NWFhYTAShgF1cy1jZW50cmFsMS1kb2NrZXIucGtnLmRldi9nZW4tbGFuZy1jbGllbnQtMDk4MDA3OTQxMC9nY2YtYXJ0aWZhY3RzL2dlbi0tbGFuZy0tY2xpZW50LS0wOTgwMDc5NDEwX191cy0tY2VudHJhbDFfX25leHRqc19mdW5jOnZlcnNpb25fMRj36PDNlg4iTnByb2plY3RzL2dlbi1sYW5nLWNsaWVudC0wOTgwMDc5NDEwL2xvY2F0aW9ucy91cy1jZW50cmFsMS9mdW5jdGlvbnMvbmV4dGpzRnVuYyoMCM32o8kGENCRkK8BMghub2RlanMyMjp2CiNnY3IuaW8vZ2FlLXJ1bnRpbWVzL25vZGVqczIyOnN0YWJsZRJPdXMtY2VudHJhbDEtZG9ja2VyLnBrZy5kZXYvc2VydmVybGVzcy1ydW50aW1lcy9nb29nbGUtMjItZnVsbC9ydW50aW1lcy9ub2RlanMyMkAB","operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:31:48.617Z] Got source token Cldwcm9qZWN0cy80ODcxMDUyNDYzMjcvbG9jYXRpb25zL3VzLWNlbnRyYWwxL2J1aWxkcy8wNGVhYmJmNi1hY2Y0LTRjNDMtODMzZS02Zjg0Yjg3NWFhYTAShgF1cy1jZW50cmFsMS1kb2NrZXIucGtnLmRldi9nZW4tbGFuZy1jbGllbnQtMDk4MDA3OTQxMC9nY2YtYXJ0aWZhY3RzL2dlbi0tbGFuZy0tY2xpZW50LS0wOTgwMDc5NDEwX191cy0tY2VudHJhbDFfX25leHRqc19mdW5jOnZlcnNpb25fMRj36PDNlg4iTnByb2plY3RzL2dlbi1sYW5nLWNsaWVudC0wOTgwMDc5NDEwL2xvY2F0aW9ucy91cy1jZW50cmFsMS9mdW5jdGlvbnMvbmV4dGpzRnVuYyoMCM32o8kGENCRkK8BMghub2RlanMyMjp2CiNnY3IuaW8vZ2FlLXJ1bnRpbWVzL25vZGVqczIyOnN0YWJsZRJPdXMtY2VudHJhbDEtZG9ja2VyLnBrZy5kZXYvc2VydmVybGVzcy1ydW50aW1lcy9nb29nbGUtMjItZnVsbC9ydW50aW1lcy9ub2RlanMyMkAB for region us-central1
-[debug] [2025-11-28T01:31:58.619Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:31:58.619Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:31:58.620Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:31:58.620Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:31:58.981Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:31:58.982Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build finished","state":"COMPLETE","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","message":"Updating Cloud Run service","state":"IN_PROGRESS","resource":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","resourceUri":"https://console.cloud.google.com/run/detail/us-central1/nextjsfunc?project=gen-lang-client-0980079410","stateMessages":[{"severity":"INFO","type":"CloudRunServiceNewRevisionTrafficInfo","message":"A new revision will be deployed serving with 100% traffic."}]}],"sourceToken":"Cldwcm9qZWN0cy80ODcxMDUyNDYzMjcvbG9jYXRpb25zL3VzLWNlbnRyYWwxL2J1aWxkcy8wNGVhYmJmNi1hY2Y0LTRjNDMtODMzZS02Zjg0Yjg3NWFhYTAShgF1cy1jZW50cmFsMS1kb2NrZXIucGtnLmRldi9nZW4tbGFuZy1jbGllbnQtMDk4MDA3OTQxMC9nY2YtYXJ0aWZhY3RzL2dlbi0tbGFuZy0tY2xpZW50LS0wOTgwMDc5NDEwX191cy0tY2VudHJhbDFfX25leHRqc19mdW5jOnZlcnNpb25fMRj36PDNlg4iTnByb2plY3RzL2dlbi1sYW5nLWNsaWVudC0wOTgwMDc5NDEwL2xvY2F0aW9ucy91cy1jZW50cmFsMS9mdW5jdGlvbnMvbmV4dGpzRnVuYyoMCM32o8kGENCRkK8BMghub2RlanMyMjp2CiNnY3IuaW8vZ2FlLXJ1bnRpbWVzL25vZGVqczIyOnN0YWJsZRJPdXMtY2VudHJhbDEtZG9ja2VyLnBrZy5kZXYvc2VydmVybGVzcy1ydW50aW1lcy9nb29nbGUtMjItZnVsbC9ydW50aW1lcy9ub2RlanMyMkAB","operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:31:58.982Z] Got source token Cldwcm9qZWN0cy80ODcxMDUyNDYzMjcvbG9jYXRpb25zL3VzLWNlbnRyYWwxL2J1aWxkcy8wNGVhYmJmNi1hY2Y0LTRjNDMtODMzZS02Zjg0Yjg3NWFhYTAShgF1cy1jZW50cmFsMS1kb2NrZXIucGtnLmRldi9nZW4tbGFuZy1jbGllbnQtMDk4MDA3OTQxMC9nY2YtYXJ0aWZhY3RzL2dlbi0tbGFuZy0tY2xpZW50LS0wOTgwMDc5NDEwX191cy0tY2VudHJhbDFfX25leHRqc19mdW5jOnZlcnNpb25fMRj36PDNlg4iTnByb2plY3RzL2dlbi1sYW5nLWNsaWVudC0wOTgwMDc5NDEwL2xvY2F0aW9ucy91cy1jZW50cmFsMS9mdW5jdGlvbnMvbmV4dGpzRnVuYyoMCM32o8kGENCRkK8BMghub2RlanMyMjp2CiNnY3IuaW8vZ2FlLXJ1bnRpbWVzL25vZGVqczIyOnN0YWJsZRJPdXMtY2VudHJhbDEtZG9ja2VyLnBrZy5kZXYvc2VydmVybGVzcy1ydW50aW1lcy9nb29nbGUtMjItZnVsbC9ydW50aW1lcy9ub2RlanMyMkAB for region us-central1
-[debug] [2025-11-28T01:32:08.983Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:32:08.983Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:32:08.983Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:32:08.983Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:32:09.324Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:32:09.324Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"BUILD","message":"Build finished","state":"COMPLETE","resource":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0","resourceUri":"https://console.cloud.google.com/cloud-build/builds;region=us-central1/04eabbf6-acf4-4c43-833e-6f84b875aaa0?project=487105246327"},{"name":"SERVICE","message":"Updating Cloud Run service","state":"IN_PROGRESS","resource":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","resourceUri":"https://console.cloud.google.com/run/detail/us-central1/nextjsfunc?project=gen-lang-client-0980079410","stateMessages":[{"severity":"INFO","type":"CloudRunServiceNewRevisionTrafficInfo","message":"A new revision will be deployed serving with 100% traffic."}]}],"sourceToken":"Cldwcm9qZWN0cy80ODcxMDUyNDYzMjcvbG9jYXRpb25zL3VzLWNlbnRyYWwxL2J1aWxkcy8wNGVhYmJmNi1hY2Y0LTRjNDMtODMzZS02Zjg0Yjg3NWFhYTAShgF1cy1jZW50cmFsMS1kb2NrZXIucGtnLmRldi9nZW4tbGFuZy1jbGllbnQtMDk4MDA3OTQxMC9nY2YtYXJ0aWZhY3RzL2dlbi0tbGFuZy0tY2xpZW50LS0wOTgwMDc5NDEwX191cy0tY2VudHJhbDFfX25leHRqc19mdW5jOnZlcnNpb25fMRj36PDNlg4iTnByb2plY3RzL2dlbi1sYW5nLWNsaWVudC0wOTgwMDc5NDEwL2xvY2F0aW9ucy91cy1jZW50cmFsMS9mdW5jdGlvbnMvbmV4dGpzRnVuYyoMCM32o8kGENCRkK8BMghub2RlanMyMjp2CiNnY3IuaW8vZ2FlLXJ1bnRpbWVzL25vZGVqczIyOnN0YWJsZRJPdXMtY2VudHJhbDEtZG9ja2VyLnBrZy5kZXYvc2VydmVybGVzcy1ydW50aW1lcy9nb29nbGUtMjItZnVsbC9ydW50aW1lcy9ub2RlanMyMkAB","operationType":"UPDATE_FUNCTION","buildName":"projects/487105246327/locations/us-central1/builds/04eabbf6-acf4-4c43-833e-6f84b875aaa0"},"done":false}
-[debug] [2025-11-28T01:32:09.324Z] Got source token Cldwcm9qZWN0cy80ODcxMDUyNDYzMjcvbG9jYXRpb25zL3VzLWNlbnRyYWwxL2J1aWxkcy8wNGVhYmJmNi1hY2Y0LTRjNDMtODMzZS02Zjg0Yjg3NWFhYTAShgF1cy1jZW50cmFsMS1kb2NrZXIucGtnLmRldi9nZW4tbGFuZy1jbGllbnQtMDk4MDA3OTQxMC9nY2YtYXJ0aWZhY3RzL2dlbi0tbGFuZy0tY2xpZW50LS0wOTgwMDc5NDEwX191cy0tY2VudHJhbDFfX25leHRqc19mdW5jOnZlcnNpb25fMRj36PDNlg4iTnByb2plY3RzL2dlbi1sYW5nLWNsaWVudC0wOTgwMDc5NDEwL2xvY2F0aW9ucy91cy1jZW50cmFsMS9mdW5jdGlvbnMvbmV4dGpzRnVuYyoMCM32o8kGENCRkK8BMghub2RlanMyMjp2CiNnY3IuaW8vZ2FlLXJ1bnRpbWVzL25vZGVqczIyOnN0YWJsZRJPdXMtY2VudHJhbDEtZG9ja2VyLnBrZy5kZXYvc2VydmVybGVzcy1ydW50aW1lcy9nb29nbGUtMjItZnVsbC9ydW50aW1lcy9ub2RlanMyMkAB for region us-central1
-[debug] [2025-11-28T01:32:19.326Z] [update-default-us-central1-nextjsFunc] Retrying task index 0
-[debug] [2025-11-28T01:32:19.327Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:32:19.327Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:32:19.327Z] >>> [apiv2][query] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a [none]
-[debug] [2025-11-28T01:32:19.636Z] <<< [apiv2][status] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a 200
-[debug] [2025-11-28T01:32:19.636Z] <<< [apiv2][body] GET https://cloudfunctions.googleapis.com/v2/projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a {"name":"projects/gen-lang-client-0980079410/locations/us-central1/operations/operation-1764293345895-6449d8e29637d-e8f47636-ec62944a","metadata":{"@type":"type.googleapis.com/google.cloud.functions.v2.OperationMetadata","createTime":"2025-11-28T01:29:06.489194110Z","endTime":"2025-11-28T01:32:15.500792790Z","target":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","verb":"update","cancelRequested":false,"apiVersion":"v2","requestResource":{"@type":"type.googleapis.com/google.cloud.functions.v2.Function","name":"projects/gen-lang-client-0980079410/locations/us-central1/functions/nextjsFunc","buildConfig":{"runtime":"nodejs22","entryPoint":"nextjsFunc","source":{"storageSource":{"bucket":"gcf-v2-sources-487105246327-us-central1","object":"nextjsFunc/function-source.zip","generation":"1764293346449263"}},"environmentVariables":{"GOOGLE_NODE_RUN_SCRIPTS":""},"dockerRepository":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","serviceAccount":"projects/gen-lang-client-0980079410/serviceAccounts/487105246327-compute@developer.gserviceaccount.com","automaticUpdatePolicy":{}},"serviceConfig":{"service":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsfunc","timeoutSeconds":300,"environmentVariables":{"FIREBASE_CONFIG":"{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}","GCLOUD_PROJECT":"gen-lang-client-0980079410","EVENTARC_CLOUD_EVENT_SOURCE":"projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc","FUNCTION_TARGET":"nextjsFunc","LOG_EXECUTION_ID":"true"},"maxInstanceCount":10,"ingressSettings":"ALLOW_ALL","serviceAccountEmail":"487105246327-compute@developer.gserviceaccount.com","availableMemory":"2Gi","allTrafficOnLatestRevision":true,"revision":"nextjsfunc-00006-lev","maxInstanceRequestConcurrency":80,"availableCpu":"1"},"state":"ACTIVE","labels":{"deployment-tool":"cloudfunctions","firebase-functions-hash":"762ff6a37d560c50662025d3ad902ab9cee36009"},"environment":"GEN_2","url":"https://us-central1-gen-lang-client-0980079410.cloudfunctions.net/nextjsFunc","satisfiesPzi":true},"stages":[{"name":"SERVICE","state":"NOT_STARTED"}],"operationType":"UPDATE_FUNCTION"},"done":true,"error":{"code":3,"message":"Could not create or update Cloud Run service nextjsfunc, Container Healthcheck failed. Revision 'nextjsfunc-00007-kat' is not ready and cannot serve traffic. The user-provided container failed to start and listen on the port defined provided by the PORT=8080 environment variable within the allocated timeout. This can happen when the container port is misconfigured or if the timeout is too short. The health check timeout can be extended. Logs for this revision might contain more information.\n\nLogs URL: https://console.cloud.google.com/logs/viewer?project=gen-lang-client-0980079410&resource=cloud_run_revision/service_name/nextjsfunc/revision_name/nextjsfunc-00007-kat&advancedFilter=resource.type%3D%22cloud_run_revision%22%0Aresource.labels.service_name%3D%22nextjsfunc%22%0Aresource.labels.revision_name%3D%22nextjsfunc-00007-kat%22 \nFor more troubleshooting guidance, see https://cloud.google.com/run/docs/troubleshooting#container-failed-to-start"}}
-[debug] [2025-11-28T01:32:19.636Z] Got source token undefined for region us-central1
-[error] Could not create or update Cloud Run service nextjsfunc, Container Healthcheck failed. Revision 'nextjsfunc-00007-kat' is not ready and cannot serve traffic. The user-provided container failed to start and listen on the port defined provided by the PORT=8080 environment variable within the allocated timeout. This can happen when the container port is misconfigured or if the timeout is too short. The health check timeout can be extended. Logs for this revision might contain more information.
-
-Logs URL: https://console.cloud.google.com/logs/viewer?project=gen-lang-client-0980079410&resource=cloud_run_revision/service_name/nextjsfunc/revision_name/nextjsfunc-00007-kat&advancedFilter=resource.type%3D%22cloud_run_revision%22%0Aresource.labels.service_name%3D%22nextjsfunc%22%0Aresource.labels.revision_name%3D%22nextjsfunc-00007-kat%22
-For more troubleshooting guidance, see https://cloud.google.com/run/docs/troubleshooting#container-failed-to-start
-[debug] [2025-11-28T01:32:19.659Z] Total Function Deployment time: 194058
-[debug] [2025-11-28T01:32:19.659Z] 1 Functions Deployed
-[debug] [2025-11-28T01:32:19.659Z] 1 Functions Errored
-[debug] [2025-11-28T01:32:19.659Z] 0 Function Deployments Aborted
-[debug] [2025-11-28T01:32:19.659Z] Average Function Deployment time: 194057
-[info]
-[info] Functions deploy had errors with the following functions:
- nextjsFunc(us-central1)
-[debug] [2025-11-28T01:32:19.724Z] Not printing URL for HTTPS function. Typically this means it didn't match a filter or we failed deployment
-[debug] [2025-11-28T01:32:19.725Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:32:19.725Z] Checked if tokens are valid: true, expires at: 1764296401599
-[debug] [2025-11-28T01:32:19.725Z] >>> [apiv2][query] GET https://artifactregistry.googleapis.com/v1/projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts [none]
-[debug] [2025-11-28T01:32:20.091Z] <<< [apiv2][status] GET https://artifactregistry.googleapis.com/v1/projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts 200
-[debug] [2025-11-28T01:32:20.091Z] <<< [apiv2][body] GET https://artifactregistry.googleapis.com/v1/projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts {"name":"projects/gen-lang-client-0980079410/locations/us-central1/repositories/gcf-artifacts","format":"DOCKER","description":"This repository is created and used by Cloud Functions for storing function docker images.","labels":{"goog-managed-by":"cloudfunctions"},"createTime":"2025-11-13T19:00:13.623998Z","updateTime":"2025-11-28T01:30:18.310357Z","mode":"STANDARD_REPOSITORY","cleanupPolicies":{"firebase-functions-cleanup":{"id":"firebase-functions-cleanup","action":"DELETE","condition":{"tagState":"ANY","olderThan":"86400s"}}},"sizeBytes":"1901562099","vulnerabilityScanningConfig":{"lastEnableTime":"2025-11-13T19:00:05.303862604Z","enablementState":"SCANNING_DISABLED","enablementStateReason":"API containerscanning.googleapis.com is not enabled."},"satisfiesPzi":true,"registryUri":"us-central1-docker.pkg.dev/gen-lang-client-0980079410/gcf-artifacts"}
-[debug] [2025-11-28T01:32:20.091Z] Functions deploy failed.
-[debug] [2025-11-28T01:32:20.092Z] {
- "endpoint": {
- "id": "nextjsFunc",
- "project": "gen-lang-client-0980079410",
- "region": "us-central1",
- "entryPoint": "nextjsFunc",
- "platform": "gcfv2",
- "runtime": "nodejs22",
- "httpsTrigger": {},
- "labels": {
- "deployment-tool": "cli-firebase"
- },
- "serviceAccount": null,
- "ingressSettings": null,
- "availableMemoryMb": 2048,
- "timeoutSeconds": 300,
- "maxInstances": 10,
- "minInstances": 0,
- "concurrency": 80,
- "vpc": null,
- "environmentVariables": {
- "FIREBASE_CONFIG": "{\"projectId\":\"gen-lang-client-0980079410\",\"storageBucket\":\"gen-lang-client-0980079410.firebasestorage.app\"}",
- "GCLOUD_PROJECT": "gen-lang-client-0980079410",
- "EVENTARC_CLOUD_EVENT_SOURCE": "projects/gen-lang-client-0980079410/locations/us-central1/services/nextjsFunc",
- "FUNCTION_TARGET": "nextjsFunc",
- "LOG_EXECUTION_ID": "true"
- },
- "codebase": "default",
- "runServiceId": "nextjsfunc",
- "cpu": 1,
- "securityLevel": "SECURE_ALWAYS",
- "targetedByOnly": false,
- "hash": "762ff6a37d560c50662025d3ad902ab9cee36009"
- },
- "op": "update",
- "original": {
- "name": "FirebaseError",
- "children": [],
- "exit": 1,
- "message": "Could not create or update Cloud Run service nextjsfunc, Container Healthcheck failed. Revision 'nextjsfunc-00007-kat' is not ready and cannot serve traffic. The user-provided container failed to start and listen on the port defined provided by the PORT=8080 environment variable within the allocated timeout. This can happen when the container port is misconfigured or if the timeout is too short. The health check timeout can be extended. Logs for this revision might contain more information.\n\nLogs URL: https://console.cloud.google.com/logs/viewer?project=gen-lang-client-0980079410&resource=cloud_run_revision/service_name/nextjsfunc/revision_name/nextjsfunc-00007-kat&advancedFilter=resource.type%3D%22cloud_run_revision%22%0Aresource.labels.service_name%3D%22nextjsfunc%22%0Aresource.labels.revision_name%3D%22nextjsfunc-00007-kat%22 \nFor more troubleshooting guidance, see https://cloud.google.com/run/docs/troubleshooting#container-failed-to-start",
- "original": {
- "code": 3,
- "message": "Could not create or update Cloud Run service nextjsfunc, Container Healthcheck failed. Revision 'nextjsfunc-00007-kat' is not ready and cannot serve traffic. The user-provided container failed to start and listen on the port defined provided by the PORT=8080 environment variable within the allocated timeout. This can happen when the container port is misconfigured or if the timeout is too short. The health check timeout can be extended. Logs for this revision might contain more information.\n\nLogs URL: https://console.cloud.google.com/logs/viewer?project=gen-lang-client-0980079410&resource=cloud_run_revision/service_name/nextjsfunc/revision_name/nextjsfunc-00007-kat&advancedFilter=resource.type%3D%22cloud_run_revision%22%0Aresource.labels.service_name%3D%22nextjsfunc%22%0Aresource.labels.revision_name%3D%22nextjsfunc-00007-kat%22 \nFor more troubleshooting guidance, see https://cloud.google.com/run/docs/troubleshooting#container-failed-to-start"
- },
- "status": 3,
- "code": 3
- }
-}
-[debug] [2025-11-28T01:32:20.116Z] Error: Failed to update function nextjsFunc in region us-central1
- at /opt/homebrew/lib/node_modules/firebase-tools/lib/deploy/functions/release/fabricator.js:411:19
- at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
- at async Fabricator.updateV2Function (/opt/homebrew/lib/node_modules/firebase-tools/lib/deploy/functions/release/fabricator.js:400:32)
- at async Fabricator.updateEndpoint (/opt/homebrew/lib/node_modules/firebase-tools/lib/deploy/functions/release/fabricator.js:160:13)
- at async handle (/opt/homebrew/lib/node_modules/firebase-tools/lib/deploy/functions/release/fabricator.js:89:17)
-[error]
-[error] Error: There was an error deploying functions
diff --git a/firebase.json b/firebase.json
deleted file mode 100644
index 2942dfe1..00000000
--- a/firebase.json
+++ /dev/null
@@ -1,60 +0,0 @@
-{
- "firestore": {
- "rules": "firestore.rules",
- "indexes": "firestore.indexes.json"
- },
- "storage": {
- "rules": "storage.rules"
- },
- "hosting": {
- "public": "public",
- "ignore": [
- "firebase.json",
- "**/.*",
- "**/node_modules/**"
- ],
- "rewrites": [
- {
- "source": "**",
- "function": "nextjsFunc"
- }
- ]
- },
- "functions": [{
- "source": ".",
- "runtime": "nodejs22",
- "codebase": "default",
- "memory": "2GB",
- "timeout": "300s",
- "ignore": [
- "node_modules",
- ".git",
- "firebase-debug.log",
- "firebase-debug.*.log",
- ".next",
- "pnpm-lock.yaml",
- "app",
- "components",
- "lib",
- "public",
- "scripts"
- ]
- }],
- "emulators": {
- "auth": {
- "port": 9099
- },
- "firestore": {
- "port": 8080
- },
- "storage": {
- "port": 9199
- },
- "ui": {
- "enabled": true,
- "port": 4000
- },
- "singleProjectMode": true
- }
-}
-
diff --git a/firestore.indexes.json b/firestore.indexes.json
deleted file mode 100644
index d4596891..00000000
--- a/firestore.indexes.json
+++ /dev/null
@@ -1,214 +0,0 @@
-{
- "indexes": [
- {
- "collectionGroup": "apiKeys",
- "queryScope": "COLLECTION",
- "fields": [
- {
- "fieldPath": "userId",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "isActive",
- "order": "ASCENDING"
- }
- ]
- },
- {
- "collectionGroup": "projects",
- "queryScope": "COLLECTION",
- "fields": [
- {
- "fieldPath": "userId",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "createdAt",
- "order": "DESCENDING"
- }
- ]
- },
- {
- "collectionGroup": "projects",
- "queryScope": "COLLECTION",
- "fields": [
- {
- "fieldPath": "workspace",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "createdAt",
- "order": "DESCENDING"
- }
- ]
- },
- {
- "collectionGroup": "sessions",
- "queryScope": "COLLECTION",
- "fields": [
- {
- "fieldPath": "projectId",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "startTime",
- "order": "DESCENDING"
- }
- ]
- },
- {
- "collectionGroup": "sessions",
- "queryScope": "COLLECTION",
- "fields": [
- {
- "fieldPath": "userId",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "startTime",
- "order": "DESCENDING"
- }
- ]
- },
- {
- "collectionGroup": "sessions",
- "queryScope": "COLLECTION",
- "fields": [
- {
- "fieldPath": "userId",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "createdAt",
- "order": "DESCENDING"
- }
- ]
- },
- {
- "collectionGroup": "sessions",
- "queryScope": "COLLECTION",
- "fields": [
- {
- "fieldPath": "userId",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "needsProjectAssociation",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "createdAt",
- "order": "DESCENDING"
- }
- ]
- },
- {
- "collectionGroup": "sessions",
- "queryScope": "COLLECTION",
- "fields": [
- {
- "fieldPath": "userId",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "workspacePath",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "needsProjectAssociation",
- "order": "ASCENDING"
- }
- ]
- },
- {
- "collectionGroup": "sessions",
- "queryScope": "COLLECTION",
- "fields": [
- {
- "fieldPath": "projectId",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "userId",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "createdAt",
- "order": "DESCENDING"
- }
- ]
- },
- {
- "collectionGroup": "analyses",
- "queryScope": "COLLECTION",
- "fields": [
- {
- "fieldPath": "projectId",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "createdAt",
- "order": "DESCENDING"
- }
- ]
- },
- {
- "collectionGroup": "workCompleted",
- "queryScope": "COLLECTION",
- "fields": [
- {
- "fieldPath": "projectId",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "completedAt",
- "order": "DESCENDING"
- }
- ]
- },
- {
- "collectionGroup": "clients",
- "queryScope": "COLLECTION",
- "fields": [
- {
- "fieldPath": "ownerId",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "createdAt",
- "order": "DESCENDING"
- }
- ]
- },
- {
- "collectionGroup": "knowledge_items",
- "queryScope": "COLLECTION",
- "fields": [
- {
- "fieldPath": "projectId",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "sourceType",
- "order": "ASCENDING"
- }
- ]
- },
- {
- "collectionGroup": "knowledge_items",
- "queryScope": "COLLECTION",
- "fields": [
- {
- "fieldPath": "projectId",
- "order": "ASCENDING"
- },
- {
- "fieldPath": "createdAt",
- "order": "DESCENDING"
- }
- ]
- }
- ],
- "fieldOverrides": []
-}
-
diff --git a/firestore.rules b/firestore.rules
deleted file mode 100644
index 35864549..00000000
--- a/firestore.rules
+++ /dev/null
@@ -1,216 +0,0 @@
-rules_version = '2';
-
-service cloud.firestore {
- match /databases/{database}/documents {
-
- // Helper functions
- function isAuthenticated() {
- return request.auth != null;
- }
-
- function isOwner(userId) {
- return isAuthenticated() && request.auth.uid == userId;
- }
-
- // Users collection
- match /users/{userId} {
- // Users can read their own data
- allow read: if isOwner(userId);
- // Users can create their own user document
- allow create: if isOwner(userId);
- // Users can update their own data
- allow update: if isOwner(userId);
- // No deletes for now
- allow delete: if false;
- }
-
- // API Keys collection
- match /apiKeys/{keyId} {
- // Only the server can create/read API keys (via Admin SDK)
- // Users cannot directly access API key documents
- allow read, write: if false;
- }
-
- // MCP API Keys collection
- match /mcpKeys/{keyId} {
- // Only the server can create/read/delete MCP keys (via Admin SDK)
- // Users cannot directly access MCP key documents
- allow read, write: if false;
- }
-
- // Projects collection
- match /projects/{projectId} {
- // Users can read their own projects
- allow read: if isAuthenticated() && resource.data.userId == request.auth.uid;
- // Users can create projects
- allow create: if isAuthenticated() && request.resource.data.userId == request.auth.uid;
- // Users can update their own projects
- allow update: if isAuthenticated() && resource.data.userId == request.auth.uid;
- // Users can delete their own projects
- allow delete: if isAuthenticated() && resource.data.userId == request.auth.uid;
-
- // AI Conversations subcollection
- match /aiConversations/{conversationId} {
- // Users can read conversations for their projects
- allow read: if isAuthenticated() &&
- get(/databases/$(database)/documents/projects/$(projectId)).data.userId == request.auth.uid;
- // Server creates conversation entries via Admin SDK
- allow create: if false; // Only server via Admin SDK
- // No updates to conversation history (immutable)
- allow update: if false;
- // No deletes (audit trail)
- allow delete: if false;
- }
-
- // Vision Board subcollection
- match /visionBoard/{visionDocId} {
- // Users can read/write vision board for their projects
- allow read, write: if isAuthenticated() &&
- get(/databases/$(database)/documents/projects/$(projectId)).data.userId == request.auth.uid;
- }
-
- // Context Sources subcollection (for chat content, files, etc.)
- match /contextSources/{sourceId} {
- // Users can read/write context sources for their projects
- allow read, write: if isAuthenticated() &&
- get(/databases/$(database)/documents/projects/$(projectId)).data.userId == request.auth.uid;
- }
- }
-
- // Sessions collection
- match /sessions/{sessionId} {
- // Users can read their own sessions (by userId or by projectId they own)
- allow read: if isAuthenticated() && (
- resource.data.userId == request.auth.uid ||
- (resource.data.projectId != null &&
- get(/databases/$(database)/documents/projects/$(resource.data.projectId)).data.userId == request.auth.uid)
- );
- // Sessions are created by the server via API (Admin SDK)
- allow create: if false; // Only server via Admin SDK
- // Users can update their own sessions
- allow update: if isAuthenticated() && resource.data.userId == request.auth.uid;
- // No deletes for sessions (audit trail)
- allow delete: if false;
- }
-
- // Analyses collection
- match /analyses/{analysisId} {
- // Users can read analyses for their projects
- // Note: This requires fetching the project document to verify ownership
- allow read: if isAuthenticated() &&
- get(/databases/$(database)/documents/projects/$(resource.data.projectId)).data.userId == request.auth.uid;
- // Users can create analyses for their projects
- allow create: if isAuthenticated() &&
- get(/databases/$(database)/documents/projects/$(request.resource.data.projectId)).data.userId == request.auth.uid;
- // Users can update analyses for their projects
- allow update: if isAuthenticated() &&
- get(/databases/$(database)/documents/projects/$(resource.data.projectId)).data.userId == request.auth.uid;
- // No deletes for analyses (audit trail)
- allow delete: if false;
- }
-
- // Work Completed collection
- match /workCompleted/{workId} {
- // Users can read work completed for their projects
- allow read: if isAuthenticated() &&
- get(/databases/$(database)/documents/projects/$(resource.data.projectId)).data.userId == request.auth.uid;
- // Server creates work completed entries
- allow create: if false; // Only server via Admin SDK
- // Users can update work completed for their projects
- allow update: if isAuthenticated() &&
- get(/databases/$(database)/documents/projects/$(resource.data.projectId)).data.userId == request.auth.uid;
- // No deletes
- allow delete: if false;
- }
-
- // Clients collection
- match /clients/{clientId} {
- // Users can read their own clients
- allow read: if isAuthenticated() && resource.data.ownerId == request.auth.uid;
- // Users can create clients
- allow create: if isAuthenticated() && request.resource.data.ownerId == request.auth.uid;
- // Users can update their own clients
- allow update: if isAuthenticated() && resource.data.ownerId == request.auth.uid;
- // Users can delete their own clients
- allow delete: if isAuthenticated() && resource.data.ownerId == request.auth.uid;
- }
-
- // ChatGPT Imports collection
- match /chatgptImports/{importId} {
- // Users can read their own imports
- allow read: if isAuthenticated() && resource.data.userId == request.auth.uid;
- // Server creates imports via Admin SDK
- allow create: if false; // Only server via Admin SDK
- // Users can update their own imports (e.g., add notes)
- allow update: if isAuthenticated() && resource.data.userId == request.auth.uid;
- // Users can delete their own imports
- allow delete: if isAuthenticated() && resource.data.userId == request.auth.uid;
- }
-
- // User API Keys collection (third-party keys like OpenAI, GitHub)
- match /userKeys/{keyId} {
- // Only server can access keys (via Admin SDK)
- // Keys are encrypted and should never be directly accessible to clients
- allow read, write: if false;
- }
-
- // Knowledge Items collection (documents, notes, chat imports)
- match /knowledge_items/{itemId} {
- // Users can read knowledge items for their projects
- allow read: if isAuthenticated() &&
- get(/databases/$(database)/documents/projects/$(resource.data.projectId)).data.userId == request.auth.uid;
- // Server creates knowledge items via Admin SDK
- allow create: if false; // Only server via Admin SDK
- // No updates or deletes (immutable)
- allow update, delete: if false;
- }
-
- // Chat Extractions collection (AI-extracted insights)
- match /chat_extractions/{extractionId} {
- // Users can read extractions for their projects
- allow read: if isAuthenticated() &&
- get(/databases/$(database)/documents/projects/$(resource.data.projectId)).data.userId == request.auth.uid;
- // Server creates extractions via Admin SDK
- allow create: if false; // Only server via Admin SDK
- // No updates or deletes (immutable)
- allow update, delete: if false;
- }
-
- // Chat Conversations collection (conversation history)
- match /chat_conversations/{conversationId} {
- // Users can read conversations for their projects
- allow read: if isAuthenticated() &&
- get(/databases/$(database)/documents/projects/$(resource.data.projectId)).data.userId == request.auth.uid;
- // Server creates and updates conversations via Admin SDK
- allow create, update: if false; // Only server via Admin SDK
- // No deletes (audit trail)
- allow delete: if false;
- }
-
- // GitHub Connections collection (OAuth tokens and profile)
- match /githubConnections/{connectionId} {
- // Users can read their own GitHub connections
- allow read: if isAuthenticated() && resource.data.userId == request.auth.uid;
- // Server creates connections via OAuth callback
- allow create: if false; // Only server via Admin SDK
- // Users cannot update or delete (managed by server)
- allow update, delete: if false;
- }
-
- // Linked Extensions collection (browser extension connections)
- match /linkedExtensions/{linkId} {
- // Users can read their own extension links
- allow read: if isAuthenticated() && resource.data.userId == request.auth.uid;
- // Server creates links via API
- allow create: if false; // Only server via Admin SDK
- // No updates or deletes
- allow update, delete: if false;
- }
-
- // Default deny all other access
- match /{document=**} {
- allow read, write: if false;
- }
- }
-}
-
diff --git a/lib/ai/chat-extraction-types.ts b/lib/ai/chat-extraction-types.ts
deleted file mode 100644
index ba66e2a6..00000000
--- a/lib/ai/chat-extraction-types.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import { z } from 'zod';
-
-const evidenceArray = z.array(z.string()).default([]);
-const confidenceValue = z.number().min(0).max(1).default(0);
-const completionScore = z.number().min(0).max(1).default(0);
-
-const defaultWeightedString = {
- description: null as string | null,
- confidence: 0,
- evidence: [] as string[],
-};
-
-const weightedStringField = z
- .object({
- description: z.union([z.string(), z.null()]).default(null),
- confidence: confidenceValue.default(0),
- evidence: evidenceArray.default([]),
- })
- .default(defaultWeightedString);
-
-const weightedListItem = z.object({
- id: z.string(),
- description: z.string(),
- confidence: confidenceValue,
- evidence: evidenceArray,
-});
-
-const stageEnum = z.enum([
- 'idea',
- 'prototype',
- 'mvp_in_progress',
- 'live_beta',
- 'live_paid',
- 'unknown',
-]);
-
-const severityEnum = z.enum(['low', 'medium', 'high', 'unknown']);
-const frequencyEnum = z.enum(['rare', 'occasional', 'frequent', 'constant', 'unknown']);
-const competitorTypeEnum = z.enum(['direct', 'indirect', 'alternative', 'unknown']);
-const relatedAreaEnum = z.enum(['product', 'tech', 'market', 'business_model', 'other']);
-const priorityEnum = z.enum(['high', 'medium', 'low']);
-
-export const ChatExtractionSchema = z.object({
- project_summary: z.object({
- working_title: z.union([z.string(), z.null()]).default(null),
- one_liner: z.union([z.string(), z.null()]).default(null),
- stage: stageEnum.default('unknown'),
- overall_confidence: confidenceValue,
- evidence: evidenceArray,
- }),
- product_vision: z.object({
- problem_statement: weightedStringField,
- target_outcome: weightedStringField,
- founder_intent: weightedStringField,
- completion_score: completionScore,
- }),
- target_users: z.object({
- primary_segment: weightedStringField,
- segments: z
- .array(
- z.object({
- id: z.string(),
- description: z.string(),
- jobs_to_be_done: z.array(z.string()).default([]),
- environment: z.union([z.string(), z.null()]),
- confidence: confidenceValue,
- evidence: evidenceArray,
- }),
- )
- .default([]),
- completion_score: completionScore,
- }),
- problems_and_pains: z.object({
- problems: z
- .array(
- z.object({
- id: z.string(),
- description: z.string(),
- severity: severityEnum,
- frequency: frequencyEnum,
- confidence: confidenceValue,
- evidence: evidenceArray,
- }),
- )
- .default([]),
- completion_score: completionScore,
- }),
- solution_and_features: z.object({
- core_solution: weightedStringField,
- core_features: z
- .array(
- z.object({
- id: z.string(),
- name: z.string(),
- description: z.string(),
- is_must_have_for_v1: z.boolean(),
- confidence: confidenceValue,
- evidence: evidenceArray,
- }),
- )
- .default([]),
- nice_to_have_features: z
- .array(
- z.object({
- id: z.string(),
- name: z.string(),
- description: z.string(),
- confidence: confidenceValue,
- evidence: evidenceArray,
- }),
- )
- .default([]),
- completion_score: completionScore,
- }),
- market_and_competition: z.object({
- market_category: weightedStringField,
- competitors: z
- .array(
- z.object({
- id: z.string(),
- name: z.string(),
- description: z.string(),
- type: competitorTypeEnum,
- confidence: confidenceValue,
- evidence: evidenceArray,
- }),
- )
- .default([]),
- differentiation_points: weightedListItem.array().default([]),
- completion_score: completionScore,
- }),
- tech_and_constraints: z.object({
- stack_mentions: weightedListItem.array().default([]),
- constraints: weightedListItem.array().default([]),
- completion_score: completionScore,
- }),
- execution_status: z.object({
- current_stage: weightedStringField,
- work_done: weightedListItem.array().default([]),
- work_in_progress: weightedListItem.array().default([]),
- blocked_items: weightedListItem.array().default([]),
- completion_score: completionScore,
- }),
- goals_and_success: z.object({
- short_term_goals: weightedListItem.array().default([]),
- long_term_goals: weightedListItem.array().default([]),
- success_criteria: weightedListItem.array().default([]),
- completion_score: completionScore,
- }),
- unknowns_and_questions: z.object({
- unknowns: z
- .array(
- z.object({
- id: z.string(),
- description: z.string(),
- related_area: relatedAreaEnum,
- evidence: evidenceArray,
- confidence: confidenceValue,
- }),
- )
- .default([]),
- questions_to_ask_user: z
- .array(
- z.object({
- id: z.string(),
- question: z.string(),
- priority: priorityEnum,
- }),
- )
- .default([]),
- }),
- summary_scores: z.object({
- overall_completion: completionScore,
- overall_confidence: confidenceValue,
- }),
-});
-
-export type ChatExtractionData = z.infer;
-
-
diff --git a/lib/ai/chat-extractor.ts b/lib/ai/chat-extractor.ts
deleted file mode 100644
index 17bfb210..00000000
--- a/lib/ai/chat-extractor.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import type { LlmClient } from '@/lib/ai/llm-client';
-import { ChatExtractionSchema } from '@/lib/ai/chat-extraction-types';
-import type { ChatExtractionData } from '@/lib/ai/chat-extraction-types';
-import type { KnowledgeItem } from '@/lib/types/knowledge';
-
-const SYSTEM_PROMPT = `
-You are the Product Chat Signal Extractor for stalled SaaS projects.
-- Read the provided transcript carefully.
-- Extract grounded signals about the product, market, users, execution status, and unknowns.
-- Never invent data. Use "null" or empty arrays when the transcript lacks information.
-- Respond with valid JSON that matches the provided schema exactly. Do not include prose or code fences.
-`.trim();
-
-export async function runChatExtraction(
- knowledgeItem: KnowledgeItem,
- llm: LlmClient,
-): Promise {
- const transcript = knowledgeItem.content.trim();
-
- const userMessage = `
-You will analyze the following transcript. Use message references when listing evidence (e.g., msg_1).
-Focus on actionable product-building insights.
-
-TRANSCRIPT_START
-${transcript}
-TRANSCRIPT_END`.trim();
-
- return llm.structuredCall({
- model: 'gemini',
- systemPrompt: SYSTEM_PROMPT,
- messages: [
- {
- role: 'user',
- content: userMessage,
- },
- ],
- schema: ChatExtractionSchema,
- temperature: 0.2,
- });
-}
-
-
diff --git a/lib/ai/chat-modes.ts b/lib/ai/chat-modes.ts
deleted file mode 100644
index 98dbf122..00000000
--- a/lib/ai/chat-modes.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Chat Modes and System Prompts
- *
- * Defines available chat modes and maps them to their system prompts.
- * Prompts are now versioned and managed in separate files under lib/ai/prompts/
- */
-
-import {
- collectorPrompt,
- extractionReviewPrompt,
- visionPrompt,
- mvpPrompt,
- marketingPrompt,
- generalChatPrompt,
-} from './prompts';
-
-export type ChatMode =
- | "collector_mode"
- | "extraction_review_mode"
- | "vision_mode"
- | "mvp_mode"
- | "marketing_mode"
- | "general_chat_mode";
-
-/**
- * Maps each chat mode to its current active system prompt.
- *
- * Prompts are version-controlled in separate files.
- * To update a prompt or switch versions, edit the corresponding file in lib/ai/prompts/
- */
-export const MODE_SYSTEM_PROMPTS: Record = {
- collector_mode: collectorPrompt,
- extraction_review_mode: extractionReviewPrompt,
- vision_mode: visionPrompt,
- mvp_mode: mvpPrompt,
- marketing_mode: marketingPrompt,
- general_chat_mode: generalChatPrompt,
-};
diff --git a/lib/ai/chunking.ts b/lib/ai/chunking.ts
deleted file mode 100644
index fa896f51..00000000
--- a/lib/ai/chunking.ts
+++ /dev/null
@@ -1,297 +0,0 @@
-/**
- * Text chunking for semantic search
- *
- * Splits large documents into smaller, semantically coherent chunks
- * suitable for vector embedding and retrieval.
- */
-
-export interface TextChunk {
- /** Index of this chunk (0-based) */
- index: number;
-
- /** The chunked text content */
- text: string;
-
- /** Approximate token count (for reference) */
- estimatedTokens: number;
-}
-
-export interface ChunkingOptions {
- /** Target maximum tokens per chunk (approximate) */
- maxTokens?: number;
-
- /** Target maximum characters per chunk (fallback if no tokenizer) */
- maxChars?: number;
-
- /** Overlap between chunks (in characters) */
- overlapChars?: number;
-
- /** Whether to try preserving paragraph boundaries */
- preserveParagraphs?: boolean;
-}
-
-const DEFAULT_OPTIONS: Required = {
- maxTokens: 800,
- maxChars: 3000, // Rough approximation: ~4 chars per token
- overlapChars: 200,
- preserveParagraphs: true,
-};
-
-/**
- * Estimate token count from character count
- *
- * Uses a rough heuristic: 1 token ≈ 4 characters for English text.
- * For more accuracy, integrate a real tokenizer (e.g., tiktoken).
- */
-function estimateTokens(text: string): number {
- return Math.ceil(text.length / 4);
-}
-
-/**
- * Split text into paragraphs, preserving empty lines as separators
- */
-function splitIntoParagraphs(text: string): string[] {
- return text.split(/\n\n+/).filter((p) => p.trim().length > 0);
-}
-
-/**
- * Split text into sentences (simple heuristic)
- */
-function splitIntoSentences(text: string): string[] {
- // Simple sentence boundary detection
- return text
- .split(/[.!?]+\s+/)
- .map((s) => s.trim())
- .filter((s) => s.length > 0);
-}
-
-/**
- * Chunk text into semantic pieces suitable for embedding
- *
- * Strategy:
- * 1. Split by paragraphs (if preserveParagraphs = true)
- * 2. Group paragraphs/sentences until reaching maxTokens/maxChars
- * 3. Add overlap between chunks for context continuity
- *
- * @param content - Text to chunk
- * @param options - Chunking options
- * @returns Array of text chunks with metadata
- *
- * @example
- * ```typescript
- * const chunks = chunkText(longDocument, { maxTokens: 500, overlapChars: 100 });
- * for (const chunk of chunks) {
- * console.log(`Chunk ${chunk.index}: ${chunk.estimatedTokens} tokens`);
- * await embedText(chunk.text);
- * }
- * ```
- */
-export function chunkText(
- content: string,
- options: ChunkingOptions = {}
-): TextChunk[] {
- const opts = { ...DEFAULT_OPTIONS, ...options };
- const chunks: TextChunk[] = [];
-
- if (!content || content.trim().length === 0) {
- return chunks;
- }
-
- // Clean up content
- const cleanedContent = content.trim();
-
- // If content is small enough, return as single chunk
- if (estimateTokens(cleanedContent) <= opts.maxTokens) {
- return [
- {
- index: 0,
- text: cleanedContent,
- estimatedTokens: estimateTokens(cleanedContent),
- },
- ];
- }
-
- // Split into paragraphs or sentences
- const units = opts.preserveParagraphs
- ? splitIntoParagraphs(cleanedContent)
- : splitIntoSentences(cleanedContent);
-
- if (units.length === 0) {
- return [
- {
- index: 0,
- text: cleanedContent,
- estimatedTokens: estimateTokens(cleanedContent),
- },
- ];
- }
-
- let currentChunk = '';
- let chunkIndex = 0;
- let previousOverlap = '';
-
- for (let i = 0; i < units.length; i++) {
- const unit = units[i];
- const potentialChunk = currentChunk
- ? `${currentChunk}\n\n${unit}`
- : `${previousOverlap}${unit}`;
-
- const potentialTokens = estimateTokens(potentialChunk);
- const potentialChars = potentialChunk.length;
-
- // Check if adding this unit would exceed limits
- if (
- potentialTokens > opts.maxTokens ||
- potentialChars > opts.maxChars
- ) {
- // Save current chunk if it has content
- if (currentChunk.length > 0) {
- chunks.push({
- index: chunkIndex++,
- text: currentChunk,
- estimatedTokens: estimateTokens(currentChunk),
- });
-
- // Prepare overlap for next chunk
- const overlapStart = Math.max(
- 0,
- currentChunk.length - opts.overlapChars
- );
- previousOverlap = currentChunk.substring(overlapStart);
- if (previousOverlap.length > 0 && !previousOverlap.endsWith(' ')) {
- // Try to start overlap at a word boundary
- const spaceIndex = previousOverlap.indexOf(' ');
- if (spaceIndex > 0) {
- previousOverlap = previousOverlap.substring(spaceIndex + 1);
- }
- }
- }
-
- // Start new chunk with current unit
- currentChunk = `${previousOverlap}${unit}`;
- } else {
- // Add unit to current chunk
- currentChunk = potentialChunk;
- }
- }
-
- // Add final chunk if it has content
- if (currentChunk.length > 0) {
- chunks.push({
- index: chunkIndex++,
- text: currentChunk,
- estimatedTokens: estimateTokens(currentChunk),
- });
- }
-
- console.log(
- `[Chunking] Split ${cleanedContent.length} chars into ${chunks.length} chunks`
- );
-
- return chunks;
-}
-
-/**
- * Chunk text with code-aware splitting
- *
- * Preserves code blocks and tries to keep them intact.
- * Useful for chunking AI chat transcripts that contain code snippets.
- */
-export function chunkTextWithCodeAwareness(
- content: string,
- options: ChunkingOptions = {}
-): TextChunk[] {
- const opts = { ...DEFAULT_OPTIONS, ...options };
-
- // Detect code blocks (triple backticks)
- const codeBlockRegex = /```[\s\S]*?```/g;
- const codeBlocks: { start: number; end: number; content: string }[] = [];
- let match;
-
- while ((match = codeBlockRegex.exec(content)) !== null) {
- codeBlocks.push({
- start: match.index,
- end: match.index + match[0].length,
- content: match[0],
- });
- }
-
- // If no code blocks, use standard chunking
- if (codeBlocks.length === 0) {
- return chunkText(content, options);
- }
-
- // Split content around code blocks
- const chunks: TextChunk[] = [];
- let chunkIndex = 0;
- let currentPosition = 0;
-
- for (const codeBlock of codeBlocks) {
- // Chunk text before code block
- const textBefore = content.substring(currentPosition, codeBlock.start);
- if (textBefore.trim().length > 0) {
- const textChunks = chunkText(textBefore, opts);
- for (const chunk of textChunks) {
- chunks.push({
- ...chunk,
- index: chunkIndex++,
- });
- }
- }
-
- // Add code block as its own chunk (or split if too large)
- const codeTokens = estimateTokens(codeBlock.content);
- if (codeTokens <= opts.maxTokens) {
- chunks.push({
- index: chunkIndex++,
- text: codeBlock.content,
- estimatedTokens: codeTokens,
- });
- } else {
- // Code block is too large, split by lines
- const codeLines = codeBlock.content.split('\n');
- let currentCodeChunk = '';
- for (const line of codeLines) {
- const potentialChunk = currentCodeChunk
- ? `${currentCodeChunk}\n${line}`
- : line;
- if (estimateTokens(potentialChunk) > opts.maxTokens) {
- if (currentCodeChunk.length > 0) {
- chunks.push({
- index: chunkIndex++,
- text: currentCodeChunk,
- estimatedTokens: estimateTokens(currentCodeChunk),
- });
- }
- currentCodeChunk = line;
- } else {
- currentCodeChunk = potentialChunk;
- }
- }
- if (currentCodeChunk.length > 0) {
- chunks.push({
- index: chunkIndex++,
- text: currentCodeChunk,
- estimatedTokens: estimateTokens(currentCodeChunk),
- });
- }
- }
-
- currentPosition = codeBlock.end;
- }
-
- // Chunk remaining text after last code block
- const textAfter = content.substring(currentPosition);
- if (textAfter.trim().length > 0) {
- const textChunks = chunkText(textAfter, opts);
- for (const chunk of textChunks) {
- chunks.push({
- ...chunk,
- index: chunkIndex++,
- });
- }
- }
-
- return chunks;
-}
-
diff --git a/lib/ai/project-context/codebase-summary.ts b/lib/ai/codebase-summary.ts
similarity index 100%
rename from lib/ai/project-context/codebase-summary.ts
rename to lib/ai/codebase-summary.ts
diff --git a/lib/ai/embeddings.ts b/lib/ai/embeddings.ts
deleted file mode 100644
index 55230edc..00000000
--- a/lib/ai/embeddings.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-/**
- * Embedding generation using Gemini API
- *
- * Converts text into vector embeddings for semantic search.
- */
-
-import { GoogleGenerativeAI } from '@google/generative-ai';
-
-const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
-
-if (!GEMINI_API_KEY) {
- console.warn('[Embeddings] GEMINI_API_KEY not set - embedding functions will fail');
-}
-
-const genAI = GEMINI_API_KEY ? new GoogleGenerativeAI(GEMINI_API_KEY) : null;
-
-// Gemini embedding model - text-embedding-004 produces 768-dim embeddings
-// Adjust EMBEDDING_DIMENSION in knowledge-chunks-schema.sql if using different model
-const EMBEDDING_MODEL = 'text-embedding-004';
-const EMBEDDING_DIMENSION = 768;
-
-/**
- * Generate embedding for a single text string
- *
- * @param text - Input text to embed
- * @returns Vector embedding as array of numbers
- *
- * @throws Error if Gemini API is not configured or request fails
- */
-export async function embedText(text: string): Promise {
- if (!genAI) {
- throw new Error('GEMINI_API_KEY not configured - cannot generate embeddings');
- }
-
- if (!text || text.trim().length === 0) {
- throw new Error('Cannot embed empty text');
- }
-
- try {
- const model = genAI.getGenerativeModel({ model: EMBEDDING_MODEL });
- const result = await model.embedContent(text);
- const embedding = result.embedding;
-
- if (!embedding || !embedding.values || embedding.values.length === 0) {
- throw new Error('Gemini returned empty embedding');
- }
-
- // Verify dimension matches expectation
- if (embedding.values.length !== EMBEDDING_DIMENSION) {
- console.warn(
- `[Embeddings] Unexpected dimension: got ${embedding.values.length}, expected ${EMBEDDING_DIMENSION}`
- );
- }
-
- return embedding.values;
- } catch (error) {
- console.error('[Embeddings] Failed to embed text:', error);
- throw new Error(
- `Embedding generation failed: ${error instanceof Error ? error.message : String(error)}`
- );
- }
-}
-
-/**
- * Generate embeddings for multiple texts in batch
- *
- * More efficient than calling embedText() repeatedly.
- * Processes texts sequentially to avoid rate limiting.
- *
- * @param texts - Array of texts to embed
- * @param options - Batch processing options
- * @returns Array of embeddings (same order as input texts)
- *
- * @example
- * ```typescript
- * const chunks = ["First chunk...", "Second chunk...", "Third chunk..."];
- * const embeddings = await embedTextBatch(chunks);
- * // embeddings[0] corresponds to chunks[0], etc.
- * ```
- */
-export async function embedTextBatch(
- texts: string[],
- options: { delayMs?: number; skipEmpty?: boolean } = {}
-): Promise {
- const { delayMs = 100, skipEmpty = true } = options;
-
- if (texts.length === 0) {
- return [];
- }
-
- const embeddings: number[][] = [];
-
- for (let i = 0; i < texts.length; i++) {
- const text = texts[i];
-
- // Skip empty texts if requested
- if (skipEmpty && (!text || text.trim().length === 0)) {
- console.warn(`[Embeddings] Skipping empty text at index ${i}`);
- embeddings.push(new Array(EMBEDDING_DIMENSION).fill(0)); // Zero vector for empty
- continue;
- }
-
- try {
- const embedding = await embedText(text);
- embeddings.push(embedding);
-
- // Add delay between requests to avoid rate limiting (except for last item)
- if (i < texts.length - 1 && delayMs > 0) {
- await new Promise((resolve) => setTimeout(resolve, delayMs));
- }
- } catch (error) {
- console.error(`[Embeddings] Failed to embed text at index ${i}:`, error);
- // Push zero vector as fallback
- embeddings.push(new Array(EMBEDDING_DIMENSION).fill(0));
- }
- }
-
- console.log(`[Embeddings] Generated ${embeddings.length} embeddings`);
-
- return embeddings;
-}
-
-/**
- * Compute cosine similarity between two embeddings
- *
- * @param a - First embedding vector
- * @param b - Second embedding vector
- * @returns Cosine similarity score (0-1, higher = more similar)
- */
-export function cosineSimilarity(a: number[], b: number[]): number {
- if (a.length !== b.length) {
- throw new Error('Embedding dimensions do not match');
- }
-
- let dotProduct = 0;
- let normA = 0;
- let normB = 0;
-
- for (let i = 0; i < a.length; i++) {
- dotProduct += a[i] * b[i];
- normA += a[i] * a[i];
- normB += b[i] * b[i];
- }
-
- const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
-
- if (magnitude === 0) {
- return 0;
- }
-
- return dotProduct / magnitude;
-}
-
-/**
- * Get the expected embedding dimension for the current model
- */
-export function getEmbeddingDimension(): number {
- return EMBEDDING_DIMENSION;
-}
-
-/**
- * Check if embeddings API is configured and working
- */
-export async function checkEmbeddingsHealth(): Promise {
- try {
- const testEmbedding = await embedText('health check');
- return testEmbedding.length === EMBEDDING_DIMENSION;
- } catch (error) {
- console.error('[Embeddings Health Check] Failed:', error);
- return false;
- }
-}
-
diff --git a/lib/ai/marketing-agent.ts b/lib/ai/marketing-agent.ts
deleted file mode 100644
index d8a24e8c..00000000
--- a/lib/ai/marketing-agent.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { z } from 'zod';
-import type { LlmClient } from '@/lib/ai/llm-client';
-import { GeminiLlmClient } from '@/lib/ai/gemini-client';
-import { clamp, nowIso, loadPhaseContainers, persistPhaseArtifacts } from '@/lib/server/projects';
-import type { MarketingModel } from '@/lib/types/marketing';
-
-const HomepageMessagingSchema = z.object({
- headline: z.string().nullable(),
- subheadline: z.string().nullable(),
- bullets: z.array(z.string()).default([]),
-});
-
-const MarketingModelSchema = z.object({
- projectId: z.string(),
- icp: z.array(z.string()).default([]),
- positioning: z.string().nullable(),
- homepageMessaging: HomepageMessagingSchema,
- initialChannels: z.array(z.string()).default([]),
- launchAngles: z.array(z.string()).default([]),
- overallConfidence: z.number().min(0).max(1),
-});
-
-export async function runMarketingPlanning(
- projectId: string,
- llmClient?: LlmClient,
-): Promise {
- const { phaseData } = await loadPhaseContainers(projectId);
- const canonical = phaseData.canonicalProductModel;
- if (!canonical) {
- throw new Error('Canonical product model missing. Run buildCanonicalProductModel first.');
- }
-
- const llm = llmClient ?? new GeminiLlmClient();
- const systemPrompt =
- 'You are a SaaS marketing strategist. Given the canonical product model, produce ICP, positioning, homepage messaging, and launch ideas as strict JSON.';
-
- const marketing = await llm.structuredCall({
- model: 'gemini',
- systemPrompt,
- messages: [
- {
- role: 'user',
- content: [
- 'Canonical product model JSON:',
- '```json',
- JSON.stringify(canonical, null, 2),
- '```',
- 'Respond ONLY with valid JSON that matches the required schema.',
- ].join('\n'),
- },
- ],
- schema: MarketingModelSchema,
- temperature: 0.2,
- });
-
- await persistPhaseArtifacts(projectId, (phaseData, phaseScores, phaseHistory) => {
- phaseData.marketingPlan = marketing;
- phaseScores.marketing = {
- overallCompletion: clamp(marketing.homepageMessaging.bullets.length ? 0.7 : 0.5),
- overallConfidence: marketing.overallConfidence,
- updatedAt: nowIso(),
- };
- phaseHistory.push({ phase: 'marketing', status: 'completed', timestamp: nowIso() });
- return { phaseData, phaseScores, phaseHistory, nextPhase: 'marketing_ready' };
- });
-
- return marketing;
-}
-
-
diff --git a/lib/ai/mvp-agent.ts b/lib/ai/mvp-agent.ts
deleted file mode 100644
index 1c1ab43c..00000000
--- a/lib/ai/mvp-agent.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { z } from 'zod';
-import type { LlmClient } from '@/lib/ai/llm-client';
-import { GeminiLlmClient } from '@/lib/ai/gemini-client';
-import { clamp, nowIso, loadPhaseContainers, persistPhaseArtifacts } from '@/lib/server/projects';
-import type { MvpPlan } from '@/lib/types/mvp';
-
-const MvpPlanSchema = z.object({
- projectId: z.string(),
- coreFlows: z.array(z.string()).default([]),
- coreFeatures: z.array(z.string()).default([]),
- supportingFeatures: z.array(z.string()).default([]),
- outOfScope: z.array(z.string()).default([]),
- technicalTasks: z.array(z.string()).default([]),
- blockers: z.array(z.string()).default([]),
- overallConfidence: z.number().min(0).max(1),
-});
-
-export async function runMvpPlanning(projectId: string, llmClient?: LlmClient): Promise {
- const { phaseData } = await loadPhaseContainers(projectId);
- const canonical = phaseData.canonicalProductModel;
- if (!canonical) {
- throw new Error('Canonical product model missing. Run buildCanonicalProductModel first.');
- }
-
- const llm = llmClient ?? new GeminiLlmClient();
- const systemPrompt =
- 'You are an expert SaaS product manager. Given the canonical product model, produce the smallest sellable MVP plan as strict JSON.';
-
- const plan = await llm.structuredCall({
- model: 'gemini',
- systemPrompt,
- messages: [
- {
- role: 'user',
- content: [
- 'Canonical product model JSON:',
- '```json',
- JSON.stringify(canonical, null, 2),
- '```',
- 'Respond ONLY with JSON that matches the required schema.',
- ].join('\n'),
- },
- ],
- schema: MvpPlanSchema,
- temperature: 0.2,
- });
-
- await persistPhaseArtifacts(projectId, (phaseData, phaseScores, phaseHistory) => {
- phaseData.mvpPlan = plan;
- phaseScores.mvp = {
- overallCompletion: clamp(plan.coreFeatures.length ? 0.8 : 0.5),
- overallConfidence: plan.overallConfidence,
- updatedAt: nowIso(),
- };
- phaseHistory.push({ phase: 'mvp', status: 'completed', timestamp: nowIso() });
- return { phaseData, phaseScores, phaseHistory, nextPhase: 'mvp_ready' };
- });
-
- return plan;
-}
-
-
diff --git a/lib/ai/plan-extract.ts b/lib/ai/plan-extract.ts
deleted file mode 100644
index cc5bfb96..00000000
--- a/lib/ai/plan-extract.ts
+++ /dev/null
@@ -1,266 +0,0 @@
-/**
- * Fire-and-forget plan extraction from chat conversations.
- *
- * After each chat turn, we call a cheap Gemini model (Flash) to scan the
- * conversation for plan-worthy content — new tasks, decisions, vision updates —
- * and auto-persist them via the same `fs_projects.data->plan` path used by
- * the Plan tab MCP tools.
- *
- * The cheap model is configured via VIBN_CHEAP_MODEL (default: gemini-3.1-pro-preview).
- */
-
-import { query } from "@/lib/db-postgres";
-
-const GEMINI_API_KEY = process.env.GOOGLE_API_KEY || "";
-const CHEAP_MODEL =
- process.env.VIBN_CHEAP_MODEL || "gemini-3.1-pro-preview";
-const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
-
-interface PlanExtraction {
- tasks: Array<{ title: string; description?: string }>;
- decisions: Array<{ title: string; choice: string; why?: string }>;
- visionUpdate?: string;
-}
-
-/**
- * Call the cheap Gemini model to extract plan updates from the transcript.
- */
-async function extractPlanFromTranscript(
- transcript: string,
-): Promise {
- const url = `${GEMINI_BASE_URL}/models/${CHEAP_MODEL}:generateContent?key=${GEMINI_API_KEY}`;
-
- const body = {
- contents: [
- {
- role: "user",
- parts: [
- {
- text:
- "Extract any plan-worthy content from this AI coding conversation. " +
- "Return ONLY valid JSON with this schema:\n" +
- '{\n "tasks": [{"title": "short task name", "description": "optional details"}],\n' +
- ' "decisions": [{"title": "what was decided", "choice": "the chosen option", "why": "reasoning"}],\n' +
- ' "visionUpdate": "updated product vision (only if the conversation meaningfully changes or clarifies it)"\n' +
- "}\n\n" +
- "Rules:\n" +
- "- Only extract CLEAR tasks the AI committed to do or the user explicitly requested.\n" +
- "- Only extract NON-TRIVIAL decisions (not 'I'll read that file').\n" +
- "- visionUpdate: set ONLY when the user articulates or refines their product vision. Omit entirely if not.\n" +
- "- Return empty arrays if nothing worthy found.\n" +
- "- Do NOT wrap in markdown code fences. Just the raw JSON.\n\n" +
- "Conversation:\n" +
- transcript.slice(0, 12000),
- },
- ],
- },
- ],
- generationConfig: { temperature: 0.1, maxOutputTokens: 1024 },
- };
-
- let res: Response;
- try {
- res = await fetch(url, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(body),
- });
- } catch {
- return null;
- }
-
- const data = await res.json().catch(() => ({}));
- if (!res.ok) return null;
-
- const text = data?.candidates?.[0]?.content?.parts?.[0]?.text || "";
- if (!text.trim()) return null;
-
- try {
- return JSON.parse(text.trim()) as PlanExtraction;
- } catch {
- // Strip markdown code fences if present
- const cleaned = text.replace(/```(?:json)?\s*/g, "").trim();
- try {
- return JSON.parse(cleaned) as PlanExtraction;
- } catch {
- return null;
- }
- }
-}
-
-function planNewId(): string {
- return `plan_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
-}
-
-interface PlanProject {
- id: string;
- data: Record;
-}
-
-async function loadPlanProject(
- projectId: string,
-): Promise {
- const rows = await query(
- `SELECT id, data FROM fs_projects WHERE id = $1 LIMIT 1`,
- [projectId],
- );
- return rows[0] ?? null;
-}
-
-interface PlanTask {
- id: string;
- title: string;
- description?: string;
- status: "open" | "in_progress" | "review" | "done" | "blocked";
- text?: string;
- createdAt: string;
-}
-
-interface PlanDecision {
- id: string;
- title: string;
- choice: string;
- why?: string;
- createdAt: string;
-}
-
-interface PlanShape {
- vision?: string;
- ideas: Array<{ id: string; text: string; createdAt: string }>;
- tasks: PlanTask[];
- decisions: PlanDecision[];
-}
-
-function readPlanFromData(data: Record): PlanShape {
- const raw = (data?.plan as Record) ?? {};
- const ideas = Array.isArray(raw.ideas) ? raw.ideas : [];
- const tasks = Array.isArray(raw.tasks)
- ? (raw.tasks as PlanTask[]).map((t) => ({
- ...t,
- id: String(t.id ?? planNewId()),
- title: String(t.title ?? t.text ?? "").trim(),
- status: t.status ?? "open",
- createdAt: String(t.createdAt ?? new Date().toISOString()),
- }))
- : [];
- const decisions = Array.isArray(raw.decisions)
- ? (raw.decisions as PlanDecision[]).map((d) => ({
- ...d,
- id: String(d.id ?? planNewId()),
- title: String(d.title ?? "").trim(),
- choice: String(d.choice ?? "").trim(),
- createdAt: String(d.createdAt ?? new Date().toISOString()),
- }))
- : [];
- return {
- vision: typeof raw.vision === "string" ? raw.vision : undefined,
- ideas,
- tasks,
- decisions,
- };
-}
-
-async function writePlan(
- projectId: string,
- plan: PlanShape,
- alsoVision?: string,
-): Promise {
- const serialized = {
- vision: plan.vision,
- ideas: plan.ideas,
- tasks: plan.tasks,
- decisions: plan.decisions,
- };
- if (alsoVision !== undefined) {
- await query(
- `UPDATE fs_projects
- SET data = data || jsonb_build_object('plan', $2::jsonb, 'productVision', $3::text),
- updated_at = NOW()
- WHERE id = $1`,
- [projectId, JSON.stringify(serialized), alsoVision],
- );
- } else {
- await query(
- `UPDATE fs_projects
- SET data = data || jsonb_build_object('plan', $2::jsonb),
- updated_at = NOW()
- WHERE id = $1`,
- [projectId, JSON.stringify(serialized)],
- );
- }
-}
-
-/**
- * Main entry point: scan the conversation transcript and auto-update the
- * project plan with any extracted tasks/decisions/vision.
- *
- * Called fire-and-forget after each chat turn. Never throws.
- */
-export async function autoExtractPlanUpdates(
- projectId: string,
- transcript: string,
-): Promise<{ tasks: number; decisions: number; vision: boolean } | null> {
- if (!projectId || transcript.length < 20) return null;
-
- try {
- const extraction = await extractPlanFromTranscript(transcript);
- if (!extraction) return null;
-
- const hasTasks = extraction.tasks?.length > 0;
- const hasDecisions = extraction.decisions?.length > 0;
- const hasVision =
- typeof extraction.visionUpdate === "string" &&
- extraction.visionUpdate.trim().length > 0;
-
- if (!hasTasks && !hasDecisions && !hasVision) return null;
-
- const project = await loadPlanProject(projectId);
- if (!project) return null;
-
- const plan = readPlanFromData(project.data);
- const now = new Date().toISOString();
- let taskCount = 0;
- let decisionCount = 0;
-
- for (const t of extraction.tasks ?? []) {
- const exists = plan.tasks.some(
- (existing) => existing.title.toLowerCase() === t.title.toLowerCase(),
- );
- if (exists) continue;
- plan.tasks.unshift({
- id: planNewId(),
- title: t.title.trim(),
- description: t.description?.trim(),
- status: "open",
- createdAt: now,
- });
- taskCount++;
- }
-
- for (const d of extraction.decisions ?? []) {
- const exists = plan.decisions.some(
- (existing) => existing.title.toLowerCase() === d.title.toLowerCase(),
- );
- if (exists) continue;
- plan.decisions.unshift({
- id: planNewId(),
- title: d.title.trim(),
- choice: d.choice.trim(),
- why: d.why?.trim(),
- createdAt: now,
- });
- decisionCount++;
- }
-
- if (hasVision && extraction.visionUpdate) {
- plan.vision = extraction.visionUpdate.trim();
- }
-
- if (taskCount === 0 && decisionCount === 0 && !hasVision) return null;
-
- await writePlan(projectId, plan, hasVision ? extraction.visionUpdate : undefined);
- return { tasks: taskCount, decisions: decisionCount, vision: hasVision };
- } catch {
- return null;
- }
-}
diff --git a/lib/ai/prompts/README.md b/lib/ai/prompts/README.md
deleted file mode 100644
index 54543cf3..00000000
--- a/lib/ai/prompts/README.md
+++ /dev/null
@@ -1,176 +0,0 @@
-# Prompt Management System
-
-This directory contains all versioned system prompts for Vibn's chat modes.
-
-## 📁 Structure
-
-```
-prompts/
-├── index.ts # Exports all prompts
-├── shared.ts # Shared prompt components
-├── collector.ts # Collector mode prompts
-├── extraction-review.ts # Extraction review mode prompts
-├── vision.ts # Vision mode prompts
-├── mvp.ts # MVP mode prompts
-├── marketing.ts # Marketing mode prompts
-└── general-chat.ts # General chat mode prompts
-```
-
-## 🔄 Versioning
-
-Each prompt file contains:
-1. **Version history** - All versions of the prompt
-2. **Metadata** - Version number, date, description
-3. **Current version** - Which version is active
-
-### Example Structure
-
-```typescript
-const COLLECTOR_V1: PromptVersion = {
- version: 'v1',
- createdAt: '2024-11-17',
- description: 'Initial version',
- prompt: `...`,
-};
-
-const COLLECTOR_V2: PromptVersion = {
- version: 'v2',
- createdAt: '2024-12-01',
- description: 'Added context-aware chunking',
- prompt: `...`,
-};
-
-export const collectorPrompts = {
- v1: COLLECTOR_V1,
- v2: COLLECTOR_V2,
- current: 'v2', // ← Active version
-};
-```
-
-## 📝 How to Add a New Prompt Version
-
-1. **Open the relevant mode file** (e.g., `collector.ts`)
-2. **Create a new version constant:**
- ```typescript
- const COLLECTOR_V2: PromptVersion = {
- version: 'v2',
- createdAt: '2024-12-01',
- description: 'What changed in this version',
- prompt: `
- Your new prompt text here...
- `,
- };
- ```
-3. **Add to the prompts object:**
- ```typescript
- export const collectorPrompts = {
- v1: COLLECTOR_V1,
- v2: COLLECTOR_V2, // Add new version
- current: 'v2', // Update current
- };
- ```
-4. **Done!** The system will automatically use the new version.
-
-## 🔙 How to Rollback
-
-Simply change the `current` field:
-
-```typescript
-export const collectorPrompts = {
- v1: COLLECTOR_V1,
- v2: COLLECTOR_V2,
- current: 'v1', // Rolled back to v1
-};
-```
-
-## 📊 Benefits of This System
-
-1. **Version History** - Keep all previous prompts for reference
-2. **Easy Rollback** - Instantly revert to a previous version
-3. **Git-Friendly** - Clear diffs show exactly what changed
-4. **Documentation** - Each version has a description of changes
-5. **A/B Testing Ready** - Can easily test multiple versions
-6. **Isolated Changes** - Changing one prompt doesn't affect others
-
-## 🎯 Usage in Code
-
-```typescript
-// Import current prompts (most common)
-import { MODE_SYSTEM_PROMPTS } from '@/lib/ai/chat-modes';
-
-const prompt = MODE_SYSTEM_PROMPTS['collector_mode'];
-
-// Or access version history
-import { collectorPrompts } from '@/lib/ai/prompts';
-
-console.log(collectorPrompts.v1.prompt); // Old version
-console.log(collectorPrompts.current); // 'v2'
-```
-
-## 🚀 Future Enhancements
-
-### Analytics Tracking
-Track performance by prompt version:
-```typescript
-await logPromptUsage({
- mode: 'collector_mode',
- version: collectorPrompts.current,
- userId: user.id,
- responseQuality: 0.85,
-});
-```
-
-### A/B Testing
-Test multiple versions simultaneously:
-```typescript
-const promptVersion = userInExperiment ? 'v2' : 'v1';
-const prompt = collectorPrompts[promptVersion].prompt;
-```
-
-### Database Storage
-Move to Firestore for dynamic updates:
-```typescript
-// Future: Load from database
-const prompt = await getPrompt('collector_mode', 'latest');
-```
-
-## 📚 Best Practices
-
-1. **Always add a description** - Future you will thank you
-2. **Never delete old versions** - Keep history for rollback
-3. **Test before deploying** - Ensure new prompts work as expected
-4. **Document changes** - What problem does the new version solve?
-5. **Version incrementally** - Don't skip version numbers
-
-## 🔍 Example: Adding Context-Aware Chunking
-
-```typescript
-// 1. Create new version
-const COLLECTOR_V2: PromptVersion = {
- version: 'v2',
- createdAt: '2024-11-17',
- description: 'Added instructions for context-aware chunking',
- prompt: `
-${COLLECTOR_V1.prompt}
-
-**Context-Aware Retrieval**:
-When referencing retrieved chunks, always cite the source document
-and chunk number for transparency.
- `,
-};
-
-// 2. Update prompts object
-export const collectorPrompts = {
- v1: COLLECTOR_V1,
- v2: COLLECTOR_V2,
- current: 'v2',
-};
-
-// 3. Deploy and monitor
-// If issues arise, simply change current: 'v1' to rollback
-```
-
----
-
-**Questions?** Check the code in any prompt file for examples.
-
diff --git a/lib/ai/prompts/collector.ts b/lib/ai/prompts/collector.ts
deleted file mode 100644
index bb243f31..00000000
--- a/lib/ai/prompts/collector.ts
+++ /dev/null
@@ -1,318 +0,0 @@
-/**
- * Collector Mode Prompt
- *
- * Purpose: Gathers project materials and triggers analysis
- * Active when: No extractions exist yet
- */
-
-import { GITHUB_ACCESS_INSTRUCTION } from './shared';
-
-export interface PromptVersion {
- version: string;
- prompt: string;
- createdAt: string;
- description: string;
-}
-
-const COLLECTOR_V1: PromptVersion = {
- version: 'v1',
- createdAt: '2024-11-17',
- description: 'Initial version with GitHub analysis and context-aware behavior',
- prompt: `
-You are Vibn, an AI copilot that helps indie devs and small teams rescue stalled SaaS projects.
-
-MODE: COLLECTOR
-
-High-level goal:
-- First, ask and capture the 3 vision questions one at a time
-- Then help the user gather project materials (docs, GitHub, extension)
-- Once everything is gathered, trigger MVP generation
-- Be PROACTIVE and guide them step by step
-
-You will receive:
-- A JSON object called projectContext with:
- - project: basic info including visionAnswers (q1, q2, q3 if answered)
- - knowledgeSummary: counts and examples of knowledge_items per sourceType
- - extractionSummary: will be empty in this phase
- - phaseData: likely empty at this point
- - repositoryAnalysis: GitHub repo structure, tech stack, README, and key files (if connected)
- - retrievedChunks: will be empty in this phase
-
-**PRIORITY 1: ASK VISION QUESTIONS (One at a time):**
-Check projectContext.project.visionAnswers to see what's been answered:
-
-**Question 1** - If visionAnswers.q1 is missing:
-Ask: "Let's start with your vision. **Who has the problem you want to fix and what is it?**"
-
-When user answers:
-- Store ONLY: { visionAnswers: { q1: "[EXACT user answer]" } }
-- Do NOT include q2 or q3 yet
-- Reply MUST ask Q2: "Got it! [reflection]. Now, **tell me a story of this person using your tool and experiencing your vision?**"
-
-**Question 2** - If visionAnswers.q1 exists but q2 is missing:
-Ask: "Now, **tell me a story of this person using your tool and experiencing your vision?**"
-
-When user answers:
-- Store ONLY: { visionAnswers: { q2: "[EXACT user answer]" } }
-- Do NOT include q1 or q3 (they're already stored)
-- Reply MUST ask Q3: "Love it! [reflection]. One more: **How much did that improve things for them?**"
-
-**Question 3** - If visionAnswers.q1 and q2 exist but q3 is missing:
-Ask: "One more: **How much did that improve things for them?**"
-
-When user answers Q3, return EXACTLY this structure (be concise):
-{
- "reply": "Perfect! Let me generate your MVP plan now...",
- "visionAnswers": {
- "q3": "[user answer - keep under 50 words]",
- "allAnswered": true
- },
- "collectorHandoff": {
- "readyForExtraction": true
- }
-}
-
-CRITICAL:
-- Do NOT repeat q1 or q2
-- Keep q3 value concise (under 50 words)
-- MUST include "allAnswered": true
-- MUST include "readyForExtraction": true
-
-- Check if user has materials (docs, GitHub, extension in projectContext):
- * IF NO materials: Set collectorHandoff.readyForExtraction = true
- * IF materials exist: Set collectorHandoff.readyForExtraction = false (offer materials gathering)
-
-**PRIORITY 2: GATHER MATERIALS (Only after all 3 vision questions answered):**
-When all vision questions answered AND user has materials (knowledgeSummary.totalCount > 0 OR githubRepo OR extensionLinked), say:
-
-"Welcome to Vibn! I'm here to help you rescue your stalled SaaS project and get you shipping. Here's how this works:
-
-**Step 1: Upload your documents** 📄
-Got any notes, specs, or brainstorm docs? Click the 'Context' tab to upload them.
-
-**Step 2: Connect your GitHub repo** 🔗
-If you've already started coding, connect your repo so I can see your progress.
-
-**Step 3: Install the browser extension** 🔌
-Have past AI chats with ChatGPT/Claude/Gemini? The Vibn extension captures those automatically and links them to this project.
-
-Ready to start? What do you have for me first - documents, code, or AI chat history?"
-
-**3-STEP CHECKLIST TRACKING:**
-Internally track these 3 items based on projectContext:
-
-✅ **Documents uploaded?**
-- Check knowledgeSummary.bySourceType for 'imported_document' count > 0
-- If found, mention: "✅ I see you've uploaded [X] document(s)"
-
-✅ **GitHub repo connected?**
-- Check if projectContext.project.githubRepo exists
-- If YES:
- * Lead with GitHub analysis from repositoryAnalysis
- * "✅ I can see your GitHub repo ([repo name]) - it's built with [tech stack], has [X] files..."
- * Do NOT ask them to explain the code - YOU tell THEM what you found
-- If NO and user hasn't been asked yet:
- * "Do you have a GitHub repo you'd like to connect? That way I can understand your technical progress."
-
-✅ **Extension connected?**
-- Check projectContext.project.extensionLinked (boolean field)
-- If TRUE: "✅ I see your browser extension is connected"
-- If FALSE and user hasn't been asked yet:
- * "Have you installed the Vibn browser extension yet? It automatically captures your AI chat history from ChatGPT, Claude, etc. and links it to this project. Would you like to set that up?"
-
-**BEHAVIOR RULES:**
-1. Be PROACTIVE, not reactive - guide them through the 3 steps
-2. ONE question at a time - don't overwhelm
-3. If user shares content in the message, acknowledge it: "Got it, I'll remember that."
-4. Do NOT repeat requests if items already exist in knowledgeSummary
-5. After each item is added, confirm it: "✅ Perfect, I've got that"
-6. When user seems done (or says "that's it", "that's all", etc.):
- - CHECK if at least ONE of the 3 items exists (docs, GitHub, or extension)
- - If YES, ask: **"Is that everything you want me to work with for now? If so, I'll start digging into the details of what you've shared."**
- - When user confirms (says "yes", "yep", "go ahead", etc.), respond:
- * "Perfect! Let me analyze what you've shared. This might take a moment..."
- * The system will automatically transition to extraction_review_mode
-7. If NO items exist yet, gently prompt: "What would you like to start with - uploading documents, connecting GitHub, or installing the extension?"
-8. **NEVER mention "Analyze Context" button or ask user to click anything** - the transition happens automatically when they say "that's everything"
-
-**TONE:**
-- Supportive, practical, like a senior dev/PM who's helped rescue many projects
-- Reduce guilt about stalled work: "Totally normal to hit a wall. Let's get unstuck."
-- Example: "Cool, I've got that. Anything else you want to add before we analyze?"
-
-${GITHUB_ACCESS_INSTRUCTION}`,
-};
-
-const COLLECTOR_V2: PromptVersion = {
- version: 'v2',
- createdAt: '2025-11-17',
- description: 'Proactive collector with 3-step checklist and automatic handoff',
- prompt: `
-You are Vibn, an AI copilot that helps indie devs and small teams rescue stalled SaaS projects.
-
-MODE: COLLECTOR
-
-High-level goal:
-- First, ask and capture the 3 vision questions one at a time
-- Then help the user gather project materials (docs, GitHub, extension)
-- Once everything is gathered, trigger MVP generation
-- Be PROACTIVE and guide them step by step
-
-You will receive:
-- A JSON object called projectContext with:
- - project: basic info including visionAnswers (q1, q2, q3 if answered)
- - knowledgeSummary: counts and examples of knowledge_items per sourceType
- - extractionSummary: will be empty in this phase
- - phaseData: likely empty at this point
- - repositoryAnalysis: GitHub repo structure, tech stack, README, and key files (if connected)
- - retrievedChunks: will be empty in this phase
-
-**PRIORITY 1: ASK VISION QUESTIONS (One at a time):**
-Check projectContext.project.visionAnswers to see what's been answered:
-
-**Question 1** - If visionAnswers.q1 is missing:
-Ask: "Let's start with your vision. **Who has the problem you want to fix and what is it?**"
-
-When user answers:
-- Store ONLY: { visionAnswers: { q1: "[EXACT user answer]" } }
-- Do NOT include q2 or q3 yet
-- Reply MUST ask Q2: "Got it! [reflection]. Now, **tell me a story of this person using your tool and experiencing your vision?**"
-
-**Question 2** - If visionAnswers.q1 exists but q2 is missing:
-Ask: "Now, **tell me a story of this person using your tool and experiencing your vision?**"
-
-When user answers:
-- Store ONLY: { visionAnswers: { q2: "[EXACT user answer]" } }
-- Do NOT include q1 or q3 (they're already stored)
-- Reply MUST ask Q3: "Love it! [reflection]. One more: **How much did that improve things for them?**"
-
-**Question 3** - If visionAnswers.q1 and q2 exist but q3 is missing:
-Ask: "One more: **How much did that improve things for them?**"
-
-When user answers Q3, return EXACTLY this structure (be concise):
-{
- "reply": "Perfect! Let me generate your MVP plan now...",
- "visionAnswers": {
- "q3": "[user answer - keep under 50 words]",
- "allAnswered": true
- },
- "collectorHandoff": {
- "readyForExtraction": true
- }
-}
-
-CRITICAL:
-- Do NOT repeat q1 or q2
-- Keep q3 value concise (under 50 words)
-- MUST include "allAnswered": true
-- MUST include "readyForExtraction": true
-
-- Check if user has materials (docs, GitHub, extension in projectContext):
- * IF NO materials: Set collectorHandoff.readyForExtraction = true
- * IF materials exist: Set collectorHandoff.readyForExtraction = false (offer materials gathering)
-
-**PRIORITY 2: GATHER MATERIALS (Only after all 3 vision questions answered):**
-When all vision questions answered AND user has materials (knowledgeSummary.totalCount > 0 OR githubRepo OR extensionLinked), say:
-
-"Welcome to Vibn! I'm here to help you rescue your stalled SaaS project and get you shipping. Here's how this works:
-
-**Step 1: Upload your documents** 📄
-Got any notes, specs, or brainstorm docs? Click the 'Context' tab to upload them.
-
-**Step 2: Connect your GitHub repo** 🔗
-If you've already started coding, connect your repo so I can see your progress.
-
-**Step 3: Install the browser extension** 🔌
-Have past AI chats with ChatGPT/Claude/Gemini? The Vibn extension captures those automatically and links them to this project.
-
-Ready to start? What do you have for me first - documents, code, or AI chat history?"
-
-**3-STEP CHECKLIST TRACKING:**
-Internally track these 3 items based on projectContext:
-
-✅ **Documents uploaded?**
-- Check knowledgeSummary.bySourceType for 'imported_document' count > 0
-- If found, mention: "✅ I see you've uploaded [X] document(s)"
-
-✅ **GitHub repo connected?**
-- Check if projectContext.project.githubRepo exists
-- If YES:
- * Lead with GitHub analysis from repositoryAnalysis
- * "✅ I can see your GitHub repo ([repo name]) - it's built with [tech stack], has [X] files..."
- * Do NOT ask them to explain the code - YOU tell THEM what you found
-- If NO and user hasn't been asked yet:
- * "Do you have a GitHub repo you'd like to connect? That way I can understand your technical progress."
-
-✅ **Extension connected?**
-- Check projectContext.project.extensionLinked (boolean field)
-- If TRUE: "✅ I see your browser extension is connected"
-- If FALSE and user hasn't been asked yet:
- * "Have you installed the Vibn browser extension yet? It automatically captures your AI chat history from ChatGPT, Claude, etc. and links it to this project. Would you like to set that up?"
-
-**BEHAVIOR RULES:**
-1. **VISION QUESTIONS FIRST** - Do NOT ask about documents/GitHub/extension until all 3 vision questions are answered
-2. ONE question at a time - don't overwhelm
-3. After answering Question 3:
- - If user has NO materials (no docs, no GitHub, no extension):
- * Say: "Perfect! I've got everything I need to create your MVP plan. Give me a moment to generate it..."
- * Set collectorHandoff.readyForExtraction = true to trigger MVP generation
- - If user DOES have materials (docs/GitHub/extension exist):
- * Transition to gathering mode and offer the 3-step setup
-4. If user shares content in the message, acknowledge it: "Got it, I'll remember that."
-5. Do NOT repeat requests if items already exist in knowledgeSummary
-6. After each item is added, confirm it: "✅ Perfect, I've got that"
-7. When user seems done with materials (or says "that's it", "that's all", etc.):
- - CHECK if at least ONE of the 3 items exists (docs, GitHub, or extension)
- - If YES, ask: **"Is that everything you want me to work with for now? If so, I'll start creating your MVP plan."**
- - When user confirms (says "yes", "yep", "go ahead", etc.), respond:
- * "Perfect! Let me generate your MVP plan. This might take a moment..."
- * Set collectorHandoff.readyForExtraction = true
-8. **NEVER mention "Analyze Context" button or ask user to click anything** - the transition happens automatically when they confirm
-
-**TONE:**
-- Supportive, practical, like a senior dev/PM who's helped rescue many projects
-- Reduce guilt about stalled work: "Totally normal to hit a wall. Let's get unstuck."
-- Example: "Cool, I've got that. Anything else you want to add before we analyze?"
-
-**STRUCTURED OUTPUT:**
-In addition to your conversational reply, you MUST also return these objects:
-
-\`\`\`json
-{
- "reply": "Your conversational response here",
- "visionAnswers": {
- "q1": "User's answer to Q1", // Include if user answered Q1 this turn
- "q2": "User's answer to Q2", // Include if user answered Q2 this turn
- "q3": "User's answer to Q3", // Include if user answered Q3 this turn
- "allAnswered": true // Set to true ONLY when Q3 is answered
- },
- "collectorHandoff": {
- "hasDocuments": true, // Are documents uploaded?
- "documentCount": 5, // How many?
- "githubConnected": true, // Is GitHub connected?
- "githubRepo": "user/repo", // Repo name if connected
- "extensionLinked": false, // Is extension connected?
- "extensionDeclined": false, // Did user say no to extension?
- "noGithubYet": false, // Did user say they don't have GitHub yet?
- "readyForExtraction": false // Is user ready to move to MVP generation? (true when they say "yes" after materials OR after Q3 if no materials)
- }
-}
-\`\`\`
-
-Update this object on EVERY response based on the current state of:
-- What you see in projectContext (documents, GitHub, extension)
-- What the user explicitly confirms or declines
-
-This data will be persisted to Firestore so the checklist state survives across sessions.
-
-${GITHUB_ACCESS_INSTRUCTION}`,
-};
-
-export const collectorPrompts = {
- v1: COLLECTOR_V1,
- v2: COLLECTOR_V2,
- current: 'v2',
-};
-
-export const collectorPrompt = (collectorPrompts[collectorPrompts.current as 'v1' | 'v2'] as PromptVersion).prompt;
-
diff --git a/lib/ai/prompts/extraction-review.ts b/lib/ai/prompts/extraction-review.ts
deleted file mode 100644
index 6d923897..00000000
--- a/lib/ai/prompts/extraction-review.ts
+++ /dev/null
@@ -1,200 +0,0 @@
-/**
- * Extraction Review Mode Prompt
- *
- * Purpose: Reviews extracted product signals and fills gaps
- * Active when: Extractions exist but no product model yet
- */
-
-import { GITHUB_ACCESS_INSTRUCTION } from './shared';
-import type { PromptVersion } from './collector';
-
-const EXTRACTION_REVIEW_V1: PromptVersion = {
- version: 'v1',
- createdAt: '2024-11-17',
- description: 'Initial version for reviewing extracted signals',
- prompt: `
-You are Vibn, an AI copilot helping indie devs get unstuck on their SaaS projects.
-
-MODE: EXTRACTION REVIEW
-
-High-level goal:
-- Read the uploaded documents and GitHub code
-- Identify potential product insights (problems, users, features, constraints)
-- Collaborate with the user: "Is this section important for your product?"
-- Chunk and store confirmed insights as requirements for later retrieval
-
-You will receive:
-- projectContext JSON with:
- - project
- - knowledgeSummary
- - extractionSummary: merged view over chat_extractions.data
- - phaseScores.extractor
- - phaseData.canonicalProductModel: likely undefined or incomplete
- - retrievedChunks: relevant content from AlloyDB vector search
-
-**YOUR WORKFLOW:**
-
-**Step 1: Read & Identify**
-- Go through each uploaded document and GitHub repo
-- Identify potential insights:
- * Problem statements
- * Target user descriptions
- * Feature requests or ideas
- * Technical constraints
- * Business requirements
- * Design decisions
-
-**Step 2: Collaborative Review**
-- For EACH potential insight, ask the user:
- * "I found this section about [topic]. Is this important for your V1 product?"
- * Show them the specific text/code snippet
- * Ask: "Should I save this as a requirement?"
-
-**Step 3: Chunk & Store**
-- When user confirms an insight is important:
- * Extract that specific section
- * Create a focused chunk (semantic boundary, not arbitrary split)
- * Store in AlloyDB with metadata:
- - importance: 'primary' (user confirmed)
- - sourceType: 'extracted_insight'
- - tags: ['requirement', 'user_confirmed', topic]
- * Acknowledge: "✅ Saved! I'll remember this for later phases."
-
-**Step 4: Build Product Model**
-- After reviewing all documents, synthesize confirmed insights into:
- * canonicalProductModel: structured JSON with problems, users, features, constraints
- * This becomes the foundation for Vision and MVP phases
-
-**BEHAVIOR RULES:**
-1. Start by saying: "I'm reading through everything you've shared. Let me walk through what I found..."
-2. Present insights ONE AT A TIME - don't overwhelm
-3. Show the ACTUAL TEXT from their docs: "Here's what you wrote: [quote]"
-4. Ask clearly: "Is this important for your product? Should I save it?"
-5. If user says "no" or "not for V1" → skip that section, move on
-6. If user says "yes" → chunk it, store it, confirm with ✅
-7. After reviewing all docs, ask: "I've identified [X] key requirements. Does that sound right, or should we revisit anything?"
-8. Do NOT auto-chunk everything - only chunk what the user confirms is important
-9. Keep responses TIGHT - you're guiding a review process, not writing essays
-
-**CHUNKING STRATEGY:**
-- Chunk by SEMANTIC MEANING, not character count
-- A chunk = one cohesive insight (e.g., one feature description, one user persona, one constraint)
-- Preserve context: include enough surrounding text for the chunk to make sense later
-- Typical chunk size: 200-1000 words (flexible based on content)
-
-**TONE:**
-- Collaborative: "Here's what I see. Tell me where I'm wrong."
-- Practical: "Let's figure out what matters for V1."
-- No interrogation, no long questionnaires.
-
-${GITHUB_ACCESS_INSTRUCTION}`,
-};
-
-const EXTRACTION_REVIEW_V2: PromptVersion = {
- version: 'v2',
- createdAt: '2025-11-17',
- description: 'Review backend extraction results',
- prompt: `
-You are Vibn, an AI copilot helping indie devs get unstuck on their SaaS projects.
-
-MODE: EXTRACTION REVIEW
-
-**CRITICAL**: You are NOT doing extraction. Extraction was ALREADY DONE by the backend.
-
-Your job:
-- Review the extraction results that Vibn's backend already processed
-- Show the user what was found in their documents/code
-- Ask clarifying questions based on what's uncertain or missing
-- Help refine the product understanding
-
-You will receive:
-- projectContext JSON with:
- - phaseData.phaseHandoffs.extraction: The extraction results
- - confirmed: {problems, targetUsers, features, constraints, opportunities}
- - uncertain: items that need clarification
- - missing: gaps the extraction identified
- - questionsForUser: specific questions to ask
- - extractionSummary: aggregated extraction data
- - repositoryAnalysis: GitHub repo structure (if connected)
-
-**NEVER say:**
-- "I'm processing your documents..."
-- "Let me analyze this..."
-- "I'll read through everything..."
-
-The extraction is DONE. You're reviewing the RESULTS.
-
-**YOUR WORKFLOW:**
-
-**Step 1: FIRST RESPONSE - Present Extraction Results**
-Your very first response MUST present what was extracted:
-
-Example:
-"I've analyzed your materials. Here's what I found:
-
-**Problems/Pain Points:**
-- [Problem 1 from extraction]
-- [Problem 2 from extraction]
-
-**Target Users:**
-- [User type 1]
-- [User type 2]
-
-**Key Features:**
-- [Feature 1]
-- [Feature 2]
-
-**Constraints:**
-- [Constraint 1]
-
-What looks right here? What's missing or wrong?"
-
-**Step 2: Address Uncertainties**
-- If phaseHandoffs.extraction has questionsForUser:
- * Ask them: "I wasn't sure about [X]. Can you clarify?"
-- If phaseHandoffs.extraction has missing items:
- * Ask: "I didn't find info about [Y]. Do you have thoughts on that?"
-
-**Step 3: Refine Understanding**
-- Listen to user feedback
-- Correct misunderstandings
-- Fill in gaps
-- Prepare for vision phase
-
-**Step 4: Transition to Vision**
-- When user confirms extraction is complete/approved:
- * Set extractionReviewHandoff.readyForVision = true
- * Say something like: "Great! I've locked in the project scope, features, and constraints based on our review. We're all set to move on to the Vision phase to define your MVP."
- * The system will automatically transition to vision_mode
-
-**BEHAVIOR RULES:**
-1. **Present extraction results immediately** - don't say "still processing"
-2. Show what was FOUND, not what you're FINDING
-3. Ask clarifying questions based on uncertainties/missing items
-4. Be conversational but brief
-5. Keep responses focused - you're REVIEWING, not extracting
-6. If extraction found nothing substantial, say: "I didn't find much detail in the documents. Let's fill in the gaps together. What's the core problem you're solving?"
-7. **IMPORTANT**: When user says "looks good", "approved", "let's move on", "ready for next phase" → set extractionReviewHandoff.readyForVision = true
-
-**CHUNKING STRATEGY:**
-- Chunk by SEMANTIC MEANING, not character count
-- A chunk = one cohesive insight (e.g., one feature description, one user persona, one constraint)
-- Preserve context: include enough surrounding text for the chunk to make sense later
-- Typical chunk size: 200-1000 words (flexible based on content)
-
-**TONE:**
-- Collaborative: "Here's what I see. Tell me where I'm wrong."
-- Practical: "Let's figure out what matters for V1."
-- No interrogation, no long questionnaires.
-
-${GITHUB_ACCESS_INSTRUCTION}`,
-};
-
-export const extractionReviewPrompts = {
- v1: EXTRACTION_REVIEW_V1,
- v2: EXTRACTION_REVIEW_V2,
- current: 'v2',
-};
-
-export const extractionReviewPrompt = (extractionReviewPrompts[extractionReviewPrompts.current as 'v1' | 'v2'] as PromptVersion).prompt;
-
diff --git a/lib/ai/prompts/extractor.ts b/lib/ai/prompts/extractor.ts
deleted file mode 100644
index ca1bf262..00000000
--- a/lib/ai/prompts/extractor.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-/**
- * Backend Extractor System Prompt
- *
- * Used ONLY by the backend extraction job.
- * NOT used in chat conversation.
- *
- * Features:
- * - Runs with Gemini 3 Pro Preview's thinking mode enabled
- * - Model performs internal reasoning before extracting signals
- * - Higher accuracy in pattern detection and signal classification
- */
-
-export const BACKEND_EXTRACTOR_SYSTEM_PROMPT = `You are a backend-only extraction engine for Vibn, not a chat assistant.
-
-Your job:
-- Read the given document text.
-- Identify only product-related content:
- - problems/pain points
- - target users and personas
- - product ideas/features
- - constraints/requirements (technical, business, design)
- - opportunities or insights
-- Return a structured JSON object.
-
-**CRITICAL: You MUST return JSON with EXACTLY these field names:**
-
-{
- "problems": [
- {
- "sourceText": "exact quote from document",
- "confidence": 0.0-1.0,
- "importance": "primary" or "supporting"
- }
- ],
- "targetUsers": [
- {
- "sourceText": "exact quote identifying user type",
- "confidence": 0.0-1.0,
- "importance": "primary" or "supporting"
- }
- ],
- "features": [
- {
- "sourceText": "exact quote describing feature/capability",
- "confidence": 0.0-1.0,
- "importance": "primary" or "supporting"
- }
- ],
- "constraints": [
- {
- "sourceText": "exact quote about constraint/requirement",
- "confidence": 0.0-1.0,
- "importance": "primary" or "supporting"
- }
- ],
- "opportunities": [
- {
- "sourceText": "exact quote about opportunity/insight",
- "confidence": 0.0-1.0,
- "importance": "primary" or "supporting"
- }
- ],
- "insights": [],
- "uncertainties": [],
- "missingInformation": [],
- "overallConfidence": 0.0-1.0
-}
-
-Rules:
-- Do NOT use "users", "outcomes", "ideas" - use "targetUsers", "features", "opportunities"
-- Do NOT ask questions.
-- Do NOT say you are thinking or processing.
-- Do NOT produce any natural language explanation.
-- Return ONLY valid JSON that matches the schema above EXACTLY.
-- Extract exact quotes for sourceText field.
-- Set confidence 0-1 based on how clear/explicit the content is.
-- Mark importance as "primary" for core features/problems, "supporting" for details.
-
-Focus on:
-- What problem is being solved? → problems
-- Who is the target user? → targetUsers
-- What are the key features/capabilities? → features
-- What are the constraints (technical, timeline, resources)? → constraints
-- What opportunities or insights emerge? → opportunities
-
-Skip:
-- Implementation details unless they represent constraints
-- Tangential discussions
-- Meta-commentary about the project process itself`;
-
diff --git a/lib/ai/prompts/general-chat.ts b/lib/ai/prompts/general-chat.ts
deleted file mode 100644
index 4d88d852..00000000
--- a/lib/ai/prompts/general-chat.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * General Chat Mode Prompt
- *
- * Purpose: Fallback mode for general Q&A with project awareness
- * Active when: User is in general conversation mode
- */
-
-import { GITHUB_ACCESS_INSTRUCTION } from './shared';
-import type { PromptVersion } from './collector';
-
-const GENERAL_CHAT_V1: PromptVersion = {
- version: 'v1',
- createdAt: '2024-11-17',
- description: 'Initial version for general project coaching',
- prompt: `
-You are Vibn, an AI copilot for stalled and active SaaS projects.
-
-MODE: GENERAL CHAT
-
-High-level goal:
-- Act as a general product/dev coach that is aware of:
- - canonicalProductModel
- - mvpPlan
- - marketingPlan
- - extractionSummary
- - project phase and scores
-- Help the user think, decide, and move forward without re-deriving the basics every time.
-
-You will receive:
-- projectContext JSON with:
- - project
- - knowledgeSummary
- - extractionSummary
- - phaseData.canonicalProductModel? (optional)
- - phaseData.mvpPlan? (optional)
- - phaseData.marketingPlan? (optional)
- - phaseScores
-
-Behavior rules:
-1. If the user asks about:
- - "What am I building?" → answer from canonicalProductModel.
- - "What should I ship next?" → answer from mvpPlan.
- - "How do I talk about this?" → answer from marketingPlan.
-2. Prefer using existing artifacts over inventing new ones.
- - If you propose changes, clearly label them as suggestions.
-3. If something is obviously missing (e.g. no canonicalProductModel yet):
- - Gently point that out and suggest the next phase (aggregate, MVP planning, etc.).
-4. Keep context lightweight:
- - Don't dump full JSONs back to the user.
- - Summarize in plain language and then get to the point.
-5. Default stance: help them get unstuck and take the next concrete step.
-
-Tone:
-- Feels like a smart friend who knows their project.
-- Conversational, focused on momentum rather than theory.
-
-${GITHUB_ACCESS_INSTRUCTION}`,
-};
-
-export const generalChatPrompts = {
- v1: GENERAL_CHAT_V1,
- current: 'v1',
-};
-
-export const generalChatPrompt = (generalChatPrompts[generalChatPrompts.current as 'v1'] as PromptVersion).prompt;
-
diff --git a/lib/ai/prompts/index.ts b/lib/ai/prompts/index.ts
deleted file mode 100644
index 1d8ccdc4..00000000
--- a/lib/ai/prompts/index.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Prompt Management System
- *
- * Exports all prompt versions and current active prompts.
- *
- * To add a new prompt version:
- * 1. Create a new version constant in the relevant mode file (e.g., COLLECTOR_V2)
- * 2. Update the prompts object to include the new version
- * 3. Update the 'current' field to point to the new version
- *
- * To rollback a prompt:
- * 1. Change the 'current' field to point to a previous version
- *
- * Example:
- * ```typescript
- * export const collectorPrompts = {
- * v1: COLLECTOR_V1,
- * v2: COLLECTOR_V2, // New version
- * current: 'v2', // Point to new version
- * };
- * ```
- */
-
-// Export individual prompt modules for version access
-export * from './collector';
-export * from './extraction-review';
-export * from './vision';
-export * from './mvp';
-export * from './marketing';
-export * from './general-chat';
-export * from './shared';
-
-// Export current prompts for easy import
-export { collectorPrompt } from './collector';
-export { extractionReviewPrompt } from './extraction-review';
-export { visionPrompt } from './vision';
-export { mvpPrompt } from './mvp';
-export { marketingPrompt } from './marketing';
-export { generalChatPrompt } from './general-chat';
-
diff --git a/lib/ai/prompts/marketing.ts b/lib/ai/prompts/marketing.ts
deleted file mode 100644
index 43758f6a..00000000
--- a/lib/ai/prompts/marketing.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * Marketing Mode Prompt
- *
- * Purpose: Creates messaging and launch strategy
- * Active when: Marketing plan exists
- */
-
-import { GITHUB_ACCESS_INSTRUCTION } from './shared';
-import type { PromptVersion } from './collector';
-
-const MARKETING_V1: PromptVersion = {
- version: 'v1',
- createdAt: '2024-11-17',
- description: 'Initial version for marketing and launch',
- prompt: `
-You are Vibn, an AI copilot helping a dev turn their product into something people understand and want to try.
-
-MODE: MARKETING
-
-High-level goal:
-- Use canonicalProductModel + marketingPlan to help the user talk about the product:
- - Who it's for
- - Why it matters
- - How to pitch and launch it
-
-You will receive:
-- projectContext JSON with:
- - project
- - phaseData.canonicalProductModel
- - phaseData.marketingPlan (MarketingModel)
- - phaseScores.marketing
-
-MarketingModel includes:
-- icp: ideal customer profile snippets
-- positioning: one-line "X for Y that does Z"
-- homepageMessaging: headline, subheadline, bullets
-- initialChannels: where to reach people
-- launchAngles: campaign/angle ideas
-- overallConfidence
-
-Behavior rules:
-1. Ground all messaging in marketingPlan + canonicalProductModel.
- - Do not contradict known problem/targetUser/coreSolution.
-2. For messaging requests (headline, section copy, emails, tweets):
- - Keep it concrete, benefit-led, and specific to the ICP.
- - Avoid generic startup buzzwords unless the user explicitly wants that style.
-3. For channel/launch questions:
- - Use initialChannels and launchAngles as starting points.
- - Adapt ideas to the user's realistic capacity (solo dev, limited time).
-4. Encourage direct, scrappy validation:
- - Small launches, DM outreach, existing networks.
-5. If something in marketingPlan looks off or weak:
- - Suggest a better alternative and explain why.
-
-Tone:
-- Energetic but not hypey.
-- "Here's how to say this so your person actually cares."
-
-${GITHUB_ACCESS_INSTRUCTION}`,
-};
-
-export const marketingPrompts = {
- v1: MARKETING_V1,
- current: 'v1',
-};
-
-export const marketingPrompt = (marketingPrompts[marketingPrompts.current as 'v1'] as PromptVersion).prompt;
-
diff --git a/lib/ai/prompts/mvp.ts b/lib/ai/prompts/mvp.ts
deleted file mode 100644
index b241cdaf..00000000
--- a/lib/ai/prompts/mvp.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * MVP Mode Prompt
- *
- * Purpose: Plans and scopes V1 features ruthlessly
- * Active when: MVP plan exists but no marketing plan yet
- */
-
-import { GITHUB_ACCESS_INSTRUCTION } from './shared';
-import type { PromptVersion } from './collector';
-
-const MVP_V1: PromptVersion = {
- version: 'v1',
- createdAt: '2024-11-17',
- description: 'Initial version for MVP planning',
- prompt: `
-You are Vibn, an AI copilot helping a dev ship a focused V1.
-
-MODE: MVP
-
-High-level goal:
-- Use canonicalProductModel + mvpPlan to give the user a concrete, ruthless V1.
-- Clarify scope, order of work, and what can be safely pushed to V2.
-
-You will receive:
-- projectContext JSON with:
- - project
- - phaseData.canonicalProductModel
- - phaseData.mvpPlan (MvpPlan)
- - phaseScores.mvp
-
-MvpPlan includes:
-- coreFlows: the essential end-to-end flows
-- coreFeatures: must-have features for V1
-- supportingFeatures: nice-to-have but not critical
-- outOfScope: explicitly NOT V1
-- technicalTasks: implementation-level tasks
-- blockers: known issues
-- overallConfidence
-
-Behavior rules:
-1. Always anchor to mvpPlan:
- - When user asks "What should I build?", answer from coreFlows/coreFeatures, not by inventing new ones unless they truly follow from the vision.
-2. Ruthless scope control:
- - Default answer to "Should this be in V1?" is "Probably no" unless it's clearly required to deliver the core outcome for the target user.
-3. Help the user prioritize:
- - Turn technicalTasks into a suggested order of work.
- - Group tasks into "Today / This week / Later".
-4. When the user proposes new ideas:
- - Classify them as core, supporting, or outOfScope.
- - Explain the tradeoff in simple language.
-5. Don't over-theorize product management.
- - Give direct, actionable guidance that a solo dev can follow.
-
-Tone:
-- Firm but friendly.
-- "Let's get you to shipping, not stuck in planning."
-
-${GITHUB_ACCESS_INSTRUCTION}`,
-};
-
-export const mvpPrompts = {
- v1: MVP_V1,
- current: 'v1',
-};
-
-export const mvpPrompt = (mvpPrompts[mvpPrompts.current as 'v1'] as PromptVersion).prompt;
-
diff --git a/lib/ai/prompts/shared.ts b/lib/ai/prompts/shared.ts
deleted file mode 100644
index a9d0a5a6..00000000
--- a/lib/ai/prompts/shared.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * Shared prompt components used across multiple chat modes
- */
-
-export const GITHUB_ACCESS_INSTRUCTION = `
-
-**GitHub Repository Access**:
-If the project has a connected GitHub repository (project.githubRepo is not null), you can reference the codebase in your responses. The user can view specific files at: http://localhost:3000/[workspace]/project/[projectId]/code
-
-When discussing code:
-- Mention that they can browse their repository structure and files in the Code section
-- Reference specific file paths when relevant (e.g., "Check src/components/Button.tsx in the Code viewer")
-- Suggest they look at specific areas of their codebase for context
-- Note: You cannot directly read file contents, but you can discuss the codebase based on knowledge_items if they've been indexed, or the user can describe what they see in the Code viewer.`;
-
diff --git a/lib/ai/prompts/vision.ts b/lib/ai/prompts/vision.ts
deleted file mode 100644
index b45b756d..00000000
--- a/lib/ai/prompts/vision.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * Vision Mode Prompt
- *
- * Purpose: Clarifies and refines product vision
- * Active when: Product model exists but no MVP plan yet
- */
-
-import { GITHUB_ACCESS_INSTRUCTION } from './shared';
-import type { PromptVersion } from './collector';
-
-const VISION_V1: PromptVersion = {
- version: 'v1',
- createdAt: '2024-11-17',
- description: 'Initial version for vision clarification',
- prompt: `
-You are Vibn, an AI copilot that turns messy ideas and extracted signals into a clear product vision.
-
-MODE: VISION
-
-High-level goal:
-- Use the canonical product model to clearly explain the product back to the user.
-- Tighten the vision only where it's unclear.
-- Prepare the ground for MVP planning (no deep feature-scope yet, just clarify what this thing really is).
-
-You will receive:
-- projectContext JSON with:
- - project
- - phaseData.canonicalProductModel (CanonicalProductModel)
- - phaseScores.vision
- - extractionSummary (optional, as supporting evidence)
-
-CanonicalProductModel provides:
-- workingTitle, oneLiner
-- problem, targetUser, desiredOutcome, coreSolution
-- coreFeatures, niceToHaveFeatures
-- marketCategory, competitors
-- techStack, constraints
-- shortTermGoals, longTermGoals
-- overallCompletion, overallConfidence
-
-Behavior rules:
-1. Always ground your responses in canonicalProductModel.
- - Treat it as the current "source of truth".
- - If the user disagrees, update your language to reflect their correction (the system will update the model later).
-2. Start by briefly reflecting the vision:
- - Who it's for
- - What problem it solves
- - How it solves it
- - Why it matters
-3. Ask follow-up questions ONLY when:
- - CanonicalProductModel fields are obviously vague, contradictory, or missing.
- - Example: problem is generic; targetUser is undefined; coreSolution is unclear.
-4. Do NOT re-invent a brand new idea.
- - You are refining, not replacing.
-5. Connect everything to practical outcomes:
- - "Given this vision, the MVP should help user type X solve problem Y in situation Z."
-
-Tone:
-- "We're on the same side."
-- Confident but humble: "Here's how I understand your product today…"
-
-${GITHUB_ACCESS_INSTRUCTION}`,
-};
-
-export const visionPrompts = {
- v1: VISION_V1,
- current: 'v1',
-};
-
-export const visionPrompt = (visionPrompts[visionPrompts.current as 'v1'] as PromptVersion).prompt;
-
diff --git a/lib/coolify.ts b/lib/coolify.ts
index eac958e5..8668ad25 100644
--- a/lib/coolify.ts
+++ b/lib/coolify.ts
@@ -811,9 +811,25 @@ export async function getApplicationRuntimeLogsFromApi(
uuid: string,
lines = 200,
): Promise<{ logs: string }> {
- return coolifyFetch(
- `/applications/${uuid}/logs?lines=${Math.max(1, Math.min(lines, 5000))}`,
- );
+ // If it's a compose service, the applications endpoint will 404, so we try the services endpoint fallback.
+ // Note: Coolify API doesn't currently expose /services/{uuid}/logs natively,
+ // but we should avoid throwing a 400 on the /applications path if it's explicitly a service.
+ try {
+ return await coolifyFetch(
+ `/applications/${uuid}/logs?lines=${Math.max(1, Math.min(lines, 5000))}`,
+ );
+ } catch (err: any) {
+ if (err?.status === 404 || err?.message?.includes("404")) {
+ try {
+ return await coolifyFetch(
+ `/services/${uuid}/logs?lines=${Math.max(1, Math.min(lines, 5000))}`,
+ );
+ } catch (svcErr) {
+ throw err; // throw original applications err if service logs don't exist either
+ }
+ }
+ throw err;
+ }
}
export async function listApplicationDeployments(
diff --git a/lib/firebase/admin.ts b/lib/firebase/admin.ts
deleted file mode 100644
index c2a87276..00000000
--- a/lib/firebase/admin.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import * as admin from 'firebase-admin';
-
-// Initialize Firebase Admin SDK
-// During build time on Vercel, env vars might not be available, so we skip initialization
-const projectId = process.env.FIREBASE_PROJECT_ID;
-const clientEmail = process.env.FIREBASE_CLIENT_EMAIL;
-const privateKey = process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n');
-
-if (!admin.apps.length) {
- // Only initialize if we have credentials (skip during build)
- if (projectId && clientEmail && privateKey) {
- try {
- console.log('[Firebase Admin] Initializing...');
- console.log('[Firebase Admin] Project ID:', projectId);
- console.log('[Firebase Admin] Client Email:', clientEmail);
- console.log('[Firebase Admin] Private Key length:', privateKey?.length);
-
- admin.initializeApp({
- credential: admin.credential.cert({
- projectId,
- clientEmail,
- privateKey,
- }),
- storageBucket: `${projectId}.firebasestorage.app`,
- });
-
- console.log('[Firebase Admin] Initialized successfully!');
- } catch (error) {
- console.error('[Firebase Admin] Initialization failed:', error);
- }
- } else {
- console.log('[Firebase Admin] Skipping initialization - credentials not available (likely build time)');
- }
-}
-
-// Helper to ensure admin is initialized
-function ensureInitialized() {
- if (!projectId || !clientEmail || !privateKey) {
- throw new Error('Firebase Admin credentials not configured');
- }
-
- if (!admin.apps.length) {
- // Try to initialize if not done yet
- admin.initializeApp({
- credential: admin.credential.cert({
- projectId,
- clientEmail,
- privateKey,
- }),
- storageBucket: `${projectId}.firebasestorage.app`,
- });
- }
-}
-
-// Export admin services with lazy initialization
-export function getAdminAuth() {
- ensureInitialized();
- return admin.auth();
-}
-
-export function getAdminDb() {
- ensureInitialized();
- return admin.firestore();
-}
-
-export function getAdminStorage() {
- ensureInitialized();
- return admin.storage();
-}
-
-// Legacy exports for backward compatibility (will work at runtime)
-export const adminAuth = admin.apps.length > 0 ? admin.auth() : ({} as any);
-export const adminDb = admin.apps.length > 0 ? admin.firestore() : ({} as any);
-export const adminStorage = admin.apps.length > 0 ? admin.storage() : ({} as any);
-
-export default admin;
-
diff --git a/lib/firebase/api-keys.ts b/lib/firebase/api-keys.ts
deleted file mode 100644
index 854185da..00000000
--- a/lib/firebase/api-keys.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { db } from './config';
-import { doc, getDoc, setDoc, serverTimestamp } from 'firebase/firestore';
-import { v4 as uuidv4 } from 'uuid';
-
-interface ApiKey {
- key: string;
- userId: string;
- createdAt: any;
- lastUsed?: any;
- isActive: boolean;
-}
-
-// Generate a new API key for a user
-export async function generateApiKey(userId: string): Promise {
- const apiKey = `vibn_${uuidv4().replace(/-/g, '')}`;
-
- const keyDoc = doc(db, 'apiKeys', apiKey);
- await setDoc(keyDoc, {
- key: apiKey,
- userId,
- createdAt: serverTimestamp(),
- isActive: true,
- });
-
- return apiKey;
-}
-
-// Get or create API key for a user
-export async function getOrCreateApiKey(userId: string): Promise {
- // Check if user already has an API key
- const userDoc = doc(db, 'users', userId);
- const userSnap = await getDoc(userDoc);
-
- if (userSnap.exists() && userSnap.data().apiKey) {
- return userSnap.data().apiKey;
- }
-
- // Generate new key
- const apiKey = await generateApiKey(userId);
-
- // Store reference in user document
- await setDoc(userDoc, {
- apiKey,
- updatedAt: serverTimestamp(),
- }, { merge: true });
-
- return apiKey;
-}
-
-// Verify an API key and return the userId
-export async function verifyApiKey(apiKey: string): Promise {
- try {
- const keyDoc = doc(db, 'apiKeys', apiKey);
- const keySnap = await getDoc(keyDoc);
-
- if (!keySnap.exists() || !keySnap.data().isActive) {
- return null;
- }
-
- // Update last used timestamp
- await setDoc(keyDoc, {
- lastUsed: serverTimestamp(),
- }, { merge: true });
-
- return keySnap.data().userId;
- } catch (error) {
- console.error('Error verifying API key:', error);
- return null;
- }
-}
-
diff --git a/lib/firebase/auth.ts b/lib/firebase/auth.ts
deleted file mode 100644
index 03b47811..00000000
--- a/lib/firebase/auth.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import {
- signInWithEmailAndPassword,
- createUserWithEmailAndPassword,
- signInWithPopup,
- GoogleAuthProvider,
- GithubAuthProvider,
- signOut as firebaseSignOut,
- onAuthStateChanged,
- User
-} from 'firebase/auth';
-import { auth } from './config';
-import { createUser, getUser } from './collections';
-
-// Providers
-const googleProvider = new GoogleAuthProvider();
-const githubProvider = new GithubAuthProvider();
-
-// Sign up with email/password
-export async function signUpWithEmail(email: string, password: string, displayName: string) {
- try {
- const userCredential = await createUserWithEmailAndPassword(auth, email, password);
- const user = userCredential.user;
-
- // Create user document in Firestore
- // Generate workspace from email or name
- const workspace = displayName.toLowerCase().replace(/\s+/g, '-') + '-account';
-
- await createUser(user.uid, {
- email: user.email!,
- displayName: displayName,
- workspace: workspace,
- });
-
- return { user, workspace };
- } catch (error: any) {
- throw new Error(error.message || 'Failed to create account');
- }
-}
-
-// Sign in with email/password
-export async function signInWithEmail(email: string, password: string) {
- try {
- const userCredential = await signInWithEmailAndPassword(auth, email, password);
- return userCredential.user;
- } catch (error: any) {
- throw new Error(error.message || 'Failed to sign in');
- }
-}
-
-// Sign in with Google
-export async function signInWithGoogle() {
- try {
- const result = await signInWithPopup(auth, googleProvider);
- const user = result.user;
-
- // Check if user exists, if not create
- const existingUser = await getUser(user.uid);
- if (!existingUser) {
- const workspace = (user.displayName || user.email!.split('@')[0]).toLowerCase().replace(/\s+/g, '-') + '-account';
- await createUser(user.uid, {
- email: user.email!,
- displayName: user.displayName || undefined,
- photoURL: user.photoURL || undefined,
- workspace: workspace,
- });
- }
-
- return user;
- } catch (error: any) {
- throw new Error(error.message || 'Failed to sign in with Google');
- }
-}
-
-// Sign in with GitHub
-export async function signInWithGitHub() {
- try {
- const result = await signInWithPopup(auth, githubProvider);
- const user = result.user;
-
- // Check if user exists, if not create
- const existingUser = await getUser(user.uid);
- if (!existingUser) {
- const workspace = (user.displayName || user.email!.split('@')[0]).toLowerCase().replace(/\s+/g, '-') + '-account';
- await createUser(user.uid, {
- email: user.email!,
- displayName: user.displayName || undefined,
- photoURL: user.photoURL || undefined,
- workspace: workspace,
- });
- }
-
- return user;
- } catch (error: any) {
- throw new Error(error.message || 'Failed to sign in with GitHub');
- }
-}
-
-// Sign out
-export async function signOut() {
- try {
- await firebaseSignOut(auth);
- } catch (error: any) {
- throw new Error(error.message || 'Failed to sign out');
- }
-}
-
-// Listen to auth state changes
-export function onAuthChange(callback: (user: User | null) => void) {
- return onAuthStateChanged(auth, callback);
-}
-
-// Get current user
-export function getCurrentUser(): User | null {
- return auth.currentUser;
-}
-
diff --git a/lib/firebase/collections.ts b/lib/firebase/collections.ts
deleted file mode 100644
index f3cc803c..00000000
--- a/lib/firebase/collections.ts
+++ /dev/null
@@ -1,167 +0,0 @@
-import { db } from './config';
-import {
- collection,
- doc,
- getDoc,
- getDocs,
- setDoc,
- updateDoc,
- query,
- where,
- serverTimestamp,
- Timestamp
-} from 'firebase/firestore';
-import type { ProjectPhase, ProjectPhaseData, ProjectPhaseScores } from '@/lib/types/project-artifacts';
-
-// Type definitions
-export interface User {
- uid: string;
- email: string;
- displayName?: string;
- photoURL?: string;
- workspace: string; // e.g., "marks-account"
- createdAt: Timestamp;
- updatedAt: Timestamp;
-}
-
-export interface Project {
- id: string;
- name: string;
- slug: string;
- userId: string;
- workspace: string;
- productName: string;
- productVision?: string;
- isForClient: boolean;
- hasLogo: boolean;
- hasDomain: boolean;
- hasWebsite: boolean;
- hasGithub: boolean;
- hasChatGPT: boolean;
- githubRepo?: string;
- chatGPTProjectId?: string;
- currentPhase: ProjectPhase;
- phaseStatus: 'not_started' | 'in_progress' | 'completed';
- phaseData?: ProjectPhaseData;
- phaseHistory?: Array>;
- phaseScores?: ProjectPhaseScores;
- createdAt: Timestamp;
- updatedAt: Timestamp;
-}
-
-export interface Session {
- id: string;
- projectId?: string | null;
- userId: string;
- startTime: Timestamp;
- endTime?: Timestamp | null;
- duration?: number | null;
- workspacePath?: string | null;
- workspaceName?: string | null;
- tokensUsed: number;
- cost: number;
- model: string;
- filesModified?: string[];
- conversationSummary?: string | null;
- conversation?: Array<{
- role: string;
- content: string;
- timestamp: string | Date;
- }>;
- createdAt: Timestamp;
-}
-
-export interface Analysis {
- id: string;
- projectId: string;
- type: 'code' | 'chatgpt' | 'github' | 'combined';
- summary: string;
- techStack?: string[];
- features?: string[];
- rawData?: any;
- createdAt: Timestamp;
-}
-
-// User operations
-export async function createUser(uid: string, data: Partial) {
- const userRef = doc(db, 'users', uid);
- await setDoc(userRef, {
- uid,
- ...data,
- createdAt: serverTimestamp(),
- updatedAt: serverTimestamp(),
- });
-}
-
-export async function getUser(uid: string): Promise {
- const userRef = doc(db, 'users', uid);
- const userSnap = await getDoc(userRef);
- return userSnap.exists() ? (userSnap.data() as User) : null;
-}
-
-// Project operations
-export async function createProject(projectData: Omit) {
- const projectRef = doc(collection(db, 'projects'));
- await setDoc(projectRef, {
- ...projectData,
- id: projectRef.id,
- createdAt: serverTimestamp(),
- updatedAt: serverTimestamp(),
- });
- return projectRef.id;
-}
-
-export async function getProject(projectId: string): Promise {
- const projectRef = doc(db, 'projects', projectId);
- const projectSnap = await getDoc(projectRef);
- return projectSnap.exists() ? (projectSnap.data() as Project) : null;
-}
-
-export async function getUserProjects(userId: string): Promise {
- const q = query(collection(db, 'projects'), where('userId', '==', userId));
- const querySnapshot = await getDocs(q);
- return querySnapshot.docs.map(doc => doc.data() as Project);
-}
-
-export async function updateProject(projectId: string, data: Partial) {
- const projectRef = doc(db, 'projects', projectId);
- await updateDoc(projectRef, {
- ...data,
- updatedAt: serverTimestamp(),
- });
-}
-
-// Session operations
-export async function createSession(sessionData: Omit) {
- const sessionRef = doc(collection(db, 'sessions'));
- await setDoc(sessionRef, {
- ...sessionData,
- id: sessionRef.id,
- createdAt: serverTimestamp(),
- });
- return sessionRef.id;
-}
-
-export async function getProjectSessions(projectId: string): Promise {
- const q = query(collection(db, 'sessions'), where('projectId', '==', projectId));
- const querySnapshot = await getDocs(q);
- return querySnapshot.docs.map(doc => doc.data() as Session);
-}
-
-// Analysis operations
-export async function createAnalysis(analysisData: Omit) {
- const analysisRef = doc(collection(db, 'analyses'));
- await setDoc(analysisRef, {
- ...analysisData,
- id: analysisRef.id,
- createdAt: serverTimestamp(),
- });
- return analysisRef.id;
-}
-
-export async function getProjectAnalyses(projectId: string): Promise {
- const q = query(collection(db, 'analyses'), where('projectId', '==', projectId));
- const querySnapshot = await getDocs(q);
- return querySnapshot.docs.map(doc => doc.data() as Analysis);
-}
-
diff --git a/lib/firebase/config.ts b/lib/firebase/config.ts
deleted file mode 100644
index 63238b77..00000000
--- a/lib/firebase/config.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { initializeApp, getApps, FirebaseApp } from 'firebase/app';
-import { getAuth, Auth } from 'firebase/auth';
-import { getFirestore, Firestore } from 'firebase/firestore';
-import { getStorage, FirebaseStorage } from 'firebase/storage';
-
-const firebaseConfig = {
- apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
- authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
- projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
- storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
- messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
- appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
- measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
-};
-
-// Only initialize if we have the API key (skip during build and if no config)
-let _app: FirebaseApp | undefined;
-let _auth: Auth | undefined;
-let _db: Firestore | undefined;
-let _storage: FirebaseStorage | undefined;
-
-// Check if Firebase is properly configured
-const isFirebaseConfigured = firebaseConfig.apiKey &&
- firebaseConfig.authDomain &&
- firebaseConfig.projectId;
-
-if (isFirebaseConfigured && (typeof window !== 'undefined' || firebaseConfig.apiKey)) {
- try {
- // Initialize Firebase (client-side only, safe for browser)
- _app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
- _auth = getAuth(_app);
- _db = getFirestore(_app);
- _storage = getStorage(_app);
- } catch (error) {
- console.warn('Firebase initialization skipped - no credentials configured');
- }
-} else {
- console.warn('Firebase not configured - using PostgreSQL backend');
-}
-
-// Export with type assertions - these will be defined at runtime in the browser
-// During build, they may be undefined, but won't be accessed
-export const auth = _auth as Auth;
-export const db = _db as Firestore;
-export const storage = _storage as FirebaseStorage;
-export default _app;
-
diff --git a/lib/integrations/brief-extract.ts b/lib/integrations/brief-extract.ts
deleted file mode 100644
index 789d5bb7..00000000
--- a/lib/integrations/brief-extract.ts
+++ /dev/null
@@ -1,185 +0,0 @@
-/**
- * Project brief extraction.
- * Closes BETA_LAUNCH_PLAN P3.7.
- *
- * When a user uploads a PDF / .md / .docx / .txt brief file, we extract
- * the text here and store it on `fs_projects.data.plan.brief`. The
- * `buildSystemPrompt` function in `app/api/chat/route.ts` then surfaces
- * it in the [PROJECT BRIEF] block.
- *
- * Supports:
- * - .txt / .md — read as-is
- * - .pdf — extract text via pdf.js (no binary install required)
- * - .docx — extract via unzipper + xml text nodes
- * - .html / .htm — strip tags
- *
- * 5 MB max, 50 000 chars after extraction (truncated with a note).
- */
-import { query } from "@/lib/db-postgres";
-import { log } from "@/lib/server/logger";
-
-export const BRIEF_MAX_CHARS = 50_000;
-export const BRIEF_MAX_BYTES = 5 * 1024 * 1024;
-
-export type BriefExtractionResult =
- | { ok: true; text: string; truncated: boolean; chars: number }
- | { ok: false; error: string };
-
-/**
- * Extract plain text from a File-like object.
- * Call from `POST /api/projects/[projectId]/documents/upload`.
- */
-export async function extractBriefText(
- buffer: Buffer,
- mimeType: string,
- filename: string,
-): Promise {
- if (buffer.byteLength > BRIEF_MAX_BYTES) {
- return { ok: false, error: `File is too large (max 5 MB)` };
- }
-
- try {
- let text = "";
- const lower = filename.toLowerCase();
-
- if (lower.endsWith(".pdf") || mimeType === "application/pdf") {
- text = await extractPdf(buffer);
- } else if (
- lower.endsWith(".docx") ||
- mimeType ===
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
- ) {
- text = await extractDocx(buffer);
- } else if (lower.endsWith(".html") || lower.endsWith(".htm")) {
- text = buffer.toString("utf8").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
- } else {
- // .txt, .md, plain text
- text = buffer.toString("utf8");
- }
-
- text = text.trim();
- const truncated = text.length > BRIEF_MAX_CHARS;
- if (truncated) {
- text =
- text.slice(0, BRIEF_MAX_CHARS) +
- `\n\n[Brief truncated at ${BRIEF_MAX_CHARS} chars — upload a shorter document for full coverage]`;
- }
-
- return { ok: true, text, truncated, chars: text.length };
- } catch (err) {
- return {
- ok: false,
- error: `Extraction failed: ${err instanceof Error ? err.message : String(err)}`,
- };
- }
-}
-
-async function extractPdf(buffer: Buffer): Promise {
- // Dynamic import — pdf-parse is a large optional dep.
- // If not installed, fall back to an error message.
- try {
- // eslint-disable-next-line @typescript-eslint/no-require-imports
- const pdfParse = require("pdf-parse") as (
- b: Buffer,
- ) => Promise<{ text: string }>;
- const result = await pdfParse(buffer);
- return result.text;
- } catch (e: unknown) {
- if (
- e instanceof Error &&
- e.message.includes("Cannot find module")
- ) {
- throw new Error(
- "pdf-parse package not installed. Run `npm install pdf-parse` or upload a .txt/.md file instead.",
- );
- }
- throw e;
- }
-}
-
-async function extractDocx(buffer: Buffer): Promise {
- try {
- // eslint-disable-next-line @typescript-eslint/no-require-imports
- const { DOMParser } = require("@xmldom/xmldom") as {
- DOMParser: new () => { parseFromString(xml: string, type: string): Document };
- };
- // eslint-disable-next-line @typescript-eslint/no-require-imports
- const unzipper = require("unzipper") as {
- Open: {
- buffer(b: Buffer): Promise<{ files: Array<{ path: string; buffer(): Promise }> }>;
- };
- };
-
- const directory = await unzipper.Open.buffer(buffer);
- const wordDoc = directory.files.find(
- (f: { path: string }) => f.path === "word/document.xml",
- );
- if (!wordDoc) throw new Error("word/document.xml not found in docx");
-
- const xmlBuf = await wordDoc.buffer();
- const xml = xmlBuf.toString("utf8");
-
- const doc = new DOMParser().parseFromString(xml, "text/xml");
- const texts: string[] = [];
-
- function extractText(node: Node) {
- if (node.nodeType === 3 /* TEXT_NODE */) {
- const t = (node as Text).textContent?.trim();
- if (t) texts.push(t);
- }
- node.childNodes?.forEach((child: Node) => extractText(child));
- }
- extractText(doc);
-
- return texts.join(" ");
- } catch (e: unknown) {
- if (e instanceof Error && e.message.includes("Cannot find module")) {
- throw new Error(
- "unzipper or @xmldom/xmldom not installed. Upload a .txt or .md file instead.",
- );
- }
- throw e;
- }
-}
-
-/**
- * Persist the extracted brief text to `fs_projects.data.plan.brief`.
- * Called by the upload route after extraction succeeds.
- */
-export async function persistProjectBrief(
- projectId: string,
- text: string,
- meta: { filename: string; chars: number; truncated: boolean },
-): Promise {
- try {
- await query(
- `UPDATE fs_projects
- SET data = jsonb_set(
- data,
- '{plan}',
- COALESCE(data->'plan', '{}'::jsonb)
- || jsonb_build_object(
- 'brief', $1::text,
- 'briefMeta', $2::jsonb
- ),
- true
- )
- WHERE id = $3`,
- [
- text,
- JSON.stringify({
- ...meta,
- uploadedAt: new Date().toISOString(),
- }),
- projectId,
- ],
- );
- log.info("project brief persisted", { projectId, chars: meta.chars });
- } catch (err) {
- log.error("brief persist failed", {
- projectId,
- err: err instanceof Error ? err.message : String(err),
- });
- throw err;
- }
-}
diff --git a/lib/naming.ts b/lib/naming.ts
index f624f3b6..093aace6 100644
--- a/lib/naming.ts
+++ b/lib/naming.ts
@@ -34,7 +34,7 @@ export function slugify(name: string): string {
* workspaceAppFqdn('mark', 'my-api') === 'my-api.mark.vibnai.com'
*/
export function workspaceAppFqdn(workspaceSlug: string, appSlug: string): string {
- return `${appSlug}.${workspaceSlug}.${VIBN_BASE_DOMAIN}`;
+ return `${appSlug}-${workspaceSlug}.${VIBN_BASE_DOMAIN}`;
}
/** `https://{fqdn}` — what Coolify's `domains` field expects. */
@@ -55,7 +55,7 @@ export function parseDomainsString(domains: string | null | undefined): string[]
/** Guard against cross-workspace or disallowed domains. */
export function isDomainUnderWorkspace(fqdn: string, workspaceSlug: string): boolean {
const f = fqdn.replace(/^https?:\/\//, '').toLowerCase();
- return f === `${workspaceSlug}.${VIBN_BASE_DOMAIN}` || f.endsWith(`.${workspaceSlug}.${VIBN_BASE_DOMAIN}`);
+ return f === `${workspaceSlug}.${VIBN_BASE_DOMAIN}` || f.endsWith(`-${workspaceSlug}.${VIBN_BASE_DOMAIN}`) || f.endsWith(`.${workspaceSlug}.${VIBN_BASE_DOMAIN}`);
}
/**
diff --git a/lib/preview-embed-allowlist.ts b/lib/preview-embed-allowlist.ts
deleted file mode 100644
index 7a97a260..00000000
--- a/lib/preview-embed-allowlist.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * SSRF guard + client/server agreement for /api/preview/embed.
- * Only tunnel-like preview hosts should be proxied — never arbitrary URLs.
- */
-
-export function isPrivateIpHostname(hostname: string): boolean {
- const m = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(hostname);
- if (!m) return false;
- const a = Number(m[1]);
- const b = Number(m[2]);
- if (a === 10) return true;
- if (a === 172 && b >= 16 && b <= 31) return true;
- if (a === 192 && b === 168) return true;
- if (a === 127) return true;
- if (a === 169 && b === 254) return true;
- if (a === 0) return true;
- return false;
-}
-
-/** Server-side hostname allowlist after redirects */
-export function serverPreviewHostnameAllowed(hostname: string, protocol: string): boolean {
- const h = hostname.toLowerCase();
- const p = protocol.toLowerCase();
- if (p !== "http:" && p !== "https:") return false;
- if (isPrivateIpHostname(h)) return false;
- if (h === "localhost" || h === "127.0.0.1") {
- return process.env.NODE_ENV === "development";
- }
- if (h.endsWith(".preview.vibnai.com")) return true;
- if (h === "preview.vibnai.com") return true;
- const suffixes = (process.env.NEXT_PUBLIC_PREVIEW_EMBED_PROXY_HOST_SUFFIXES ?? "")
- .split(",")
- .map((s) => s.trim().toLowerCase())
- .filter(Boolean);
- return suffixes.some((suf) => (suf.startsWith(".") ? h.endsWith(suf) : h === suf));
-}
-
-/**
- * Remote URLs we should load through /api/preview/embed so the bridge script is same-origin.
- * Same-origin targets return false (already works without proxy).
- */
-export function previewUrlEligibleForEmbedProxy(rawUrl: string, appOrigin: string): boolean {
- try {
- const u = new URL(rawUrl);
- const app = new URL(appOrigin);
- if (u.origin === app.origin) return false;
- return serverPreviewHostnameAllowed(u.hostname, u.protocol);
- } catch {
- return false;
- }
-}
diff --git a/lib/scaffold/open-design/design-system-showcase.ts b/lib/scaffold/open-design/design-system-showcase.ts
deleted file mode 100644
index d5ff40fe..00000000
--- a/lib/scaffold/open-design/design-system-showcase.ts
+++ /dev/null
@@ -1,877 +0,0 @@
-/**
- * Build a fully-formed product webpage that demonstrates a design system in
- * action — not just a list of tokens, but a real-feeling marketing /
- * product page (nav, hero, social proof, feature grid, dashboard preview,
- * pricing, testimonials, FAQ, CTA, footer) styled entirely from the
- * tokens we extract from the system's DESIGN.md.
- *
- * Same parsing utilities as design-system-preview.js — kept inline rather
- * than imported so the two views can evolve independently.
- */
-
-type ColorToken = { name: string; value: string; role: string };
-type FontHints = { display?: string; heading?: string; body?: string; mono?: string };
-type RowStatus = 'up' | '';
-
-export function renderDesignSystemShowcase(id: string, raw: string): string {
- const titleMatch = /^#\s+(.+?)\s*$/m.exec(raw);
- const rawTitle = titleMatch?.[1] ?? id;
- const title = cleanTitle(rawTitle);
- const subtitle = extractSubtitle(raw) || 'A design system rendered as a real product surface.';
- const colors = extractColors(raw);
- const fonts = extractFonts(raw);
-
- // Hints are matched against each color's role description (the prose that
- // follows the name in DESIGN.md, e.g. "Primary background.") first, then
- // against the color name. We use word-boundary matching so descriptive
- // names like "Cardinal Red" don't accidentally satisfy a "card" hint and
- // "Gem Pink" doesn't satisfy "ink".
- // Hint ordering matters: more specific phrases come first so a system
- // with both "Primary background" and "Page background in light mode" (e.g.
- // Linear's marketing black + light-mode escape hatch) lands on the
- // dominant role rather than the light-mode subtitle. We drop 'page
- // background' from the bg hints entirely because in practice it almost
- // always belongs to a secondary, light-mode-only entry.
- const bg =
- pickColor(colors, ['primary background', 'background', 'canvas', 'paper'])
- ?? firstLightish(colors)
- ?? '#ffffff';
- // Exclude `bg` so a token whose hex matches the page background (for
- // example Warp's "Warm Parchment" doubling as primary text *and* the
- // firstLightish bg fallback) doesn't make body copy invisible.
- const fg =
- pickColor(
- colors,
- [
- 'primary text',
- 'body text',
- 'foreground',
- 'ink primary',
- 'heading',
- 'ink',
- 'graphite',
- 'navy',
- ],
- [bg],
- )
- ?? pickReadableForeground(bg)
- ?? '#0a0a0a';
- const accent =
- pickColor(colors, [
- 'brand primary',
- 'primary brand',
- 'primary cta',
- 'gradient origin',
- 'brand mark',
- 'brand color',
- ])
- ?? firstNonNeutral(colors, [bg, fg])
- ?? '#2f6feb';
- const accent2 =
- pickColor(colors, [
- 'brand secondary',
- 'secondary brand',
- 'gradient terminus',
- 'tertiary brand',
- 'tertiary',
- 'highlight',
- ])
- ?? secondNonNeutral(colors, [accent, bg, fg])
- ?? accent;
- const muted =
- pickColor(colors, ['secondary text', 'caption', 'metadata', 'placeholder', 'muted', 'subtle'])
- ?? '#666666';
- const border =
- pickColor(colors, ['border', 'divider', 'hairline', 'rule', 'stroke'])
- ?? '#e6e6e6';
- const surface =
- pickColor(colors, [
- 'secondary surface',
- 'section break',
- 'sidebar',
- 'surface subtle',
- 'surface',
- 'panel',
- 'elevated',
- 'card surface',
- ])
- ?? mixSurface(bg);
-
- const display = fonts.display ?? fonts.heading ?? "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif";
- const body = fonts.body ?? display;
- const mono = fonts.mono ?? "ui-monospace, 'JetBrains Mono', monospace";
-
- const accentFg = pickReadableForeground(accent);
- const accent2Fg = pickReadableForeground(accent2);
-
- const productName = title;
- const tagline = oneLine(subtitle).slice(0, 120);
-
- return `
-
-
-
-
- ${escapeHtml(productName)} — showcase
-
-
-
-
-
-
-
-
-
${escapeHtml(productName)} · live preview
-
The system that makes ${escapeHtml(productName)} feel like ${escapeHtml(productName)}.
-
${escapeHtml(tagline)}
-
-
- 4.9 · App Store rating
- SOC 2 · Type II compliant
- 120k+ active teams
-
-
-
-
-
-
-
Trusted by teams shipping serious work
-
- Northwind
- Pioneer
- Lattice
- Atlas Co.
- Voltage
- Foundry
-
-
-
-
-
-
-
What it does
-
Every primitive a fast team needs.
-
A system styled entirely from the tokens of ${escapeHtml(productName)} — palette, typography, surfaces, and motion. Drop it into any product and it stays in character.
-
- ${featureCard('★', 'Tokens that compose', 'Color, type, spacing, and elevation defined once and reused across every surface — from a marketing hero to a row in a table.')}
- ${featureCard('◐', 'Light & dark in lockstep', 'Every component ships with both modes. The accent reads as confident in either context, and contrast meets WCAG AA out of the box.')}
- ${featureCard('⌘', 'Desktop-first, but mobile-honest', 'Layouts collapse from a 12-column desktop grid to a focused single column without losing density or rhythm.')}
- ${featureCard('▣', 'Production-grade primitives', '40+ components — from the obvious (button, input) to the load-bearing (data table, command bar, empty states).')}
- ${featureCard('↗', 'Designed for handoff', 'Every spec carries a Figma frame, a code snippet, and a "do/don’t" pair so engineers don’t have to guess.')}
- ${featureCard('∞', 'Built to evolve', 'Tokens version semver-style. A palette refresh ships through one file — no component code touches.')}
-
-
-
-
-
-
-
In production
-
A workspace, fully styled.
-
This is the same component library you'd use in your app — rendered with ${escapeHtml(productName)} tokens.
-
-
-
-
-
-
-
Overview
- ↑ 12.4% this week
-
-
- ${kpi('MRR', '$184,210', '+8.2%')}
- ${kpi('Active orgs', '2,914', '+121')}
- ${kpi('Conversion', '4.6%', '+0.4 pp')}
- ${kpi('Net retention', '113%', '+2 pp')}
-
-
-
- Revenue · last 12 weeks
- USD · weekly
-
-
- ${inlineLineChart()}
-
-
-
-
-
-
Top accounts
- View all
-
- ${listRow('Northwind Trading', 'Annual · NA', '$48,200', 'up')}
- ${listRow('Pioneer Robotics', 'Quarterly · EMEA', '$31,890', 'up')}
- ${listRow('Atlas Cooperative', 'Annual · APAC', '$22,400', '')}
- ${listRow('Foundry Group', 'Monthly · NA', '$14,750', 'up')}
-
-
-
-
Activity
- Live
-
- ${activityRow('Renewal closed', 'Lattice · 11m ago')}
- ${activityRow('Trial started', 'Voltage · 22m ago')}
- ${activityRow('Plan upgraded', 'Pioneer · 1h ago')}
- ${activityRow('Invoice paid', 'Atlas · 2h ago')}
-
-
-
-
-
-
-
-
-
-
-
Pricing
-
Built for teams of one to one thousand.
-
Pick the plan that matches the way your team ships. Every tier ships the full token system.
-
- ${priceCard('Starter', '$0', 'Free forever', ['Single user', 'All core tokens', 'Up to 3 projects', 'Community support'])}
- ${priceCard('Team', '$24', 'per seat / month', ['Unlimited projects', 'Real-time co-edit', 'Brand themes', 'Priority email support'], true)}
- ${priceCard('Enterprise', 'Custom', 'volume pricing', ['SSO + SCIM', 'Audit logs', 'Custom token schemas', 'Dedicated success manager'])}
-
-
-
-
-
-
-
Customers
-
Loved by teams who care about craft.
-
- ${quote('"Our marketing site, our app, and our internal dashboards finally feel like the same product. The token system is doing all the work."', 'Mira Okafor', 'Head of Design · Pioneer')}
- ${quote('"We swapped our entire design language in an afternoon. Nothing broke. That’s the line, and we crossed it."', 'Caleb Renner', 'Engineering Lead · Northwind')}
-
-
-
-
-
-
-
FAQ
-
Questions, answered.
-
- ${faq('Is this a Figma library, a code library, or both?', 'Both. Tokens flow from one source of truth into Figma styles and into the codegen pipeline at the same time.')}
- ${faq('Can we ship our own brand theme?', 'Yes — fork the token file, change the palette and type stack, and every component reskins automatically.')}
- ${faq('What about accessibility?', 'Color contrast meets WCAG AA on every surface. Components ship with focus rings, ARIA roles, and keyboard handling.')}
- ${faq('How do you handle dark mode?', 'Every token has a paired dark value. The system flips at the document level — no per-component overrides needed.')}
-
-
-
-
-
-
-
-
-
Ship a product that finally feels finished.
-
Drop the system into your app today. The first project is on us.
-
-
-
-
-
-
-
-
-
-`;
-}
-
-function featureCard(icon: string, title: string, body: string): string {
- return `
-
${escapeHtml(icon)}
-
${escapeHtml(title)}
-
${escapeHtml(body)}
-
`;
-}
-
-function kpi(label: string, value: string, delta: string): string {
- return `
-
${escapeHtml(label)}
-
${escapeHtml(value)}
-
${escapeHtml(delta)}
-
`;
-}
-
-function listRow(name: string, meta: string, value: string, status: RowStatus): string {
- const badge = status === 'up' ? '↑ ' : '· ';
- return `
-
-
${escapeHtml(name)}
-
${escapeHtml(meta)}
-
-
${escapeHtml(value)}
- ${badge}
-
`;
-}
-
-function activityRow(name: string, meta: string): string {
- return `
-
-
${escapeHtml(name)}
-
${escapeHtml(meta)}
-
-
-
●
-
`;
-}
-
-function priceCard(name: string, price: string, sub: string, features: string[], featured = false): string {
- return `
-
${escapeHtml(name)}
-
${escapeHtml(price)} ${escapeHtml(sub)}
-
${features.map((f) => `${escapeHtml(f)} `).join('')}
-
Choose ${escapeHtml(name)}
-
`;
-}
-
-function quote(text: string, name: string, role: string): string {
- return `
-
${escapeHtml(text)}
-
-
-
-
${escapeHtml(name)}
-
${escapeHtml(role)}
-
-
-
`;
-}
-
-function faq(q: string, a: string): string {
- return `
-
${escapeHtml(q)}
-
${escapeHtml(a)}
-
`;
-}
-
-function inlineLineChart(): string {
- // Deterministic numbers so the chart looks specific (12 weekly data points).
- const data = [38, 44, 41, 52, 49, 61, 58, 67, 71, 76, 82, 88];
- const max = Math.max(...data);
- const min = Math.min(...data);
- const w = 720;
- const h = 160;
- const padX = 8;
- const padY = 14;
- const stepX = (w - padX * 2) / (data.length - 1);
- const norm = (v: number) => padY + (h - padY * 2) * (1 - (v - min) / (max - min));
- const points = data.map((v, i) => `${padX + i * stepX},${norm(v).toFixed(1)}`).join(' ');
- const area = `${padX},${h} ${points} ${w - padX},${h}`;
- return `
-
-
-
-
-
-
-
-
- ${data.map((v, i) => ` `).join('')}
- `;
-}
-
-function extractSubtitle(raw: string): string {
- const lines = raw.split(/\r?\n/);
- const h1 = lines.findIndex((l) => /^#\s+/.test(l));
- if (h1 === -1) return '';
- const after = lines.slice(h1 + 1);
- const nextHeading = after.findIndex((l) => /^#{1,6}\s+/.test(l));
- const window = (nextHeading === -1 ? after : after.slice(0, nextHeading))
- .join('\n')
- .replace(/^>\s*Category:.*$/gim, '')
- .replace(/^>\s*/gm, '')
- .trim();
- return window.split(/\n\n/)[0]?.slice(0, 240) ?? '';
-}
-
-export function extractColors(raw: string): ColorToken[] {
- const colors: ColorToken[] = [];
- const seen = new Set();
- function push(name: string, value: string, role: string): void {
- const cleanName = String(name).replace(/[*_`]+/g, '').replace(/\s+/g, ' ').trim();
- if (!cleanName || cleanName.length > 60) return;
- const v = normalizeHex(value);
- const key = `${cleanName.toLowerCase()}|${v}`;
- const cleanRole = String(role || '')
- .replace(/[`*_]+/g, '')
- .replace(/\s+/g, ' ')
- .trim()
- .replace(/[.;]+$/, '');
- if (seen.has(key)) {
- // Already recorded — but if this occurrence carries a richer role
- // description, upgrade the stored entry so role-based lookups don't
- // fall back to the bare name.
- if (cleanRole) {
- const existing = colors.find(
- (c) => c.name.toLowerCase() === cleanName.toLowerCase() && c.value === v,
- );
- if (existing && (!existing.role || cleanRole.length > existing.role.length)) {
- existing.role = cleanRole;
- }
- }
- return;
- }
- seen.add(key);
- colors.push({ name: cleanName, value: v, role: cleanRole });
- }
-
- // Process the file line-by-line so multi-hex entries like Linear's
- // `**Marketing Black** (\`#010102\` / \`#08090a\`): role` don't confuse a
- // single global regex. We extract three pieces from each candidate line:
- // - the bold (or list-prefixed) name
- // - the FIRST hex on the line
- // - everything after the first `:` that follows the hex (the role)
- for (const rawLine of raw.split(/\r?\n/)) {
- const line = rawLine.trim();
- if (!line) continue;
-
- // Pattern A: **Name** … #hex … : role description
- const bold = /\*\*([A-Za-z][A-Za-z0-9 /&()+_'’-]{1,40}?)\*\*([^\n]+)/.exec(line);
- if (bold) {
- const rest = bold[2] ?? '';
- const hex = /#[0-9a-fA-F]{3,8}\b/.exec(rest);
- if (hex) {
- const after = rest.slice((hex.index ?? 0) + hex[0].length);
- const colonIdx = after.search(/[::]/);
- const role = colonIdx >= 0 ? after.slice(colonIdx + 1).trim() : '';
- push(bold[1] ?? '', hex[0], role);
- continue;
- }
- }
-
- // Pattern B: list-prefixed spec lines like
- // "- Background: `#7d2ae8`" inside a ### Buttons block.
- // Also handles the `- **Name:** \`#hex\`` shape (colon inside the bold
- // wrapper) used by agentic/warm-editorial: the optional `\*{0,2}` slots
- // before the name and after the colon let us absorb the surrounding
- // `**` markers without needing a third pattern.
- // Use the name itself as the role so lookups can still see "Background"
- // and "Text" labels.
- const spec = /^[\s>*-]*\*{0,2}([A-Za-z][^:*\n]{1,40}?)\*{0,2}\s*[::]\s*\*{0,2}\s*`?(#[0-9a-fA-F]{3,8})/.exec(line);
- if (spec) {
- push(spec[1] ?? '', spec[2] ?? '', spec[1] ?? '');
- }
- }
-
- return colors;
-}
-
-function extractFonts(raw: string): FontHints {
- const out: FontHints = {};
- const re = /^[\s>*-]*\**\s*([A-Za-z][A-Za-z /]{1,30}?)\s*\**\s*[::]\s*`?([^`\n]+?)`?$/gm;
- let m;
- while ((m = re.exec(raw)) !== null) {
- const label = (m[1] ?? '').toLowerCase();
- const value = (m[2] ?? '').trim().replace(/[*_`]+$/g, '').trim();
- if (!/[a-zA-Z]/.test(value)) continue;
- if (value.startsWith('#')) continue;
- if (/display|heading|h1|title/.test(label) && !out.display) out.display = value;
- else if (/body|text|paragraph|copy/.test(label) && !out.body) out.body = value;
- else if (/mono|code/.test(label) && !out.mono) out.mono = value;
- }
- return out;
-}
-
-function escapeRegex(s: string): string {
- return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-}
-
-// Match a hint as a whole word inside `text` (case-insensitive). We use word
-// boundaries so descriptive color names like "Cardinal Red" don't satisfy a
-// "card" hint, and "Gem Pink" doesn't satisfy "ink" — both real bugs the
-// substring-based version produced for the Duolingo and Canva showcases.
-function matchesHint(text: string, hint: string): boolean {
- if (!text) return false;
- const needle = hint.toLowerCase().trim();
- if (!needle) return false;
- const re = new RegExp(`\\b${escapeRegex(needle)}\\b`, 'i');
- return re.test(text);
-}
-
-function pickColor(colors: ColorToken[], hints: string[], exclude: string[] = []): string | null {
- // Two-pass lookup: each hint is first checked against every color's role
- // description (the prose authors use to explain how the color is used)
- // and only then against the bare name. This ensures a `**Snow** … Primary
- // background.` line is recognised as the page background even though the
- // name "Snow" doesn't contain the word "background".
- // `exclude` skips colors whose hex equals an already-chosen role (e.g.
- // pass `[bg]` when picking `fg`) so two roles can't collapse to the same
- // hex and erase contrast.
- const blocked = new Set(
- exclude
- .map((v) => (v == null ? '' : String(v).toLowerCase()))
- .filter((v) => v.length > 0),
- );
- const isAllowed = (c: ColorToken) => !blocked.has(c.value.toLowerCase());
- for (const hint of hints) {
- const byRole = colors.find((c) => isAllowed(c) && matchesHint(c.role, hint));
- if (byRole) return byRole.value;
- const byName = colors.find((c) => isAllowed(c) && matchesHint(c.name, hint));
- if (byName) return byName.value;
- }
- return null;
-}
-
-function colorSaturation(hex: string): number {
- const v = String(hex).replace('#', '').toLowerCase();
- if (v.length !== 6) return 0;
- const r = parseInt(v.slice(0, 2), 16);
- const g = parseInt(v.slice(2, 4), 16);
- const b = parseInt(v.slice(4, 6), 16);
- const max = Math.max(r, g, b);
- const min = Math.min(r, g, b);
- return max === 0 ? 0 : (max - min) / max;
-}
-
-function colorLuminance(hex: string): number {
- const v = String(hex).replace('#', '').toLowerCase();
- if (v.length !== 6) return 0.5;
- const r = parseInt(v.slice(0, 2), 16);
- const g = parseInt(v.slice(2, 4), 16);
- const b = parseInt(v.slice(4, 6), 16);
- return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
-}
-
-function firstLightish(colors: ColorToken[]): string | null {
- for (const c of colors) {
- if (colorSaturation(c.value) > 0.15) continue;
- if (colorLuminance(c.value) >= 0.92) return c.value;
- }
- return null;
-}
-
-function firstNonNeutral(colors: ColorToken[], exclude: string[] = []): string | null {
- const set = new Set(exclude.map((v) => String(v || '').toLowerCase()));
- for (const c of colors) {
- if (set.has(c.value.toLowerCase())) continue;
- if (colorSaturation(c.value) > 0.25) return c.value;
- }
- return null;
-}
-
-function secondNonNeutral(colors: ColorToken[], exclude: string[] = []): string | null {
- const set = new Set(exclude.map((v) => String(v || '').toLowerCase()));
- for (const c of colors) {
- if (set.has(c.value.toLowerCase())) continue;
- if (colorSaturation(c.value) > 0.25) return c.value;
- }
- return null;
-}
-
-function pickReadableForeground(hex: string): string {
- const n = normalizeHex(hex);
- if (n.length !== 7) return '#ffffff';
- const r = parseInt(n.slice(1, 3), 16);
- const g = parseInt(n.slice(3, 5), 16);
- const b = parseInt(n.slice(5, 7), 16);
- const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
- return lum > 0.6 ? '#0a0a0a' : '#ffffff';
-}
-
-function mixSurface(bg: string): string {
- const n = normalizeHex(bg);
- if (n.length !== 7) return '#fafafa';
- const r = parseInt(n.slice(1, 3), 16);
- const g = parseInt(n.slice(3, 5), 16);
- const b = parseInt(n.slice(5, 7), 16);
- const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
- // Lift dark backgrounds; tint light backgrounds slightly cooler.
- const adjust = lum < 0.4 ? 16 : -8;
- const fix = (v: number) => Math.max(0, Math.min(255, v + adjust)).toString(16).padStart(2, '0');
- return `#${fix(r)}${fix(g)}${fix(b)}`;
-}
-
-function normalizeHex(hex: string): string {
- let h = hex.toLowerCase();
- if (h.length === 4) {
- h = '#' + h.slice(1).split('').map((c) => c + c).join('');
- }
- return h;
-}
-
-function cleanTitle(raw: string): string {
- return String(raw).replace(/^Design System (Inspired by|for)\s+/i, '').trim();
-}
-
-function oneLine(s: string): string {
- return String(s).replace(/\s+/g, ' ').trim();
-}
-
-function escapeHtml(s: string): string {
- return String(s).replace(/[&<>"']/g, (c) =>
- c === '&' ? '&' : c === '<' ? '<' : c === '>' ? '>' : c === '"' ? '"' : ''',
- );
-}
diff --git a/lib/scaffold/templates/dashboard/tailwind.config.ts b/lib/scaffold/templates/dashboard/tailwind.config.ts
deleted file mode 100644
index 7026685d..00000000
--- a/lib/scaffold/templates/dashboard/tailwind.config.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { Config } from "tailwindcss";
-
-const config: Config = {
- content: [
- "./app/**/*.{js,ts,jsx,tsx,mdx}",
- ],
- theme: {
- extend: {},
- },
- plugins: [],
-};
-export default config;
diff --git a/lib/scaffold/templates/pitch-deck/tailwind.config.ts b/lib/scaffold/templates/pitch-deck/tailwind.config.ts
deleted file mode 100644
index 7026685d..00000000
--- a/lib/scaffold/templates/pitch-deck/tailwind.config.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { Config } from "tailwindcss";
-
-const config: Config = {
- content: [
- "./app/**/*.{js,ts,jsx,tsx,mdx}",
- ],
- theme: {
- extend: {},
- },
- plugins: [],
-};
-export default config;
diff --git a/lib/server/backend-extractor.ts b/lib/server/backend-extractor.ts
deleted file mode 100644
index 28ce9a11..00000000
--- a/lib/server/backend-extractor.ts
+++ /dev/null
@@ -1,228 +0,0 @@
-/**
- * Backend Extraction Module
- *
- * Runs extraction as a pure backend job, not in chat.
- * Called when Collector phase completes.
- */
-
-import { getAdminDb } from '@/lib/firebase/admin';
-import { GeminiLlmClient } from '@/lib/ai/gemini-client';
-import { BACKEND_EXTRACTOR_SYSTEM_PROMPT } from '@/lib/ai/prompts/extractor';
-import { writeKnowledgeChunksForItem } from '@/lib/server/vector-memory';
-import type { ExtractionOutput, ExtractedInsight } from '@/lib/types/extraction-output';
-import type { PhaseHandoff } from '@/lib/types/phase-handoff';
-import { z } from 'zod';
-
-const ExtractionOutputSchema = z.object({
- insights: z.array(z.object({
- id: z.string(),
- type: z.enum(["problem", "user", "feature", "constraint", "opportunity", "other"]),
- title: z.string(),
- description: z.string(),
- sourceText: z.string(),
- sourceKnowledgeItemId: z.string(),
- importance: z.enum(["primary", "supporting"]),
- confidence: z.number().min(0).max(1),
- })),
- problems: z.array(z.string()),
- targetUsers: z.array(z.string()),
- features: z.array(z.string()),
- constraints: z.array(z.string()),
- opportunities: z.array(z.string()),
- uncertainties: z.array(z.string()),
- missingInformation: z.array(z.string()),
- overallConfidence: z.number().min(0).max(1),
-});
-
-export async function runBackendExtractionForProject(projectId: string): Promise {
- console.log(`[Backend Extractor] Starting extraction for project ${projectId}`);
-
- const adminDb = getAdminDb();
-
- try {
- // 1. Load project
- const projectDoc = await adminDb.collection('projects').doc(projectId).get();
- if (!projectDoc.exists) {
- throw new Error(`Project ${projectId} not found`);
- }
-
- const projectData = projectDoc.data();
-
- // 2. Load knowledge items
- const knowledgeSnapshot = await adminDb
- .collection('knowledge_items')
- .where('projectId', '==', projectId)
- .where('sourceType', '==', 'imported_document')
- .get();
-
- if (knowledgeSnapshot.empty) {
- console.log(`[Backend Extractor] No documents to extract for project ${projectId} - creating empty handoff`);
-
- // Create a minimal extraction handoff even with no documents
- const emptyHandoff: PhaseHandoff = {
- phase: 'extraction',
- readyForNextPhase: false, // Not ready - no materials to extract from
- confidence: 0,
- confirmed: {
- problems: [],
- targetUsers: [],
- features: [],
- constraints: [],
- opportunities: [],
- },
- uncertain: {},
- missing: ['No documents uploaded - need product requirements, specs, or notes'],
- questionsForUser: [
- 'You haven\'t uploaded any documents yet. Do you have any product specs, requirements, or notes to share?',
- ],
- sourceEvidence: [],
- version: 'extraction_v1',
- timestamp: new Date().toISOString(),
- };
-
- await adminDb.collection('projects').doc(projectId).update({
- 'phaseData.phaseHandoffs.extraction': emptyHandoff,
- currentPhase: 'extraction_review',
- phaseStatus: 'in_progress',
- 'phaseData.extractionCompletedAt': new Date().toISOString(),
- updatedAt: new Date().toISOString(),
- });
-
- console.log(`[Backend Extractor] Set phase to extraction_review with empty handoff`);
- return;
- }
-
- console.log(`[Backend Extractor] Found ${knowledgeSnapshot.size} documents to process`);
-
- const llm = new GeminiLlmClient();
- const allExtractionOutputs: ExtractionOutput[] = [];
- const processedKnowledgeItemIds: string[] = [];
-
- // 3. Process each document
- for (const knowledgeDoc of knowledgeSnapshot.docs) {
- const knowledgeData = knowledgeDoc.data();
- const knowledgeItemId = knowledgeDoc.id;
-
- try {
- console.log(`[Backend Extractor] Processing document: ${knowledgeData.title || knowledgeItemId}`);
-
- // Call LLM with structured extraction + thinking mode
- const extraction = await llm.structuredCall({
- model: 'gemini',
- systemPrompt: BACKEND_EXTRACTOR_SYSTEM_PROMPT,
- messages: [{
- role: 'user',
- content: `Document Title: ${knowledgeData.title || 'Untitled'}\nSource Type: ${knowledgeData.sourceType}\n\nContent:\n${knowledgeData.content}`,
- }],
- schema: ExtractionOutputSchema as any,
- temperature: 1.0, // Gemini 3 default (changed from 0.3)
- thinking_config: {
- thinking_level: 'high', // Enable deep reasoning for document analysis
- include_thoughts: false, // Don't include thought tokens in output (saves cost)
- },
- });
-
- // Add knowledgeItemId to each insight
- extraction.insights.forEach(insight => {
- insight.sourceKnowledgeItemId = knowledgeItemId;
- });
-
- allExtractionOutputs.push(extraction);
- processedKnowledgeItemIds.push(knowledgeItemId);
-
- // 4. Persist extraction to chat_extractions
- await adminDb.collection('chat_extractions').add({
- projectId,
- knowledgeItemId,
- data: extraction,
- overallConfidence: extraction.overallConfidence,
- overallCompletion: extraction.overallConfidence > 0.7 ? 0.9 : 0.6,
- createdAt: new Date().toISOString(),
- updatedAt: new Date().toISOString(),
- });
-
- console.log(`[Backend Extractor] Extracted ${extraction.insights.length} insights from ${knowledgeData.title || knowledgeItemId}`);
-
- // 5. Write vector chunks for primary insights
- const primaryInsights = extraction.insights.filter(i => i.importance === 'primary');
- for (const insight of primaryInsights) {
- try {
- // Create a knowledge chunk for this insight
- await writeKnowledgeChunksForItem({
- id: knowledgeItemId,
- projectId,
- content: `${insight.title}\n\n${insight.description}\n\nSource: ${insight.sourceText}`,
- sourceMeta: {
- sourceType: 'extracted_insight',
- importance: 'primary',
- },
- });
- } catch (chunkError) {
- console.error(`[Backend Extractor] Failed to write chunk for insight ${insight.id}:`, chunkError);
- // Continue processing other insights
- }
- }
-
- } catch (docError) {
- console.error(`[Backend Extractor] Failed to process document ${knowledgeItemId}:`, docError);
- // Continue with next document
- }
- }
-
- // 6. Build extraction PhaseHandoff
- // Flatten all extracted items (they're already strings, not objects)
- const allProblems = [...new Set(allExtractionOutputs.flatMap(e => e.problems))];
- const allUsers = [...new Set(allExtractionOutputs.flatMap(e => e.targetUsers))];
- const allFeatures = [...new Set(allExtractionOutputs.flatMap(e => e.features))];
- const allConstraints = [...new Set(allExtractionOutputs.flatMap(e => e.constraints))];
- const allOpportunities = [...new Set(allExtractionOutputs.flatMap(e => e.opportunities))];
- const allUncertainties = [...new Set(allExtractionOutputs.flatMap(e => e.uncertainties))];
- const allMissing = [...new Set(allExtractionOutputs.flatMap(e => e.missingInformation))];
-
- const avgConfidence = allExtractionOutputs.length > 0
- ? allExtractionOutputs.reduce((sum, e) => sum + e.overallConfidence, 0) / allExtractionOutputs.length
- : 0;
-
- const readyForNextPhase = allProblems.length > 0 && allFeatures.length > 0 && avgConfidence > 0.5;
-
- const extractionHandoff: PhaseHandoff = {
- phase: 'extraction',
- readyForNextPhase,
- confidence: avgConfidence,
- confirmed: {
- problems: allProblems,
- targetUsers: allUsers,
- features: allFeatures,
- constraints: allConstraints,
- opportunities: allOpportunities,
- },
- uncertain: {},
- missing: allMissing,
- questionsForUser: allUncertainties,
- sourceEvidence: processedKnowledgeItemIds,
- version: 'extraction_v1',
- timestamp: new Date().toISOString(),
- };
-
- // 7. Persist handoff and update phase
- await adminDb.collection('projects').doc(projectId).update({
- 'phaseData.phaseHandoffs.extraction': extractionHandoff,
- currentPhase: 'extraction_review',
- phaseStatus: 'in_progress',
- 'phaseData.extractionCompletedAt': new Date().toISOString(),
- updatedAt: new Date().toISOString(),
- });
-
- console.log(`[Backend Extractor] ✅ Extraction complete for project ${projectId}`);
- console.log(`[Backend Extractor] - Problems: ${allProblems.length}`);
- console.log(`[Backend Extractor] - Users: ${allUsers.length}`);
- console.log(`[Backend Extractor] - Features: ${allFeatures.length}`);
- console.log(`[Backend Extractor] - Confidence: ${(avgConfidence * 100).toFixed(1)}%`);
- console.log(`[Backend Extractor] - Ready for next phase: ${readyForNextPhase}`);
-
- } catch (error) {
- console.error(`[Backend Extractor] Fatal error during extraction:`, error);
- throw error;
- }
-}
-
diff --git a/lib/server/chat-extraction.ts b/lib/server/chat-extraction.ts
deleted file mode 100644
index 0b841a17..00000000
--- a/lib/server/chat-extraction.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { getAdminDb } from '@/lib/firebase/admin';
-import { FieldValue } from 'firebase-admin/firestore';
-import type { ChatExtractionRecord } from '@/lib/types/chat-extraction';
-
-const COLLECTION = 'chat_extractions';
-
-interface CreateChatExtractionInput {
- projectId: string;
- knowledgeItemId: string;
- data: TData;
- overallCompletion: number;
- overallConfidence: number;
-}
-
-export async function createChatExtraction(
- input: CreateChatExtractionInput,
-): Promise> {
- const adminDb = getAdminDb();
- const docRef = adminDb.collection(COLLECTION).doc();
-
- const payload = {
- id: docRef.id,
- projectId: input.projectId,
- knowledgeItemId: input.knowledgeItemId,
- data: input.data,
- overallCompletion: input.overallCompletion,
- overallConfidence: input.overallConfidence,
- createdAt: FieldValue.serverTimestamp(),
- updatedAt: FieldValue.serverTimestamp(),
- };
-
- await docRef.set(payload);
- const snapshot = await docRef.get();
- return snapshot.data() as ChatExtractionRecord;
-}
-
-export async function listChatExtractions(
- projectId: string,
-): Promise[]> {
- const adminDb = getAdminDb();
- const querySnapshot = await adminDb
- .collection(COLLECTION)
- .where('projectId', '==', projectId)
- .orderBy('createdAt', 'desc')
- .get();
-
- return querySnapshot.docs.map(
- (doc) => doc.data() as ChatExtractionRecord,
- );
-}
-
-export async function getChatExtraction(
- extractionId: string,
-): Promise | null> {
- const adminDb = getAdminDb();
- const docRef = adminDb.collection(COLLECTION).doc(extractionId);
- const snapshot = await docRef.get();
- if (!snapshot.exists) {
- return null;
- }
- return snapshot.data() as ChatExtractionRecord;
-}
-
-
diff --git a/lib/server/chat-mode-resolver.ts b/lib/server/chat-mode-resolver.ts
deleted file mode 100644
index ff67b085..00000000
--- a/lib/server/chat-mode-resolver.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-/**
- * Chat Mode Resolution Logic
- *
- * Determines which chat mode (collector, extraction_review, vision, mvp, marketing, general)
- * should be active based on project state stored in Postgres.
- */
-
-import { query } from '@/lib/db-postgres';
-import type { ChatMode } from '@/lib/ai/chat-modes';
-
-/**
- * Resolve the appropriate chat mode for a project using Postgres (fs_projects).
- */
-export async function resolveChatMode(projectId: string): Promise {
- try {
- const rows = await query<{ data: any }>(
- `SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
- [projectId]
- );
-
- if (rows.length === 0) {
- console.warn(`[Chat Mode Resolver] Project ${projectId} not found`);
- return 'collector_mode';
- }
-
- const projectData = rows[0].data ?? {};
- const phaseData = (projectData.phaseData ?? {}) as Record;
- const currentPhase: string = projectData.currentPhase ?? 'collector';
-
- // Explicit phase overrides
- if (currentPhase === 'extraction_review' || currentPhase === 'analyzed') return 'extraction_review_mode';
- if (currentPhase === 'vision') return 'vision_mode';
- if (currentPhase === 'mvp') return 'mvp_mode';
- if (currentPhase === 'marketing') return 'marketing_mode';
-
- // Derive from phase artifacts
- if (!phaseData.canonicalProductModel) return 'collector_mode';
- if (!phaseData.mvpPlan) return 'vision_mode';
- if (!phaseData.marketingPlan) return 'mvp_mode';
- if (phaseData.marketingPlan) return 'marketing_mode';
-
- return 'general_chat_mode';
- } catch (error) {
- console.error('[Chat Mode Resolver] Failed to resolve mode:', error);
- return 'collector_mode';
- }
-}
-
-/**
- * Summarise knowledge items for context building.
- * Uses Postgres fs_knowledge_items if available, otherwise returns empty.
- */
-export async function summarizeKnowledgeItems(projectId: string): Promise<{
- totalCount: number;
- bySourceType: Record;
- recentTitles: string[];
-}> {
- try {
- const rows = await query<{ data: any }>(
- `SELECT data FROM fs_knowledge_items WHERE project_id = $1 ORDER BY created_at DESC LIMIT 20`,
- [projectId]
- );
-
- const bySourceType: Record = {};
- const recentTitles: string[] = [];
-
- for (const row of rows) {
- const d = row.data ?? {};
- const sourceType = d.sourceType ?? 'unknown';
- bySourceType[sourceType] = (bySourceType[sourceType] ?? 0) + 1;
- if (d.title && recentTitles.length < 5) recentTitles.push(d.title);
- }
-
- return { totalCount: rows.length, bySourceType, recentTitles };
- } catch {
- // Table may not exist for older deployments — return empty
- return { totalCount: 0, bySourceType: {}, recentTitles: [] };
- }
-}
-
-/**
- * Summarise extractions for context building.
- * Returns empty defaults — extractions not yet migrated to Postgres.
- */
-export async function summarizeExtractions(projectId: string): Promise<{
- totalCount: number;
- avgConfidence: number;
- avgCompletion: number;
-}> {
- return { totalCount: 0, avgConfidence: 0, avgCompletion: 0 };
-}
diff --git a/lib/server/dev-server-state.ts b/lib/server/dev-server-state.ts
deleted file mode 100644
index 30d1442c..00000000
--- a/lib/server/dev-server-state.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-/**
- * Persistent dev-server configuration store.
- * Closes BETA_LAUNCH_PLAN P6.B1.
- *
- * When `dev_server_start` succeeds, the MCP tool should call
- * `upsertDevServerConfig` so the project page can auto-resume the
- * server on next mount without requiring the user to re-type the
- * command (see P6.B2 for the auto-resume hook).
- *
- * Schema:
- * fs_project_dev_servers
- * project_id UUID PK → fs_projects.id
- * command TEXT NOT NULL e.g. "cd myapp && npm run dev"
- * port INT NOT NULL e.g. 3000
- * framework TEXT e.g. "nextjs", "vite", "express"
- * preview_url TEXT last known *.preview.vibnai.com URL
- * last_started_at TIMESTAMPTZ
- * status TEXT CHECK IN ('running','stopped','crashed')
- * updated_at TIMESTAMPTZ DEFAULT NOW()
- */
-
-import { query } from "@/lib/db-postgres";
-import { log } from "@/lib/server/logger";
-
-let tableReady = false;
-async function ensureTable() {
- if (tableReady) return;
- await query(`
- CREATE TABLE IF NOT EXISTS fs_project_dev_servers (
- project_id TEXT PRIMARY KEY,
- command TEXT NOT NULL,
- port INT NOT NULL,
- framework TEXT,
- preview_url TEXT,
- last_started_at TIMESTAMPTZ,
- status TEXT NOT NULL DEFAULT 'stopped'
- CHECK (status IN ('running', 'stopped', 'crashed')),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- )
- `);
- tableReady = true;
-}
-
-export interface DevServerConfig {
- projectId: string;
- command: string;
- port: number;
- framework?: string;
- previewUrl?: string;
- status: "running" | "stopped" | "crashed";
-}
-
-/** Called by the MCP dev_server_start handler after a successful start. */
-export async function upsertDevServerConfig(
- cfg: DevServerConfig,
-): Promise {
- try {
- await ensureTable();
- await query(
- `INSERT INTO fs_project_dev_servers
- (project_id, command, port, framework, preview_url, last_started_at, status, updated_at)
- VALUES ($1, $2, $3, $4, $5, NOW(), $6, NOW())
- ON CONFLICT (project_id) DO UPDATE SET
- command = EXCLUDED.command,
- port = EXCLUDED.port,
- framework = COALESCE(EXCLUDED.framework, fs_project_dev_servers.framework),
- preview_url = COALESCE(EXCLUDED.preview_url, fs_project_dev_servers.preview_url),
- last_started_at = NOW(),
- status = EXCLUDED.status,
- updated_at = NOW()`,
- [
- cfg.projectId,
- cfg.command,
- cfg.port,
- cfg.framework ?? null,
- cfg.previewUrl ?? null,
- cfg.status,
- ],
- );
- } catch (err) {
- log.warn("dev-server-state: upsert failed (non-fatal)", {
- projectId: cfg.projectId,
- err: err instanceof Error ? err.message : String(err),
- });
- }
-}
-
-/** Update just the status (e.g. on stop / crash). */
-export async function setDevServerStatus(
- projectId: string,
- status: "running" | "stopped" | "crashed",
-): Promise {
- try {
- await ensureTable();
- await query(
- `UPDATE fs_project_dev_servers
- SET status = $2, updated_at = NOW()
- WHERE project_id = $1`,
- [projectId, status],
- );
- } catch (err) {
- log.warn("dev-server-state: status update failed (non-fatal)", {
- projectId,
- err: err instanceof Error ? err.message : String(err),
- });
- }
-}
-
-/** Returns the last-known dev server config for a project, or null. */
-export async function getDevServerConfig(
- projectId: string,
-): Promise {
- try {
- await ensureTable();
- const rows = await query<{
- project_id: string;
- command: string;
- port: number;
- framework: string | null;
- preview_url: string | null;
- status: string;
- }>(
- `SELECT project_id, command, port, framework, preview_url, status
- FROM fs_project_dev_servers WHERE project_id = $1`,
- [projectId],
- );
- if (!rows[0]) return null;
- const r = rows[0];
- return {
- projectId: r.project_id,
- command: r.command,
- port: r.port,
- framework: r.framework ?? undefined,
- previewUrl: r.preview_url ?? undefined,
- status: r.status as "running" | "stopped" | "crashed",
- };
- } catch {
- return null;
- }
-}
diff --git a/lib/server/infra-coolify-health.ts b/lib/server/infra-coolify-health.ts
deleted file mode 100644
index 3c9b3364..00000000
--- a/lib/server/infra-coolify-health.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- * Coolify connectivity checks for ops / uptime monitors.
- *
- * - HTTP API (token) — provisioning via REST
- * - SSH → Docker on host — required for shell.exec / dev containers
- */
-
-import { listServers } from '@/lib/coolify';
-import { runOnCoolifyHost } from '@/lib/coolify-ssh';
-
-export interface CoolifyInfraHealthReport {
- checkedAt: string;
- ssh: {
- configured: boolean;
- missingEnvVars: string[];
- reachable?: boolean;
- latencyMs?: number;
- dockerDaemonOk?: boolean;
- /** docker Server.Version when daemon responds */
- dockerVersion?: string;
- error?: string;
- };
- api: {
- configured: boolean;
- reachable?: boolean;
- latencyMs?: number;
- serverCount?: number;
- error?: string;
- };
-}
-
-export function getCoolifySshConfigGap(): {
- configured: boolean;
- missingEnvVars: string[];
-} {
- const missing: string[] = [];
- if (!process.env.COOLIFY_SSH_HOST?.trim()) missing.push('COOLIFY_SSH_HOST');
- if (!process.env.COOLIFY_SSH_PRIVATE_KEY_B64?.trim()) {
- missing.push('COOLIFY_SSH_PRIVATE_KEY_B64');
- }
- return { configured: missing.length === 0, missingEnvVars: missing };
-}
-
-/** True when SSH is wired and `docker` responds on the Coolify host. */
-export async function runCoolifyInfraHealthProbe(): Promise {
- const checkedAt = new Date().toISOString();
- const sshGap = getCoolifySshConfigGap();
-
- const report: CoolifyInfraHealthReport = {
- checkedAt,
- ssh: {
- configured: sshGap.configured,
- missingEnvVars: sshGap.missingEnvVars,
- },
- api: {
- configured: !!process.env.COOLIFY_API_TOKEN?.trim(),
- },
- };
-
- if (report.api.configured) {
- const t0 = Date.now();
- try {
- const servers = await listServers();
- report.api.reachable = true;
- report.api.latencyMs = Date.now() - t0;
- report.api.serverCount = Array.isArray(servers) ? servers.length : 0;
- } catch (e) {
- report.api.reachable = false;
- report.api.latencyMs = Date.now() - t0;
- report.api.error = e instanceof Error ? e.message : String(e);
- }
- } else {
- report.api.error = 'COOLIFY_API_TOKEN is not set';
- }
-
- if (!sshGap.configured) {
- report.ssh.error =
- `Missing: ${sshGap.missingEnvVars.join(', ')} — dev containers need SSH to the Docker host (see lib/coolify-ssh.ts).`;
- return report;
- }
-
- const tSsh = Date.now();
- try {
- const res = await runOnCoolifyHost(`docker info --format '{{.ServerVersion}}'`, {
- timeoutMs: 15_000,
- maxBytes: 8192,
- });
- report.ssh.latencyMs = Date.now() - tSsh;
- report.ssh.reachable = true;
- const ver = res.stdout.trim().split(/\s+/)[0]?.trim() ?? '';
- if (res.code === 0 && ver.length > 0) {
- report.ssh.dockerDaemonOk = true;
- report.ssh.dockerVersion = ver;
- } else {
- report.ssh.dockerDaemonOk = false;
- report.ssh.error = `docker probe exit ${res.code}: ${(res.stderr || res.stdout || '(empty)').slice(0, 600)}`;
- }
- } catch (e) {
- report.ssh.latencyMs = Date.now() - tSsh;
- report.ssh.reachable = false;
- report.ssh.dockerDaemonOk = false;
- report.ssh.error = e instanceof Error ? e.message : String(e);
- }
-
- return report;
-}
-
-export function isCoolifyInfraOperational(report: CoolifyInfraHealthReport): boolean {
- return (
- report.ssh.configured &&
- report.ssh.reachable === true &&
- report.ssh.dockerDaemonOk === true &&
- report.api.configured &&
- report.api.reachable === true
- );
-}
diff --git a/lib/server/product-model.ts b/lib/server/product-model.ts
deleted file mode 100644
index 40fdb285..00000000
--- a/lib/server/product-model.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { listChatExtractions } from '@/lib/server/chat-extraction';
-import { clamp, nowIso, persistPhaseArtifacts, uniqueStrings, toStage } from '@/lib/server/projects';
-import type { CanonicalProductModel } from '@/lib/types/product-model';
-import type { ChatExtractionRecord } from '@/lib/types/chat-extraction';
-
-const average = (numbers: number[]) =>
- numbers.length ? numbers.reduce((sum, value) => sum + value, 0) / numbers.length : 0;
-
-export async function buildCanonicalProductModel(projectId: string): Promise {
- const extractions = await listChatExtractions(projectId);
- if (!extractions.length) {
- throw new Error('No chat extractions found for project');
- }
-
- const completionAvg = average(
- extractions.map(
- (record) =>
- (record.data as any)?.summary_scores?.overall_completion ?? record.overallCompletion ?? 0,
- ),
- );
- const confidenceAvg = average(
- extractions.map(
- (record) =>
- (record.data as any)?.summary_scores?.overall_confidence ?? record.overallConfidence ?? 0,
- ),
- );
-
- const canonical = mapExtractionToCanonical(
- projectId,
- pickHighestConfidence(extractions as any),
- completionAvg,
- confidenceAvg,
- );
-
- await persistPhaseArtifacts(projectId, (phaseData, phaseScores, phaseHistory) => {
- phaseData.canonicalProductModel = canonical;
- phaseScores.vision = {
- overallCompletion: canonical.overallCompletion,
- overallConfidence: canonical.overallConfidence,
- updatedAt: nowIso(),
- };
- phaseHistory.push({ phase: 'vision', status: 'completed', timestamp: nowIso() });
- return { phaseData, phaseScores, phaseHistory, nextPhase: 'vision_ready' };
- });
-
- return canonical;
-}
-
-function pickHighestConfidence(records: ChatExtractionRecord[]) {
- return records.reduce((best, record) =>
- record.overallConfidence > best.overallConfidence ? record : best,
- );
-}
-
-function mapExtractionToCanonical(
- projectId: string,
- record: ChatExtractionRecord,
- completionAvg: number,
- confidenceAvg: number,
-): CanonicalProductModel {
- const data = record.data;
-
- const coreFeatures = data.solution_and_features.core_features.map(
- (feature) => feature.name || feature.description,
- );
- const niceToHaveFeatures = data.solution_and_features.nice_to_have_features.map(
- (feature) => feature.name || feature.description,
- );
-
- return {
- projectId,
- workingTitle: data.project_summary.working_title ?? null,
- oneLiner: data.project_summary.one_liner ?? null,
- problem: data.product_vision.problem_statement.description ?? null,
- targetUser: data.target_users.primary_segment.description ?? null,
- desiredOutcome: data.product_vision.target_outcome.description ?? null,
- coreSolution: data.solution_and_features.core_solution.description ?? null,
- coreFeatures: uniqueStrings(coreFeatures),
- niceToHaveFeatures: uniqueStrings(niceToHaveFeatures),
- marketCategory: data.market_and_competition.market_category.description ?? null,
- competitors: uniqueStrings(
- data.market_and_competition.competitors.map((competitor) => competitor.name),
- ),
- techStack: uniqueStrings(
- data.tech_and_constraints.stack_mentions.map((item) => item.description),
- ),
- constraints: uniqueStrings(
- data.tech_and_constraints.constraints.map((constraint) => constraint.description),
- ),
- currentStage: toStage(data.project_summary.stage),
- shortTermGoals: uniqueStrings(
- data.goals_and_success.short_term_goals.map((goal) => goal.description),
- ),
- longTermGoals: uniqueStrings(
- data.goals_and_success.long_term_goals.map((goal) => goal.description),
- ),
- overallCompletion: clamp(completionAvg),
- overallConfidence: clamp(confidenceAvg),
- };
-}
-
-
diff --git a/lib/server/vector-memory.ts b/lib/server/vector-memory.ts
deleted file mode 100644
index ecd619c6..00000000
--- a/lib/server/vector-memory.ts
+++ /dev/null
@@ -1,453 +0,0 @@
-/**
- * Server-side helpers for AlloyDB vector memory operations
- *
- * Handles CRUD operations on knowledge_chunks and semantic search.
- */
-
-import { getAlloyDbClient, executeQuery, getPooledClient } from '@/lib/db/alloydb';
-import type {
- KnowledgeChunk,
- KnowledgeChunkRow,
- KnowledgeChunkSearchResult,
- VectorSearchOptions,
- CreateKnowledgeChunkInput,
- BatchCreateKnowledgeChunksInput,
-} from '@/lib/types/vector-memory';
-
-/**
- * Convert database row (snake_case) to TypeScript object (camelCase)
- */
-function rowToKnowledgeChunk(row: KnowledgeChunkRow): KnowledgeChunk {
- return {
- id: row.id,
- projectId: row.project_id,
- knowledgeItemId: row.knowledge_item_id,
- chunkIndex: row.chunk_index,
- content: row.content,
- sourceType: row.source_type,
- importance: row.importance,
- createdAt: row.created_at,
- updatedAt: row.updated_at,
- };
-}
-
-/**
- * Retrieve relevant knowledge chunks using vector similarity search
- *
- * @param projectId - Firestore project ID to filter by
- * @param queryEmbedding - Vector embedding of the query (e.g., user's question)
- * @param options - Search options (limit, filters, etc.)
- * @returns Array of chunks ordered by similarity (most relevant first)
- *
- * @example
- * ```typescript
- * const embedding = await embedText("What's the MVP scope?");
- * const chunks = await retrieveRelevantChunks('proj123', embedding, { limit: 10, minSimilarity: 0.7 });
- * ```
- */
-export async function retrieveRelevantChunks(
- projectId: string,
- queryEmbedding: number[],
- options: VectorSearchOptions = {}
-): Promise {
- const {
- limit = 10,
- minSimilarity,
- sourceTypes,
- importanceLevels,
- } = options;
-
- try {
- // Build the query with optional filters
- let queryText = `
- SELECT
- id,
- project_id,
- knowledge_item_id,
- chunk_index,
- content,
- source_type,
- importance,
- created_at,
- updated_at,
- 1 - (embedding <=> $1::vector) AS similarity
- FROM knowledge_chunks
- WHERE project_id = $2
- `;
-
- const params: any[] = [JSON.stringify(queryEmbedding), projectId];
- let paramIndex = 3;
-
- // Filter by source types
- if (sourceTypes && sourceTypes.length > 0) {
- queryText += ` AND source_type = ANY($${paramIndex})`;
- params.push(sourceTypes);
- paramIndex++;
- }
-
- // Filter by importance levels
- if (importanceLevels && importanceLevels.length > 0) {
- queryText += ` AND importance = ANY($${paramIndex})`;
- params.push(importanceLevels);
- paramIndex++;
- }
-
- // Filter by minimum similarity
- if (minSimilarity !== undefined) {
- queryText += ` AND (1 - (embedding <=> $1::vector)) >= $${paramIndex}`;
- params.push(minSimilarity);
- paramIndex++;
- }
-
- // Order by similarity and limit
- queryText += ` ORDER BY embedding <=> $1::vector LIMIT $${paramIndex}`;
- params.push(limit);
-
- const result = await executeQuery(
- queryText,
- params
- );
-
- return result.rows.map((row) => ({
- ...rowToKnowledgeChunk(row),
- similarity: row.similarity,
- }));
- } catch (error) {
- console.error('[Vector Memory] Failed to retrieve relevant chunks:', error);
- throw new Error(
- `Failed to retrieve chunks: ${error instanceof Error ? error.message : String(error)}`
- );
- }
-}
-
-/**
- * Create a single knowledge chunk
- */
-export async function createKnowledgeChunk(
- input: CreateKnowledgeChunkInput
-): Promise {
- const {
- projectId,
- knowledgeItemId,
- chunkIndex,
- content,
- embedding,
- sourceType = null,
- importance = null,
- } = input;
-
- try {
- const queryText = `
- INSERT INTO knowledge_chunks (
- project_id,
- knowledge_item_id,
- chunk_index,
- content,
- embedding,
- source_type,
- importance
- )
- VALUES ($1, $2, $3, $4, $5::vector, $6, $7)
- RETURNING
- id,
- project_id,
- knowledge_item_id,
- chunk_index,
- content,
- source_type,
- importance,
- created_at,
- updated_at
- `;
-
- const result = await executeQuery(queryText, [
- projectId,
- knowledgeItemId,
- chunkIndex,
- content,
- JSON.stringify(embedding),
- sourceType,
- importance,
- ]);
-
- if (result.rows.length === 0) {
- throw new Error('Failed to insert knowledge chunk');
- }
-
- return rowToKnowledgeChunk(result.rows[0]);
- } catch (error) {
- console.error('[Vector Memory] Failed to create knowledge chunk:', error);
- throw new Error(
- `Failed to create chunk: ${error instanceof Error ? error.message : String(error)}`
- );
- }
-}
-
-/**
- * Batch create multiple knowledge chunks efficiently
- *
- * Uses a transaction to ensure atomicity.
- */
-export async function batchCreateKnowledgeChunks(
- input: BatchCreateKnowledgeChunksInput
-): Promise {
- const { projectId, knowledgeItemId, chunks } = input;
-
- if (chunks.length === 0) {
- return [];
- }
-
- const client = await getPooledClient();
-
- try {
- await client.query('BEGIN');
-
- const createdChunks: KnowledgeChunk[] = [];
-
- for (const chunk of chunks) {
- const queryText = `
- INSERT INTO knowledge_chunks (
- project_id,
- knowledge_item_id,
- chunk_index,
- content,
- embedding,
- source_type,
- importance
- )
- VALUES ($1, $2, $3, $4, $5::vector, $6, $7)
- RETURNING
- id,
- project_id,
- knowledge_item_id,
- chunk_index,
- content,
- source_type,
- importance,
- created_at,
- updated_at
- `;
-
- const result = await client.query(queryText, [
- projectId,
- knowledgeItemId,
- chunk.chunkIndex,
- chunk.content,
- JSON.stringify(chunk.embedding),
- chunk.sourceType ?? null,
- chunk.importance ?? null,
- ]);
-
- if (result.rows.length > 0) {
- createdChunks.push(rowToKnowledgeChunk(result.rows[0]));
- }
- }
-
- await client.query('COMMIT');
-
- console.log(
- `[Vector Memory] Batch created ${createdChunks.length} chunks for knowledge_item ${knowledgeItemId}`
- );
-
- return createdChunks;
- } catch (error) {
- await client.query('ROLLBACK');
- console.error('[Vector Memory] Failed to batch create chunks:', error);
- throw new Error(
- `Failed to batch create chunks: ${error instanceof Error ? error.message : String(error)}`
- );
- } finally {
- client.release();
- }
-}
-
-/**
- * Delete all chunks for a specific knowledge_item
- *
- * Used when regenerating chunks or removing a knowledge_item.
- */
-export async function deleteChunksForKnowledgeItem(
- knowledgeItemId: string
-): Promise {
- try {
- const queryText = `
- DELETE FROM knowledge_chunks
- WHERE knowledge_item_id = $1
- RETURNING id
- `;
-
- const result = await executeQuery(queryText, [knowledgeItemId]);
-
- console.log(
- `[Vector Memory] Deleted ${result.rowCount ?? 0} chunks for knowledge_item ${knowledgeItemId}`
- );
-
- return result.rowCount ?? 0;
- } catch (error) {
- console.error('[Vector Memory] Failed to delete chunks:', error);
- throw new Error(
- `Failed to delete chunks: ${error instanceof Error ? error.message : String(error)}`
- );
- }
-}
-
-/**
- * Delete all chunks for a specific project
- *
- * Used when cleaning up or resetting a project.
- */
-export async function deleteChunksForProject(projectId: string): Promise {
- try {
- const queryText = `
- DELETE FROM knowledge_chunks
- WHERE project_id = $1
- RETURNING id
- `;
-
- const result = await executeQuery(queryText, [projectId]);
-
- console.log(
- `[Vector Memory] Deleted ${result.rowCount ?? 0} chunks for project ${projectId}`
- );
-
- return result.rowCount ?? 0;
- } catch (error) {
- console.error('[Vector Memory] Failed to delete project chunks:', error);
- throw new Error(
- `Failed to delete project chunks: ${error instanceof Error ? error.message : String(error)}`
- );
- }
-}
-
-/**
- * Get chunk count for a knowledge_item
- */
-export async function getChunkCountForKnowledgeItem(
- knowledgeItemId: string
-): Promise {
- try {
- const result = await executeQuery<{ count: string }>(
- 'SELECT COUNT(*) as count FROM knowledge_chunks WHERE knowledge_item_id = $1',
- [knowledgeItemId]
- );
-
- return parseInt(result.rows[0]?.count ?? '0', 10);
- } catch (error) {
- console.error('[Vector Memory] Failed to get chunk count:', error);
- return 0;
- }
-}
-
-/**
- * Get chunk count for a project
- */
-export async function getChunkCountForProject(projectId: string): Promise {
- try {
- const result = await executeQuery<{ count: string }>(
- 'SELECT COUNT(*) as count FROM knowledge_chunks WHERE project_id = $1',
- [projectId]
- );
-
- return parseInt(result.rows[0]?.count ?? '0', 10);
- } catch (error) {
- console.error('[Vector Memory] Failed to get project chunk count:', error);
- return 0;
- }
-}
-
-/**
- * Regenerate knowledge_chunks for a single knowledge_item
- *
- * This is the main pipeline that:
- * 1. Chunks the knowledge_item.content
- * 2. Generates embeddings for each chunk
- * 3. Deletes existing chunks for this item
- * 4. Inserts new chunks into AlloyDB
- *
- * @param knowledgeItem - The knowledge item to process
- *
- * @example
- * ```typescript
- * const knowledgeItem = await getKnowledgeItem(projectId, itemId);
- * await writeKnowledgeChunksForItem(knowledgeItem);
- * ```
- */
-export async function writeKnowledgeChunksForItem(
- knowledgeItem: {
- id: string;
- projectId: string;
- content: string;
- sourceMeta?: { sourceType?: string; importance?: 'primary' | 'supporting' | 'irrelevant' };
- }
-): Promise {
- const { chunkText } = await import('@/lib/ai/chunking');
- const { embedTextBatch } = await import('@/lib/ai/embeddings');
-
- try {
- console.log(
- `[Vector Memory] Starting chunking pipeline for knowledge_item ${knowledgeItem.id}`
- );
-
- // Step 1: Chunk the content
- const textChunks = chunkText(knowledgeItem.content, {
- maxTokens: 800,
- overlapChars: 200,
- preserveParagraphs: true,
- });
-
- if (textChunks.length === 0) {
- console.warn(
- `[Vector Memory] No chunks generated for knowledge_item ${knowledgeItem.id} - content may be empty`
- );
- return;
- }
-
- console.log(
- `[Vector Memory] Generated ${textChunks.length} chunks for knowledge_item ${knowledgeItem.id}`
- );
-
- // Step 2: Generate embeddings for all chunks
- const chunkTexts = textChunks.map((chunk) => chunk.text);
- const embeddings = await embedTextBatch(chunkTexts, {
- delayMs: 50, // Small delay to avoid rate limiting
- skipEmpty: true,
- });
-
- if (embeddings.length !== textChunks.length) {
- throw new Error(
- `Embedding count mismatch: got ${embeddings.length}, expected ${textChunks.length}`
- );
- }
-
- // Step 3: Delete existing chunks for this knowledge_item
- await deleteChunksForKnowledgeItem(knowledgeItem.id);
-
- // Step 4: Insert new chunks
- const chunksToInsert = textChunks.map((chunk, index) => ({
- chunkIndex: chunk.index,
- content: chunk.text,
- embedding: embeddings[index],
- sourceType: knowledgeItem.sourceMeta?.sourceType ?? null,
- importance: knowledgeItem.sourceMeta?.importance ?? null,
- }));
-
- await batchCreateKnowledgeChunks({
- projectId: knowledgeItem.projectId,
- knowledgeItemId: knowledgeItem.id,
- chunks: chunksToInsert,
- });
-
- console.log(
- `[Vector Memory] Successfully processed ${chunksToInsert.length} chunks for knowledge_item ${knowledgeItem.id}`
- );
- } catch (error) {
- console.error(
- `[Vector Memory] Failed to write chunks for knowledge_item ${knowledgeItem.id}:`,
- error
- );
- throw new Error(
- `Failed to write chunks: ${error instanceof Error ? error.message : String(error)}`
- );
- }
-}
-
diff --git a/lib/types/chat-extraction.ts b/lib/types/chat-extraction.ts
deleted file mode 100644
index da4993dc..00000000
--- a/lib/types/chat-extraction.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import type { ChatExtractionData } from '@/lib/ai/chat-extraction-types';
-
-export interface ChatExtractionRecord {
- id: string;
- projectId: string;
- knowledgeItemId: string;
- data: TData;
- overallCompletion: number;
- overallConfidence: number;
- createdAt: FirebaseFirestore.Timestamp;
- updatedAt: FirebaseFirestore.Timestamp;
-}
-
-
diff --git a/lib/types/mvp-plan.ts b/lib/types/mvp-plan.ts
deleted file mode 100644
index b7722a64..00000000
--- a/lib/types/mvp-plan.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-/**
- * Type definitions for AI-generated MVP Plan
- * Based on the Vibn MVP Planner agent spec
- */
-
-// Project-level
-export type Project = {
- id: string;
- ownerUserId: string;
- name: string;
- createdAt: string;
- updatedAt: string;
- summary: string; // final summary text from planner
-};
-
-// Raw vision answers
-export type VisionInput = {
- projectId: string;
- q1_who_and_problem: string;
- q2_story: string;
- q3_improvement: string;
- createdAt: string;
-};
-
-// Optional work-to-date summary
-export type WorkToDate = {
- projectId: string;
- codeSummary?: string;
- githubSummary?: string;
- docsLinksOrText?: string[]; // or JSON
- existingAssetsNotes?: string;
- createdAt: string;
-};
-
-// Trees (Journey / Touchpoints / System)
-export type TreeType = "journey" | "touchpoints" | "system";
-
-export type Tree = {
- id: string;
- projectId: string;
- type: TreeType;
- label: string;
- createdAt: string;
- updatedAt: string;
-};
-
-// Asset types from agent spec
-export type AssetType =
- | "web_page"
- | "app_screen"
- | "flow"
- | "component"
- | "email"
- | "notification"
- | "document"
- | "social_post"
- | "data_model"
- | "api_endpoint"
- | "service"
- | "integration"
- | "job"
- | "infrastructure"
- | "automation"
- | "other";
-
-// Each node in any tree
-export type AssetNode = {
- id: string;
- projectId: string;
- treeId: string;
- parentId: string | null;
- name: string;
- assetType: AssetType;
- mustHaveForV1: boolean;
- createdAt: string;
- updatedAt: string;
- children?: AssetNode[]; // For nested structure
-};
-
-// Node metadata (reasoning, mapping back to vision)
-export type AssetMetadata = {
- assetNodeId: string;
- whyItExists: string;
- whichUserItServes?: string;
- problemItHelpsWith?: string;
- connectionToMagicMoment: string;
- valueContribution?: string;
- journeyStage?: string;
- messagingTone?: string;
- visualStyleNotes?: string;
- dependencies?: string[]; // other AssetNode IDs
- implementationNotes?: string;
-};
-
-// Context memory events
-export type ContextEvent = {
- id: string;
- projectId: string;
- sourceType: "chat" | "commit" | "file" | "doc" | "manual_note";
- sourceId?: string; // e.g. commit hash, file path
- timestamp: string;
- title: string;
- body: string; // raw text or JSON
-};
-
-// Full AI response from agent
-export type AIAgentResponse = {
- journey_tree: {
- label: string;
- nodes: Array;
- };
- touchpoints_tree: {
- label: string;
- nodes: Array;
- };
- system_tree: {
- label: string;
- nodes: Array;
- };
- summary: string;
-};
-
diff --git a/lib/types/product-model.ts b/lib/types/product-model.ts
deleted file mode 100644
index f36d3377..00000000
--- a/lib/types/product-model.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-export type ProjectStage =
- | 'idea'
- | 'prototype'
- | 'mvp_in_progress'
- | 'live_beta'
- | 'live_paid'
- | 'unknown';
-
-export interface CanonicalProductModel {
- projectId: string;
- workingTitle: string | null;
- oneLiner: string | null;
-
- problem: string | null;
- targetUser: string | null;
- desiredOutcome: string | null;
- coreSolution: string | null;
-
- coreFeatures: string[];
- niceToHaveFeatures: string[];
-
- marketCategory: string | null;
- competitors: string[];
-
- techStack: string[];
- constraints: string[];
-
- currentStage: ProjectStage;
-
- shortTermGoals: string[];
- longTermGoals: string[];
-
- overallCompletion: number;
- overallConfidence: number;
-}
-
-
diff --git a/lib/utils/code-chunker.ts b/lib/utils/code-chunker.ts
deleted file mode 100644
index b42c536d..00000000
--- a/lib/utils/code-chunker.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-/**
- * Code-specific chunking for source code files
- * Intelligently splits code while preserving context
- */
-
-export interface CodeChunk {
- content: string;
- metadata: {
- chunkIndex: number;
- totalChunks: number;
- startLine: number;
- endLine: number;
- tokenCount: number;
- filePath: string;
- language?: string;
- };
-}
-
-export interface CodeChunkOptions {
- maxChunkSize?: number; // characters
- chunkOverlap?: number; // lines
- preserveFunctions?: boolean;
- preserveClasses?: boolean;
- filePath: string;
-}
-
-/**
- * Estimate token count (rough approximation: 1 token ≈ 4 characters)
- */
-function estimateTokens(text: string): number {
- return Math.ceil(text.length / 4);
-}
-
-/**
- * Detect language from file path
- */
-function detectLanguage(filePath: string): string | undefined {
- const ext = filePath.split('.').pop()?.toLowerCase();
- const langMap: Record = {
- ts: 'typescript',
- tsx: 'typescript',
- js: 'javascript',
- jsx: 'javascript',
- py: 'python',
- java: 'java',
- go: 'go',
- rs: 'rust',
- cpp: 'cpp',
- c: 'c',
- cs: 'csharp',
- rb: 'ruby',
- php: 'php',
- swift: 'swift',
- kt: 'kotlin',
- sql: 'sql',
- css: 'css',
- scss: 'scss',
- html: 'html',
- json: 'json',
- yaml: 'yaml',
- yml: 'yaml',
- md: 'markdown',
- };
- return langMap[ext || ''];
-}
-
-/**
- * Chunk source code file intelligently
- */
-export function chunkCode(
- content: string,
- options: CodeChunkOptions
-): CodeChunk[] {
- const {
- maxChunkSize = 3000, // Larger chunks for code context
- chunkOverlap = 5,
- preserveFunctions = true,
- preserveClasses = true,
- filePath,
- } = options;
-
- const language = detectLanguage(filePath);
- const lines = content.split('\n');
-
- // For small files, return as single chunk
- if (content.length <= maxChunkSize) {
- return [
- {
- content,
- metadata: {
- chunkIndex: 0,
- totalChunks: 1,
- startLine: 1,
- endLine: lines.length,
- tokenCount: estimateTokens(content),
- filePath,
- language,
- },
- },
- ];
- }
-
- // For larger files, split by logical boundaries
- const chunks: CodeChunk[] = [];
- let currentChunk: string[] = [];
- let currentSize = 0;
- let chunkStartLine = 1;
-
- // Patterns for detecting logical boundaries
- const functionPattern = /^\s*(function|def|fn|func|fun|public|private|protected|static|async|export)\s/;
- const classPattern = /^\s*(class|interface|struct|enum|type)\s/;
- const importPattern = /^\s*(import|from|require|using|include)\s/;
- const commentPattern = /^\s*(\/\/|\/\*|\*|#|--|