Commit Graph

23 Commits

Author SHA1 Message Date
70d2176cb4 feat(quotas): per-workspace soft caps + AI recovery rule
Soft caps on the two resources a bad-actor signup could pump fastest:
  - 3 active projects per workspace
  - 3 active (running/provisioning) dev containers per workspace

Suspended dev containers don't count (they're free), so a power
user can have many projects with most containers idle. Limits are
overridable via env vars (VIBN_QUOTA_MAX_*) for a global lift.

Hits surface as HTTP 402 with structured payload {error, code,
current, limit}. AI's error-recovery middleware matches the
QUOTA_EXCEEDED code and synthesizes guidance: tell the user which
cap was hit, offer to suspend something or contact support, do NOT
retry blindly.

Wired:
  - lib/quotas.ts                        — assertProjectQuota,
                                            assertDevContainerQuota,
                                            getQuotaStatus
  - app/api/projects/create/route.ts     — checks before create
  - lib/dev-container.ts                 — checks before resume +
                                            net-new ensure
  - app/api/mcp/route.ts                 — devcontainer.ensure
                                            translates QuotaExceededError
                                            to 402
  - lib/ai/error-recovery.ts             — workspace-quota-exceeded rule

Closes BETA_LAUNCH_PLAN.md task 4.6.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 12:54:51 -07:00
9ddbe5b7d8 feat(sentry-as-product): auto-provision per-project + AI feedback loop
Implements all 4 stages from SENTRY_AS_PRODUCT.md:

Stage 1 — Auto-provision per-project Sentry:
- New module lib/integrations/sentry.ts with idempotent
  ensureSentryProject(): creates Sentry project under shared
  vibnai org, fetches DSN, persists to fs_projects.data.sentry.
- Wired into POST /api/projects/create (provision early so DSN is
  ready before first deploy) and into applyEnvsAndDeploy in MCP
  (lazy retry + env var injection on every apps.create).
- applySentryEnvToCoolifyApp upserts NEXT_PUBLIC_SENTRY_DSN +
  SENTRY_AUTH_TOKEN onto the Coolify app, so the very first build
  inlines the DSN into the client bundle and uploads source maps.

Stage 2 — Bake into scaffolds:
- New module lib/scaffold/sentry-snippets.ts exposes canonical
  Next.js + Vite+React snippets the AI copies verbatim (keeps
  outputs deterministic across chats).
- AI system prompt updated: explicit instructions to wire Sentry
  on every new app, env vars are guaranteed available, project
  Sentry slug comes from projects_get.
- projects.get MCP response now includes `sentry: {slug, dsn,
  provisionedAt}` so the AI can substitute the slug into
  withSentryConfig({ project: <slug> }).

Stage 3 — Expose error feed to the AI:
- Three new MCP tools registered:
    project_recent_errors  — list unresolved issues
    project_error_detail   — stack trace + breadcrumbs + replay url
    project_error_resolve  — mark resolved after a verified fix
- Tenant-safe: each tool re-checks projectId belongs to caller's
  workspace before talking to Sentry.

Stage 4 — Auto-surface at chat-turn start:
- chat/route.ts pulls listRecentSentryIssues for the active
  project (last 6h, count ≥ 2 to skip noise) and appends a
  [PROJECT HEALTH] block to the system prompt. AI decides
  whether to surface a one-liner; if user's message is about a
  broken thing, AI prefers Sentry stack trace over guessing.

End state: a Vibn user's deployed app crashes for a real user →
Sentry captures with source-mapped stack trace + Session Replay →
next AI chat turn the AI knows about it and can offer a fix
without the user pasting the error.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 12:52:17 -07:00
c105b42d0c feat(ai): tool-error recovery middleware
Pattern-matches known-recoverable MCP tool failures and injects a
synthetic imperative message into the conversation right after the
failing tool result. Static prompt rules lose to accumulated tool
reality (we've shipped 4 orphan twenty-* services because the model
ignored the "no delete-and-recreate" rule); a fresh role:'user'
message at decision time does not.

Initial rules cover the three highest-confidence Docker failure
patterns: orphan container conflict (use apps_unstick), image pull
denied (use apps_repair), port already allocated (identify holder).
Each rule names the wrong-but-tempting move explicitly.

