mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Backend: ProjectSummaryResponse gains task_counts (done/active/blocked) + ci_watch_enabled. ProjectService.task_counts_for_projects does one GROUP BY project_id over TaskTable for every distinct project_id in the list (a project with no tasks is absent — route falls back to None). ci_watch_enabled is read straight off the Project row (already a column) — a 0-cost schema extension, honest signal that CI-watch is armed, no live-conclusion fan-out. project_to_summary takes an optional task_counts. No migration. Frontend: ProjectTable gains a Tasks column (done/active/blocked + health dot, amber at-risk when blocked>0) and a CI-Watch badge under the project name when ci_watch_enabled. Both desktop Table and mobile ResponsiveTableCard variants. Mock projects carry the new shape (two sample repos). Co-authored-by: Renn F <rennf93@users.noreply.github.com>
291 lines
8.7 KiB
TypeScript
291 lines
8.7 KiB
TypeScript
import api from "./client";
|
|
import { Team } from "@/types";
|
|
import type {
|
|
Project,
|
|
ProjectCreate,
|
|
ProjectUpdate,
|
|
ProjectSummary,
|
|
} from "@/types";
|
|
import { isMockMode } from "@/lib/mock-data";
|
|
|
|
// Mock data for offline mode
|
|
const mockProjects: Project[] = [
|
|
{
|
|
id: "proj-mock-1",
|
|
name: "roboco",
|
|
slug: "roboco",
|
|
git_url: "https://github.com/rennf93/roboco.git",
|
|
default_branch: "master",
|
|
protected_branches: ["master", "slave"],
|
|
assigned_cell: Team.BACKEND,
|
|
is_active: true,
|
|
has_git_token: false,
|
|
ci_watch_enabled: true,
|
|
ci_watch_workflow: "CI",
|
|
video_engine_enabled: false,
|
|
workspace_path: "/data/workspaces/roboco",
|
|
created_by: "ceo",
|
|
created_at: "2026-06-01T00:00:00Z",
|
|
updated_at: null,
|
|
} as Project,
|
|
{
|
|
id: "proj-mock-2",
|
|
name: "roboco-website",
|
|
slug: "roboco-website",
|
|
git_url: "https://github.com/rennf93/roboco-website.git",
|
|
default_branch: "master",
|
|
protected_branches: ["master"],
|
|
assigned_cell: Team.FRONTEND,
|
|
is_active: true,
|
|
has_git_token: false,
|
|
ci_watch_enabled: false,
|
|
ci_watch_workflow: null,
|
|
video_engine_enabled: false,
|
|
workspace_path: null,
|
|
created_by: "ceo",
|
|
created_at: "2026-06-15T00:00:00Z",
|
|
updated_at: null,
|
|
} as Project,
|
|
];
|
|
|
|
// Mock task counts per project (mock-mode only — real data comes from the
|
|
// backend's grouped query). Keyed by project id.
|
|
const mockTaskCounts: Record<string, { done: number; active: number; blocked: number }> = {
|
|
"proj-mock-1": { done: 120, active: 8, blocked: 1 },
|
|
"proj-mock-2": { done: 34, active: 2, blocked: 0 },
|
|
};
|
|
|
|
export interface ProjectFilters {
|
|
assigned_cell?: Team;
|
|
active_only?: boolean;
|
|
limit?: number;
|
|
offset?: number;
|
|
}
|
|
|
|
export const projectsApi = {
|
|
// List projects with optional filters
|
|
list: async (filters?: ProjectFilters): Promise<ProjectSummary[]> => {
|
|
if (isMockMode()) {
|
|
let projects = [...mockProjects];
|
|
if (filters?.assigned_cell) {
|
|
projects = projects.filter(
|
|
(p) => p.assigned_cell === filters.assigned_cell,
|
|
);
|
|
}
|
|
if (filters?.active_only) {
|
|
projects = projects.filter((p) => p.is_active);
|
|
}
|
|
return projects.map((p) => ({
|
|
id: p.id,
|
|
name: p.name,
|
|
slug: p.slug,
|
|
git_url: p.git_url,
|
|
assigned_cell: p.assigned_cell,
|
|
is_active: p.is_active,
|
|
has_workspace: !!p.workspace_path,
|
|
has_git_token: false, // Mock mode has no tokens
|
|
video_engine_enabled: p.video_engine_enabled,
|
|
ci_watch_enabled: !!p.ci_watch_enabled,
|
|
task_counts: mockTaskCounts[p.id] ?? null,
|
|
}));
|
|
}
|
|
|
|
const params = new URLSearchParams();
|
|
if (filters?.assigned_cell)
|
|
params.append("assigned_cell", filters.assigned_cell);
|
|
if (filters?.active_only !== undefined)
|
|
params.append("active_only", String(filters.active_only));
|
|
if (filters?.limit) params.append("limit", String(filters.limit));
|
|
if (filters?.offset) params.append("offset", String(filters.offset));
|
|
|
|
const url = "/projects?" + params.toString();
|
|
const { data } = await api.get<ProjectSummary[]>(url);
|
|
return data;
|
|
},
|
|
|
|
// Get single project
|
|
get: async (projectId: string): Promise<Project> => {
|
|
if (isMockMode()) {
|
|
const project = mockProjects.find((p) => p.id === projectId);
|
|
if (!project) throw new Error("Project not found");
|
|
return project;
|
|
}
|
|
|
|
const { data } = await api.get<Project>("/projects/" + projectId);
|
|
return data;
|
|
},
|
|
|
|
// Create project (PM only)
|
|
create: async (project: ProjectCreate): Promise<Project> => {
|
|
if (isMockMode()) {
|
|
const now = new Date().toISOString();
|
|
const newProject: Project = {
|
|
id: `project-${Date.now()}`,
|
|
name: project.name,
|
|
slug: project.slug,
|
|
git_url: project.git_url,
|
|
default_branch: project.default_branch ?? "main",
|
|
protected_branches: project.protected_branches ?? ["main", "master"],
|
|
assigned_cell: project.assigned_cell,
|
|
has_git_token: !!project.git_token, // Mock token status
|
|
is_active: true,
|
|
test_command: project.test_command ?? null,
|
|
lint_command: project.lint_command ?? null,
|
|
format_command: project.format_command ?? null,
|
|
typecheck_command: project.typecheck_command ?? null,
|
|
build_command: project.build_command ?? null,
|
|
quality_command: project.quality_command ?? null,
|
|
ci_watch_enabled: false,
|
|
ci_watch_workflow: null,
|
|
video_engine_enabled: false,
|
|
dep_update_command: null,
|
|
dep_update_paths: null,
|
|
sandbox_services: null,
|
|
sandbox_extensions: null,
|
|
workspace_path: null,
|
|
last_synced_at: null,
|
|
head_commit: null,
|
|
created_by: "mock-user",
|
|
created_at: now,
|
|
updated_at: null,
|
|
};
|
|
mockProjects.push(newProject);
|
|
return newProject;
|
|
}
|
|
const { data } = await api.post<Project>("/projects", project);
|
|
return data;
|
|
},
|
|
|
|
// Update project (PM only)
|
|
update: async (
|
|
projectId: string,
|
|
updates: ProjectUpdate,
|
|
): Promise<Project> => {
|
|
if (isMockMode()) {
|
|
const idx = mockProjects.findIndex((p) => p.id === projectId);
|
|
if (idx === -1) throw new Error("Project not found");
|
|
const now = new Date().toISOString();
|
|
mockProjects[idx] = { ...mockProjects[idx], ...updates, updated_at: now };
|
|
return mockProjects[idx];
|
|
}
|
|
const { data } = await api.patch<Project>(
|
|
"/projects/" + projectId,
|
|
updates,
|
|
);
|
|
return data;
|
|
},
|
|
|
|
// Set workspace path for local development
|
|
setWorkspace: async (
|
|
projectId: string,
|
|
workspacePath: string,
|
|
): Promise<Project> => {
|
|
if (isMockMode()) {
|
|
const idx = mockProjects.findIndex((p) => p.id === projectId);
|
|
if (idx === -1) throw new Error("Project not found");
|
|
const now = new Date().toISOString();
|
|
mockProjects[idx] = {
|
|
...mockProjects[idx],
|
|
workspace_path: workspacePath,
|
|
updated_at: now,
|
|
};
|
|
return mockProjects[idx];
|
|
}
|
|
const { data } = await api.post<Project>(
|
|
"/projects/" + projectId + "/workspace",
|
|
{ workspace_path: workspacePath },
|
|
);
|
|
return data;
|
|
},
|
|
|
|
// Update sync state (for tracking git status)
|
|
updateSyncState: async (
|
|
projectId: string,
|
|
headCommit: string,
|
|
): Promise<Project> => {
|
|
if (isMockMode()) {
|
|
const idx = mockProjects.findIndex((p) => p.id === projectId);
|
|
if (idx === -1) throw new Error("Project not found");
|
|
const now = new Date().toISOString();
|
|
mockProjects[idx] = {
|
|
...mockProjects[idx],
|
|
head_commit: headCommit,
|
|
last_synced_at: now,
|
|
updated_at: now,
|
|
};
|
|
return mockProjects[idx];
|
|
}
|
|
const { data } = await api.post<Project>(
|
|
"/projects/" + projectId + "/sync-state",
|
|
{
|
|
head_commit: headCommit,
|
|
},
|
|
);
|
|
return data;
|
|
},
|
|
|
|
// Deactivate project (soft delete)
|
|
deactivate: async (projectId: string): Promise<Project> => {
|
|
if (isMockMode()) {
|
|
const idx = mockProjects.findIndex((p) => p.id === projectId);
|
|
if (idx === -1) throw new Error("Project not found");
|
|
const now = new Date().toISOString();
|
|
mockProjects[idx] = {
|
|
...mockProjects[idx],
|
|
is_active: false,
|
|
updated_at: now,
|
|
};
|
|
return mockProjects[idx];
|
|
}
|
|
const { data } = await api.patch<Project>("/projects/" + projectId, {
|
|
is_active: false,
|
|
});
|
|
return data;
|
|
},
|
|
|
|
// Delete project permanently
|
|
delete: async (projectId: string): Promise<void> => {
|
|
if (isMockMode()) {
|
|
const idx = mockProjects.findIndex((p) => p.id === projectId);
|
|
if (idx !== -1) mockProjects.splice(idx, 1);
|
|
return;
|
|
}
|
|
await api.delete("/projects/" + projectId);
|
|
},
|
|
|
|
// Grant agent access to project
|
|
grantAccess: async (projectId: string, agentId: string): Promise<void> => {
|
|
if (isMockMode()) {
|
|
return;
|
|
}
|
|
await api.post("/projects/" + projectId + "/access/" + agentId);
|
|
},
|
|
|
|
// Revoke agent access from project
|
|
revokeAccess: async (projectId: string, agentId: string): Promise<void> => {
|
|
if (isMockMode()) {
|
|
return;
|
|
}
|
|
await api.delete("/projects/" + projectId + "/access/" + agentId);
|
|
},
|
|
|
|
// Trigger git sync for project
|
|
sync: async (projectId: string): Promise<Project> => {
|
|
if (isMockMode()) {
|
|
const idx = mockProjects.findIndex((p) => p.id === projectId);
|
|
if (idx === -1) throw new Error("Project not found");
|
|
const now = new Date().toISOString();
|
|
mockProjects[idx] = {
|
|
...mockProjects[idx],
|
|
last_synced_at: now,
|
|
updated_at: now,
|
|
};
|
|
return mockProjects[idx];
|
|
}
|
|
const { data } = await api.post<Project>(
|
|
"/projects/" + projectId + "/sync",
|
|
);
|
|
return data;
|
|
},
|
|
};
|