fix(projects): audit pass — 8 correctness and consistency fixes

- Project.to_dict() now includes user_id and auto_summary
- Status validation unified to (active/completed/archived) on both
  create and update project routes; update route previously had none
- Milestone routes: replace get_project (ownership-only) with
  get_project_for_user so shared viewers/editors can access milestones
- Add get_milestone_in_project() to milestones service for project-
  scoped lookup without user_id filter; all milestone routes use it
- Milestone PATCH now validates status as 'active'|'done'; fix tool
  enum which was wrongly ['active','completed','cancelled']
- Write mutation routes (POST/PATCH/DELETE milestones) now check
  can_write_project() and return 403 for read-only shared users
- update_project tool now exposes title and color fields so projects
  can be renamed or recolored via chat
- create_project tool now exposes color field
- GET /api/projects?include_summary=true embeds summaries in one
  backend pass; ProjectListView switches to this, eliminating N+1
  per-project fetches

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 13:14:42 -04:00
parent ed715dcc23
commit c5191837fb
7 changed files with 87 additions and 37 deletions
+5 -12
View File
@@ -16,11 +16,15 @@ interface MilestoneSummary {
interface Project {
id: number;
user_id: number;
title: string;
description: string | null;
goal: string | null;
status: "active" | "completed" | "archived";
color: string | null;
auto_summary: string | null;
permission?: string;
is_shared?: boolean;
created_at: string;
updated_at: string;
summary?: {
@@ -54,19 +58,8 @@ async function loadProjects() {
loading.value = true;
error.value = null;
try {
const data = await apiGet<{ projects: Project[] }>("/api/projects");
const data = await apiGet<{ projects: Project[] }>("/api/projects?include_summary=true");
projects.value = data.projects;
// Fetch summaries (including milestone_summary) in parallel
await Promise.allSettled(
projects.value.map(async (p) => {
try {
const full = await apiGet<Project>(`/api/projects/${p.id}`);
p.summary = full.summary;
} catch {
// non-fatal
}
})
);
} catch {
error.value = "Failed to load projects.";
} finally {
+3
View File
@@ -19,11 +19,14 @@ interface Milestone {
interface Project {
id: number;
user_id: number;
title: string;
description: string | null;
goal: string | null;
status: "active" | "completed" | "archived";
color: string | null;
auto_summary: string | null;
permission?: string;
created_at: string;
updated_at: string;
summary?: {