feat: Complete Real-Time Sync implementation 🔄
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

BACKEND:
- Fix JWT authentication in FamiliesGateway
  * Configure JwtModule with ConfigService in FamiliesModule
  * Load JWT_SECRET from environment variables
  * Enable proper token verification for WebSocket connections
- Fix circular dependency in TrackingModule
  * Use forwardRef pattern for FamiliesGateway injection
  * Make FamiliesGateway optional in TrackingService
  * Emit WebSocket events when activities are created/updated/deleted

FRONTEND:
- Create WebSocket service (336 lines)
  * Socket.IO client with auto-reconnection (exponential backoff 1s → 30s)
  * Family room join/leave management
  * Presence tracking (online users per family)
  * Event handlers for activities, children, members
  * Connection recovery with auto-rejoin
- Create useWebSocket hook (187 lines)
  * Auto-connect on user authentication
  * Auto-join user's family room
  * Connection status tracking
  * Presence indicators
  * Hooks: useRealTimeActivities, useRealTimeChildren, useRealTimeFamilyMembers
- Expose access token in AuthContext
  * Add token property to AuthContextType interface
  * Load token from tokenStorage on initialization
  * Update token state on login/register/logout
  * Enable WebSocket authentication
- Integrate real-time sync across app
  * AppShell: Connection status indicator + online count badge
  * Activities page: Auto-refresh on family activity events
  * Home page: Auto-refresh daily summary on activity changes
  * Family page: Real-time member updates
- Fix accessibility issues
  * Remove deprecated legacyBehavior from Link components (Next.js 15)
  * Fix color contrast in EmailVerificationBanner (WCAG AA)
  * Add missing aria-labels to IconButtons
  * Fix React key warnings in family member list

DOCUMENTATION:
- Update implementation-gaps.md
  * Mark Real-Time Sync as COMPLETED 
  * Document WebSocket room management implementation
  * Document connection recovery and presence indicators
  * Update summary statistics (49 features completed)

FILES CREATED:
- maternal-web/hooks/useWebSocket.ts (187 lines)
- maternal-web/lib/websocket.ts (336 lines)

FILES MODIFIED (14):
Backend (4):
- families.gateway.ts (JWT verification fix)
- families.module.ts (JWT config with ConfigService)
- tracking.module.ts (forwardRef for FamiliesModule)
- tracking.service.ts (emit WebSocket events)

Frontend (9):
- lib/auth/AuthContext.tsx (expose access token)
- components/layouts/AppShell/AppShell.tsx (connection status + presence)
- app/activities/page.tsx (real-time activity updates)
- app/page.tsx (real-time daily summary refresh)
- app/family/page.tsx (accessibility fixes)
- app/(auth)/login/page.tsx (remove legacyBehavior)
- components/common/EmailVerificationBanner.tsx (color contrast fix)

Documentation (1):
- docs/implementation-gaps.md (updated status)

IMPACT:
 Real-time family collaboration achieved
 Activities sync instantly across all family members' devices
 Presence tracking shows who's online
 Connection recovery handles poor network conditions
 Accessibility improvements (WCAG AA compliance)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-02 22:06:24 +00:00
parent 29960e7d24
commit 7f9226b943
14 changed files with 871 additions and 95 deletions

View File

