fix(frontend): Fix critical type safety issues from code review

Address all Critical and High Priority issues identified in frontend code review report:

Critical Issues Fixed:
- Created unified logger utility (lib/utils/logger.ts) to replace all console.log statements
- Consolidated User type definitions - removed duplicate from authStore, using single source from types/user.ts
- Eliminated 'any' types in API client - added proper generic types with AxiosRequestConfig
- Fixed SignalR ConnectionManager - replaced 'any' with generic types <T>
- Created API error types (lib/types/errors.ts) with ApiError and getErrorMessage helper
- Fixed IssueCard component - removed all type assertions, created discriminated union types for Kanban items
- Added React.memo to IssueCard for performance optimization
- Added proper ARIA labels and accessibility attributes to IssueCard

High Priority Issues Fixed:
- Fixed hardcoded user ID in CreateProjectDialog - now uses actual user from authStore
- Added useCallback to CreateProjectDialog onSubmit handler
- Fixed error handlers in use-epics.ts - replaced 'any' with ApiError type
- Updated all error handling to use logger and getErrorMessage

Type Safety Improvements:
- Created KanbanItem discriminated union (KanbanEpic | KanbanStory | KanbanTask) with proper type guards
- Added 'never' types to prevent invalid property access
- Fixed User interface to include all required fields (createdAt, updatedAt)
- Maintained backward compatibility with LegacyKanbanBoard for existing code

Files Changed:
- lib/utils/logger.ts - New centralized logging utility
- lib/types/errors.ts - New API error types and helpers
- types/user.ts - Consolidated User type with TenantRole
- types/kanban.ts - New discriminated union types for type-safe Kanban items
- components/features/kanban/IssueCard.tsx - Type-safe with React.memo
- components/features/projects/CreateProjectDialog.tsx - Fixed hardcoded user ID, added useCallback
- lib/api/client.ts - Eliminated 'any', added proper generics
- lib/signalr/ConnectionManager.ts - Replaced console.log, added generics
- lib/hooks/use-epics.ts - Fixed error handler types
- stores/authStore.ts - Removed duplicate User type
- lib/hooks/useAuth.ts - Added createdAt field to User

TypeScript compilation:  All type checks passing (0 errors)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Yaojia Wang
2025-11-05 19:11:48 +01:00
parent 90e3d2416c
commit ea67d90880
21 changed files with 1459 additions and 120 deletions

View File

@@ -1,6 +1,7 @@
import * as signalR from '@microsoft/signalr';
import { tokenManager } from '@/lib/api/client';
import { SIGNALR_CONFIG } from './config';
import { logger } from '@/lib/utils/logger';
export type ConnectionState =
| 'disconnected'
@@ -23,13 +24,13 @@ export class SignalRConnectionManager {
this.connection &&
this.connection.state === signalR.HubConnectionState.Connected
) {
console.log('[SignalR] Already connected');
logger.debug('[SignalR] Already connected');
return;
}
const token = tokenManager.getAccessToken();
if (!token) {
console.warn('[SignalR] No access token found, cannot connect');
logger.warn('[SignalR] No access token found, cannot connect');
return;
}
@@ -52,11 +53,11 @@ export class SignalRConnectionManager {
try {
this.notifyStateChange('connecting');
await this.connection.start();
console.log(`[SignalR] Connected to ${this.hubUrl}`);
logger.info(`[SignalR] Connected to ${this.hubUrl}`);
this.notifyStateChange('connected');
this.reconnectAttempt = 0;
} catch (error) {
console.error('[SignalR] Connection error:', error);
logger.error('[SignalR] Connection error', error);
this.notifyStateChange('disconnected');
this.scheduleReconnect();
}
@@ -67,17 +68,17 @@ export class SignalRConnectionManager {
await this.connection.stop();
this.connection = null;
this.notifyStateChange('disconnected');
console.log('[SignalR] Disconnected');
logger.info('[SignalR] Disconnected');
}
}
on(methodName: string, callback: (...args: any[]) => void): void {
on<T = unknown>(methodName: string, callback: (data: T) => void): void {
if (this.connection) {
this.connection.on(methodName, callback);
}
}
off(methodName: string, callback?: (...args: any[]) => void): void {
off<T = unknown>(methodName: string, callback?: (data: T) => void): void {
if (this.connection && callback) {
this.connection.off(methodName, callback);
} else if (this.connection) {
@@ -85,7 +86,7 @@ export class SignalRConnectionManager {
}
}
async invoke(methodName: string, ...args: any[]): Promise<any> {
async invoke<T = unknown>(methodName: string, ...args: unknown[]): Promise<T> {
if (
!this.connection ||
this.connection.state !== signalR.HubConnectionState.Connected
@@ -93,7 +94,7 @@ export class SignalRConnectionManager {
throw new Error('SignalR connection is not established');
}
return await this.connection.invoke(methodName, ...args);
return await this.connection.invoke<T>(methodName, ...args);
}
onStateChange(listener: (state: ConnectionState) => void): () => void {
@@ -109,18 +110,18 @@ export class SignalRConnectionManager {
if (!this.connection) return;
this.connection.onclose((error) => {
console.log('[SignalR] Connection closed', error);
logger.info('[SignalR] Connection closed', error);
this.notifyStateChange('disconnected');
this.scheduleReconnect();
});
this.connection.onreconnecting((error) => {
console.log('[SignalR] Reconnecting...', error);
logger.info('[SignalR] Reconnecting...', error);
this.notifyStateChange('reconnecting');
});
this.connection.onreconnected((connectionId) => {
console.log('[SignalR] Reconnected', connectionId);
logger.info('[SignalR] Reconnected', connectionId);
this.notifyStateChange('connected');
this.reconnectAttempt = 0;
});
@@ -128,14 +129,14 @@ export class SignalRConnectionManager {
private scheduleReconnect(): void {
if (this.reconnectAttempt >= SIGNALR_CONFIG.RECONNECT_DELAYS.length) {
console.error('[SignalR] Max reconnect attempts reached');
logger.error('[SignalR] Max reconnect attempts reached');
return;
}
const delay = SIGNALR_CONFIG.RECONNECT_DELAYS[this.reconnectAttempt];
this.reconnectAttempt++;
console.log(
logger.info(
`[SignalR] Scheduling reconnect in ${delay}ms (attempt ${this.reconnectAttempt})`
);