fix: Comprehensive validation and bug fixes

Infrastructure:
- Add Redis and Celery workers to all docker-compose files
- Fix celery migration race condition in entrypoint.sh
- Add healthchecks and resource limits to dev compose
- Update .env.template with Redis/Celery variables

Backend Models & Schemas:
- Rename Sprint.completed_points to velocity (per requirements)
- Add AgentInstance.name as required field
- Rename Issue external tracker fields for consistency
- Add IssueSource and TrackerType enums
- Add Project.default_tracker_type field

Backend Fixes:
- Add Celery retry configuration with exponential backoff
- Remove unused sequence counter from EventBus
- Add mypy overrides for test dependencies
- Fix test file using wrong schema (UserUpdate -> dict)

Frontend Fixes:
- Fix memory leak in useProjectEvents (proper cleanup)
- Fix race condition with stale closure in reconnection
- Sync TokenWithUser type with regenerated API client
- Fix expires_in null handling in useAuth
- Clean up unused imports in prototype pages
- Add ESLint relaxed rules for prototype files

CI/CD:
- Add E2E testing stage with Testcontainers
- Add security scanning with Trivy and pip-audit
- Add dependency caching for faster builds

Tests:
- Update all tests to use renamed fields (velocity, name, etc.)
- Fix 14 schema test failures
- All 1500 tests pass with 91% coverage

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 10:35:30 +01:00
parent 6ea9edf3d1
commit 742ce4c9c8
57 changed files with 1062 additions and 332 deletions

View File

