Commit Graph

133 Commits

Author SHA1 Message Date
0bdf598984 fix(workspace-panel): resolve workspace via /api/workspaces, not URL slug
The panel was fetching /api/workspaces/{urlSlug} where {urlSlug}
is whatever is in the `[workspace]` dynamic segment (e.g.
"mark-account"). That slug has nothing to do with vibn_workspaces.slug,
which is derived from the user's email — so the fetch 404'd, the
component showed "Loading workspace…" forever, and minting/revoking
would target a non-existent workspace.

Now:
- GET /api/workspaces lazy-creates a workspace row if the signed-in
  user has none (migration path for accounts created before the
  signIn hook was added).
- WorkspaceKeysPanel discovers the user's actual workspace from that
  list and uses *its* slug for all subsequent calls (details, keys,
  provisioning, revocation).
- Empty / error states render a proper card with a retry button
  instead of a bare "Workspace not found." line.

Made-with: Cursor
2026-04-20 20:43:46 -07:00
acb63a2a5a feat(workspaces): per-account tenancy + AI access keys + Cursor integration
Adds logical multi-tenancy on top of Coolify + Gitea so every Vibn
account gets its own isolated tenant boundary, and exposes that
boundary to AI agents (Cursor, Claude Code, scripts) through
per-workspace bearer tokens.

Schema (additive, idempotent — run /api/admin/migrate once after deploy)
  - vibn_workspaces: slug, name, owner, coolify_project_uuid,
    coolify_team_id (reserved for when Coolify ships POST /teams),
    gitea_org, provision_status
  - vibn_workspace_members: room for multi-user workspaces later
  - vibn_workspace_api_keys: sha256-hashed bearer tokens
  - fs_projects.vibn_workspace_id: nullable FK linking projects
    to their workspace

Provisioning
  - On first sign-in, ensureWorkspaceForUser() inserts the row
    (no network calls — keeps signin fast).
  - On first project create, ensureWorkspaceProvisioned() lazily
    creates a Coolify Project (vibn-ws-{slug}) and a Gitea org
    (vibn-{slug}). Failures are recorded on the row, not thrown,
    and POST /api/workspaces/{slug}/provision retries.

Auth surface
  - lib/auth/workspace-auth.ts: requireWorkspacePrincipal() accepts
    either a NextAuth session or "Authorization: Bearer vibn_sk_...".
    The bearer key is hard-pinned to one workspace — it cannot
    reach any other tenant.
  - mintWorkspaceApiKey / listWorkspaceApiKeys / revokeWorkspaceApiKey

Routes
  - GET    /api/workspaces                         list
  - GET    /api/workspaces/[slug]                  details
  - POST   /api/workspaces/[slug]/provision        retry provisioning
  - GET    /api/workspaces/[slug]/keys             list keys
  - POST   /api/workspaces/[slug]/keys             mint key (token shown once)
  - DELETE /api/workspaces/[slug]/keys/[keyId]     revoke

UI
  - components/workspace/WorkspaceKeysPanel.tsx: identity card,
    keys CRUD with one-time secret reveal, and a "Connect Cursor"
    block with copy/download for:
      .cursor/rules/vibn-workspace.mdc — rule telling the agent
        about the API + workspace IDs + house rules
      ~/.cursor/mcp.json — MCP server registration with key
        embedded (server URL is /api/mcp; HTTP MCP route lands next)
      .env.local — VIBN_API_KEY + smoke-test curl
  - Slotted into existing /[workspace]/settings between Workspace
    and Notifications cards (no other layout changes).

projects/create
  - Resolves the user's workspace (creating + provisioning lazily).
  - Repos go under workspace.gitea_org (falls back to GITEA_ADMIN_USER
    for backwards compat).
  - Coolify services are created inside workspace.coolify_project_uuid
    (renamed {slug}-{appName} to stay unique within the namespace) —
    no more per-Vibn-project Coolify Project sprawl.
  - Stamps vibn_workspace_id on fs_projects.

lib/gitea
  - createOrg, getOrg, addOrgOwner, getUser
  - createRepo now routes /orgs/{owner}/repos when owner != admin

Also includes prior-turn auth hardening that was already in
authOptions.ts (CredentialsProvider for dev-local, isLocalNextAuth
cookie config) bundled in to keep the auth layer in one consistent
state.

