Commit Graph

342 Commits

Author SHA1 Message Date
4dd8974b43 fix(delete-project): cascade dev container + Coolify resources
The old /api/projects/delete only removed the fs_projects row.
Everything else — the dev container service in Coolify, the row in
fs_project_dev_containers, linked Coolify apps/services, and the
per-project Coolify project shell — kept existing as orphans. The
biggest user-visible consequence: ghost containers from deleted
projects keep counting against the 3-active-dev-container quota,
so users hit the cap with stuff they thought was already gone.
Smoke test on 2026-05-04 caught the user with 2/3 quota slots
held by ghosts from a previous Manifest project + Twenty CRM.

New cascade:
  1. Stop+delete dev container Coolify service (deleteVolumes,
     dockerCleanup, deleteConnectedNetworks all true).
  2. Delete every fs_project_resources-linked Coolify resource
     (apps + services; databases preserved because they hold user
     data and we have no signal the user wanted them destroyed).
  3. Delete the per-project Coolify project shell IF no other Vibn
     project still references it (legacy vibn-ws-* shared projects
     are skipped).
  4. Drop fs_project_dev_containers + fs_project_resources rows.
  5. Unlink fs_sessions (preserve content for re-association).
  6. Delete fs_projects row last.

Deliberately NOT deleted:
  - Gitea repo (user's code is sacred; URL returned in response).
  - Sentry project (error history is independent evidence).

Failures inside the cascade are logged but don't abort. A partial
delete leaves orphans for manual cleanup, which beats a rollback to
a half-deleted state.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 10:39:13 -07:00
836733536e feat(devcontainer): auto-clone Gitea repo + auto-commit on each AI turn
The smoke test caught the biggest beta-blocker yet: everything the AI
writes inside the dev container was invisible in the UI because the
Product/Hosting/Infrastructure tabs all read from Gitea + Coolify, not
from the dev container's volume. Plan tab worked; nothing else did.

Two-part fix:

1. lib/dev-container-git.ts — new module with two helpers:
   - ensureProjectRepoCloned(): clones the project's Gitea repo into
     /workspace/<slug>/ using the AI's gitea token, embedding the auth
     into the remote URL so subsequent pushes work without prompts.
     Idempotent: tri-state probe handles 'git' (real repo, no-op),
     'dir' (path exists from pre-fix AI work, init in place), and
     'absent' (full clone). Has an empty-repo fallback for fresh Gitea
     repos where 'git clone' warns and produces nothing checked out.
   - commitAndPushIfDirty(): stages all changes under /workspace/<slug>,
     commits with a one-line message + pushes to origin. Bails fast
     with reason='clean' when there's nothing to commit. Never throws.

2. app/api/chat/route.ts wiring:
   - Pre-loop: fire-and-forget ensureProjectRepoCloned so the repo is
     on disk before the AI's first filesystem-mutating tool call.
   - Post-loop: fire-and-forget commitAndPushIfDirty after the assistant
     message is persisted; commit message is the assistant's first
     sentence (≤180 chars) or 'AI checkpoint' fallback.
   - System prompt now tells the AI: project repo is at /workspace/<slug>,
     write everything you want in the UI under that path, and don't
     manually commit (harness handles it).

Cred plumbing: GITEA_API_URL/GITEA_USERNAME/GITEA_API_TOKEN are read
from process.env in the harness; the dev container never sees the
token outside of the embedded URL. Same blast radius as shell.exec.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 14:02:16 -07:00
8c5fbad782 fix(devcontainer): self-heal stuck provisioning state, stop AI poll-loop
Before this fix, devcontainer.status was a read-only DB query that
returned whatever state the row currently held. The state only flips
provisioning→running via touchActivity() inside execInDevContainer.
That created a deadlock: the AI polls devcontainer.status waiting
for 'running'; status will never flip until something else execs.
Caught live in smoke test 2026-05-01 (manifest project) — the AI
fired devcontainer.status three times in a row, hit the loop guard,
and surfaced the dead-end to the user.

Two fixes:

1. getDevContainerStatus() now does a cheap 'true' exec probe when
   the row says 'provisioning'. If the probe lands, it flips the
   row to 'running' via touchActivity and reports selfHealed=true.
   If the probe fails AND the row is older than 120s, it reports
   likelyFailed=true so callers can stop polling and escalate.
   Also returns ageSeconds for the AI to reason about wait windows.
   Coolify's own service status is not used because dev containers
   have no fqdn/healthcheck and Coolify reports running:unknown for
   any such service forever.

2. New error-recovery rule 'devcontainer-still-provisioning' that
   fires whenever a status response contains state:'provisioning'.
   Tells the AI to send one status message, wait 15s, and prefer
   shell.exec (which lazy-provisions and proves reachability) over
   another devcontainer.status call. Explicit antipattern: do not
   poll status in a tight loop.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 13:46:23 -07:00
7ce8909555 feat(chat): warm welcome + suggested prompts for empty threads
The smoke-test runbook step 3 promised 'AI should greet you and
offer to scaffold something' but the empty-thread state was just
a logo + tagline. Replaced with:

- Project-aware greeting ('Welcome to <project name>.')
- One paragraph that names the affordances (paste brief, drop
  link, pick a starter)
- Four click-to-prefill suggestion chips that map to the smoke
  test entry points: scaffold a todo app, walk through a scope
  doc, plan a new idea, tour Vibn's capabilities.

Chips set the input value; user still hits send to commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 13:37:51 -07:00
95793d061b fix(auth): make signup path obvious for first-time users
The /auth page was titled 'Welcome back' which made first-time
visitors think they needed an existing account. Google OAuth
already creates accounts on first sign-in, so this was a copy
problem, not a missing flow.

- Default copy is now neutral ('Sign in or sign up'); explicitly
  notes that an account is auto-created on first sign-in.
- All marketing 'Get started' / 'Start free' CTAs now link to
  /auth?new=1 which switches the page to 'Create your account'
  copy. Plain 'Log in' links keep the neutral default.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 13:12:41 -07:00
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
871f465079 feat(ux): URL chip overflow popover + status pill deep-link
3.4: Bumped MAX_VISIBLE chips to 3 (was 2). When >3 endpoints exist,
extras collapse into a "+N" pill that opens a click-to-open popover
listing the rest as real clickable links. Replaces the previous
title-attribute tooltip which was undiscoverable on touch and
keyboard. Closes on outside click and Escape.

3.5: Status pill "Logs" affordance now deep-links to the Coolify
project page (one click from build logs) instead of Coolify's root.
Also surfaced on `deploying` and `down` states, not just
`build_failed` — when something's happening Coolify-side, users
want to peek regardless of the exact state.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 12:42:48 -07:00
e415268115 build(sentry): verbose source map upload output during beta
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 12:22:53 -07:00
04d189bc0d build(sentry): accept Sentry build args in Dockerfile
NEXT_PUBLIC_SENTRY_DSN must be present at build time so Next.js
inlines it into the client bundle. SENTRY_AUTH_TOKEN must be
present at build time so withSentryConfig can upload source maps
to Sentry. Coolify already passes both as --build-arg; this
declares the matching ARG lines so the builder stage actually
consumes them.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 11:57:44 -07:00
9eab86f8c2 feat(observability): wire Sentry for runtime error capture
Adds @sentry/nextjs v10 with the Next.js 16 instrumentation pattern:
- instrumentation.ts        — server + edge runtime init
- instrumentation-client.ts — browser init with Session Replay
                              (free tier, mask all text/inputs by
                              default since chat content is sensitive)
- app/global-error.tsx      — catches root-layout crashes that escape
                              every other error boundary
- app/sentry-example-page   — verification page; click both buttons
- next.config.ts            — wrapped with withSentryConfig, source
                              maps upload to Sentry on every build,
                              client error events tunneled through
                              /monitoring to bypass ad-blockers

Runtime capture works as soon as NEXT_PUBLIC_SENTRY_DSN is in Coolify
env (already added). Full source-map de-minification of prod stack
traces requires SENTRY_AUTH_TOKEN in Coolify env — pending user.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 11:24:10 -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
f7fdc34af1 docs(prompt): tighten Vite HMR config to match verified-working shape
Spike on 2026-05-01 confirmed HMR works end-to-end through Traefik
when ALL of these are set:

  server: {
    host: '0.0.0.0',
    port: <3000-3009>,
    strictPort: true,
    hmr: { clientPort: 443, protocol: 'wss', host: '<previewUrl host>' },
  }

The previous prompt omitted hmr.host, which lets Vite's HMR client
guess the wrong host and silently fail the WS upgrade. Adding the
host explicitly. Verified test: 101 Switching Protocols, vite-hmr
subprotocol negotiated, js-update messages fire within ~1s.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 10:25:34 -07:00
41f5f02c68 fix(chat-ui): interleave thoughts + tools chronologically; per-thought pills
The bug: 12 rounds of model thinking were concatenated into one giant
msg.thoughts string and rendered as a single ThinkingBubble blob. User
saw a wall of meta-narrated reasoning ("Okay, here's my interpretation
of those thoughts..." x12) with no visual breaks between rounds.

Replace separate thoughts/toolEvents fields on Message with a single
chronological timeline of mixed entries:

  type TimelineEntry =
    | { kind: "thought"; text: string }
    | { kind: "tool"; name: string; status; result? }

Each thought event from the SSE stream becomes its own timeline entry
and renders as its own collapsed ThinkingBubble pill. Adjacent same-
named tool entries still collapse into a ×N TimelineToolGroup.

Visual flow now matches what actually happened in the turn:
  [thought pill] [dev_server.list ✓] [thought pill]
  [dev_server.stop ×2 ✓] [thought pill] [shell.exec ×2 ✓]
  [final summary text]

Each pill is independently expandable, so a user who wants to read
the model's reasoning for round 7 can click round 7 — they don't
have to scroll through a single 4000-char blob.

Made-with: Cursor
2026-04-30 23:42:19 -07:00
c77f3fbc7f fix(chat-ui): render tool tray inside the bubble, collapse repeats
Two UX wins from this change:

1. Tool pills now render INSIDE the assistant message bubble, ABOVE
   the streaming text. Previously: messages list rendered first, tool
   tray rendered after — so the user saw the summary text mid-screen
   and the tool pills below it, having to scroll UP to read the
   summary they actually cared about. Now the visual flow is:
     [user question] → [tool pills, chronological] → [summary text]
   and the user's eye lands on the summary at the bottom.

2. Adjacent runs of the same tool name collapse into a single pill
   with ×N counter. Click to expand. Kills the "8× shell.exec ✓"
   wall of identical pills the user saw in prod (Dr Dave thread).

Tool events are now scoped to their owning assistant message
(message.toolEvents) rather than a global state — also means
stop/restart can't bleed events across turns.

Made-with: Cursor
2026-04-30 23:21:31 -07:00
b395546529 fix(chat): never end a turn silent + loop detection + status nudge
The big UX failure: model fires 20 tool calls in silence, persists turn
with content_len=0, user has to re-prompt to get any answer. Confirmed
in prod (Dr Dave / "are you able to give me a preview url?" thread).

Five changes:

1. Recovery summary now fires on ANY silent-tool-tray turn end (not just
   MAX_TOOL_ROUNDS): hit the cap, broke a detected loop, OR ended with
   empty assistantText. Previously the recovery was gated to round-cap
   only, so voluntary silent stops slipped through.

2. Recovery summary has a deterministic fallback. If Gemini returns
   empty text on the recovery call, emit a static "ran N tools, didn't
   reach a clean stopping point" message instead of silently swallowing
   the empty string. The user always gets something readable.

3. Loop detection: track tool-call fingerprints (name + first 120
   chars of args) per turn; if the same fingerprint fires 3× within
   the last 8 calls, break the loop and surface to user via recovery
   summary. Kills the dev_server.start → logs → stop → start → ...
   pattern at its root.

4. Status nudge every 4 silent rounds: inject a synthetic system
   instruction telling the model to send a one-liner before any more
   tool calls. The user's only signal of life on long chains.

5. Prompt: soften "don't narrate intent" → "don't narrate SINGLE
   calls; on chains 3+ deep send a one-liner before each batch".
   Adds explicit "never end a turn silent" rule.

Also: error-path now uses safeClose() instead of bare controller.close()
to honor the streamClosed guard like every other close site.

Made-with: Cursor
2026-04-30 23:18:46 -07:00
6586c8ae1d feat(chat): rewrite system prompt — sharper identity, leaner token cost
- Adds high-agency identity framing at the top ("you own the outcome")
- Adds explicit decision defaults (Postgres > Mongo, monoliths > microservices, etc.)
- Adds adaptive-communication rule (uncertain user → narrow choices; experienced user → denser)
- Removes stale instruction "preview URLs land in week 2" (they're live)
- Removes stale instruction "ship tool lands soon" (it shipped weeks ago)
- Tightens prose throughout — keeps every named tool, recipe, and earned-from-pain story
  (orphan-twenty-* recovery, anchor-on-current-state-first, trust idempotency, etc.)
- Drops dead streamGeminiChat import

Made-with: Cursor
2026-04-30 23:10:43 -07:00
cbd4ab44a5 fix: use letsencrypt-dns resolver for vibn-dev preview Traefik routes
The *.preview.vibnai.com wildcard cert is issued via DNS-01 and stored
under the letsencrypt-dns resolver. Using letsencrypt (HTTP challenge)
would cause Traefik to attempt individual per-subdomain certs instead
of using the existing wildcard.

Made-with: Cursor
2026-04-30 19:02:45 -07:00
b21e6ea038 fix: remove invalid YAML escape in Traefik Host() label
\` is not a valid escape sequence in YAML double-quoted strings,
causing Coolify to fail parsing the dev-container compose on provision.
Backticks are literal characters in YAML double-quoted strings and
need no escaping — only the JS template literal escape (\`) is required.

Made-with: Cursor
2026-04-30 18:52:34 -07:00
8969371134 fix: redirect to /product after project creation instead of /overview
/overview does not exist as a route; the correct landing tab is /product.

Made-with: Cursor
2026-04-30 18:50:55 -07:00
9358326b6d fix(github-oauth): use NEXTAUTH_URL for redirect_uri instead of req.url.origin
Behind Coolify's proxy, req.url resolves to 0.0.0.0:3000 which GitHub
rejects as an unregistered redirect URI. Prefer NEXTAUTH_URL env var.

Made-with: Cursor
2026-04-30 18:32:21 -07:00
bc6b2cc63f fix(routing): resolve duplicate /settings route conflict breaking Turbopack build
Move project settings page from bare /settings into (home) route group so it
shares the project-header layout; delete the conflicting (workspace)/settings
page that was causing "two parallel pages resolve to the same path" error.

Made-with: Cursor
2026-04-30 17:54:19 -07:00
c862104e35 feat(ux): empty-state prompt nudges + workspace delete
- Plan/Product/Infrastructure: empty states now suggest a concrete AI
  prompt so non-technical users know exactly what to type rather than
  staring at a blank category ("Try: Add a Postgres database…")
- Workspace settings danger zone: wired Delete Workspace to a new
  POST /api/workspaces/delete endpoint (deletes projects + chat
  threads; Coolify resources intentionally untouched)

Made-with: Cursor
2026-04-30 17:17:22 -07:00
41fbed31f3 fix(prod): ESM transpile, healthcheck, hosting UX, settings, error msgs
- next.config.ts: add react-markdown + entire unified/remark/rehype
  ecosystem to transpilePackages — fixes TypeError 'z'/'j'/'aa' prod
  crashes caused by ESM-only packages not being bundled for webpack
- Dockerfile: bake HEALTHCHECK --start-period=60s on 127.0.0.1 so
  rolling deploys pass on first health probe (was failing on ::1 IPv6)
- Hosting tab: full rewrite — live URL chip, copy button, redeploy
  button, inline log viewer, domain list, empty state with prompt
  nudge. Single-card layout replaces master-detail for 1-3 endpoints.
- Settings page: new /project/:id/settings route with danger zone +
  typed "delete" confirmation for project deletion
- Status pill: "View logs" link appears on build failures
- URL chips: collapse extras into "+N more" pill when >2 visible
- Chat errors: structured "Tool error:" prefix; network errors
  distinguished from server errors

Made-with: Cursor
2026-04-30 17:12:48 -07:00
73b672f2c9 fix(anatomy): harvest inner-app fqdns + prioritise custom domains
Coolify stores user-facing domains on the inner application of a
service (e.g. Twenty's `twenty` app), not on the parent service. The
anatomy endpoint was reading the service-level fqdn and getting null,
so live URL chips never rendered.

- smartServiceMetaFor (replaces smartServiceStatusFor): collects
  fqdns from non-excluded inner apps in addition to status
- prioritiseFqdns: pushes auto-generated *.sslip.io / *.coolify.app
  URLs to the back so real custom domains surface first
- fqdnsOf: strips default ports (443, 80, container 3000) so chips
  link to the public Traefik-served URL, not the internal port

Made-with: Cursor
2026-04-30 13:45:06 -07:00
996b875983 feat(project-header): smart status pill + live/preview URL chips
- Status pill derives richer states (Empty / Deploying / Build failed /
  Down / Live) from anatomy with self-polling while deploying
- Tooltip explains what's happening (last build status, transient
  containers, Coolify build phase)
- New ProjectHeaderUrls component renders clickable chips for live
  domains and active dev-server preview URLs to the left of the pill
- useAnatomy gains pollMs option for client-driven refresh

Made-with: Cursor
2026-04-30 13:44:57 -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
652e45ac00 fix(mcp): inline magic env URLs in compose so service domains survive parse()
Coolify's Service::parse() (called inside the deploy job) iterates ALL
ServiceApplications in a multi-container template and overwrites
SERVICE_URL_<NAME> / SERVICE_FQDN_<NAME> env vars based on whichever
inner app's fqdn it sees first — frequently a worker still pointing at
the auto-generated *.sslip.io fallback. The result: the user's custom
domain is saved, the SPA loads, but its baked-in REACT_APP_SERVER_BASE_URL
points at sslip.io and every API call 404s.

apps_domains_set now substitutes ${SERVICE_URL_<NAME>} and
${SERVICE_FQDN_<NAME>} (with optional _<port> suffix) directly in the
docker_compose_raw before calling updateCompose(), and stops calling
service->parse() — so the literal URL survives any future redeploy.

Solves Twenty CRM domain bug; also unlocks reliable custom domains for
n8n, Plausible, Mautic, and every other Coolify service template that
follows the same SERVICE_URL_X pattern.

Made-with: Cursor
2026-04-30 11:40:13 -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
eb4086d296 fix(ai): close remaining duplication + stale-context gaps
Round two of AI-hardening based on what bit us with the twenty-* fan-out:

1. apps_create idempotency now covers ALL four pathways (template /
   image / composeRaw / repo), not just templates. Same dedup-by-name
   check inside the project, same alreadyExisted: true response shape.
   Pass force: true to opt out for legitimate dev/staging duplicates.

2. databases_create gets the same idempotency treatment — and now
   also scopes to the per-project Coolify project when projectId is
   supplied (previously only apps_create did this).

3. New shared helper findExistingResourceByName scans apps + services
   + databases in a project and matches case-insensitively on name.

4. System prompt: three new hard rules teaching the model how to
   handle tool results and anchor on reality:
   - Tool results are authoritative; conversation history is not.
     If a tool contradicts an earlier assertion, discard the
     assertion. Don't keep telling the user it's broken when
     apps_get now says it's healthy.
   - When the user reports an error, FIRST tool call is a
     current-state read (apps_get / databases_get / apps_logs).
     Stop re-debugging problems that were already fixed.
   - Trust idempotency. alreadyExisted means done; don't loop
     trying a different name.

Made-with: Cursor
2026-04-30 11:07:14 -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
5ecb0349d7 feat(plan): add Plan tab as the first project surface
A new home for everything that happens BEFORE building:
- Vision    — one-line elevator pitch (mirrors productVision)
- Ideas     — the "park-it" bin for raw thoughts
- Tasks     — what needs to happen next (open / done)
- Decisions — log of "we chose X over Y because Z"

Storage is appended under fs_projects.data.plan so no schema migration
is needed. CRUD lives at /api/projects/[projectId]/plan.

The bare project URL now redirects to /plan instead of /product, and
the AI chat receives decisions + open tasks in its active-project
context block — so it stops re-litigating settled questions and knows
what's queued up.

Made-with: Cursor
2026-04-29 18:02:02 -07:00
b706fa0e89 feat(chat): scope AI conversations to the active project
The chat panel now reads projectId from the URL and tags every thread
to it, so:
- Threads listed in the side panel are filtered to the project the user
  is currently viewing (workspace-level chats still work from /projects).
- New conversations started from a project page are persisted with that
  project_id, surviving page reloads.
- The system prompt prepends an ACTIVE PROJECT block so tool calls
  (apps_create, devcontainer_ensure, etc.) use the right projectId
  without the user having to name it.
- A small chip in the chat header shows which project the AI is
  currently talking about.

Schema migrates idempotently on first request (project_id column +
composite index on fs_chat_threads).

Made-with: Cursor
2026-04-29 17:41:45 -07:00
85db68636b ui(infrastructure): strip explainer prose from empty + detail panes
Empty Infrastructure tabs were a wall of teaching text — every empty
category showed a dashed-border explainer button in the rail, the
right pane carried multi-paragraph explainer prose for every category,
and the LLM/Stripe/Storage/Secrets details all included instructional
prose for things the user hasn't done yet. On a brand-new project the
whole tab was 90% explainer.

Removed (everywhere):
  - Per-category dashed empty-state buttons under each category
    header. Empty categories now show just the header + "0", which
    is enough — clicking the header still opens the details pane if
    the user wants it.
  - Right-pane Overview lead paragraph ("Infrastructure here is
    auto-discovered…").
  - CategoryDetail lead paragraph (def.label + def.blurb) and the
    "Examples" chip row.
  - CategoryDetail "Nothing detected yet — set a matching env var…"
    info box. Replaced with a one-word "None yet." line.
  - LLM CategoryDetail BYOK explainer box.
  - DatabaseDetail "Expand this database in the left rail…" hint.
  - DatabaseDetail "Set <KEY> on any app or service…" prose.
    Renamed the section "How apps connect" → "Connection env" and
    kept just the env-var snippet.
  - StorageDetail unprovisioned-state explainer paragraph + "you can
    also provision it now" info box. Replaced with a one-word
    "No bucket provisioned yet." empty.
  - StorageDetail "Vibn-bundled storage is GCS exposed via…" prose.
    Kept the env snippet under a renamed "Connection env" section.
  - ProviderDetail Stripe webhook hint box.
  - ProviderDetail bottom "Values aren't shown here…" note.
  - SecretsDetail "Values are never read on this surface…" note.

Kept (functional, non-prose):
  - All KvRows and code snippets.
  - Stripe "Connect Stripe (coming soon)" CTA.
  - Storage "View workspace bucket" CTA.
  - Provider dashboard external-link buttons.
  - Edit / Rotate icon buttons on secrets (still disabled, tooltipped).

Cleaned up the now-unused Info icon import. The category def's
blurb / examples / dashboards fields stay in the source — they're
still useful as tooltip / search seed material for a follow-up
iteration that puts the teaching content somewhere less in-your-face
(like a help drawer).

Made-with: Cursor
2026-04-29 17:32:41 -07:00
b6eaa85733 fix(tenancy): stop leaking workspace-level Coolify services across projects
CRITICAL: every Vibn project was rendering every other project's
services in the same workspace (Twenty CRM, n8n, all databases,
all secrets). Tenancy was effectively broken — cross-project data
exposure inside a workspace.

Root cause:
  - Coolify's POST /projects validates `description` against a strict
    allowlist (letters, numbers, spaces, and `- _ . , ! ? ( ) ' " + = * / @ &`).
  - Our description "Vibn project: <name> (workspace: <slug>)" contains
    two colons. Every project-create on Coolify returned 422.
  - lib/projects.ts caught that 422 and fell back to
    `workspace.coolify_project_uuid` so deploys "weren't blocked."
  - That UUID is shared by every Vibn project in the workspace, so
    listServicesInProject(coolifyProjectUuid) returned the union of
    all projects' services, applications, and databases for any
    project in the workspace. The Product, Hosting, and Infrastructure
    tabs all rendered cross-tenant data as if it were the current
    project's.

Fixes (defense in depth — fix at every layer):

  1. lib/coolify.ts createProject(): sanitize the description against
     Coolify's allowlist at the boundary so no caller can ever ship
     a description that 422s. Replaces disallowed chars with `-`,
     collapses runs, caps at 255 chars.

  2. lib/projects.ts ensureProjectCoolifyProject():
     - Pre-sanitize the description we pass (belt + suspenders).
     - Detect when `stored === workspace.coolify_project_uuid` (the
       legacy bad state) and re-provision a dedicated project.
     - REMOVE the workspace-UUID fallback on create failure. A 422
       now leaves coolifyProjectUuid null and the UI shows an empty
       state, which is correct: better to surface "no resources" than
       to lie about which project owns what.
     - Export sanitizeCoolifyDescription helper for reuse.

  3. /api/projects/[projectId]/anatomy/route.ts: SELF-HEAL on every
     read. If the project's stored Coolify UUID matches the
     workspace's UUID, we treat it as missing, re-provision a
     dedicated Coolify project on the fly (idempotent — reuses the
     existing one if found by name), persist the new UUID, and
     continue serving with the corrected scope. If provisioning
     fails we fall back to undefined, NOT the workspace UUID, so
     no cross-tenant data ever surfaces again.

The self-heal means existing already-broken projects will fix
themselves on the next page load — no manual data migration needed.

Made-with: Cursor
2026-04-29 17:16:33 -07:00
90bed6ab31 feat(github): OAuth integration + repo picker for Import flow
User can now click "Connect GitHub" inside the Import-existing-code
flow, sign in via GitHub, and pick a repo from a searchable list of
their own + collaborator + org repos. Both public and private repos
work — the encrypted access token on the user's account is auto-
attached when the create endpoint runs the agent-runner mirror.

OAuth flow:
  - GET  /api/integrations/github/connect    — generates state, sets
         a 10-min httpOnly cookie, 302s to GitHub authorize.
  - GET  /api/integrations/github/callback   — verifies state,
         exchanges code for token, fetches /user, encrypts the
         token with secret-box (AES-256-GCM, VIBN_SECRETS_KEY) and
         persists it on fs_users.data.integrations.github.
         Bounces back to ?gh_connected=login or ?gh_error=msg.
  - GET  /api/integrations/github/repos      — server-side fetches
         the connected user's repos (per_page=100, sort=pushed,
         affiliation=owner+collaborator+org_member). Returns the
         GitHub login + a stripped repo summary; never the token.
  - POST /api/integrations/github/disconnect — drops the integration
         from fs_users (does NOT revoke on github.com).

Scopes requested: repo, read:user.

Token storage:
  - Encrypted at rest with secret-box (lib/auth/secret-box.ts) using
    VIBN_SECRETS_KEY. Tokens never leave the server.
  - One token per fs_users row, keyed by email.

ImportSetup UI:
  - On mount, fires /repos to detect connection state.
  - If connected: shows a connected-as-@login chip with disconnect
    link, a search-as-you-type repo picker (max 220px scroll, badges
    for Private / language), and a "paste a different URL instead"
    escape hatch.
  - If not connected: shows a Connect GitHub card with a public-URL
    fallback inline.
  - On return from OAuth (?gh_connected=… or ?gh_error=…), surfaces
    a toast and silently refreshes the repo list.
  - Selected repo carries default_branch + repo id into the create
    payload so we can store them on the project for later UI hints.

/api/projects/create:
  - When a githubRepoUrl is mirrored, falls back to the user's
    OAuth-linked token if no PAT is explicitly passed. Means the
    flow "just works" for private repos once GitHub is connected.

Required env (already set in production):
  - GITHUB_CLIENT_ID
  - GITHUB_CLIENT_SECRET

Made-with: Cursor
2026-04-29 16:44:13 -07:00
c7bb0eea58 feat(project-creation): replace owner-style picker with audience picker
"Myself / A client" was about who *owns* the project (a billing
concern), but at creation time we want to know who *uses* it — that's
what determines which Infrastructure providers we should pre-stage.

  team       = internal users (your team / employees)
               → SSO-style auth, no payments by default, simple roles
  customers  = external users (the public)
               → public sign-up + payments + transactional email by
                 default, custom domain matters

Both choices are reversible from the Infrastructure tab later — the
selector copy makes that explicit so users don't feel locked in.

Changes: - setup-shared: ForWhomSelector ("Myself" / "A client") replaced by
    AudienceSelector ("My team" / "Customers"), with an "you can
    change this later" hint underneath. New Audience union type
    exported for the three setup screens to share.
  - BuildSetup / OssSetup / ImportSetup: swap state + import + payload.
    Defaults: BuildSetup → customers (most "vibe coder" projects are
    public products), ImportSetup → customers (existing repos usually
    are too), OssSetup → team (Twenty / n8n / Plausible style tools
    are most often deployed for internal use).
  - /api/projects/create: drop isForClient (we never read it
    anywhere), persist audience as a first-class field on the
    project record so the AI can branch on it during the first chat.
Made-with: Cursor
2026-04-29 16:24:54 -07:00
7a9cd68ea8 feat(project-creation): 3-path wizard — Build / OSS / Import
User feedback: the previous flow was a single-screen "name + audience"
dialog that gave AI no context about what the user actually wanted to
make. That worked for the demo but produced messy projects in practice
because everything was decided after the fact in chat.

The new flow asks the user one human question first ("How would you
like to begin?") and then captures the minimum context needed to seed
the AI's first conversation in the project.

Three paths, each is a 2-step setup screen with internal step dots:

  - Build your own idea  — Step 1: name + audience.  Step 2: free-text
    "what do you want to build". Becomes the project's vision and the
    AI's first-message context.

  - Run an open source tool  — Step 1: name + audience.  Step 2:
    segmented tabs to either (a) paste a GitHub link or (b) describe
    the kind of tool you want and have Vibn find one. Vision is set
    to either "Install and host this open-source project: <url>" or
    "Find and install an open-source tool that fits this need: <desc>"
    so the AI knows which mode to operate in on first chat.

  - Import existing code  — Step 1: name + audience + repo URL.
    Step 2: optional "what do you want to do with it" textarea.
    Public repos only for v1; private-repo OAuth lands later.

Backend:
  - /api/projects/create now accepts and persists `creationMode` and
    `sourceData` on the project record under a `kickoff` blob:
      { mode, sourceData, vision, createdAt }
    The chat endpoint will read this on first turn to seed the AI
    with the user's stated intent rather than asking them to re-type
    it in chat.

Cleanup:
  - Removed FreshIdeaSetup, CodeImportSetup, ChatImportSetup,
    MigrateSetup — replaced by BuildSetup, OssSetup, ImportSetup.
  - Removed the unused initialWorkspacePath prop from
    project-association-prompt (the new flow doesn't take it).
  - TypeSelector defaults are restored — the modal opens on the
    type-picker step now, not directly on a setup form.

UI building blocks added to setup-shared:
  - TextArea (multi-line input)
  - StepDots (page indicator)
  - SegmentedTabs (generic-typed tab selector, used in OSS Step 2)
  - SecondaryButton (used as ← Back inside Step 2)

Made-with: Cursor
2026-04-29 16:16:53 -07:00
2260f3c280 fix(db-introspect): scan all non-template databases, not just $POSTGRES_DB
Coolify exposes a single `postgres_db` per database resource (usually
"postgres"), but the cluster typically holds more than one db inside.
Twenty CRM connects to `default`; our prior query connected to
`postgres` and so reported the database as empty even when Twenty had
hundreds of tables.

Fix:
  - pgListDatabases() enumerates every non-template, connectable db in
    the cluster (`SELECT datname FROM pg_database WHERE datistemplate
    = false AND datallowconn = true`).
  - pgListTables() now unions table listings across all of them.
    Schema is stamped as `<db>.<schema>` only when there's more than
    one db, so single-db clusters keep the bare `public` flatten in
    the UI.
  - pgPreviewTable() understands the dotted `db.schema` form and
    routes the preview `psql` invocation to the correct database.
    Identifier whitelist applied to all three components (db, schema,
    table) before splicing into SQL.

Hard caps unchanged (50 tables total, 8s SSH wall-clock).

Made-with: Cursor
2026-04-29 15:36:28 -07:00
7b359e399e feat(infra): collapse to 7 categories + live Postgres table inspection
UX rework after iteration with the user:

  - Drop SMS, Analytics, Search, Monitoring categories from the rail.
    They were detection-only with no first-class UX behind them; surface
    is cleaner without them and they can return when each gets real
    flows (auth-style "edit configurables", payment-style "connect").
  - Storage no longer tries to detect S3/R2/GCS env vars. Instead it
    surfaces the workspace's bundled Vibn-provisioned GCS bucket
    (S3-compatible HMAC), with status, region, access id, and a
    one-shot env snippet for app config.
  - Email category no longer mixes in SMS providers.
  - LLM renamed to "Models"; empty state mentions BYOK as upcoming.
  - Payments empty state has a "Connect Stripe (coming soon)" CTA;
    Stripe detail surfaces the webhook URL guidance.
  - Secrets detail now lists actual env-var key names per resource,
    grouped by detected provider (Stripe block, OpenAI block, etc.)
    with an "Other (project-defined)" catch-all. Each row has Edit +
    Rotate icon buttons (currently disabled with tooltips — wire-up
    to apps.envs.upsert / services.envs.upsert lands in iter 2).

Live database inspection (Postgres only for now):

  - New /api/projects/[id]/databases/[uuid]/tables — auth-scoped, lists
    user-tables across non-system schemas via SSH-exec into the
    database container's psql. Hard caps: 50 tables, 8s timeout, no
    mutating queries possible (only SELECT row_to_json with LIMIT).
  - New /api/projects/[id]/databases/[uuid]/preview — returns first 50
    rows of a single table. Identifiers locked to /[A-Za-z0-9_]+/ so
    splicing them into the SELECT is safe.
  - DatabaseTableTree (lazy-fetch, schema-grouped, public-flat,
    approximate row counts from pg_class.reltuples) and TableViewer
    (sticky-header data grid, zebra rows, per-cell ellipsis at 360px).
  - Fix in lib/coolify.ts: listDatabasesInProject was flattening every
    db endpoint array (postgresqls, redises, mongodbs…) without
    tagging the output rows with the engine. Every consumer was
    seeing type=undefined which then bucketed as "unknown" and
    blocked the table inspector. Now we tag at the flatten step so
    every CoolifyDatabase has a stable type.
  - Infrastructure tab: database tile is now expandable inline like
    Codebases on Product. Auto-expands the first DB; click any table
    to preview rows on the right.

Made-with: Cursor
2026-04-29 15:22:58 -07:00
63f18d46a5 feat(project): wire Infrastructure tab to live Coolify data
Three sub-areas, all real, no static placeholders:

  Databases — listDatabasesInProject(coolifyProjectUuid). Type is
              normalised (postgresql / redis / mongodb / mysql / keydb
              / dragonfly / clickhouse) so the tile subtitle is stable
              regardless of how Coolify spells the engine.

  Providers — auto-detected from env-var keys across every app + service
              in the project. 35+ patterns covering Auth (Clerk, Auth0,
              Supabase, NextAuth, SuperTokens, WorkOS, Firebase Auth),
              Email (Resend, Mailgun, Postmark, SendGrid, SES, Loops),
              SMS (Twilio, Vonage), Payments (Stripe, LemonSqueezy,
              Paddle), Analytics (PostHog, Mixpanel, Amplitude, Plausible,
              Umami), LLM (OpenAI, Anthropic, Google AI, Mistral, Cohere,
              Groq, OpenRouter), Storage (S3, R2, GCS, Supabase),
              Search (Algolia, Meilisearch, Typesense), Monitoring
              (Sentry, Datadog, LogSnag). Each tile drills down to show
              which app/service the keys live in and which keys matched.

  Secrets   — env-var totals per app/service, sorted by count. Values
              are never read or returned from this surface — keys only.
              The detail pane explains how to read/edit (via AI chat
              with services.envs.* / apps.envs.* MCP tools).

Anatomy endpoint extended in the same single-fetch shape: env vars are
loaded once, then both detectProviders() and summariseSecrets() run
against that one source so we don't double-fetch.

The static What-lives-here grid is gone — every tile shown corresponds
to something that actually exists in the project.

Made-with: Cursor
2026-04-29 14:42:23 -07:00
307c3ca858 feat(project): unify Product+Hosting around code/images and live/previews
Anatomy + UI rewrite — locked the conceptual model after user feedback:

Product = "what makes up the thing you're shipping":
  - Codebases (Gitea repos)
  - Images (Coolify services backed by upstream Docker images: Twenty
    CRM, n8n, etc.)
  - Dev containers no longer surface here. The vibn-dev-* container is
    the AI's workshop, not a product surface; previews it serves still
    appear under Hosting → Previews.

Hosting = "where it lives + how it gets there", unified:
  - Live: every running endpoint as one list. Each item carries a
    source badge ("repo" | "image"), status dot, attached domain, and
    last-build summary inline. No separate Build, Domains or Services
    categories — those are properties on each Live item.
  - Previews: dev container preview URLs (unchanged).

Anatomy endpoint reshaped accordingly:
  - product.{codebases, images}
  - hosting.{live, previews}  (was production/services/previewUrls/domains)
  - lastBuild summary fetched per repo-app via listApplicationDeployments
    in parallel.

ProjectStagePill rewired to derive Live/Down/Building from hosting.live
+ hosting.previews. dev-container-detail.tsx removed.

services.* MCP tools added so AI agents can manage Coolify services
(Twenty CRM, n8n, …) the same way they manage apps:
  - services.list, services.get
  - services.start, services.stop
  - services.envs.list, services.envs.upsert
All tenant-scoped via getServiceInWorkspace + getOwnedCoolifyProjectUuids.
vibn-dev-* containers stay hidden from services.list.

Made-with: Cursor
2026-04-28 19:36:35 -07:00
3db7191146 feat(project): split dev containers into Product; convert Hosting to tile-rail
The vibn-dev-* services that the AI authors code in conceptually
belong to Product (build surface), not Hosting (runtime + reach).
Anatomy endpoint now splits Coolify services by name prefix:
  - vibn-dev-* → product.devContainers[]
  - everything else → hosting.services[]

Product tab gains a "Workspace" section above the codebases stack
with a single dev-container tile. Selecting it shows status +
active dev servers in the right pane. Codebase + file selection
behaves the same as before.

Hosting tab restructured from a stack of always-visible cards to
the same tile-rail pattern Product uses: left rail has 4 always-
present categories (Production / Services / Previews / Domains)
each with a count badge, items inside are clickable tiles, right
pane shows details for the selected item. Empty categories show a
one-liner explaining what would appear there — teaches the user
the model on a brand-new project without being preachy.

Made-with: Cursor
2026-04-28 18:54:19 -07:00
ba69a78a5f fix(projects): surface API errors instead of silently rendering empty list
Made-with: Cursor
2026-04-28 17:47:34 -07:00
6fca78dca9 feat(project): unified anatomy endpoint + live Hosting tab + truthful Live pill
Adds GET /api/projects/[id]/anatomy returning the full project shape
in one shot — codebases (Gitea), production apps (Coolify
applications matched by repo URL), dev services (Coolify services in
the project's coolifyProjectUuid), preview URLs (active fs_dev_servers
rows), and aggregated domains. Each tab reads its own slice via the
new useAnatomy() hook so the page never fans out 3+ requests.

Hosting tab is now real: surfaces production / dev services / preview
URLs / domains with empty-state CTAs explaining what each means and
why it's empty when applicable. Includes a banner when nothing at all
is deployed for the project.

Project header pill (previously hard-coded from data.status, which
historically lied) now derives stage from hosting reality:
  - any production app running → Live (green)
  - any failed app             → Down (red)
  - any service / preview      → Building (blue)
  - else                       → fallback to data.status

Product tab refactored onto the same useAnatomy hook so we no longer
maintain two near-identical fetchers.

Made-with: Cursor
2026-04-28 17:38:57 -07:00
b9adcb76b6 fix(project): surface API errors instead of hanging on Loading…
Adds a 10s timeout + AbortController to the codebases fetch and
parses error bodies defensively (so a non-JSON 4xx/5xx still shows
a status code instead of disappearing). The Product tab will now
show a clear message ("Request timed out", "HTTP 401 Unauthorized",
etc.) instead of spinning forever when the API can't respond.

Made-with: Cursor
2026-04-28 17:17:15 -07:00
56d4cc36c7 feat(project): IDE-style Product tab — codebase tile expands inline, files preview right
Each codebase becomes its own panel with a header and an expandable
Gitea file tree inside. Clicking any file selects it and renders its
content in the right-hand preview panel (monospaced; no syntax
highlight yet). Single-codebase projects auto-expand the only
codebase on load so the tree is visible immediately.

Tree leaves are now interactive when an onSelectFile callback is
provided; selected rows highlight subtly so the user can tell where
the right pane's content came from.

Made-with: Cursor
2026-04-28 17:08:27 -07:00
6e4e9c02ff feat(project): auto-discover codebases from Gitea instead of hard-coding
Adds GET /api/projects/[id]/codebases that inspects the project's
Gitea repo:
  - apps/* present → one codebase per subdir (Turborepo)
  - else          → single codebase rooted at the repo root
  - no repo       → empty list with reason="no_repo"

Product tab now fetches this list, picks the first as the default
selection, and surfaces explicit loading / error / empty states
(previously it hung on "Loading…" when apps/web 404'd in single-
repo projects).

Made-with: Cursor
2026-04-28 16:49:34 -07:00
69c3a1258c feat(project): Product/Infrastructure/Hosting tab shell with live Gitea preview
Replaces the old two-tile project landing with a tabbed shell anchored
on three sections: Product (codebases), Infrastructure (swappable
services), Hosting (runtime + reachability). Bare project URL
redirects to /product so the founder always lands on the most
actionable surface.

Product tab is the only one wired with real data so far: each
codebase tile is selectable and renders a lazy-loading Gitea file
tree for apps/<codebase>/ in the right column. Both columns share
height + a heading slot so panels stay visually aligned even when
the right side is sparse.

Infrastructure and Hosting are stubs ready for Phase 2 wiring (no
behavioural change vs today). The old (workspace)/infrastructure
route is removed in favour of the new tab; the other 15 sidebar
routes are untouched and still reachable for the migration window.

Made-with: Cursor
2026-04-28 16:37:38 -07:00