Skip to content

VER-500: complete operator project lifecycle management - #35

Merged
mgunnin merged 4 commits into
stagingfrom
feat/ver-500-project-lifecycle-management
Jul 31, 2026
Merged

VER-500: complete operator project lifecycle management#35
mgunnin merged 4 commits into
stagingfrom
feat/ver-500-project-lifecycle-management

Conversation

@mgunnin

@mgunnin mgunnin commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add typed project detail, update, and soft-archive client operations
  • share one accessible validated form across project creation and editing
  • add explicit archive confirmation, failure recovery, and cache synchronization
  • cover API persistence, hook cache behavior, and project detail interactions

Fixes VER-500

Verification

  • pnpm test: 29 files, 348 tests passed
  • pnpm --filter ui test: 15 files, 49 tests passed
  • pnpm --filter ui exec tsc -b
  • pnpm run build
  • git diff --check
  • Impeccable detector: clean
  • GitNexus detect-changes: medium scope, expected project lifecycle flows
  • autoreview --mode local: clean

CodeRabbit / Review Notes

  • Ready for CodeRabbit and Qodo review.

Risk / Rollout Notes

  • Uses the existing PATCH and soft-archive endpoints; no migrations or environment changes.
  • Archive preserves project data and history and returns the operator to the project list.

Screenshots / UI Notes

  • UI behavior will be verified on the staging deployment before production promotion.

View with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is enabled.

Summary by CodeRabbit

  • New Features
    • Added project detail pages with loading, retry, and error states.
    • Added project editing with fields for name, description, status, and repository URL.
    • Added project archiving with confirmation, success notifications, and navigation back to the project list.
    • Added responsive project actions, including edit and archive controls.
  • Bug Fixes
    • Repository links now accept only valid HTTP(S) URLs and unsafe links are hidden.
    • Project updates and archived status remain consistent across project lists and detail views.

@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
eidolon Ready Ready Preview Jul 31, 2026 7:34pm

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d1de78ee-f2df-4385-8ff8-5f8f3bcb01ec

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds shared HTTP(S) repository URL validation, project detail APIs and hooks, a reusable create/edit modal, project editing and soft-archiving actions, cache synchronization, and server and UI tests.

Changes

Project management workflows

Layer / File(s) Summary
Project URL validation and lifecycle tests
server/src/routes/projects.ts, server/src/__tests__/projects.test.ts
Project creation and update requests now accept only HTTP(S) repository URLs. Tests cover updates, detail reads, soft-archiving, and rejected invalid URLs.
Project API and cache operations
ui/src/lib/api.ts, ui/src/lib/hooks.ts, ui/test/project-hooks.test.tsx
The UI adds project detail retrieval, partial updates, archiving, and React Query cache synchronization for project lists and detail entries.
Reusable project create and edit form
ui/src/components/projects/ProjectFormModal.tsx, ui/src/components/projects/CreateProjectModal.tsx, ui/test/CreateProjectModal.test.tsx
A shared controlled modal handles project creation and editing, validation, mutation errors, pending states, accessible fields, and reset behavior.
Project detail actions and lifecycle coverage
ui/src/pages/ProjectDetail.tsx, ui/test/ProjectDetail.test.tsx
The detail page loads one project, renders safe repository links, supports editing and archiving, displays errors and notifications, and navigates after successful archiving.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProjectDetail
  participant ProjectHooks
  participant ProjectAPI
  participant ProjectServer
  ProjectDetail->>ProjectHooks: load, update, or archive project
  ProjectHooks->>ProjectAPI: call project endpoint
  ProjectAPI->>ProjectServer: GET, PATCH, or DELETE request
  ProjectServer-->>ProjectAPI: project response
  ProjectAPI-->>ProjectHooks: persisted project
  ProjectHooks-->>ProjectDetail: update caches and render state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding complete operator project lifecycle management.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ver-500-project-lifecycle-management

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Project lifecycle: add project detail/edit/archive flows with shared form and cache sync

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add typed project detail, update (PATCH), and soft-archive (DELETE) client operations.
• Reuse a single validated modal form for both project creation and editing.
• Add edit/archive controls on Project Detail with confirmation, retry UX, and cache
 synchronization.
Diagram