@@ -1,42 +1,23 @@
'use client';
import { useState, useEffect } from 'react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import {
Activity,
Bot,
MessageSquare,
PlayCircle,
PauseCircle,
CheckCircle2,
AlertCircle,
Clock,
GitPullRequest,
GitBranch,
CircleDot,
XCircle,
Zap,
Users,
ChevronRight,
Settings,
Filter,
Bell,
@@ -94,6 +75,14 @@ const eventTypeConfig = {
},
};
// Filter state type
type FilterState = {
types: string[];
agents: string[];
projects: string[];
showActionRequired: boolean;
};
// Mock activity events
const mockEvents = [
{
@@ -493,18 +482,13 @@ function FilterPanel({
onFiltersChange,
onClose,
}: {
filters: {
types: string[];
agents: string[];
projects: string[];
showActionRequired: boolean;
};
onFiltersChange: (filters: typeof filters) => void;
filters: FilterState;
onFiltersChange: (filters: FilterState) => void;
onClose: () => void;
}) {
const eventTypes = Object.entries(eventTypeConfig);
const agents = ['Backend Engineer', 'Frontend Engineer', 'Architect', 'Product Owner', 'QA Engineer', 'DevOps Engineer'];
const projects = ['E-Commerce Platform', 'Mobile App', 'API Gateway'];
const _projects = ['E-Commerce Platform', 'Mobile App', 'API Gateway'];
const toggleType = (type: string) => {
const newTypes = filters.types.includes(type)

View File

@@ -42,7 +42,6 @@ import {
Zap,
Code,
FileText,
GitBranch,
CheckCircle2,
AlertTriangle,
} from 'lucide-react';
@@ -866,7 +865,7 @@ function AgentTypeEditorView({
export default function AgentConfigurationPrototype() {
const [view, setView] = useState<ViewState>('list');
const [selectedId, setSelectedId] = useState<string | null>(null);
const [_selectedId, setSelectedId] = useState<string | null>(null);
const [isCreating, setIsCreating] = useState(false);
const handleSelectType = (id: string) => {

View File

@@ -4,7 +4,6 @@ import { useState } from 'react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
@@ -53,7 +52,6 @@ import {
Calendar,
Tag,
Settings,
Download,
Upload,
Trash2,
Edit,
@@ -919,7 +917,7 @@ function IssueDetailView({ onBack }: { onBack: () => void }) {
export default function IssueManagementPrototype() {
const [view, setView] = useState<'list' | 'detail'>('list');
const [selectedIssueId, setSelectedIssueId] = useState<string | null>(null);
const [_selectedIssueId, setSelectedIssueId] = useState<string | null>(null);
const handleSelectIssue = (id: string) => {
setSelectedIssueId(id);

View File

@@ -10,7 +10,6 @@ import {
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
Select,
SelectContent,
@@ -300,7 +299,7 @@ function ProgressBar({ value, className }: { value: number; className?: string }
}
export default function ProjectDashboardPrototype() {
const [selectedView, setSelectedView] = useState('overview');
const [_selectedView, _setSelectedView] = useState('overview');
return (
<div className="min-h-screen bg-background">

View File

@@ -3,7 +3,7 @@
import { type Client, type Options as Options2, type TDataShape, urlSearchParamsBodySerializer } from './client';
import { client } from './client.gen';
import type { AdminActivateUserData, AdminActivateUserErrors, AdminActivateUserResponses, AdminAddOrganizationMemberData, AdminAddOrganizationMemberErrors, AdminAddOrganizationMemberResponses, AdminBulkUserActionData, AdminBulkUserActionErrors, AdminBulkUserActionResponses, AdminCreateOrganizationData, AdminCreateOrganizationErrors, AdminCreateOrganizationResponses, AdminCreateUserData, AdminCreateUserErrors, AdminCreateUserResponses, AdminDeactivateUserData, AdminDeactivateUserErrors, AdminDeactivateUserResponses, AdminDeleteOrganizationData, AdminDeleteOrganizationErrors, AdminDeleteOrganizationResponses, AdminDeleteUserData, AdminDeleteUserErrors, AdminDeleteUserResponses, AdminGetOrganizationData, AdminGetOrganizationErrors, AdminGetOrganizationResponses, AdminGetStatsData, AdminGetStatsResponses, AdminGetUserData, AdminGetUserErrors, AdminGetUserResponses, AdminListOrganizationMembersData, AdminListOrganizationMembersErrors, AdminListOrganizationMembersResponses, AdminListOrganizationsData, AdminListOrganizationsErrors, AdminListOrganizationsResponses, AdminListSessionsData, AdminListSessionsErrors, AdminListSessionsResponses, AdminListUsersData, AdminListUsersErrors, AdminListUsersResponses, AdminRemoveOrganizationMemberData, AdminRemoveOrganizationMemberErrors, AdminRemoveOrganizationMemberResponses, AdminUpdateOrganizationData, AdminUpdateOrganizationErrors, AdminUpdateOrganizationResponses, AdminUpdateUserData, AdminUpdateUserErrors, AdminUpdateUserResponses, ChangeCurrentUserPasswordData, ChangeCurrentUserPasswordErrors, ChangeCurrentUserPasswordResponses, CleanupExpiredSessionsData, CleanupExpiredSessionsResponses, ConfirmPasswordResetData, ConfirmPasswordResetErrors, ConfirmPasswordResetResponses, DeleteOauthClientData, DeleteOauthClientErrors, DeleteOauthClientResponses, DeleteUserData, DeleteUserErrors, DeleteUserResponses, GetCurrentUserProfileData, GetCurrentUserProfileResponses, GetMyOrganizationsData, GetMyOrganizationsErrors, GetMyOrganizationsResponses, GetOauthAuthorizationUrlData, GetOauthAuthorizationUrlErrors, GetOauthAuthorizationUrlResponses, GetOauthServerMetadataData, GetOauthServerMetadataResponses, GetOrganizationData, GetOrganizationErrors, GetOrganizationMembersData, GetOrganizationMembersErrors, GetOrganizationMembersResponses, GetOrganizationResponses, GetUserByIdData, GetUserByIdErrors, GetUserByIdResponses, HandleOauthCallbackData, HandleOauthCallbackErrors, HandleOauthCallbackResponses, HealthCheckData, HealthCheckResponses, ListMyOauthConsentsData, ListMyOauthConsentsResponses, ListMySessionsData, ListMySessionsResponses, ListOauthAccountsData, ListOauthAccountsResponses, ListOauthClientsData, ListOauthClientsResponses, ListOauthProvidersData, ListOauthProvidersResponses, ListUsersData, ListUsersErrors, ListUsersResponses, LoginData, LoginErrors, LoginOauthData, LoginOauthErrors, LoginOauthResponses, LoginResponses, LogoutAllData, LogoutAllResponses, LogoutData, LogoutErrors, LogoutResponses, OauthProviderAuthorizeData, OauthProviderAuthorizeErrors, OauthProviderAuthorizeResponses, OauthProviderConsentData, OauthProviderConsentErrors, OauthProviderConsentResponses, OauthProviderIntrospectData, OauthProviderIntrospectErrors, OauthProviderIntrospectResponses, OauthProviderRevokeData, OauthProviderRevokeErrors, OauthProviderRevokeResponses, OauthProviderTokenData, OauthProviderTokenErrors, OauthProviderTokenResponses, RefreshTokenData, RefreshTokenErrors, RefreshTokenResponses, RegisterData, RegisterErrors, RegisterOauthClientData, RegisterOauthClientErrors, RegisterOauthClientResponses, RegisterResponses, RequestPasswordResetData, RequestPasswordResetErrors, RequestPasswordResetResponses, RevokeMyOauthConsentData, RevokeMyOauthConsentErrors, RevokeMyOauthConsentResponses, RevokeSessionData, RevokeSessionErrors, RevokeSessionResponses, RootGetData, RootGetResponses, StartOauthLinkData, StartOauthLinkErrors, StartOauthLinkResponses, UnlinkOauthAccountData, UnlinkOauthAccountErrors, UnlinkOauthAccountResponses, UpdateCurrentUserData, UpdateCurrentUserErrors, UpdateCurrentUserResponses, UpdateOrganizationData, UpdateOrganizationErrors, UpdateOrganizationResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses } from './types.gen';
import type { AdminActivateUserData, AdminActivateUserErrors, AdminActivateUserResponses, AdminAddOrganizationMemberData, AdminAddOrganizationMemberErrors, AdminAddOrganizationMemberResponses, AdminBulkUserActionData, AdminBulkUserActionErrors, AdminBulkUserActionResponses, AdminCreateOrganizationData, AdminCreateOrganizationErrors, AdminCreateOrganizationResponses, AdminCreateUserData, AdminCreateUserErrors, AdminCreateUserResponses, AdminDeactivateUserData, AdminDeactivateUserErrors, AdminDeactivateUserResponses, AdminDeleteOrganizationData, AdminDeleteOrganizationErrors, AdminDeleteOrganizationResponses, AdminDeleteUserData, AdminDeleteUserErrors, AdminDeleteUserResponses, AdminGetOrganizationData, AdminGetOrganizationErrors, AdminGetOrganizationResponses, AdminGetStatsData, AdminGetStatsResponses, AdminGetUserData, AdminGetUserErrors, AdminGetUserResponses, AdminListOrganizationMembersData, AdminListOrganizationMembersErrors, AdminListOrganizationMembersResponses, AdminListOrganizationsData, AdminListOrganizationsErrors, AdminListOrganizationsResponses, AdminListSessionsData, AdminListSessionsErrors, AdminListSessionsResponses, AdminListUsersData, AdminListUsersErrors, AdminListUsersResponses, AdminRemoveOrganizationMemberData, AdminRemoveOrganizationMemberErrors, AdminRemoveOrganizationMemberResponses, AdminUpdateOrganizationData, AdminUpdateOrganizationErrors, AdminUpdateOrganizationResponses, AdminUpdateUserData, AdminUpdateUserErrors, AdminUpdateUserResponses, ChangeCurrentUserPasswordData, ChangeCurrentUserPasswordErrors, ChangeCurrentUserPasswordResponses, CleanupExpiredSessionsData, CleanupExpiredSessionsResponses, ConfirmPasswordResetData, ConfirmPasswordResetErrors, ConfirmPasswordResetResponses, DeleteOauthClientData, DeleteOauthClientErrors, DeleteOauthClientResponses, DeleteUserData, DeleteUserErrors, DeleteUserResponses, GetCurrentUserProfileData, GetCurrentUserProfileResponses, GetMyOrganizationsData, GetMyOrganizationsErrors, GetMyOrganizationsResponses, GetOauthAuthorizationUrlData, GetOauthAuthorizationUrlErrors, GetOauthAuthorizationUrlResponses, GetOauthServerMetadataData, GetOauthServerMetadataResponses, GetOrganizationData, GetOrganizationErrors, GetOrganizationMembersData, GetOrganizationMembersErrors, GetOrganizationMembersResponses, GetOrganizationResponses, GetUserByIdData, GetUserByIdErrors, GetUserByIdResponses, HandleOauthCallbackData, HandleOauthCallbackErrors, HandleOauthCallbackResponses, HealthCheckData, HealthCheckResponses, ListMyOauthConsentsData, ListMyOauthConsentsResponses, ListMySessionsData, ListMySessionsResponses, ListOauthAccountsData, ListOauthAccountsResponses, ListOauthClientsData, ListOauthClientsResponses, ListOauthProvidersData, ListOauthProvidersResponses, ListUsersData, ListUsersErrors, ListUsersResponses, LoginData, LoginErrors, LoginOauthData, LoginOauthErrors, LoginOauthResponses, LoginResponses, LogoutAllData, LogoutAllResponses, LogoutData, LogoutErrors, LogoutResponses, OauthProviderAuthorizeData, OauthProviderAuthorizeErrors, OauthProviderAuthorizeResponses, OauthProviderConsentData, OauthProviderConsentErrors, OauthProviderConsentResponses, OauthProviderIntrospectData, OauthProviderIntrospectErrors, OauthProviderIntrospectResponses, OauthProviderRevokeData, OauthProviderRevokeErrors, OauthProviderRevokeResponses, OauthProviderTokenData, OauthProviderTokenErrors, OauthProviderTokenResponses, RefreshTokenData, RefreshTokenErrors, RefreshTokenResponses, RegisterData, RegisterErrors, RegisterOauthClientData, RegisterOauthClientErrors, RegisterOauthClientResponses, RegisterResponses, RequestPasswordResetData, RequestPasswordResetErrors, RequestPasswordResetResponses, RevokeMyOauthConsentData, RevokeMyOauthConsentErrors, RevokeMyOauthConsentResponses, RevokeSessionData, RevokeSessionErrors, RevokeSessionResponses, RootGetData, RootGetResponses, SendTestEventData, SendTestEventErrors, SendTestEventResponses, StartOauthLinkData, StartOauthLinkErrors, StartOauthLinkResponses, StreamProjectEventsData, StreamProjectEventsErrors, StreamProjectEventsResponses, UnlinkOauthAccountData, UnlinkOauthAccountErrors, UnlinkOauthAccountResponses, UpdateCurrentUserData, UpdateCurrentUserErrors, UpdateCurrentUserResponses, UpdateOrganizationData, UpdateOrganizationErrors, UpdateOrganizationResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses } from './types.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
/**
@@ -1288,6 +1288,74 @@ export const getOrganizationMembers = <ThrowOnError extends boolean = false>(opt
});
};
/**
* Stream Project Events
*
* Stream real-time events for a project via Server-Sent Events (SSE).
*
* **Authentication**: Required (Bearer token)
* **Authorization**: Must have access to the project
*
* **SSE Event Format**:
* ```
* event: agent.status_changed
* id: 550e8400-e29b-41d4-a716-446655440000
* data: {"id": "...", "type": "agent.status_changed", "project_id": "...", ...}
*
* : keepalive
*
* event: issue.created
* id: 550e8400-e29b-41d4-a716-446655440001
* data: {...}
* ```
*
* **Reconnection**: Include the `Last-Event-ID` header with the last received
* event ID to resume from where you left off.
*
* **Keepalive**: The server sends a comment (`: keepalive`) every 30 seconds
* to keep the connection alive.
*
* **Rate Limit**: 10 connections/minute per IP
*/
export const streamProjectEvents = <ThrowOnError extends boolean = false>(options: Options<StreamProjectEventsData, ThrowOnError>) => {
return (options.client ?? client).sse.get<StreamProjectEventsResponses, StreamProjectEventsErrors, ThrowOnError>({
responseType: 'text',
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/api/v1/projects/{project_id}/events/stream',
...options
});
};
/**
* Send Test Event (Development Only)
*
* Send a test event to a project's event stream. This endpoint is
* intended for development and testing purposes.
*
* **Authentication**: Required (Bearer token)
* **Authorization**: Must have access to the project
*
* **Note**: This endpoint should be disabled or restricted in production.
*/
export const sendTestEvent = <ThrowOnError extends boolean = false>(options: Options<SendTestEventData, ThrowOnError>) => {
return (options.client ?? client).post<SendTestEventResponses, SendTestEventErrors, ThrowOnError>({
responseType: 'json',
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/api/v1/projects/{project_id}/events/test',
...options
});
};
/**
* OAuth Server Metadata
*

View File

@@ -3186,6 +3186,94 @@ export type GetOrganizationMembersResponses = {
export type GetOrganizationMembersResponse = GetOrganizationMembersResponses[keyof GetOrganizationMembersResponses];
export type StreamProjectEventsData = {
body?: never;
headers?: {
/**
* Last-Event-Id
*/
'Last-Event-ID'?: string | null;
};
path: {
/**
* Project Id
*/
project_id: string;
};
query?: never;
url: '/api/v1/projects/{project_id}/events/stream';
};
export type StreamProjectEventsErrors = {
/**
* Not authenticated
*/
401: unknown;
/**
* Not authorized to access this project
*/
403: unknown;
/**
* Project not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type StreamProjectEventsError = StreamProjectEventsErrors[keyof StreamProjectEventsErrors];
export type StreamProjectEventsResponses = {
/**
* SSE stream established
*/
200: unknown;
};
export type SendTestEventData = {
body?: never;
path: {
/**
* Project Id
*/
project_id: string;
};
query?: never;
url: '/api/v1/projects/{project_id}/events/test';
};
export type SendTestEventErrors = {
/**
* Not authenticated
*/
401: unknown;
/**
* Not authorized to access this project
*/
403: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type SendTestEventError = SendTestEventErrors[keyof SendTestEventErrors];
export type SendTestEventResponses = {
/**
* Response Send Test Event
*
* Test event sent
*/
200: {
[key: string]: unknown;
};
};
export type SendTestEventResponse = SendTestEventResponses[keyof SendTestEventResponses];
export type GetOauthServerMetadataData = {
body?: never;
path?: never;

View File

@@ -121,7 +121,7 @@ export function useLogin(onSuccess?: () => void) {
const { access_token, refresh_token, user, expires_in } = data;
// Update auth store with user and tokens
await setAuth(user as User, access_token, refresh_token || '', expires_in);
await setAuth(user as User, access_token, refresh_token || '', expires_in ?? undefined);
// Invalidate and refetch user data
queryClient.invalidateQueries({ queryKey: authKeys.all });
@@ -194,7 +194,7 @@ export function useRegister(onSuccess?: () => void) {
const { access_token, refresh_token, user, expires_in } = data;
// Update auth store with user and tokens (auto-login)
await setAuth(user as User, access_token, refresh_token || '', expires_in);
await setAuth(user as User, access_token, refresh_token || '', expires_in ?? undefined);
// Invalidate and refetch user data
queryClient.invalidateQueries({ queryKey: authKeys.all });

View File

@@ -7,21 +7,15 @@
* @module lib/api/types
*/
import type { Token, UserResponse } from './generated/types.gen';
import type { Token } from './generated/types.gen';
/**
* Extended Token Response
* Token with User Response
*
* The actual backend response includes additional fields not captured in OpenAPI spec:
* - user: UserResponse object
* - expires_in: Token expiration in seconds
*
* TODO: Update backend OpenAPI spec to include these fields
* Alias for Token type which now includes user and expires_in fields.
* Kept for backwards compatibility with existing type guards.
*/
export interface TokenWithUser extends Token {
user: UserResponse;
expires_in?: number;
}
export type TokenWithUser = Token;
/**
* Success Response (for operations that return success messages)

View File

@@ -129,6 +129,7 @@ export function useProjectEvents(
const currentRetryDelayRef = useRef(initialRetryDelay);
const isManualDisconnectRef = useRef(false);
const mountedRef = useRef(true);
const pingHandlerRef = useRef<(() => void) | null>(null);
/**
* Update connection state and notify callback
@@ -191,6 +192,12 @@ export function useProjectEvents(
retryTimeoutRef.current = null;
}
// Remove ping listener before closing to prevent memory leak
if (eventSourceRef.current && pingHandlerRef.current) {
eventSourceRef.current.removeEventListener('ping', pingHandlerRef.current);
pingHandlerRef.current = null;
}
if (eventSourceRef.current) {
eventSourceRef.current.close();
eventSourceRef.current = null;
@@ -286,12 +293,15 @@ export function useProjectEvents(
};
// Handle specific event types from backend
eventSource.addEventListener('ping', () => {
// Store handler reference for proper cleanup
const pingHandler = () => {
// Keep-alive ping from server, no action needed
if (config.debug.api) {
console.log('[SSE] Received ping');
}
});
};
pingHandlerRef.current = pingHandler;
eventSource.addEventListener('ping', pingHandler);
eventSource.onerror = (err) => {
if (!mountedRef.current) return;
@@ -355,30 +365,26 @@ export function useProjectEvents(
clearProjectEvents(projectId);
}, [clearProjectEvents, projectId]);
// Auto-connect on mount if enabled
// Consolidated connection management effect
// Handles both initial mount and auth state changes to prevent race conditions
useEffect(() => {
mountedRef.current = true;
if (autoConnect && isAuthenticated && projectId) {
connect();
// Connect when authenticated with a project and not manually disconnected
if (autoConnect && isAuthenticated && accessToken && projectId) {
if (connectionState === 'disconnected' && !isManualDisconnectRef.current) {
connect();
}
} else if (!isAuthenticated && connectionState !== 'disconnected') {
// Disconnect when auth is lost
disconnect();
}
return () => {
mountedRef.current = false;
cleanup();
};
}, [autoConnect, isAuthenticated, projectId, connect, cleanup]);
// Reconnect when auth changes
useEffect(() => {
if (isAuthenticated && accessToken && connectionState === 'disconnected' && autoConnect) {
if (!isManualDisconnectRef.current) {
connect();
}
} else if (!isAuthenticated && connectionState !== 'disconnected') {
disconnect();
}
}, [isAuthenticated, accessToken, connectionState, autoConnect, connect, disconnect]);
}, [autoConnect, isAuthenticated, accessToken, projectId, connectionState, connect, disconnect, cleanup]);
return {
events,

View File

@@ -8,7 +8,7 @@
*
* For custom handler behavior, use src/mocks/handlers/overrides.ts
*
* Generated: 2025-11-26T12:21:51.098Z
* Generated: 2025-12-30T02:14:59.598Z
*/
import { http, HttpResponse, delay } from 'msw';
@@ -579,4 +579,28 @@ export const generatedHandlers = [
message: 'Operation successful'
});
}),
/**
* Stream Project Events
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/events/stream`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Send Test Event (Development Only)
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/events/test`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
];