feat(llm-gateway): implement LLM Gateway MCP Server (#56) #71

Closed
cardosofelipe wants to merge 103 commits from feature/56-llm-gateway-mcp-server into dev
8 changed files with 4881 additions and 165 deletions
Showing only changes of commit 731a188a76 - Show all commits

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -4,12 +4,14 @@
* Provides data for the projects list page with filtering, * Provides data for the projects list page with filtering,
* sorting, and pagination. * sorting, and pagination.
* *
* Uses mock data until backend endpoints are available. * Uses SDK to call API (intercepted by MSW in demo mode).
* *
* @see Issue #54 * @see Issue #54
*/ */
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { listProjects as listProjectsApi } from '@/lib/api/generated';
import type { ProjectStatus as ApiProjectStatus, ProjectResponse } from '@/lib/api/generated';
import type { ProjectStatus } from '@/components/projects/types'; import type { ProjectStatus } from '@/components/projects/types';
// ============================================================================ // ============================================================================
@@ -19,6 +21,7 @@ import type { ProjectStatus } from '@/components/projects/types';
export interface ProjectListItem { export interface ProjectListItem {
id: string; id: string;
name: string; name: string;
slug: string;
description?: string; description?: string;
status: ProjectStatus; status: ProjectStatus;
complexity: 'low' | 'medium' | 'high'; complexity: 'low' | 'medium' | 'high';
@@ -33,6 +36,7 @@ export interface ProjectListItem {
name: string; name: string;
}; };
tags?: string[]; tags?: string[];
autonomyLevel: string;
} }
export interface ProjectsListParams { export interface ProjectsListParams {
@@ -56,134 +60,64 @@ export interface ProjectsListResponse {
} }
// ============================================================================ // ============================================================================
// Mock Data // Helpers
// ============================================================================ // ============================================================================
const mockProjects: ProjectListItem[] = [ /**
{ * Maps API ProjectResponse to our ProjectListItem format
id: 'proj-001', */
name: 'E-Commerce Platform Redesign', function mapProjectResponse(project: ProjectResponse & Record<string, unknown>): ProjectListItem {
description: // Map complexity from mock data format to UI format
'Complete redesign of the e-commerce platform with modern UI/UX and improved checkout flow', const rawComplexity = project.complexity as string;
status: 'active', let complexity: 'low' | 'medium' | 'high' = 'medium';
complexity: 'high', if (rawComplexity === 'script' || rawComplexity === 'simple' || rawComplexity === 'low') {
progress: 67, complexity = 'low';
openIssues: 12, } else if (rawComplexity === 'complex' || rawComplexity === 'high') {
activeAgents: 4, complexity = 'high';
currentSprint: 'Sprint 3', }
lastActivity: '2 minutes ago',
createdAt: '2025-11-15T10:00:00Z', return {
owner: { id: 'user-001', name: 'Felipe Cardoso' }, id: project.id,
tags: ['e-commerce', 'frontend', 'ux'], name: project.name,
slug: project.slug || project.name.toLowerCase().replace(/\s+/g, '-'),
description: project.description || undefined,
status: project.status as ProjectStatus,
complexity,
progress: (project.progress as number) || 0,
openIssues: (project.openIssues as number) || project.issue_count || 0,
activeAgents: (project.activeAgents as number) || project.agent_count || 0,
currentSprint: project.active_sprint_name || undefined,
lastActivity: (project.lastActivity as string) || formatRelativeTime(project.updated_at),
createdAt: project.created_at,
owner: {
id: project.owner_id || 'unknown',
name: (project.ownerName as string) || 'Unknown',
}, },
{ tags: (project.tags as string[]) || [],
id: 'proj-002', autonomyLevel: project.autonomy_level || 'milestone',
name: 'Mobile Banking App', };
description: }
'Native mobile app for banking services with biometric authentication and real-time notifications',
status: 'active', /**
complexity: 'high', * Format a date string as relative time (e.g., "2 minutes ago")
progress: 45, */
openIssues: 8, function formatRelativeTime(dateStr: string): string {
activeAgents: 5, const date = new Date(dateStr);
currentSprint: 'Sprint 2', const now = new Date();
lastActivity: '15 minutes ago', const diffMs = now.getTime() - date.getTime();
createdAt: '2025-11-20T09:00:00Z', const diffMins = Math.floor(diffMs / 60000);
owner: { id: 'user-001', name: 'Felipe Cardoso' }, const diffHours = Math.floor(diffMins / 60);
tags: ['mobile', 'fintech', 'security'], const diffDays = Math.floor(diffHours / 24);
}, const diffWeeks = Math.floor(diffDays / 7);
{ const diffMonths = Math.floor(diffDays / 30);
id: 'proj-003',
name: 'Internal HR Portal', if (diffMins < 1) return 'Just now';
description: if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
'Employee self-service portal for HR operations including leave requests and performance reviews', if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
status: 'paused', if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
complexity: 'medium', if (diffWeeks < 4) return `${diffWeeks} week${diffWeeks > 1 ? 's' : ''} ago`;
progress: 23, return `${diffMonths} month${diffMonths > 1 ? 's' : ''} ago`;
openIssues: 5, }
activeAgents: 0,
currentSprint: 'Sprint 1',
lastActivity: '2 days ago',
createdAt: '2025-10-01T08:00:00Z',
owner: { id: 'user-002', name: 'Maria Santos' },
tags: ['internal', 'hr', 'portal'],
},
{
id: 'proj-004',
name: 'API Gateway Modernization',
description:
'Migrate legacy API gateway to cloud-native architecture with improved rate limiting and caching',
status: 'active',
complexity: 'high',
progress: 82,
openIssues: 3,
activeAgents: 2,
currentSprint: 'Sprint 4',
lastActivity: '1 hour ago',
createdAt: '2025-12-01T11:00:00Z',
owner: { id: 'user-001', name: 'Felipe Cardoso' },
tags: ['api', 'cloud', 'infrastructure'],
},
{
id: 'proj-005',
name: 'Customer Analytics Dashboard',
description:
'Real-time analytics dashboard for customer behavior insights with ML-powered predictions',
status: 'completed',
complexity: 'medium',
progress: 100,
openIssues: 0,
activeAgents: 0,
lastActivity: '2 weeks ago',
createdAt: '2025-09-01T10:00:00Z',
owner: { id: 'user-003', name: 'Alex Johnson' },
tags: ['analytics', 'ml', 'dashboard'],
},
{
id: 'proj-006',
name: 'DevOps Pipeline Automation',
description: 'Automate CI/CD pipelines with AI-assisted deployments and rollback capabilities',
status: 'active',
complexity: 'medium',
progress: 35,
openIssues: 6,
activeAgents: 3,
currentSprint: 'Sprint 1',
lastActivity: '30 minutes ago',
createdAt: '2025-12-10T14:00:00Z',
owner: { id: 'user-001', name: 'Felipe Cardoso' },
tags: ['devops', 'automation', 'ci-cd'],
},
{
id: 'proj-007',
name: 'Inventory Management System',
description: 'Warehouse inventory tracking with barcode scanning and automated reordering',
status: 'archived',
complexity: 'low',
progress: 100,
openIssues: 0,
activeAgents: 0,
lastActivity: '1 month ago',
createdAt: '2025-06-15T08:00:00Z',
owner: { id: 'user-002', name: 'Maria Santos' },
tags: ['inventory', 'warehouse', 'logistics'],
},
{
id: 'proj-008',
name: 'Customer Support Chatbot',
description: 'AI-powered chatbot for 24/7 customer support with sentiment analysis',
status: 'active',
complexity: 'medium',
progress: 58,
openIssues: 4,
activeAgents: 2,
currentSprint: 'Sprint 2',
lastActivity: '45 minutes ago',
createdAt: '2025-12-05T09:00:00Z',
owner: { id: 'user-003', name: 'Alex Johnson' },
tags: ['ai', 'chatbot', 'support'],
},
];
// ============================================================================ // ============================================================================
// Hook // Hook
@@ -206,35 +140,33 @@ export function useProjects(params: ProjectsListParams = {}) {
return useQuery<ProjectsListResponse>({ return useQuery<ProjectsListResponse>({
queryKey: ['projects', { search, status, complexity, sortBy, sortOrder, page, limit }], queryKey: ['projects', { search, status, complexity, sortBy, sortOrder, page, limit }],
queryFn: async () => { queryFn: async () => {
// Simulate network delay // Call API via SDK (MSW intercepts in demo mode)
await new Promise((resolve) => setTimeout(resolve, 400)); const response = await listProjectsApi({
query: {
status: status !== 'all' ? (status as ApiProjectStatus) : undefined,
search: search || undefined,
page,
limit,
},
});
// Filter projects if (response.error) {
let filtered = [...mockProjects]; throw new Error('Failed to fetch projects');
}
// Search filter // Get raw response data
if (search) { const apiData = response.data;
const searchLower = search.toLowerCase(); let projects = apiData.data.map((p) =>
filtered = filtered.filter( mapProjectResponse(p as ProjectResponse & Record<string, unknown>)
(p) =>
p.name.toLowerCase().includes(searchLower) ||
p.description?.toLowerCase().includes(searchLower) ||
p.tags?.some((t) => t.toLowerCase().includes(searchLower))
); );
}
// Status filter // Client-side complexity filter (not supported by API)
if (status !== 'all') {
filtered = filtered.filter((p) => p.status === status);
}
// Complexity filter
if (complexity !== 'all') { if (complexity !== 'all') {
filtered = filtered.filter((p) => p.complexity === complexity); projects = projects.filter((p) => p.complexity === complexity);
} }
// Sort // Client-side sorting
filtered.sort((a, b) => { projects.sort((a, b) => {
let comparison = 0; let comparison = 0;
switch (sortBy) { switch (sortBy) {
case 'name': case 'name':
@@ -254,15 +186,12 @@ export function useProjects(params: ProjectsListParams = {}) {
return sortOrder === 'asc' ? comparison : -comparison; return sortOrder === 'asc' ? comparison : -comparison;
}); });
// Pagination // Calculate pagination
const total = filtered.length; const total = apiData.pagination.total;
const totalPages = Math.ceil(total / limit); const totalPages = Math.ceil(total / limit);
const start = (page - 1) * limit;
const end = start + limit;
const paginatedData = filtered.slice(start, end);
return { return {
data: paginatedData, data: projects,
pagination: { pagination: {
page, page,
limit, limit,

View File

@@ -0,0 +1,266 @@
/**
* Mock Project Data for Demo Mode
*
* Sample projects used by MSW handlers in demo mode.
*/
import type { ProjectResponse, ProjectStatus } from '@/lib/api/generated';
export interface ProjectListItem extends ProjectResponse {
// Extended UI fields (computed/stored separately in real app)
complexity?: 'script' | 'simple' | 'medium' | 'complex';
progress?: number;
openIssues?: number;
activeAgents?: number;
lastActivity?: string;
tags?: string[];
ownerName?: string;
}
export const sampleProjects: ProjectListItem[] = [
{
id: 'proj-001',
name: 'E-Commerce Platform Redesign',
slug: 'ecommerce-redesign',
description: 'Complete redesign of the e-commerce platform with modern UI/UX',
autonomy_level: 'milestone',
status: 'active',
settings: {},
owner_id: 'user-001',
created_at: '2025-11-15T10:00:00Z',
updated_at: new Date(Date.now() - 2 * 60 * 1000).toISOString(),
agent_count: 5,
issue_count: 70,
active_sprint_name: 'Sprint 3',
// Extended fields
complexity: 'complex',
progress: 67,
openIssues: 12,
activeAgents: 4,
lastActivity: '2 minutes ago',
tags: ['e-commerce', 'frontend', 'ux'],
ownerName: 'Felipe Cardoso',
},
{
id: 'proj-002',
name: 'Mobile Banking App',
slug: 'mobile-banking',
description: 'Native mobile app for banking services with biometric authentication',
autonomy_level: 'full_control',
status: 'active',
settings: {},
owner_id: 'user-001',
created_at: '2025-11-20T09:00:00Z',
updated_at: new Date(Date.now() - 15 * 60 * 1000).toISOString(),
agent_count: 5,
issue_count: 45,
active_sprint_name: 'Sprint 2',
complexity: 'complex',
progress: 45,
openIssues: 8,
activeAgents: 5,
lastActivity: '15 minutes ago',
tags: ['mobile', 'fintech', 'security'],
ownerName: 'Felipe Cardoso',
},
{
id: 'proj-003',
name: 'Internal HR Portal',
slug: 'hr-portal',
description: 'Employee self-service portal for HR operations',
autonomy_level: 'milestone',
status: 'paused',
settings: {},
owner_id: 'user-002',
created_at: '2025-10-01T08:00:00Z',
updated_at: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
agent_count: 3,
issue_count: 25,
active_sprint_name: 'Sprint 1',
complexity: 'medium',
progress: 23,
openIssues: 5,
activeAgents: 0,
lastActivity: '2 days ago',
tags: ['internal', 'hr', 'portal'],
ownerName: 'Maria Santos',
},
{
id: 'proj-004',
name: 'API Gateway Modernization',
slug: 'api-gateway',
description: 'Migrate legacy API gateway to cloud-native architecture',
autonomy_level: 'autonomous',
status: 'active',
settings: {},
owner_id: 'user-001',
created_at: '2025-12-01T11:00:00Z',
updated_at: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
agent_count: 3,
issue_count: 40,
active_sprint_name: 'Sprint 4',
complexity: 'complex',
progress: 82,
openIssues: 3,
activeAgents: 2,
lastActivity: '1 hour ago',
tags: ['api', 'cloud', 'infrastructure'],
ownerName: 'Felipe Cardoso',
},
{
id: 'proj-005',
name: 'Customer Analytics Dashboard',
slug: 'analytics-dashboard',
description: 'Real-time analytics dashboard for customer behavior insights',
autonomy_level: 'milestone',
status: 'completed',
settings: {},
owner_id: 'user-003',
created_at: '2025-09-01T10:00:00Z',
updated_at: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(),
agent_count: 0,
issue_count: 50,
active_sprint_name: null,
complexity: 'medium',
progress: 100,
openIssues: 0,
activeAgents: 0,
lastActivity: '2 weeks ago',
tags: ['analytics', 'ml', 'dashboard'],
ownerName: 'Alex Johnson',
},
{
id: 'proj-006',
name: 'DevOps Pipeline Automation',
slug: 'devops-automation',
description: 'Automate CI/CD pipelines with AI-assisted deployments',
autonomy_level: 'milestone',
status: 'active',
settings: {},
owner_id: 'user-001',
created_at: '2025-12-10T14:00:00Z',
updated_at: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
agent_count: 4,
issue_count: 30,
active_sprint_name: 'Sprint 1',
complexity: 'medium',
progress: 35,
openIssues: 6,
activeAgents: 3,
lastActivity: '30 minutes ago',
tags: ['devops', 'automation', 'ci-cd'],
ownerName: 'Felipe Cardoso',
},
{
id: 'proj-007',
name: 'Inventory Management System',
slug: 'inventory-system',
description: 'Warehouse inventory tracking with barcode scanning',
autonomy_level: 'full_control',
status: 'archived',
settings: {},
owner_id: 'user-002',
created_at: '2025-06-15T08:00:00Z',
updated_at: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
agent_count: 0,
issue_count: 80,
active_sprint_name: null,
complexity: 'simple',
progress: 100,
openIssues: 0,
activeAgents: 0,
lastActivity: '1 month ago',
tags: ['inventory', 'warehouse', 'logistics'],
ownerName: 'Maria Santos',
},
{
id: 'proj-008',
name: 'Customer Support Chatbot',
slug: 'support-chatbot',
description: 'AI-powered chatbot for 24/7 customer support',
autonomy_level: 'autonomous',
status: 'active',
settings: {},
owner_id: 'user-003',
created_at: '2025-12-05T09:00:00Z',
updated_at: new Date(Date.now() - 45 * 60 * 1000).toISOString(),
agent_count: 3,
issue_count: 35,
active_sprint_name: 'Sprint 2',
complexity: 'medium',
progress: 58,
openIssues: 4,
activeAgents: 2,
lastActivity: '45 minutes ago',
tags: ['ai', 'chatbot', 'support'],
ownerName: 'Alex Johnson',
},
];
// In-memory store for demo mode (allows create/update/delete)
let projectsStore = [...sampleProjects];
export function getProjects(): ProjectListItem[] {
return projectsStore;
}
export function getProjectById(id: string): ProjectListItem | undefined {
return projectsStore.find((p) => p.id === id);
}
export function getProjectBySlug(slug: string): ProjectListItem | undefined {
return projectsStore.find((p) => p.slug === slug);
}
export function createProject(data: Partial<ProjectListItem>): ProjectListItem {
const newProject: ProjectListItem = {
id: `proj-${Date.now()}`,
name: data.name || 'New Project',
slug: data.slug || data.name?.toLowerCase().replace(/\s+/g, '-') || `project-${Date.now()}`,
description: data.description || null,
autonomy_level: data.autonomy_level || 'milestone',
status: 'active',
settings: data.settings || {},
owner_id: data.owner_id || 'user-001',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
agent_count: 0,
issue_count: 0,
active_sprint_name: null,
complexity: data.complexity || 'medium',
progress: 0,
openIssues: 0,
activeAgents: 0,
lastActivity: 'Just now',
tags: data.tags || [],
ownerName: 'Demo User',
};
projectsStore.unshift(newProject);
return newProject;
}
export function updateProject(
id: string,
data: Partial<ProjectListItem>
): ProjectListItem | undefined {
const index = projectsStore.findIndex((p) => p.id === id);
if (index === -1) return undefined;
projectsStore[index] = {
...projectsStore[index],
...data,
updated_at: new Date().toISOString(),
};
return projectsStore[index];
}
export function deleteProject(id: string): boolean {
const index = projectsStore.findIndex((p) => p.id === id);
if (index === -1) return false;
projectsStore.splice(index, 1);
return true;
}
export function resetProjects(): void {
projectsStore = [...sampleProjects];
}

View File

@@ -8,7 +8,7 @@
* *
* For custom handler behavior, use src/mocks/handlers/overrides.ts * For custom handler behavior, use src/mocks/handlers/overrides.ts
* *
* Generated: 2025-12-30T02:14:59.598Z * Generated: 2026-01-03T01:13:34.961Z
*/ */
import { http, HttpResponse, delay } from 'msw'; import { http, HttpResponse, delay } from 'msw';
@@ -603,4 +603,544 @@ export const generatedHandlers = [
message: 'Operation successful' message: 'Operation successful'
}); });
}), }),
/**
* Create Project
*/
http.post(`${API_BASE_URL}/api/v1/projects`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* List Projects
*/
http.get(`${API_BASE_URL}/api/v1/projects`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Project
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Update Project
*/
http.patch(`${API_BASE_URL}/api/v1/projects/:project_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Archive Project
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Project by Slug
*/
http.get(`${API_BASE_URL}/api/v1/projects/slug/:slug`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Pause Project
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/pause`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Resume Project
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/resume`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Create Agent Type
*/
http.post(`${API_BASE_URL}/api/v1/agent-types`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* List Agent Types
*/
http.get(`${API_BASE_URL}/api/v1/agent-types`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Update Agent Type
*/
http.patch(`${API_BASE_URL}/api/v1/agent-types/:agent_type_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Deactivate Agent Type
*/
http.delete(`${API_BASE_URL}/api/v1/agent-types/:agent_type_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Agent Type
*/
http.get(`${API_BASE_URL}/api/v1/agent-types/:agent_type_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Agent Type by Slug
*/
http.get(`${API_BASE_URL}/api/v1/agent-types/slug/:slug`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Create Issue
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/issues`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* List Issues
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/issues`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Issue Statistics
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/issues/stats`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Issue
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Update Issue
*/
http.patch(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Delete Issue
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Assign Issue
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id/assign`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Unassign Issue
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id/assignment`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Trigger Issue Sync
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id/sync`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Spawn Agent Instance
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/agents`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* List Project Agents
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/agents`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Project Agent Metrics
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/agents/metrics`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Agent Details
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Update Agent
*/
http.patch(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Terminate Agent
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Pause Agent
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id/pause`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Resume Agent
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id/resume`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Agent Metrics
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id/metrics`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Create Sprint
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/sprints`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* List Sprints
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/sprints`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Active Sprint
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/active`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Project Velocity
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/velocity`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Sprint Details
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Update Sprint
*/
http.patch(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Delete Sprint
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Start Sprint
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/start`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Complete Sprint
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/complete`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Cancel Sprint
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/cancel`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Sprint Issues
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/issues`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Add Issue to Sprint
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/issues`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Remove Issue from Sprint
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/issues`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
]; ];

View File

@@ -14,6 +14,14 @@
import { http, HttpResponse, delay } from 'msw'; import { http, HttpResponse, delay } from 'msw';
import { generateMockToken } from '../utils/tokens'; import { generateMockToken } from '../utils/tokens';
import { validateCredentials, setCurrentUser, currentUser } from '../data/users'; import { validateCredentials, setCurrentUser, currentUser } from '../data/users';
import {
getProjects,
getProjectById,
getProjectBySlug,
createProject,
updateProject,
deleteProject,
} from '../data/projects';
import config from '@/config/app.config'; import config from '@/config/app.config';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000';
@@ -182,4 +190,164 @@ export const overrideHandlers = [
accounts: [], accounts: [],
}); });
}), }),
// ============================================
// PROJECT HANDLERS
// ============================================
/**
* List Projects
* Returns paginated list of projects with filtering support
*/
http.get(`${API_BASE_URL}/api/v1/projects`, async ({ request }) => {
await delay(NETWORK_DELAY);
const url = new URL(request.url);
const status = url.searchParams.get('status');
const search = url.searchParams.get('search');
const skip = parseInt(url.searchParams.get('skip') || '0');
const limit = parseInt(url.searchParams.get('limit') || '20');
let projects = getProjects();
// Filter by status
if (status && status !== 'all') {
projects = projects.filter((p) => p.status === status);
}
// Filter by search term
if (search) {
const searchLower = search.toLowerCase();
projects = projects.filter(
(p) =>
p.name.toLowerCase().includes(searchLower) ||
p.description?.toLowerCase().includes(searchLower)
);
}
const total = projects.length;
const paginatedProjects = projects.slice(skip, skip + limit);
const totalPages = Math.ceil(total / limit);
return HttpResponse.json({
data: paginatedProjects,
pagination: {
total,
page: Math.floor(skip / limit) + 1,
limit,
total_pages: totalPages,
has_next: skip + limit < total,
has_prev: skip > 0,
},
});
}),
/**
* Create Project
*/
http.post(`${API_BASE_URL}/api/v1/projects`, async ({ request }) => {
await delay(NETWORK_DELAY);
const body = (await request.json()) as any;
const newProject = createProject(body);
return HttpResponse.json(newProject, { status: 201 });
}),
/**
* Get Project by ID
*/
http.get(`${API_BASE_URL}/api/v1/projects/:projectId`, async ({ params }) => {
await delay(NETWORK_DELAY);
const { projectId } = params;
const project = getProjectById(projectId as string);
if (!project) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json(project);
}),
/**
* Get Project by Slug
*/
http.get(`${API_BASE_URL}/api/v1/projects/slug/:slug`, async ({ params }) => {
await delay(NETWORK_DELAY);
const { slug } = params;
const project = getProjectBySlug(slug as string);
if (!project) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json(project);
}),
/**
* Update Project
*/
http.patch(`${API_BASE_URL}/api/v1/projects/:projectId`, async ({ params, request }) => {
await delay(NETWORK_DELAY);
const { projectId } = params;
const body = (await request.json()) as any;
const updated = updateProject(projectId as string, body);
if (!updated) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json(updated);
}),
/**
* Archive (Delete) Project
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:projectId`, async ({ params }) => {
await delay(NETWORK_DELAY);
const { projectId } = params;
const deleted = deleteProject(projectId as string);
if (!deleted) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json({ message: 'Project archived successfully' });
}),
/**
* Pause Project
*/
http.post(`${API_BASE_URL}/api/v1/projects/:projectId/pause`, async ({ params }) => {
await delay(NETWORK_DELAY);
const { projectId } = params;
const updated = updateProject(projectId as string, { status: 'paused' as any });
if (!updated) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json(updated);
}),
/**
* Resume Project
*/
http.post(`${API_BASE_URL}/api/v1/projects/:projectId/resume`, async ({ params }) => {
await delay(NETWORK_DELAY);
const { projectId } = params;
const updated = updateProject(projectId as string, { status: 'active' as any });
if (!updated) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json(updated);
}),
]; ];

View File

@@ -10,6 +10,7 @@ describe('ProjectCard', () => {
const mockProject: ProjectListItem = { const mockProject: ProjectListItem = {
id: 'proj-1', id: 'proj-1',
name: 'Test Project', name: 'Test Project',
slug: 'test-project',
description: 'This is a test project description', description: 'This is a test project description',
status: 'active', status: 'active',
complexity: 'medium', complexity: 'medium',
@@ -21,6 +22,7 @@ describe('ProjectCard', () => {
createdAt: '2025-01-01T00:00:00Z', createdAt: '2025-01-01T00:00:00Z',
owner: { id: 'user-1', name: 'Test User' }, owner: { id: 'user-1', name: 'Test User' },
tags: ['frontend', 'react', 'typescript'], tags: ['frontend', 'react', 'typescript'],
autonomyLevel: 'milestone',
}; };
it('renders project name', () => { it('renders project name', () => {

View File

@@ -18,6 +18,7 @@ describe('ProjectsGrid', () => {
{ {
id: 'proj-1', id: 'proj-1',
name: 'Project One', name: 'Project One',
slug: 'project-one',
description: 'First project', description: 'First project',
status: 'active', status: 'active',
complexity: 'medium', complexity: 'medium',
@@ -27,10 +28,12 @@ describe('ProjectsGrid', () => {
lastActivity: '5 min ago', lastActivity: '5 min ago',
createdAt: '2025-01-01T00:00:00Z', createdAt: '2025-01-01T00:00:00Z',
owner: { id: 'user-1', name: 'User One' }, owner: { id: 'user-1', name: 'User One' },
autonomyLevel: 'milestone',
}, },
{ {
id: 'proj-2', id: 'proj-2',
name: 'Project Two', name: 'Project Two',
slug: 'project-two',
description: 'Second project', description: 'Second project',
status: 'paused', status: 'paused',
complexity: 'high', complexity: 'high',
@@ -40,6 +43,7 @@ describe('ProjectsGrid', () => {
lastActivity: '1 day ago', lastActivity: '1 day ago',
createdAt: '2025-01-02T00:00:00Z', createdAt: '2025-01-02T00:00:00Z',
owner: { id: 'user-2', name: 'User Two' }, owner: { id: 'user-2', name: 'User Two' },
autonomyLevel: 'full_control',
}, },
]; ];