forked from cardosofelipe/fast-next-template
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:
@@ -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)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user