@@ -26,8 +26,10 @@ export class FamiliesGateway
private logger = new Logger('FamiliesGateway');
private connectedClients = new Map<
string,
{ socket: Socket; userId: string; familyId: string }
{ socket: Socket; userId: string; familyId: string; username: string }
>();
// Track online users per family
private familyPresence = new Map<string, Set<string>>(); // familyId -> Set of userIds
constructor(
private jwtService: JwtService,
@@ -52,6 +54,7 @@ export class FamiliesGateway
// Verify JWT token
const payload = await this.jwtService.verifyAsync(token);
const userId = payload.userId;
const username = payload.name || payload.email || 'Unknown';
this.logger.log(`Client connected: ${client.id}, User: ${userId}`);
@@ -60,6 +63,7 @@ export class FamiliesGateway
socket: client,
userId,
familyId: null, // Will be set when user joins a family room
username,
});
// Emit connection success
@@ -83,7 +87,10 @@ export class FamiliesGateway
// Leave family room if connected
if (clientData.familyId) {
this.removeUserFromPresence(clientData.familyId, clientData.userId);
client.leave(`family:${clientData.familyId}`);
// Notify others in the family
this.broadcastPresence(clientData.familyId);
}
this.connectedClients.delete(client.id);
@@ -108,21 +115,32 @@ export class FamiliesGateway
// Leave previous family room if any
if (clientData.familyId) {
this.removeUserFromPresence(clientData.familyId, clientData.userId);
client.leave(`family:${clientData.familyId}`);
this.broadcastPresence(clientData.familyId);
}
// Join new family room
client.join(`family:${data.familyId}`);
clientData.familyId = data.familyId;
// Add user to presence tracking
this.addUserToPresence(data.familyId, clientData.userId);
this.logger.log(
`User ${clientData.userId} joined family room: ${data.familyId}`,
);
// Send current presence to the joining client
const onlineUsers = this.getOnlineUsers(data.familyId);
client.emit('familyJoined', {
familyId: data.familyId,
message: 'Successfully joined family updates',
onlineUsers,
});
// Broadcast presence update to all family members
this.broadcastPresence(data.familyId);
} catch (error) {
this.logger.error(`Failed to join family: ${error.message}`);
client.emit('error', { message: 'Failed to join family room' });
@@ -200,4 +218,51 @@ export class FamiliesGateway
this.server.to(`family:${familyId}`).emit('childDeleted', { childId });
this.logger.log(`Child deleted notification sent to family: ${familyId}`);
}
// Presence management methods
private addUserToPresence(familyId: string, userId: string) {
if (!this.familyPresence.has(familyId)) {
this.familyPresence.set(familyId, new Set());
}
this.familyPresence.get(familyId).add(userId);
}
private removeUserFromPresence(familyId: string, userId: string) {
const presence = this.familyPresence.get(familyId);
if (presence) {
presence.delete(userId);
if (presence.size === 0) {
this.familyPresence.delete(familyId);
}
}
}
private getOnlineUsers(familyId: string): string[] {
const presence = this.familyPresence.get(familyId);
return presence ? Array.from(presence) : [];
}
private broadcastPresence(familyId: string) {
const onlineUsers = this.getOnlineUsers(familyId);
this.server.to(`family:${familyId}`).emit('presenceUpdate', {
onlineUsers,
count: onlineUsers.length,
});
this.logger.log(
`Presence update sent to family ${familyId}: ${onlineUsers.length} online`,
);
}
// Public method to get presence for a family (can be called from services)
getPresenceForFamily(familyId: string): {
onlineUsers: string[];
count: number;
} {
const onlineUsers = this.getOnlineUsers(familyId);
return {
onlineUsers,
count: onlineUsers.length,
};
}
}

View File

@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { FamiliesService } from './families.service';
import { FamiliesController } from './families.controller';
import { FamiliesGateway } from './families.gateway';
@@ -11,7 +12,16 @@ import { User } from '../../database/entities/user.entity';
@Module({
imports: [
TypeOrmModule.forFeature([Family, FamilyMember, User]),
JwtModule.register({}), // Will use global JWT config from AuthModule
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: configService.get<string>('JWT_EXPIRATION', '1h'),
},
}),
inject: [ConfigService],
}),
],
controllers: [FamiliesController],
providers: [FamiliesService, FamiliesGateway],

View File

@@ -1,13 +1,17 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TrackingService } from './tracking.service';
import { TrackingController } from './tracking.controller';
import { Activity } from '../../database/entities/activity.entity';
import { Child } from '../../database/entities/child.entity';
import { FamilyMember } from '../../database/entities/family-member.entity';
import { FamiliesModule } from '../families/families.module';
@Module({
imports: [TypeOrmModule.forFeature([Activity, Child, FamilyMember])],
imports: [
TypeOrmModule.forFeature([Activity, Child, FamilyMember]),
forwardRef(() => FamiliesModule),
],
controllers: [TrackingController],
providers: [TrackingService],
exports: [TrackingService],

View File

@@ -3,6 +3,9 @@ import {
NotFoundException,
ForbiddenException,
BadRequestException,
Inject,
forwardRef,
Optional,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Between, MoreThanOrEqual, LessThanOrEqual } from 'typeorm';
@@ -12,6 +15,7 @@ import {
} from '../../database/entities/activity.entity';
import { Child } from '../../database/entities/child.entity';
import { FamilyMember } from '../../database/entities/family-member.entity';
import { FamiliesGateway } from '../families/families.gateway';
import { CreateActivityDto } from './dto/create-activity.dto';
import { UpdateActivityDto } from './dto/update-activity.dto';
@@ -24,6 +28,9 @@ export class TrackingService {
private childRepository: Repository<Child>,
@InjectRepository(FamilyMember)
private familyMemberRepository: Repository<FamilyMember>,
@Optional()
@Inject(forwardRef(() => FamiliesGateway))
private familiesGateway?: FamiliesGateway,
) {}
async create(
@@ -66,7 +73,17 @@ export class TrackingService {
: null,
});
return await this.activityRepository.save(activity);
const savedActivity = await this.activityRepository.save(activity);
// Emit WebSocket event to family members
if (this.familiesGateway) {
this.familiesGateway.notifyFamilyActivityCreated(
child.familyId,
savedActivity,
);
}
return savedActivity;
}
async findAll(