See AI_HARNESS_GAPS.md §1 for the failure case this addresses.
2026-05-01 11:08:48 -07:00
60a04e48c1 feat(plan): Objective/Sessions/Tasks tab with markdown + AI scribe
- Objective: full markdown document editor with Write/Preview tabs
- Sessions: project-scoped chat threads with AI-generated summaries
- Tasks: master-detail view with markdown spec, status pills, agent
  delegation placeholder
- Chat threads now scoped per-project and auto-summarised after each
  assistant turn (powers Sessions list)
- AI MCP scribe tools: plan_get / plan_vision_set / plan_idea_add /
  plan_task_add (title + markdown desc) / plan_task_complete /
  plan_decision_log
- Chat panel clears stale project threads when navigating to workspace

Made-with: Cursor
2026-04-30 13:44:50 -07:00
bd993123c0 fix: rolling deploys + service custom-domain support
Two product gaps surfaced from the twenty-live debugging session:

1. Vibn frontend now has a healthcheck on / port 3000. Coolify will
   wait for the new container to be healthy before swapping traffic,
   so deploys no longer drop in-flight chat SSE streams. (Setting was
   applied via Coolify API; commit just documents.)

2. apps_domains_set now handles SERVICES (template-based apps like
   Twenty CRM, n8n) — not just applications. Setting service_apps.fqdn
   in the DB alone gets reverted by Coolify's deploy pipeline, so we
   replicate the Livewire EditDomain.php save flow via tinker over SSH:
   write fqdn → save → updateCompose() → service.parse(). After this
   apps_deploy regenerates Traefik labels with the custom domain.
   Auto-detects service vs application by uuid lookup. New { port }
   parameter lets the AI pin the upstream port for services that
   require one (Coolify hard-fails the save without it).

Tool description rewritten with the new behavior + a worked example
so the AI uses the right pipeline first try.

Made-with: Cursor
2026-04-30 11:28:25 -07:00
3d525afdf7 fix(ai): stop the AI from forking duplicate services to escape errors
Three changes that compound to fix the "4 orphan twenty-* services"
problem we just hit:

1. apps_create is now idempotent within a project. If a service from
   the same template already exists in the same Vibn projectId, return
   it with alreadyExisted: true instead of creating a clone. Pass
   { force: true } to opt out for legitimate dev/staging duplicates.

2. New apps_unstick tool. SSH-cleans orphan Docker containers
   matching the resource UUID so a deploy that hit "Conflict.
   The container name X is already in use" can recover without
   deleting the entire service.

3. System prompt hardened with two new hard rules:
   - ALWAYS apps_list before apps_create (idempotency in spirit, not
     just at the API boundary)
   - NEVER delete-and-recreate a service to escape an error. The
     recovery for container conflicts is apps_unstick + apps_deploy.

Already cleaned the 3 duplicate twenty-* services from prod
(kept twenty-live, freshest healthy). Frees ~9 GB RAM on the host.

Made-with: Cursor
2026-04-29 20:27:52 -07:00
14d0b04112 feat(ai): scribe tools — let AI write to the Plan tab
Adds MCP tools so the AI can capture decisions, tasks, ideas, and the
vision in the moment instead of just reading them:

- plan_get             read full plan for context
- plan_vision_set      update vision when user refines their pitch
- plan_decision_log    log a decision PROACTIVELY when one gets settled
                        (no permission ask) so the next session doesn't
                        re-litigate it
- plan_task_add        track multi-step work or user-side follow-ups
- plan_task_complete   mark done as we go
- plan_idea_add        park stray ideas

System prompt is updated with a "be the user's scribe" section that
instructs the model to use these proactively with brief acks instead
of long confirmations.