.env.example
  - Documents GITEA_API_URL / GITEA_API_TOKEN / GITEA_ADMIN_USER /
    GITEA_WEBHOOK_SECRET and COOLIFY_URL / COOLIFY_API_TOKEN /
    COOLIFY_SERVER_UUID, with the canonical hostnames
    (git.vibnai.com, coolify.vibnai.com).

Post-deploy
  - Run once: curl -X POST https://vibnai.com/api/admin/migrate \\
      -H "x-admin-secret: \$ADMIN_MIGRATE_SECRET"
  - Existing users get a workspace row on next sign-in.
  - Existing fs_projects keep working (legacy gitea owner + their
    own per-project Coolify Projects); new projects use the
    workspace-scoped path.

Not in this commit (follow-ups)
  - Wiring requireWorkspacePrincipal into the rest of /api/projects/*
    so API keys can drive existing routes
  - HTTP MCP server at /api/mcp (the mcp.json snippet already
    points at the right URL — no client re-setup when it lands)
  - Backfill script to assign legacy fs_projects to a workspace

Made-with: Cursor
2026-04-20 17:17:12 -07:00
ccc6cc1da5 feat(justine): isolate design system — verbatim CSS + (justine) route group
- Add app/styles/justine/01-homepage.css: rules from 01_homepage.html scoped to [data-justine]
- Replace app/(marketing) with app/(justine): layout wraps data-justine + Plus Jakarta
- JustineHomePage/Nav/Footer: original class names (btn-ink, hero-grid, …) + inline styles from HTML
- Remove app/justine-marketing.css; move /features /pricing /privacy /terms under (justine)

Made-with: Cursor
2026-04-02 12:05:33 -07:00
74f8dc4282 fix(ui): make Justine palette visible on marketing + trim rainbow chrome
- Replace blue/purple gradients with ink gradient text and cream/parch CTA surface
- Step badges and transformation icons use primary (ink) fills
- /features page icons unified to text-primary; Lora section titles
- Tree view status colors use semantic tokens instead of blue/green

Made-with: Cursor
2026-04-01 21:09:18 -07:00
bada63452f feat(ui): apply Justine ink & parchment design system
- Map Justine tokens to shadcn CSS variables (--vibn-* aliases)
- Switch fonts to Inter + Lora via next/font (IBM Plex Mono for code)
- Base typography: body Inter, h1–h3 Lora; marketing hero + wordmark serif
- Project shell and global chrome use semantic colors
- Replace Outfit/Newsreader references across TSX inline styles

Made-with: Cursor
2026-04-01 21:03:40 -07:00
26429f3517 feat(agent): event timeline API, SSE stream, Coolify DDL, env template
- Add agent_session_events table + GET/POST events + SSE stream routes
- Build Agent tab: hydrate from events + EventSource while running
- entrypoint: create agent_sessions + agent_session_events on container start
- .env.example for AGENT_RUNNER_URL, AGENT_RUNNER_SECRET, DATABASE_URL

Made-with: Cursor
2026-04-01 11:48:55 -07:00
a11caafd22 feat: add 'Generate architecture' CTA banner on PRD page when arch not yet generated
Made-with: Cursor
2026-03-17 17:23:38 -07:00
8eb6c149cb fix: map Non-Functional Reqs to features_scope phase, remove circular 'Generated when PRD finalized' hint
Made-with: Cursor
2026-03-17 17:15:26 -07:00
f47205c473 rename: replace all user-facing 'Atlas' references with 'Vibn'
Updated UI text in: project-shell (tab label), AtlasChat (sender name),
FreshIdeaMain, TypeSelector, MigrateSetup, ChatImportSetup, FreshIdeaSetup,
CodeImportSetup, prd/page, build/page, projects/page, deployment/page,
activity/page, layout (page title/description), atlas-chat API route.
Code identifiers (AtlasChat component name, file names) unchanged.

Made-with: Cursor
2026-03-17 16:25:41 -07:00
317abf047b Fix auth redirect to use session email instead of hardcoded workspace
New users were being sent to /marks-account/projects. Now derives
workspace from the signed-in user's email so everyone lands on
their own workspace after Google OAuth.

Made-with: Cursor
2026-03-16 21:39:19 -07:00
63dded42a6 fix: repair JSX parse error in PRD page (nested ternary → && conditionals)
Made-with: Cursor
2026-03-10 17:29:05 -07:00
46efc41812 feat: add Architecture tab to PRD page and inject arch into COO context
- PRD page now has a tabbed view: PRD | Architecture
  Architecture tab renders apps, packages, infrastructure, integrations,
  and risk notes as structured cards. Only shown when arch doc exists.
- Advisor route now includes the architecture summary and key fields
  in the COO's knowledge context so the orchestrator knows what's
  been planned technically

Made-with: Cursor
2026-03-10 17:03:43 -07:00
cff5cd6014 fix: pass full PRD to COO without truncation
Made-with: Cursor
2026-03-10 16:36:47 -07:00
8f95270b12 feat: Assist COO routes through Orchestrator on agent runner
The advisor route now proxies to /orchestrator/chat on agents.vibnai.com
instead of calling Gemini directly. The Orchestrator (Claude Sonnet 4.6)
has full tool access — Gitea, Coolify, web search, memory, agent spawning.

- Build project knowledge_context from DB (name, vision, repo, PRD,
  phases, apps, recent sessions) and inject as COO persona + data
- Convert frontend history format (model→assistant) for the orchestrator
- Return orchestrator reply as streaming text response
- Session scoped per project for in-memory context persistence

Made-with: Cursor
2026-03-09 22:32:01 -07:00
ff0e1592fa feat(advisor): load real PRD, phases, sessions, apps into COO system prompt
Made-with: Cursor
2026-03-09 22:14:35 -07:00
1af5595e35 feat(tasks): move Tasks first in toolbar, add Tasks+PRD left nav and content
Made-with: Cursor
2026-03-09 22:02:01 -07:00
528d6bb1e3 fix: remove colon from Coolify project description — fails Coolify validation
Made-with: Cursor
2026-03-09 18:20:33 -07:00
6901a97db3 feat(migrate): wire GitHub PAT through to agent runner mirror call
MigrateSetup now sends the PAT field to the API; create route
forwards it as github_token so the agent runner can clone private repos.

Made-with: Cursor
2026-03-09 18:05:12 -07:00
0e204ced89 feat: store coolifyProjectUuid on project creation for Infrastructure panel
Made-with: Cursor
2026-03-09 17:40:21 -07:00
7979fd0518 fix: detect apps in any repo structure, not just turborepo or flagged imports
Made-with: Cursor
2026-03-09 17:23:38 -07:00
22f4c4f1c3 fix: preview URL resolved from Gitea repo via Coolify git_repository match
Made-with: Cursor
2026-03-09 17:14:55 -07:00
5778abe6c3 feat: add live app preview panel with iframe, URL bar, and reload
Made-with: Cursor
2026-03-09 17:07:33 -07:00
70c94dc60c feat: tool icons drive left nav section, remove inner pills
Made-with: Cursor
2026-03-09 16:49:47 -07:00
853e41705f feat: split top navbar to align with chat/content panels, fix Gemini API key
- Top bar left section (320px) = logo + project name, aligns with chat panel
- Top bar right section = Build|Market|Assist pills + tool icons (Preview, Tasks, Code, Design, Backend) + avatar
- Read GOOGLE_API_KEY inside POST handler (not top-level) to ensure env is resolved at request time

Made-with: Cursor
2026-03-09 16:17:31 -07:00
1ef3f9baa3 feat: top navbar (Build|Market|Assist) + persistent Assist chat in shell
- New top navbar in ProjectShell: logo + project name | Build | Market |
  Assist tabs | user avatar — replaces the left icon sidebar for project pages
- CooChat extracted to components/layout/coo-chat.tsx and moved into the
  shell so it persists across Build/Market/Assist route changes
- Build page inner layout simplified: inner nav (200px) + file viewer,
  no longer owns the chat column
- Layout: [top nav 48px] / [Assist chat 320px | content flex]

Made-with: Cursor
2026-03-09 15:51:48 -07:00
01848ba682 feat: add persistent COO/Assist chat as left-side primary AI interface
- New CooChat component: streaming Gemini-backed advisor chat, message
  bubbles, typing cursor animation, Shift+Enter for newlines
- New /api/projects/[projectId]/advisor streaming endpoint: builds a
  COO system prompt from project context (name, description, vision,
  repo), proxies Gemini SSE stream back to the client
- Restructured BuildHubInner layout:
    Left (340px): CooChat — persistent across all Build sections
    Inner nav (200px): Build pills + contextual items (apps, tree, surfaces)
    Main area: File viewer for Code, Layouts content, Infra content
- AgentMode removed from main view — execution surfaces via COO delegation

Made-with: Cursor
2026-03-09 15:34:41 -07:00
86f8960aa3 refactor: redesign Build page layout — sidebar nav+tree, agent as main, file viewer on right
- B (left sidebar, 260px): project header, Build pills (Code/Layouts/Infra),
  app list, file tree embedded below active app
- D (center): AgentMode as primary content; sessions shown as a horizontal
  chip strip at the top instead of a 220px left sidebar
- Right (460px): FileViewer — shows file selected in B's tree / code changes
- F (bottom): Terminal collapsible strip unchanged
- Split CodeContent into FileTree + FileViewer components; lifted file
  selection state to BuildHubInner so B and Right share it

Made-with: Cursor
2026-03-09 15:00:28 -07:00
2e0bc95bb0 refactor: replace code mode tabs with persistent Browse | Agent split + collapsible terminal
Removes the Browse/Agent/Terminal tab switcher from the code section.
Browse (file tree + viewer) is now the left pane, Agent chat is a
fixed 420px right pane, and Terminal is a collapsible strip at the
bottom — all visible simultaneously.

Made-with: Cursor
2026-03-09 14:29:35 -07:00
01c2d33208 fix: strip backticks from CODEBASE_MAP.md path parsing
Paths wrapped in backticks like `app/` were being captured with
the backtick character, producing invalid app names and paths.

Made-with: Cursor
2026-03-09 14:21:25 -07:00
65adcd4897 feat: detect apps for imported non-turborepo projects
- Fall back to CODEBASE_MAP.md parsing when no apps/ dir exists
- Further fallback: scan top-level dirs for deployable app signals
  (package.json, Dockerfile, requirements.txt, next.config.*, etc.)
- Skips docs, scripts, keys, and other non-app directories
- Returns isImport flag to frontend for context

Made-with: Cursor
2026-03-09 11:52:10 -07:00
9c277fd8e3 feat: add GitHub import flow, project delete fix, and analyze API
- Mirror GitHub repos to Gitea as-is on import (skip scaffold)
- Auto-trigger ImportAnalyzer agent after successful mirror
- Add POST/GET /api/projects/[projectId]/analyze route
- Fix project delete button visibility (was permanently opacity:0)
- Store isImport, importAnalysisStatus, importAnalysisJobId on projects

Made-with: Cursor
2026-03-09 11:30:51 -07:00
fc59333383 feat: auto-approve UI + session status approved
- sessions POST: look up coolifyServiceUuid, pass autoApprove:true to runner
- sessions PATCH: approved added to terminal statuses (sets completed_at)
- build/page.tsx: approved status, STATUS_COLORS/LABELS for "Shipped",
  auto-committed UI in changed files panel, bottom bar for approved state
- Architecture doc: fully updated with current state

Made-with: Cursor
2026-03-07 13:17:33 -08:00
7b228ebad2 fix(agent): context-aware task input, auto-select active session
- Running/pending: input locked with "agent is working" message
- Done: shows "+ Follow up" and "New task" buttons instead of open input
- No session: normal new-task input (unchanged UX)
- On mount: auto-selects the most recent running/pending session,
  falls back to latest session — so navigating away and back doesn't
  lose context and doesn't require manual re-selection

Made-with: Cursor
2026-03-07 13:01:16 -08:00
7f61295637 fix: remove ::uuid casts on project_id/p.id in all agent session routes
Made-with: Cursor
2026-03-07 12:44:45 -08:00
8c19dc1802 feat: agent session retry + follow-up UX
- retry/route.ts: reset failed/stopped session and re-fire agent runner
  with optional continueTask follow-up text
- build/page.tsx: Retry button and Follow up input appear on failed/stopped
  sessions so users can continue without losing context or creating a
  duplicate session; task input hint clarifies each Run = new session

Made-with: Cursor
2026-03-07 12:25:58 -08:00
28b48b74af fix: surface agent_sessions 500 and add db migration
- sessions/route.ts: replace inline CREATE TABLE DDL with a lightweight
  existence check; add `details` to all 500 responses; fix type-unsafe
  `p.id = $1::uuid` comparisons to `p.id::text = $1` to avoid the
  Postgres `text = uuid` operator error
- app/api/admin/migrate: one-shot idempotent migration endpoint secured
  with ADMIN_MIGRATE_SECRET, creates fs_* tables + agent_sessions
- scripts/migrate-fs-tables.sql: formal schema for all fs_* tables

Made-with: Cursor
2026-03-07 12:16:16 -08:00
f7d38317b2 fix: add ::uuid casts to all agent_sessions queries
PostgreSQL can't implicitly coerce text params to UUID columns.
Add explicit ::uuid casts on id and project_id in all agent session
routes (list, get, patch, stop, approve).

Made-with: Cursor
2026-03-07 11:49:40 -08:00
18f61fe95c approve & commit flow + adaptive polling in Agent mode
- Wire Approve & commit button: shows commit message input, calls
  POST /api/.../sessions/[id]/approve which asks agent runner to
  git commit + push, then marks session as approved in DB
- Adaptive polling: 500ms while session running, 5s when idle —
  output feels near-real-time without hammering the API
- Auto-refresh session list when a session completes
- Open in Theia links to theia.vibnai.com (escape hatch for manual edits)

Made-with: Cursor
2026-03-07 11:36:55 -08:00
61a43ad9b4 pass giteaRepo to agent runner; add runner secret auth on PATCH
- Sessions route now reads giteaRepo from project.data and forwards it
  to /agent/execute so the runner can clone/update the correct repo
- PATCH route now validates x-agent-runner-secret header to prevent
  unauthorized session output injection

Made-with: Cursor
2026-03-06 18:01:33 -08:00
ad3abd427b feat: agent execution scaffold — sessions DB, API, and Browse/Agent/Terminal UI
Session model:
- agent_sessions table (auto-created on first use): id, project_id,
  app_name, app_path, task, status, output (JSONB log), changed_files,
  error, timestamps
- POST /agent/sessions — create session, fires off to agent-runner
  (gracefully degrades when runner not yet wired)
- GET  /agent/sessions — list sessions newest first
- GET  /agent/sessions/[id] — full session state for polling
- PATCH /agent/sessions/[id] — internal: agent-runner appends output lines
- POST /agent/sessions/[id]/stop — stop running session

Build > Code section now has three mode tabs:
- Browse — existing file tree + code viewer
- Agent — task input, session list sidebar, live output stream,
           changed files panel, Approve & commit / Open in Theia actions,
           2s polling (Phase 3 will replace with WebSocket)
- Terminal — xterm.js placeholder (Phase 4)

Architecture documented in AGENT_EXECUTION_ARCHITECTURE.md

Made-with: Cursor
2026-03-06 17:56:10 -08:00
3cd477c295 feat: restructure project nav to Atlas | PRD | Build | Growth | Assist | Analytics
Tab bar:
- Removed: Design, Launch, Grow, Insights, Settings tabs
- Added: Growth, Assist, Analytics as top-level tabs
- Build remains, now a full hub

Build hub (/build):
- Left sub-nav groups: Code (apps), Layouts (surfaces), Infrastructure (6 items)
- Code section: scoped file browser per selected app
- Layouts section: surface overview cards with Edit link to /design
- Infrastructure section: summary panel linking to /infrastructure?tab=

Growth (/growth):
- Left nav: Marketing Site, Communications, Channels, Pages
- Each section: description + feature item grid + feedback CTA

Assist (/assist):
- Left nav: Emails, Chat Support, Support Site, Communications
- Each section: description + feature item grid + feedback CTA

Analytics (/analytics):
- Left nav: Customers, Usage, Events, Reports
- Each section: description + feature item grid + feedback CTA

Made-with: Cursor
2026-03-06 14:36:11 -08:00
3770ba1853 feat: Infrastructure section with 6 sub-sections (Builds, Databases, Services, Environment, Domains, Logs)
- Sidebar Infrastructure replaced with 6 named rows linking to /infrastructure?tab=
- New /infrastructure page with left sub-nav and per-tab content panels:
  Builds — lists deployed Coolify apps with live status
  Databases — coming soon placeholder
  Services — coming soon placeholder
  Environment — variable table with masked values (scaffold)
  Domains — lists configured domains with SSL status
  Logs — dark terminal panel, ready to stream
- Dim state on rows reflects whether data exists (e.g. no domains = dim)

Made-with: Cursor
2026-03-06 14:18:03 -08:00
39167dbe45 feat: deep-link sidebar Layouts to specific design surface
- Sidebar Layouts items now link to /design?surface=<surfaceId>
- Design page reads ?surface= param and opens that surface directly
- DesignPage split into DesignPageInner + Suspense wrapper so
  useSearchParams works in the Next.js static build

Made-with: Cursor
2026-03-06 14:12:29 -08:00
812645cae8 feat: scope Build file browser to selected app, rename Apps → Build
- Sidebar "Apps" section renamed to "Build"
- Each app now links to /build?app=<name>&root=<path> so the browser
  opens scoped to that app's subdirectory only
- Build page shows an empty-state prompt when no app is selected
- File tree header shows the selected app name, breadcrumb shows
  relative path within the app (strips the root prefix)
- Wraps useSearchParams in Suspense for Next.js static rendering

Made-with: Cursor
2026-03-06 13:51:01 -08:00
e08fcf674b feat: VIBN-branded file browser on Build tab + sidebar status dot
- Build page: full file tree (lazy-load dirs) + code preview panel
  with line numbers and token-level syntax colouring (VS Code dark theme)
- New API route /api/projects/[id]/file proxies Gitea contents API
  returning directory listings or decoded file content
- Sidebar Apps section now links to /build instead of raw Gitea URL
- Status indicator replaced with a proper coloured dot (amber/blue/green)
  alongside the status label text

Made-with: Cursor
2026-03-06 13:37:38 -08:00
ab100f2e76 feat: implement 4 project type flows with unique AI experiences
- New multi-step CreateProjectFlow replaces 2-step modal with TypeSelector
  and 4 setup components (Fresh Idea, Chat Import, Code Import, Migrate)
- overview/page.tsx routes to unique main component per creationMode
- FreshIdeaMain: wraps AtlasChat with post-discovery decision banner
  (Generate PRD vs Plan MVP Test)
- ChatImportMain: 3-stage flow (intake → extracting → review) with
  editable insight buckets (decisions, ideas, questions, architecture, users)
- CodeImportMain: 4-stage flow (input → cloning → mapping → surfaces)
  with architecture map and surface selection
- MigrateMain: 5-stage flow with audit, review, planning, and migration
  plan doc with checkbox-tracked tasks and non-destructive warning banner
- New API routes: analyze-chats, analyze-repo, analysis-status,
  generate-migration-plan (all using Gemini)
- ProjectShell: accepts creationMode prop, filters/renames tabs per type
  (code-import hides PRD, migration hides PRD/Grow/Insights, renames Atlas tab)
- Right panel adapts content based on creationMode

Made-with: Cursor
2026-03-06 12:48:28 -08:00
24812df89b design-surfaces: explicit ::text cast on every query param
Add ::text cast to all $1/$2 parameters so PostgreSQL never needs
to infer types. Split SELECT and UPDATE into separate try/catch blocks
with distinct error labels so logs show exactly which query fails.

Made-with: Cursor
2026-03-06 11:29:57 -08:00
53b098ce6a Fix Lock In 42P18: cast id::text to resolve parameter type ambiguity
PostgreSQL could not determine the type of $2 in 'WHERE id = $2'
when id column type is UUID. Casting the column (id::text = $1)
sidesteps the extended-protocol type inference issue. Also moves
projectId to $1 to match the proven working pattern in other routes.

Made-with: Cursor
2026-03-06 11:23:31 -08:00
7eaf1ca4f1 Filter color palettes by dark/light mode
- Add themeMode?: 'dark'|'light' to ThemeColor (unset = any mode)
- Tag all DaisyUI themes: 11 dark (synthwave, aqua, luxury, night, etc.)
  and 6 light (light, cupcake, valentine, cyberpunk, retro, winter)
- Tag HeroUI Marketing themes: purple/blue/teal/modern=light, dark=dark
- Aceternity accent palettes stay untagged (work with either mode)
- Filter availableColorThemes in SurfaceSection by designConfig.mode
- Auto-reset active palette when mode switches makes previously
  selected palette incompatible

Made-with: Cursor
2026-03-06 11:03:22 -08:00
5e4cce55de Fix Lock In 500 error: fs_projects has no updated_at column
The PATCH handler used SQL 'updated_at = NOW()' which doesn't exist
on fs_projects (all timestamps live inside the data JSONB blob).
Rewrote to use the same read-merge-write pattern as other working
routes: fetch current data, merge in JS, write back as data::jsonb.

Made-with: Cursor
2026-03-06 10:56:21 -08:00