graph TD
  PD["ProjectDetail page"] --> Hooks["Project hooks"] --> Api["api.ts client"] --> Srv["Server Projects API"]
  PD --> Form["ProjectFormModal"] --> Hooks
  PD --> Arch["Archive confirm modal"] --> Hooks
  Hooks --> Cache[("React Query cache")]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Invalidate-only caching (no setQueryData)
  • ➕ Simpler mutation success handlers
  • ➕ Avoids accidental cache shape mismatches
  • ➖ Slower perceived UX (extra refetch before UI reflects changes)
  • ➖ More loading states/flashing on detail and list views
2. Optimistic updates with rollback (onMutate)
  • ➕ Fastest UX for edits/archives
  • ➕ Reduced time-to-feedback even on higher latency
  • ➖ More complex correctness story (rollback, error states, race conditions)
  • ➖ Harder to test and reason about than server-confirmed updates

Recommendation: Current approach (server-confirmed mutation + setQueryData + invalidate) is a good middle ground: it updates list/detail immediately for responsiveness while still revalidating to avoid drift. Optimistic updates could be considered later if latency becomes a problem, but they add meaningful complexity.

Files changed (9) +728 / -196

Enhancement (4) +427 / -8
ProjectFormModal.tsxIntroduce shared create/edit project form modal with validation + error recovery +226/-0

Introduce shared create/edit project form modal with validation + error recovery

• Adds a reusable modal that supports both create and edit modes, including zod validation, pending-state gating, and inline failure messaging that preserves operator edits. Wires to either useCreateProject or useUpdateProject depending on whether a project prop is provided.

ui/src/components/projects/ProjectFormModal.tsx

api.tsAdd typed get/update/archive project client operations +24/-0

Add typed get/update/archive project client operations

• Introduces UpdateProjectInput and adds getProject (detail read), updateProject (PATCH), and archiveProject (DELETE) API calls. Enables typed lifecycle operations used by new hooks and UI flows.

ui/src/lib/api.ts

hooks.tsAdd React Query hooks for project detail, update, and archive with cache sync +50/-0

Add React Query hooks for project detail, update, and archive with cache sync

• Adds useProject for canonical per-project detail reads. Adds useUpdateProject and useArchiveProject mutations that update both detail and list caches and invalidate queries to ensure eventual consistency after persistence.

ui/src/lib/hooks.ts

ProjectDetail.tsxAdd edit and archive UX to ProjectDetail using canonical detail query +127/-8

Add edit and archive UX to ProjectDetail using canonical detail query

• Switches from list-derived project lookup to a dedicated detail query with explicit error handling and retry. Adds Edit Project (shared form modal), repo URL display, and an Archive Project confirmation modal with retryable error messaging; navigates back to the project list and toasts on success.

ui/src/pages/ProjectDetail.tsx

Refactor (1) +7 / -186
CreateProjectModal.tsxRefactor create modal to reuse shared ProjectFormModal +7/-186

Refactor create modal to reuse shared ProjectFormModal

• Replaces bespoke create-only form state/validation with the new shared ProjectFormModal. Keeps the external CreateProjectModal API the same while delegating behavior to the shared component.

ui/src/components/projects/CreateProjectModal.tsx

Tests (4) +294 / -2
projects.test.tsAdd lifecycle API tests for update + soft-archive durability +73/-0

Add lifecycle API tests for update + soft-archive durability

• Adds coverage that a project can be updated via PATCH and then read back via GET with persisted fields. Adds a soft-archive (DELETE) assertion and verifies archived status on subsequent detail reads, plus a validation test ensuring invalid repoUrl updates are rejected without mutating canonical state.

server/src/tests/projects.test.ts

CreateProjectModal.test.tsxUpdate CreateProjectModal tests to support shared form dependencies +6/-0

Update CreateProjectModal tests to support shared form dependencies

• Extends hook mocks to include useUpdateProject so the shared ProjectFormModal can mount in tests without runtime errors. Keeps existing create-path assertions working under the refactor.

ui/test/CreateProjectModal.test.tsx

ProjectDetail.test.tsxAdd ProjectDetail tests for edit and archive lifecycle controls +143/-0

Add ProjectDetail tests for edit and archive lifecycle controls

• Adds tests covering editing through the shared form (including keyboard submit), preserving edits on update failure, and archive confirmation behavior including failure display and post-success navigation/toast.