Also reorders the Plan tab UI to: Vision · Tasks · Decisions · Ideas
(Ideas moved to bottom — it's the lowest-signal pile).

Made-with: Cursor
2026-04-29 20:17:43 -07:00
4184baca77 feat(chat): expose Gemini's reasoning narration as a thinking pill
Today the chat shows ✓-icon tool trays with no narration between
calls — the user has no idea WHY the AI just called fs_edit or
ship. Meanwhile Gemini is producing 500-1000 chars of first-person
reasoning per round ("Updating the Express Server: A Quick
Production Deployment / Right, so we have a basic Express server
here, nothing fancy. I need to get a new version live...") and
billing us for those tokens — we just weren't asking for them.

Three layers:

1. lib/ai/gemini-chat.ts
   - generationConfig.thinkingConfig.includeThoughts = true (default
     true, opt-out via includeThoughts: false). We're already paying
     for thinking tokens regardless of this flag — it just controls
     whether the model returns the human-readable summary or only the
     compressed signature.
   - callGeminiChat now returns { text, thoughts, toolCalls,
     finishReason } and the parser splits parts by `part.thought`.
     CRITICAL bug avoided: previously `if (part.text) text += ...`
     would have lumped thoughts into the chat bubble verbatim.
   - streamGeminiChat yields `{ type: 'thinking' }` for thought parts.

2. app/api/chat/route.ts
   - New SSE event: `data: {"type":"thinking","text":"..."}`
   - Emitted on every round alongside text + tool_start.
   - Recovery-summary branch also emits thoughts so even when the
     model produces no user-facing prose, the user sees the model's
     reasoning instead of dead silence.

3. components/vibn-chat/chat-panel.tsx
   - Message gains optional `thoughts` field (in-memory only — we do
     NOT persist thoughts to fs_chat_messages; they're ephemeral and
     cheap to drop).
   - New ThinkingBubble component: dashed-border italic pill above
     the assistant bubble, collapsed by default to show one-line
     preview, click to expand for full chain. Strips Gemini's
     "**Section Heading**" prefixes from the preview.
   - SSE handler accumulates thinking chunks onto the in-flight
     assistant message.

UX impact: instead of staring at fs.read ✓ fs.edit ✓ ship ✓ icons,
the user sees "Examining the target server file..." → "Shipping the
twenty-crm project..." in real time. Costs zero additional tokens
(we already paid for the thoughts).

Cleanup: removed scripts/probe-gemini-raw.ts and
scripts/probe-recovery-summary.ts — diagnostic scripts that
identified this opportunity, no longer needed in-tree.

Made-with: Cursor
2026-04-28 15:24:49 -07:00
a897d07179 fix(ship): return commitSha + coolifyDeployUrl, prevent verification chain
After "ship" succeeded the AI was burning 7+ follow-up tool calls
(gitea_repos_list, gitea_credentials, shell.exec×4, apps_list) trying
to verify what actually got pushed and where it deployed. That ate
through MAX_TOOL_ROUNDS and the user got tool-icon spam with no
narrative summary.

Three fixes:

1. ship now returns commitSha (parsed from `git rev-parse HEAD`),
   giteaCommitUrl, giteaBranchUrl, coolifyDeployUrl, coolifyAppUuid,
   and a summaryHint string telling the AI exactly what to say next.
2. ship's tool description now explicitly tells Gemini "do NOT call
   gitea_*, shell_exec, or apps_* afterwards to verify — the result
   is authoritative."
3. MAX_TOOL_ROUNDS 12 → 18 as a safety net for genuinely long chains.

Net effect: ship goes from ~12 tool calls to verify a deploy down to
1 (just ship itself), and the next text turn has the SHA + URL
inline.

Made-with: Cursor
2026-04-28 14:46:18 -07:00
e0844b5f2e feat(path-b): preview-port slots, port-collision, gitea_file_* deprecation
Five focused improvements rolled into one deploy:

1. Pre-allocated preview ports + Traefik labels.
   Bake docker labels for ports 3000-3009 into every dev-container
   compose at ensureDevContainer() time. Each port has its own
   subdomain: preview-<slot>-<projectSlug>-<token>.preview.vibnai.com.
   Token is derived from projectId so URLs are stable across restarts
   but not enumerable across projects. Joins the coolify external
   network so Traefik can reach the container.

   This avoids the runtime compose-mutation approach (which would
   have required a Coolify redeploy on every dev_server.start, ~30s
   latency). The trade-off is a hard cap of 10 concurrent dev servers
   per project — fine for the "frontend + API" scenario, the only one
   we can practically envision.

   Wildcard DNS + Traefik DNS-01 cert remain a manual one-time setup
   (see vibn-dev/PREVIEWS.md).

2. dev_server.start: port-collision handling.
   Detect listeners via `ss` + `lsof` before launching. Three outcomes:
   - port out of slot range → PortOutOfRangeError → 400 with allowedRange
   - port owned by a different process → PortBusyError → 409
   - port owned by a tracked vibn dev server (same project) → kill
     the stale row and reuse the slot (most-recent-write-wins; matches
     AI mental model when it does an edit-restart loop)
   Surfaced via dedicated MCP error codes so the AI can recover
   intelligently instead of looping the same start call.

3. gitea_file_{read,write,delete}: hard-removed from AI tool list.
   These tools competed with fs.* and tempted the AI into the slow
   path. Pulled from VIBN_TOOL_DEFINITIONS but kept in the MCP
   dispatcher for 30 days for any external clients still using them.
   System prompt rewritten to make Path B the only documented way to
   author code; gitea_repo_* + gitea_branches_* remain because they
   handle one-time orchestration with no fs.* equivalent.

4. System prompt: HMR + preview-port discipline.
   New section covering Vite HMR (clientPort:443 wss), Next dev
   (-H 0.0.0.0), and Express (HOST=0.0.0.0). Explicit "ports must be
   3000-3009" rule + "if PORT_BUSY don't blindly retry" guidance.

5. Cron docs (vibn-dev/CRON.md).
   /etc/cron.d/vibn-path-b template + smoke commands for autosave
   and idle-sweep. Wires both 5-minute jobs that already have admin
   endpoints (POST /api/admin/path-b/{autosave,idle-sweep}).

MCP version bump 2.6.0 -> 2.7.0. Smoke test: 65 tool defs (down from
68 after gitea_file_* removal), all accepted by Gemini.

Made-with: Cursor
2026-04-28 14:39:59 -07:00
41d4d3748f feat(path-b): dev_server.*, ship, autosave, idle-suspend (weeks 2-3)
Completes the rest of the Path B tool surface:

- dev_server.{start,stop,list,logs}: nohup processes inside the dev
  container, track PID/port/preview-url in fs_dev_servers. Each gets
  a randomized preview subdomain (preview.vibnai.com base; Traefik
  wildcard wiring is staged in /vibn-dev/PREVIEWS.md but the Coolify
  compose hot-update step is deferred — see file for the recommended
  pre-allocated-port-range approach).

- ship: git init (if needed) -> add/commit/push to the project's
  Gitea repo via the workspace bot PAT, then triggers a Coolify
  production deploy if the project is linked to one. Returns push
  output + deployment_uuid.

- /api/admin/path-b/autosave [POST { projectId | sweep:true }]:
  force-pushes /workspace to vibn-autosave/main in Gitea. Throttled
  to once per 5 min per project. Records every push in fs_dev_autosaves
  for audit. Treat Gitea as canonical, container disk as ephemeral.

- /api/admin/path-b/idle-sweep [POST?minutes=30]: suspends every
  running dev container whose last_active_at is older than `minutes`.
  Wire to a 5-min cron. Idempotent.

- Compose template hardened: pull_policy: never (use locally-built
  image, no registry round-trip) + per-project bridge network
  (vibn-dev-net-<slug>) so dev containers can't reach internal Vibn
  services.

- vibn-dev/setup-on-coolify.sh: one-shot script to build vibn-dev:latest
  on the Coolify host. Run before first chat session uses Path B.

- vibn-tools.ts: dev_server_{start,stop,list,logs} + ship Gemini tool
  defs added. Smoke test passes — 68 tool definitions accepted.

- MCP version 2.5.0 -> 2.6.0 so /api/mcp tells us when the new build
  is live.

Plan doc updated to reflect what shipped vs what's still manual
(DNS wildcard, Traefik cert, build-on-host script run, gitea_file_*
hard-remove deferred to allow A/B).

Made-with: Cursor
2026-04-28 13:02:35 -07:00
4ba9407534 feat(path-b): persistent dev containers + shell.exec + fs.* tools
Kicks off Path B (AI_PATH_B_EXECUTION_PLAN.md): each Vibn project gets
its own vibn-dev Coolify service that the AI drives directly via shell
and filesystem tools. Sub-second iteration vs the 5-min Gitea redeploy
loop.

What's in this commit (week 1, slice 1):

- vibn-dev Dockerfile: small Ubuntu base (~500 MB target). git, ripgrep,
  python3, mise. Language toolchains lazy-install on first use.
- lib/dev-container.ts: ensureDevContainer / suspend / resume /
  execInDevContainer. Backed by a new fs_project_dev_containers table.
- lib/feature-flags.ts + /api/admin/path-b/{disable,enable}: kill switch.
  Bearer NEXTAUTH_SECRET flips path_b_disabled, propagates in ~10s.
- New MCP tools wired into /api/mcp: devcontainer.{ensure,status,suspend},
  shell.exec, fs.{read,write,edit,list,delete,glob,grep}. All enforce
  workspace isolation via fs_projects ownership check.
- vibn-tools.ts: 11 new Gemini tool defs (smoke test passes, 63 total).
- chat system prompt: shell-first guidance; gitea_file_* marked
  deprecated for iterative work (still available, removed week 3).

Safety nets baked in:
- pathBGuard() returns 503 from every Path B tool when the kill switch
  flips
- fs.* paths locked to /workspace
- ensureResourceInWorkspaceProjects via fs_project_dev_containers PK
- per-project resource limits (1 vCPU, 1 GiB RAM) on the compose spec

Still pending (queued):
- dev_server.* (preview URLs through Traefik)
- ship tool (push to Gitea + trigger prod deploy)
- auto-push autosave to vibn-autosave/main every 5 min
- idle-suspend cron after 30 min inactivity
- HMR-through-Traefik spike
- eval harness

Made-with: Cursor
2026-04-28 12:53:16 -07:00
c8dec7c656 feat(mcp): add gitea_* tools so the AI can write code, not just deploy it
Closes the AI's self-reported gap: "I cannot directly commit or push code".

New MCP capabilities (8) — all scoped to the workspace's Gitea org via
requireGiteaOrg + ensureRepoOwnerInOrg:

- gitea.repos.list           — discover existing repos
- gitea.repo.get             — metadata (default branch, clone URL)
- gitea.repo.create          — mint a new private repo with auto-init
- gitea.file.read            — read a file (or list a directory)
- gitea.file.write           — create/update one file in one commit
- gitea.file.delete          — delete a file (auto-resolves sha)
- gitea.branches.list        — list branches with head sha
- gitea.branch.create        — branch off an existing branch

Wired through:
- lib/gitea.ts: giteaReadFile, giteaListContents, giteaListBranches,
  giteaCreateBranch, giteaListOrgRepos, giteaDeleteFile.
- lib/ai/vibn-tools.ts: 8 new Gemini tool declarations (53 total).
- app/api/chat/route.ts: system prompt now teaches the end-to-end
  scaffold-then-deploy recipe so the AI stops deferring to the user.

MCP capability descriptor bumped to version 2.5.0.

Made-with: Cursor
2026-04-28 11:52:16 -07:00
1a686c2a23 Per-project Coolify project isolation (Stage 1)
Each Vibn project now gets its OWN Coolify project named
vibn-{workspace-slug}-{project-slug}. All apps/databases/services
deployed for the project land inside that Coolify project, giving
us clean grouping, cascading delete, and per-project domain
namespaces.

Changes:
- New lib/projects.ts: ensureProjectCoolifyProject (idempotent
  create/lookup), getProjectCoolifyUuid, getOwnedCoolifyProjectUuids
- /api/projects/create: pre-insert row, mint per-project Coolify
  project, then complete the row with productData (preserves the
  coolifyProjectUuid that was just set)
- apps.list (MCP): without projectId, aggregates across ALL
  workspace-owned Coolify projects; with projectId, scopes to
  that project's Coolify project. Returns coolifyProjectUuid
  on each result so the AI knows where things live.
- apps.create (MCP): accepts projectId; auto-mints the Vibn
  project's Coolify project on first deploy if missing
- apps_list/apps_create tool defs: projectId param surfaced
- System prompt: Project as first-class — planning + live as
  facets of ONE thing, never as separate worlds. AI told to
  always pass projectId on apps_create.

Stage 2 (next): set-aware ensureResourceInProject across all
single-resource MCP tools (apps.get/delete/exec/etc.) and
cascading delete via projects.delete.

Made-with: Cursor
2026-04-27 19:02:43 -07:00
95ab91727e Fix Gemini schema validation: ARRAY needs items, replace free OBJECT with JSON strings
Gemini's function_declarations validator requires:
- ARRAY types must declare items schema
- Free-form OBJECT (without properties) is rejected

Renamed free-object params to *Json string fields (envsJson, patchJson,
headersJson) and added server-side JSON.parse before forwarding to MCP.
Any param ending in "Json" is automatically unpacked into its real key
(e.g. envsJson string is parsed into envs object).

Made-with: Cursor
2026-04-27 18:02:03 -07:00
c4ef30066f Expand chat panel to full MCP tool surface (35+ tools)
vibn-tools.ts previously exposed only 12 of the 35+ MCP tools.
Now includes the complete surface from AI_CAPABILITIES.md:
- workspace.describe, gitea.credentials
- apps: get, update, rewire_git, delete, deploy, deployments, exec,
  volumes.list/wipe, containers.up/ps, repair, domains.list/set,
  envs.list/upsert/delete
- databases: list, create, get, update, delete
- auth: list, create, delete
- domains: search, get, attach (+ existing register, list)
- storage: describe, provision, inject_env

Action dispatch simplified: toolName.replace(/_/g, '.') maps any
tool name to the MCP action with no explicit lookup table needed.
System prompt updated to reflect full capability set.

Made-with: Cursor
2026-04-27 17:55:57 -07:00
e08405ffbf Fix thought_signature: it's a sibling of functionCall, not nested inside it
The Gemini REST API returns thoughtSignature as a sibling part field:
  { "functionCall": {...}, "thoughtSignature": "..." }
not inside functionCall. We were reading part.functionCall.thought_signature
(always undefined) and writing fc.thought_signature inside the functionCall
object (also wrong). Now correctly reads part.thoughtSignature and writes
part.thoughtSignature when building history.

Made-with: Cursor
2026-04-27 17:28:49 -07:00
8872ab606b Fix tool calling: use non-streaming generateContent for tool rounds
Gemini 3.1 Pro thinking model requires thought_signature to be echoed
in functionResponse. SSE stream doesn't reliably include it in individual
chunks. Switch tool-calling rounds to non-streaming generateContent which
always returns the complete response with thought_signature present.

Made-with: Cursor
2026-04-27 17:18:34 -07:00
d246cbaf75 Fix Gemini 3.1 Pro thought_signature in tool calls
Thinking models attach a thought_signature to functionCall parts.
Must be echoed back in functionResponse or API returns 400.
Carry it through ToolCall -> ChatMessage -> toGeminiContents().

Made-with: Cursor
2026-04-27 16:37:09 -07:00
c41d018d79 Add github_search, github_file, http_fetch tools to chat AI
Gemini can now:
- Search GitHub for MIT-licensed OSS repos matching any description
- Read specific files from any public repo (READMEs, design systems,
  package.json, docker-compose.yml, component libraries, etc.)
- Fetch any public URL for docs, APIs, or reference material

No hardcoded pipelines — Gemini decides how to use these tools
based on what the user asks for.

Made-with: Cursor
2026-04-27 15:58:02 -07:00
5e07bbf39d Add Vibn AI chat panel powered by Gemini 3.1 Pro
- Right-docked chat panel on all workspace pages ([workspace]/layout.tsx)
- Streaming SSE responses with Gemini 3.1 Pro preview via generativelanguage API
- Full tool-calling loop (up to 6 rounds): deploys apps, lists projects, registers
  domains, fetches logs — all via existing MCP dispatcher
- Persistent conversation history: fs_chat_threads + fs_chat_messages tables (Postgres)
- Thread management: create, list, rename (auto-title from first message), delete
- Panel collapses to a tab; open state persisted to localStorage
- Read-only mode hint when no MCP token is present
- Graceful content margin shift when panel is open

Made-with: Cursor
2026-04-27 15:40:32 -07:00
e7f33211b9 feat: migrate Gemini from Vertex AI to Google AI Studio API key
- gemini-client.ts: replaces Vertex AI REST + service account auth with
  direct generativelanguage.googleapis.com calls using GOOGLE_API_KEY.
  Removes all Firebase credential setup code.
- summarize/route.ts: same migration, simplified to a single fetch call.
- No longer depends on gen-lang-client-0980079410 GCP project for AI calls.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 14:35:44 -08:00
40bf8428cd VIBN Frontend for Coolify deployment 2026-02-15 19:25:52 -08:00