ui/test/ProjectDetail.test.tsx

project-hooks.test.tsxAdd hook tests for canonical detail reads and cache synchronization +72/-2

Add hook tests for canonical detail reads and cache synchronization

• Adds coverage for useProject detail loading, plus verifies useUpdateProject and useArchiveProject update both list and detail caches and trigger invalidation as expected. Extends API mocks to include getProject/updateProject/archiveProject.

ui/test/project-hooks.test.tsx

@qodo-code-review

qodo-code-review Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 2 rules

Grey Divider


Action required

1. Repo URL scheme unsafe ✓ Resolved 🐞 Bug ⛨ Security
Description
ProjectDetail renders project.repoUrl directly into an external link, but repoUrl validation on
both client and server only checks URL syntax (not allowed schemes), so a persisted javascript:
URL could execute code when clicked. This risk is introduced/activated by this PR because it adds
the new rendering path for repoUrl.
Code

ui/src/pages/ProjectDetail.tsx[R108-117]

+            {project.repoUrl && (
+              <a
+                href={project.repoUrl}
+                target="_blank"
+                rel="noreferrer"
+                className="mt-1 inline-flex max-w-full items-center gap-1 text-xs text-accent hover:underline"
+              >
+                <span className="truncate">{project.repoUrl}</span>
+                <ExternalLink className="h-3 w-3 shrink-0" aria-hidden="true" />
+              </a>
Relevance

●●● Strong

Security hardening for user-controlled external links is typically accepted; no rejection precedent
found.

PR-#1
PR-#8

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR adds a clickable anchor using href={project.repoUrl}; repoUrl is user-controlled and the
repo currently validates it only with zod URL syntax checks (no scheme allowlist), so a
javascript: URL can pass validation and become a clickable script execution vector.

ui/src/pages/ProjectDetail.tsx[108-117]
ui/src/components/projects/ProjectFormModal.tsx[23-31]
server/src/routes/projects.ts[10-22]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The UI now renders `project.repoUrl` into an `<a href>` in `ProjectDetail`. Because repoUrl is validated with `z.url()` / `z.string().url()` only, non-HTTP(S) schemes (e.g. `javascript:`) can still be considered “valid” URLs and could be stored; clicking the link can execute script.

## Issue Context
- Client-side form validation currently uses `z.url(...)` (syntax-only).
- Server-side validation currently uses `z.string().url()` (syntax-only).
- The PR introduces rendering the value as a clickable link.

## Fix Focus Areas
- ui/src/pages/ProjectDetail.tsx[108-117]
- ui/src/components/projects/ProjectFormModal.tsx[23-31]
- server/src/routes/projects.ts[10-22]

## Implementation notes
1. **Server-side (authoritative) validation**: refine `repoUrl` to only allow `http:` and `https:`.
  - Example: `z.string().url().refine((v) => ['http:','https:'].includes(new URL(v).protocol), 'Repo URL must start with http(s)')` (keeping nullable/optional as needed).
2. **Client-side validation**: mirror the same restriction in `ProjectFormModal` so users get immediate feedback.
3. **Defensive rendering**: in `ProjectDetail`, either (a) only render the link if the parsed protocol is http/https, or (b) normalize/strip invalid schemes before rendering.
4. (Optional hardening/clarity) set `rel="noopener noreferrer"` when using `target="_blank"`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Repo URL allows credentials 🐞 Bug ⛨ Security
Description
Server/client URL validation only enforces http(s) protocol, so credential-bearing URLs (e.g.,
https://token@github.com/org/repo) can be persisted. ProjectDetail then renders the full unredacted
URL as link text and href, potentially exposing embedded secrets to anyone who can view the project.
Code

server/src/routes/projects.ts[R10-17]

+const HttpUrl = z.string().url().refine((value) => {
+  try {
+    const protocol = new URL(value).protocol;
+    return protocol === 'http:' || protocol === 'https:';
+  } catch {
+    return false;
+  }
+}, 'Repository URL must start with http:// or https://');
Relevance

●●● Strong

Security-hardening validation change; repo tends to accept guardrails and correctness fixes even
without exact precedent.

PR-#1
PR-#8

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The server and client validators check only the parsed URL protocol, so URLs with embedded
credentials still pass validation, and ProjectDetail renders the persisted value as a clickable link
and visible text.

server/src/routes/projects.ts[10-17]
server/src/routes/projects.ts[23-30]
ui/src/lib/urls.ts[1-7]
ui/src/components/projects/ProjectFormModal.tsx[28-33]
ui/src/pages/ProjectDetail.tsx[81-120]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Repository URLs currently validate only that the scheme is `http:` or `https:`. This still permits URLs containing userinfo credentials (username/password/token in the URL), which can then be stored and rendered verbatim in the UI.

### Issue Context
Both the server (`HttpUrl`) and client (`isHttpUrl` + Zod refine) share the same limitation. The detail page renders the stored URL directly as an external link.

### Fix Focus Areas
- server/src/routes/projects.ts[10-17]
- ui/src/lib/urls.ts[1-7]
- ui/src/components/projects/ProjectFormModal.tsx[28-33]
- ui/src/pages/ProjectDetail.tsx[81-120]

### Suggested fix
1. Update server `HttpUrl` refine to additionally require:
  - `new URL(value).username === ""`
  - `new URL(value).password === ""`
  (Optionally also reject non-empty `hash`/suspicious query params if desired.)
2. Update `ui/src/lib/urls.ts:isHttpUrl` to apply the same username/password checks.
3. Consider hardening the display layer (`ProjectDetail`) by redacting userinfo (e.g., render `url.origin + url.pathname + url.search + url.hash` without credentials) or by suppressing the link when userinfo is present, as defense-in-depth.
4. Add/extend tests to cover credential-bearing URLs being rejected/redacted.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Previous review results

Review updated until commit af4f1a7 ⚖️ Balanced

Results up to commit e7ce3bd ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. Repo URL scheme unsafe ✓ Resolved 🐞 Bug ⛨ Security
Description
ProjectDetail renders project.repoUrl directly into an external link, but repoUrl validation on
both client and server only checks URL syntax (not allowed schemes), so a persisted javascript:
URL could execute code when clicked. This risk is introduced/activated by this PR because it adds
the new rendering path for repoUrl.
Code

ui/src/pages/ProjectDetail.tsx[R108-117]

+            {project.repoUrl && (
+              <a
+                href={project.repoUrl}
+                target="_blank"
+                rel="noreferrer"
+                className="mt-1 inline-flex max-w-full items-center gap-1 text-xs text-accent hover:underline"
+              >
+                <span className="truncate">{project.repoUrl}</span>
+                <ExternalLink className="h-3 w-3 shrink-0" aria-hidden="true" />
+              </a>
Relevance

●●● Strong

Security hardening for user-controlled external links is typically accepted; no rejection precedent
found.

PR-#1
PR-#8

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR adds a clickable anchor using href={project.repoUrl}; repoUrl is user-controlled and the
repo currently validates it only with zod URL syntax checks (no scheme allowlist), so a
javascript: URL can pass validation and become a clickable script execution vector.

ui/src/pages/ProjectDetail.tsx[108-117]
ui/src/components/projects/ProjectFormModal.tsx[23-31]
server/src/routes/projects.ts[10-22]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The UI now renders `project.repoUrl` into an `<a href>` in `ProjectDetail`. Because repoUrl is validated with `z.url()` / `z.string().url()` only, non-HTTP(S) schemes (e.g. `javascript:`) can still be considered “valid” URLs and could be stored; clicking the link can execute script.

## Issue Context
- Client-side form validation currently uses `z.url(...)` (syntax-only).
- Server-side validation currently uses `z.string().url()` (syntax-only).
- The PR introduces rendering the value as a clickable link.

## Fix Focus Areas
- ui/src/pages/ProjectDetail.tsx[108-117]
- ui/src/components/projects/ProjectFormModal.tsx[23-31]
- server/src/routes/projects.ts[10-22]

## Implementation notes
1. **Server-side (authoritative) validation**: refine `repoUrl` to only allow `http:` and `https:`.
  - Example: `z.string().url().refine((v) => ['http:','https:'].includes(new URL(v).protocol), 'Repo URL must start with http(s)')` (keeping nullable/optional as needed).
2. **Client-side validation**: mirror the same restriction in `ProjectFormModal` so users get immediate feedback.
3. **Defensive rendering**: in `ProjectDetail`, either (a) only render the link if the parsed protocol is http/https, or (b) normalize/strip invalid schemes before rendering.
4. (Optional hardening/clarity) set `rel="noopener noreferrer"` when using `target="_blank"`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit c577575 ⚖️ Balanced


🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Repo URL allows credentials 🐞 Bug ⛨ Security
Description
Server/client URL validation only enforces http(s) protocol, so credential-bearing URLs (e.g.,
https://token@github.com/org/repo) can be persisted. ProjectDetail then renders the full unredacted
URL as link text and href, potentially exposing embedded secrets to anyone who can view the project.
Code

server/src/routes/projects.ts[R10-17]

+const HttpUrl = z.string().url().refine((value) => {
+  try {
+    const protocol = new URL(value).protocol;
+    return protocol === 'http:' || protocol === 'https:';
+  } catch {
+    return false;
+  }
+}, 'Repository URL must start with http:// or https://');
Relevance

●●● Strong

Security-hardening validation change; repo tends to accept guardrails and correctness fixes even
without exact precedent.

PR-#1
PR-#8

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The server and client validators check only the parsed URL protocol, so URLs with embedded
credentials still pass validation, and ProjectDetail renders the persisted value as a clickable link
and visible text.

server/src/routes/projects.ts[10-17]
server/src/routes/projects.ts[23-30]
ui/src/lib/urls.ts[1-7]
ui/src/components/projects/ProjectFormModal.tsx[28-33]
ui/src/pages/ProjectDetail.tsx[81-120]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Repository URLs currently validate only that the scheme is `http:` or `https:`. This still permits URLs containing userinfo credentials (username/password/token in the URL), which can then be stored and rendered verbatim in the UI.

### Issue Context
Both the server (`HttpUrl`) and client (`isHttpUrl` + Zod refine) share the same limitation. The detail page renders the stored URL directly as an external link.

### Fix Focus Areas
- server/src/routes/projects.ts[10-17]
- ui/src/lib/urls.ts[1-7]
- ui/src/components/projects/ProjectFormModal.tsx[28-33]
- ui/src/pages/ProjectDetail.tsx[81-120]

### Suggested fix
1. Update server `HttpUrl` refine to additionally require:
  - `new URL(value).username === ""`
  - `new URL(value).password === ""`
  (Optionally also reject non-empty `hash`/suspicious query params if desired.)
2. Update `ui/src/lib/urls.ts:isHttpUrl` to apply the same username/password checks.
3. Consider hardening the display layer (`ProjectDetail`) by redacting userinfo (e.g., render `url.origin + url.pathname + url.search + url.hash` without credentials) or by suppressing the link when userinfo is present, as defense-in-depth.
4. Add/extend tests to cover credential-bearing URLs being rejected/redacted.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Qodo Logo

Comment thread ui/src/pages/ProjectDetail.tsx Outdated
@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo Fixer

✅ Committed (1) · ☑ Fixed (1)

Grey Divider

Commits pushed directly to this PR — no separate fix PR opened.

Process — 1 fixed
  • ☑ Fixed: Repo URL scheme unsafe

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
ui/src/lib/hooks.ts (1)

132-157: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared cache sync and drop the redundant invalidation.

The onSuccess bodies of useUpdateProject and useArchiveProject are identical. Extract one helper to keep them in sync.

React Query matches query keys by prefix. The list key ["projects", companyId] is a prefix of the detail key ["projects", companyId, project.id], so the first invalidateQueries call already invalidates the detail query. The second call adds no effect. The same overlap makes every list invalidation refetch all cached project details. If you want the two scopes to be independent, add a discriminator segment to the detail key, for example ["projects", companyId, "detail", projectId], and update useProject and ui/test/project-hooks.test.tsx accordingly.

♻️ Proposed refactor
+function syncProjectCaches(qc: ReturnType<typeof useQueryClient>, companyId: string, project: api.Project) {
+  qc.setQueryData(["projects", companyId, project.id], project);
+  qc.setQueryData<api.Project[]>(["projects", companyId], (current) =>
+    current?.map((item) => (item.id === project.id ? project : item)),
+  );
+  qc.invalidateQueries({ queryKey: ["projects", companyId] });
+}
+
 export function useUpdateProject(companyId: string) {
   const qc = useQueryClient();
@@
-    onSuccess: (project) => {
-      qc.setQueryData(["projects", companyId, project.id], project);
-      qc.setQueryData<api.Project[]>(["projects", companyId], (current) =>
-        current?.map((item) => item.id === project.id ? project : item),
-      );
-      qc.invalidateQueries({ queryKey: ["projects", companyId] });
-      qc.invalidateQueries({ queryKey: ["projects", companyId, project.id] });
-    },
+    onSuccess: (project) => syncProjectCaches(qc, companyId, project),
   });
 }

Apply the same change to useArchiveProject.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/lib/hooks.ts` around lines 132 - 157, Extract the identical project
cache synchronization from useUpdateProject and useArchiveProject into a shared
helper, then call it from both onSuccess handlers. Remove the redundant detail
invalidateQueries call, since invalidating ["projects", companyId] already
covers the detail key; preserve the existing query-key structure unless
independently updating all related consumers.
ui/test/CreateProjectModal.test.tsx (1)

19-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the shared isPending mock in useUpdateProject.

useCreateProject reads mocks.isPending, but this mock hardcodes false. A test that toggles mocks.isPending then exercises the create path only.

♻️ Proposed change
   useUpdateProject: () => ({
     mutate: mocks.updateProject,
     reset: mocks.reset,
-    isPending: false,
+    isPending: mocks.isPending,
   }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/test/CreateProjectModal.test.tsx` around lines 19 - 23, Update the
useUpdateProject mock to return the shared mocks.isPending value instead of
hardcoding false, so tests that toggle pending state exercise both create and
update paths consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/src/routes/projects.ts`:
- Around line 10-13: Guard the protocol validation in repoUrlSchema at
server/src/routes/projects.ts lines 10-13 with try/catch so malformed URLs
produce a 400 validation error. In
ui/src/components/projects/ProjectFormModal.tsx lines 27-35, replace the union
with one guarded refinement accepting empty strings and valid http(s) URLs;
extract and reuse the client-side predicate in isSafeRepoUrl at
ui/src/pages/ProjectDetail.tsx lines 30-36, and add a form test submitting
github.com/org/repo that asserts a field error.

In `@ui/src/components/projects/ProjectFormModal.tsx`:
- Around line 68-76: Update the initialization useEffect in ProjectFormModal so
it is not triggered by every project object identity change while the modal is
open. Key the effect on the project identifier (alongside open as needed), while
preserving the existing field initialization and error reset behavior when
opening or switching projects.

In `@ui/src/lib/api.ts`:
- Around line 269-293: Update getProjects to request ApiResponse<Project[]>
instead of Project[], matching the wrapped response returned by the project list
endpoint; leave getProject and the project mutation methods unchanged.

---

Nitpick comments:
In `@ui/src/lib/hooks.ts`:
- Around line 132-157: Extract the identical project cache synchronization from
useUpdateProject and useArchiveProject into a shared helper, then call it from
both onSuccess handlers. Remove the redundant detail invalidateQueries call,
since invalidating ["projects", companyId] already covers the detail key;
preserve the existing query-key structure unless independently updating all
related consumers.

In `@ui/test/CreateProjectModal.test.tsx`:
- Around line 19-23: Update the useUpdateProject mock to return the shared
mocks.isPending value instead of hardcoding false, so tests that toggle pending
state exercise both create and update paths consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e8ff81c5-7f6e-4c89-a23e-5329ce7e9cd5

📥 Commits

Reviewing files that changed from the base of the PR and between 0ed8e33 and c12a4c3.

📒 Files selected for processing (10)
  • server/src/__tests__/projects.test.ts
  • server/src/routes/projects.ts
  • ui/src/components/projects/CreateProjectModal.tsx
  • ui/src/components/projects/ProjectFormModal.tsx
  • ui/src/lib/api.ts
  • ui/src/lib/hooks.ts
  • ui/src/pages/ProjectDetail.tsx
  • ui/test/CreateProjectModal.test.tsx
  • ui/test/ProjectDetail.test.tsx
  • ui/test/project-hooks.test.tsx

Comment thread server/src/routes/projects.ts Outdated
Comment on lines +10 to +13
const repoUrlSchema = z.string().url().refine(
(value) => ['http:', 'https:'].includes(new URL(value).protocol),
'Repo URL must start with http(s)',
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Unguarded new URL() inside Zod refinements throws on invalid repository URLs. Both new repoUrl schemas run new URL(value) in a .refine callback that Zod still executes after the preceding URL format check fails, and Zod does not catch the resulting TypeError.

  • server/src/routes/projects.ts#L10-L13: wrap the protocol check in try/catch so an invalid repoUrl returns a 400 validation error instead of an unhandled error.
  • ui/src/components/projects/ProjectFormModal.tsx#L27-L35: replace the union with one guarded refinement that accepts "" and http(s) URLs, so submit shows a field error instead of throwing.

Consider one shared predicate for the client side, reused by isSafeRepoUrl in ui/src/pages/ProjectDetail.tsx#L30-L36. Add a test that submits github.com/org/repo in the form to lock this behavior.

📍 Affects 2 files
  • server/src/routes/projects.ts#L10-L13 (this comment)
  • ui/src/components/projects/ProjectFormModal.tsx#L27-L35
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/routes/projects.ts` around lines 10 - 13, Guard the protocol
validation in repoUrlSchema at server/src/routes/projects.ts lines 10-13 with
try/catch so malformed URLs produce a 400 validation error. In
ui/src/components/projects/ProjectFormModal.tsx lines 27-35, replace the union
with one guarded refinement accepting empty strings and valid http(s) URLs;
extract and reuse the client-side predicate in isSafeRepoUrl at
ui/src/pages/ProjectDetail.tsx lines 30-36, and add a form test submitting
github.com/org/repo that asserts a field error.

Comment on lines +68 to +76
useEffect(() => {
if (!open) return;
setName(project?.name ?? "");
setDescription(project?.description ?? "");
setStatus(project?.status === "archived" ? "planning" : (project?.status ?? "planning"));
setRepoUrl(project?.repoUrl ?? "");
setErrors({});
setSubmitError(null);
}, [open, project]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not re-initialize the open form on every project object identity change.

ProjectDetail.tsx passes the project object from the useProject cache. A background refetch of that query produces a new object identity with the same content. This effect then runs while the modal is open and overwrites the field values that the operator is editing. Key the initialization on the project identifier instead.

🐛 Proposed fix
   useEffect(() => {
     if (!open) return;
     setName(project?.name ?? "");
     setDescription(project?.description ?? "");
     setStatus(project?.status === "archived" ? "planning" : (project?.status ?? "planning"));
     setRepoUrl(project?.repoUrl ?? "");
     setErrors({});
     setSubmitError(null);
-  }, [open, project]);
+    // Initialize once per opened project; later cache updates must not discard operator input.
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [open, project?.id]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!open) return;
setName(project?.name ?? "");
setDescription(project?.description ?? "");
setStatus(project?.status === "archived" ? "planning" : (project?.status ?? "planning"));
setRepoUrl(project?.repoUrl ?? "");
setErrors({});
setSubmitError(null);
}, [open, project]);
useEffect(() => {
if (!open) return;
setName(project?.name ?? "");
setDescription(project?.description ?? "");
setStatus(project?.status === "archived" ? "planning" : (project?.status ?? "planning"));
setRepoUrl(project?.repoUrl ?? "");
setErrors({});
setSubmitError(null);
// Initialize once per opened project; later cache updates must not discard operator input.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, project?.id]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/components/projects/ProjectFormModal.tsx` around lines 68 - 76, Update
the initialization useEffect in ProjectFormModal so it is not triggered by every
project object identity change while the modal is open. Key the effect on the
project identifier (alongside open as needed), while preserving the existing
field initialization and error reset behavior when opening or switching
projects.

Comment thread ui/src/lib/api.ts
Comment on lines 269 to +293
export const getProjects = (companyId: string) =>
request<Project[]>(`/companies/${companyId}/projects`);

export const getProject = (companyId: string, projectId: string) =>
request<ApiResponse<Project>>(`/companies/${companyId}/projects/${projectId}`);

export const createProject = (companyId: string, data: CreateProjectInput) =>
request<ApiResponse<Project>>(`/companies/${companyId}/projects`, {
method: "POST",
body: JSON.stringify(data),
});

export const updateProject = (
companyId: string,
projectId: string,
data: UpdateProjectInput,
) => request<ApiResponse<Project>>(`/companies/${companyId}/projects/${projectId}`, {
method: "PATCH",
body: JSON.stringify(data),
});

export const archiveProject = (companyId: string, projectId: string) =>
request<ApiResponse<Project>>(`/companies/${companyId}/projects/${projectId}`, {
method: "DELETE",
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the request helper, ApiResponse type, and unwrap helper.
rg -nP -C 8 '(async function request|const request\s*=|interface ApiResponse|function unwrap)' ui/src/lib

Repository: VerticalLabs-ai/eidolon

Length of output: 2371


🏁 Script executed:

#!/bin/bash
# Inspect all project API declarations and hook call sites.
rg -n -C 12 'getProjects|getProject|createProject|updateProject|archiveProject|unwrap<' ui/src/lib/api.ts ui/src/lib/hooks.ts

Repository: VerticalLabs-ai/eidolon

Length of output: 50379


🏁 Script executed:

#!/bin/bash
# Compare neighboring API typings and inspect the server project response shapes.
printf '%s\n' '--- api.ts neighboring declarations ---'
sed -n '45,125p' ui/src/lib/api.ts
printf '%s\n' '--- server project routes and response wrappers ---'
rg -n -C 8 'projects|Project' --glob '*.py' --glob '*.ts' --glob '*.tsx' . | head -n 240

Repository: VerticalLabs-ai/eidolon

Length of output: 20198


🏁 Script executed:

#!/bin/bash
# Locate and inspect only the project route implementation and its response statements.
project_route="$(fd -t f 'projects\.js$|projects\.ts$' server)"
printf 'route=%s\n' "$project_route"
if [ -n "$project_route" ]; then
  rg -n -C 12 'res\.(json|send)|json\(|data:|router\.(get|post|patch|delete)' "$project_route"
fi

Repository: VerticalLabs-ai/eidolon

Length of output: 5330


Use ApiResponse<Project[]> for getProjects.

request<T> returns Promise<T>. The project list endpoint returns { data: rows, meta: ... }, so request<Project[]> does not describe the response shape. The ApiResponse<Project> generics for getProject and the mutations are correct.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/lib/api.ts` around lines 269 - 293, Update getProjects to request
ApiResponse<Project[]> instead of Project[], matching the wrapped response
returned by the project list endpoint; leave getProject and the project mutation
methods unchanged.

Comment on lines +10 to +17
const HttpUrl = z.string().url().refine((value) => {
try {
const protocol = new URL(value).protocol;
return protocol === 'http:' || protocol === 'https:';
} catch {
return false;
}
}, 'Repository URL must start with http:// or https://');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Repo url allows credentials 🐞 Bug ⛨ Security

Server/client URL validation only enforces http(s) protocol, so credential-bearing URLs (e.g.,
https://token@github.com/org/repo) can be persisted. ProjectDetail then renders the full unredacted
URL as link text and href, potentially exposing embedded secrets to anyone who can view the project.
Agent Prompt
### Issue description
Repository URLs currently validate only that the scheme is `http:` or `https:`. This still permits URLs containing userinfo credentials (username/password/token in the URL), which can then be stored and rendered verbatim in the UI.

### Issue Context
Both the server (`HttpUrl`) and client (`isHttpUrl` + Zod refine) share the same limitation. The detail page renders the stored URL directly as an external link.

### Fix Focus Areas
- server/src/routes/projects.ts[10-17]
- ui/src/lib/urls.ts[1-7]
- ui/src/components/projects/ProjectFormModal.tsx[28-33]
- ui/src/pages/ProjectDetail.tsx[81-120]

### Suggested fix
1. Update server `HttpUrl` refine to additionally require:
   - `new URL(value).username === ""`
   - `new URL(value).password === ""`
   (Optionally also reject non-empty `hash`/suspicious query params if desired.)
2. Update `ui/src/lib/urls.ts:isHttpUrl` to apply the same username/password checks.
3. Consider hardening the display layer (`ProjectDetail`) by redacting userinfo (e.g., render `url.origin + url.pathname + url.search + url.hash` without credentials) or by suppressing the link when userinfo is present, as defense-in-depth.
4. Add/extend tests to cover credential-bearing URLs being rejected/redacted.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit c577575

Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
@mgunnin
mgunnin merged commit fe1fccc into staging Jul 31, 2026
3 of 5 checks passed
@mgunnin
mgunnin deleted the feat/ver-500-project-lifecycle-management branch July 31, 2026 19:34
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit af4f1a7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant