diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 909e2bf..5b2717b 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -64,20 +64,22 @@ func main() { // Initialize repositories userRepo := repositories.NewUserRepository(database.GetDB()) meetingRepo := repositories.NewMeetingRepository(database.GetDB()) - summarizerRepo := repositories.NewSummarizerRepository(database.GetDB()) + sessionRepo := repositories.NewSummarizerSessionRepository(database.GetDB()) + chunkRepo := repositories.NewAudioChunkRepository(database.GetDB()) + transcriptRepo := repositories.NewTranscriptRepository(database.GetDB()) // Initialize services - userService := services.NewUserService(userRepo, meetingRepo) meetingService := services.NewMeetingService(meetingRepo) + userService := services.NewUserService(userRepo, meetingService) livekitService := services.NewLiveKitService(cfg) openRouterService := services.NewOpenRouterService(cfg) emailService := services.NewEmailService(cfg) // Dependency chain: SummarizationService <- NormalizationService <- TranscriptionService <- SummarizerService - summarizationService := services.NewSummarizationService(summarizerRepo, userRepo, openRouterService, emailService, cfg) - normalizationService := services.NewNormalizationService(summarizerRepo, summarizationService) - transcriptionService := services.NewTranscriptionService(summarizerRepo, normalizationService, cfg) - summarizerService := services.NewSummarizerService(summarizerRepo, meetingRepo, userRepo, livekitService, transcriptionService, cfg) + summarizationService := services.NewSummarizationService(sessionRepo, userService, openRouterService, emailService, cfg) + normalizationService := services.NewNormalizationService(sessionRepo, transcriptRepo, summarizationService) + transcriptionService := services.NewTranscriptionService(sessionRepo, chunkRepo, transcriptRepo, normalizationService, cfg) + summarizerService := services.NewSummarizerService(sessionRepo, chunkRepo, transcriptRepo, meetingService, livekitService, transcriptionService, cfg) // Initialize handlers userHandler := handlers.NewUserHandler(userService) @@ -90,13 +92,13 @@ func main() { // Initialize workers // Transcription worker: Run every 60 minutes, process sessions stuck for > 15 minutes - transcriptionWorker := workers.NewTranscriptionWorker(summarizerRepo, transcriptionService, 60*time.Minute, 15*time.Minute) + transcriptionWorker := workers.NewTranscriptionWorker(sessionRepo, transcriptionService, 60*time.Minute, 15*time.Minute) go transcriptionWorker.Start() // Normalization worker: Run every 60 minutes, process sessions stuck for > 15 minutes - normalizationWorker := workers.NewNormalizationWorker(summarizerRepo, normalizationService, 60*time.Minute, 15*time.Minute) + normalizationWorker := workers.NewNormalizationWorker(sessionRepo, normalizationService, 60*time.Minute, 15*time.Minute) go normalizationWorker.Start() // Summarization worker: Run every 60 minutes, process sessions stuck for > 15 minutes - summarizationWorker := workers.NewSummarizationWorker(summarizerRepo, summarizationService, 60*time.Minute, 15*time.Minute) + summarizationWorker := workers.NewSummarizationWorker(sessionRepo, summarizationService, 60*time.Minute, 15*time.Minute) go summarizationWorker.Start() // Setup routes diff --git a/backend/internal/types/livekit.go b/backend/internal/handlers/dto/livekit.go similarity index 99% rename from backend/internal/types/livekit.go rename to backend/internal/handlers/dto/livekit.go index be81133..a1ee834 100644 --- a/backend/internal/types/livekit.go +++ b/backend/internal/handlers/dto/livekit.go @@ -1,4 +1,4 @@ -package types +package dto // GenerateTokenRequest represents the request to generate a LiveKit token type GenerateTokenRequest struct { diff --git a/backend/internal/types/lobby.go b/backend/internal/handlers/dto/lobby.go similarity index 66% rename from backend/internal/types/lobby.go rename to backend/internal/handlers/dto/lobby.go index a3df289..6cc127f 100644 --- a/backend/internal/types/lobby.go +++ b/backend/internal/handlers/dto/lobby.go @@ -1,4 +1,4 @@ -package types +package dto // LobbyJoinRequest represents a request to join a meeting lobby type LobbyJoinRequest struct { @@ -18,11 +18,3 @@ type LobbyJoinResponse struct { Identity string `json:"identity,omitempty"` UserName string `json:"user_name,omitempty"` } - -// LobbyRespondRequest is sent by admin to approve/reject a request (HTTP fallback) -type LobbyRespondRequest struct { - MeetingCode string `json:"meeting_code" validate:"required"` - RequestID string `json:"request_id" validate:"required"` - Action string `json:"action" validate:"required"` // "approve" or "reject" -} - diff --git a/backend/internal/types/meeting.go b/backend/internal/handlers/dto/meeting.go similarity index 98% rename from backend/internal/types/meeting.go rename to backend/internal/handlers/dto/meeting.go index 931a0e7..8e5828b 100644 --- a/backend/internal/types/meeting.go +++ b/backend/internal/handlers/dto/meeting.go @@ -1,4 +1,4 @@ -package types +package dto import ( "mini-meeting/internal/models" diff --git a/backend/internal/types/summarizer.go b/backend/internal/handlers/dto/summarizer.go similarity index 98% rename from backend/internal/types/summarizer.go rename to backend/internal/handlers/dto/summarizer.go index d9cc247..d69bbf3 100644 --- a/backend/internal/types/summarizer.go +++ b/backend/internal/handlers/dto/summarizer.go @@ -1,4 +1,4 @@ -package types +package dto import ( "mini-meeting/internal/models" diff --git a/backend/internal/types/user.go b/backend/internal/handlers/dto/user.go similarity index 98% rename from backend/internal/types/user.go rename to backend/internal/handlers/dto/user.go index 0033399..b0c8f73 100644 --- a/backend/internal/types/user.go +++ b/backend/internal/handlers/dto/user.go @@ -1,4 +1,4 @@ -package types +package dto import "mini-meeting/internal/models" diff --git a/backend/internal/handlers/livekit_handler.go b/backend/internal/handlers/livekit_handler.go index d1742a4..5e3473d 100644 --- a/backend/internal/handlers/livekit_handler.go +++ b/backend/internal/handlers/livekit_handler.go @@ -5,8 +5,8 @@ import ( "strings" "mini-meeting/internal/config" + "mini-meeting/internal/handlers/dto" "mini-meeting/internal/services" - "mini-meeting/internal/types" "mini-meeting/pkg/utils" "github.com/gofiber/fiber/v2" @@ -56,7 +56,7 @@ func (h *LiveKitHandler) GenerateToken(c *fiber.Ctx) error { } } - var req types.GenerateTokenRequest + var req dto.GenerateTokenRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request body", @@ -131,7 +131,7 @@ func (h *LiveKitHandler) GenerateToken(c *fiber.Ctx) error { }) } - response := types.GenerateTokenResponse{ + response := dto.GenerateTokenResponse{ Token: token, URL: h.livekitService.GetURL(), RoomCode: req.MeetingCode, @@ -151,7 +151,7 @@ func (h *LiveKitHandler) RemoveParticipant(c *fiber.Ctx) error { }) } - var req types.RemoveParticipantRequest + var req dto.RemoveParticipantRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request body", @@ -210,9 +210,9 @@ func (h *LiveKitHandler) ListParticipants(c *fiber.Ctx) error { } // Convert to response format - participantInfos := make([]types.ParticipantInfo, 0, len(participants)) + participantInfos := make([]dto.ParticipantInfo, 0, len(participants)) for _, p := range participants { - participantInfos = append(participantInfos, types.ParticipantInfo{ + participantInfos = append(participantInfos, dto.ParticipantInfo{ Identity: p.Identity, Name: p.Name, State: p.State.String(), @@ -221,7 +221,7 @@ func (h *LiveKitHandler) ListParticipants(c *fiber.Ctx) error { }) } - response := types.ListParticipantsResponse{ + response := dto.ListParticipantsResponse{ Participants: participantInfos, } @@ -266,7 +266,7 @@ func (h *LiveKitHandler) MuteParticipant(c *fiber.Ctx) error { }) } - var req types.MuteParticipantRequest + var req dto.MuteParticipantRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request body", @@ -308,7 +308,7 @@ func (h *LiveKitHandler) EndMeeting(c *fiber.Ctx) error { }) } - var req types.EndMeetingRequest + var req dto.EndMeetingRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request body", diff --git a/backend/internal/handlers/lobby_handler.go b/backend/internal/handlers/lobby_handler.go index 06ec9bd..2c95883 100644 --- a/backend/internal/handlers/lobby_handler.go +++ b/backend/internal/handlers/lobby_handler.go @@ -7,8 +7,8 @@ import ( "mini-meeting/internal/cache" "mini-meeting/internal/config" + "mini-meeting/internal/handlers/dto" "mini-meeting/internal/services" - "mini-meeting/internal/types" "mini-meeting/pkg/utils" "github.com/gofiber/fiber/v2" @@ -41,7 +41,7 @@ func NewLobbyHandler( // Otherwise, a pending request is created in the lobby cache. // POST /api/v1/lobby/request func (h *LobbyHandler) RequestToJoin(c *fiber.Ctx) error { - var req types.LobbyJoinRequest + var req dto.LobbyJoinRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request body", @@ -123,7 +123,7 @@ func (h *LobbyHandler) RequestToJoin(c *fiber.Ctx) error { }) } - return c.JSON(types.LobbyJoinResponse{ + return c.JSON(dto.LobbyJoinResponse{ RequestID: "", Status: "auto_approved", Token: token, @@ -158,7 +158,7 @@ func (h *LobbyHandler) RequestToJoin(c *fiber.Ctx) error { // Notify admins via WebSocket NotifyAdminsOfNewRequest(lobbyReq) - return c.JSON(types.LobbyJoinResponse{ + return c.JSON(dto.LobbyJoinResponse{ RequestID: requestID, Status: "pending", }) @@ -195,127 +195,3 @@ func (h *LobbyHandler) CancelRequest(c *fiber.Ctx) error { "message": "Request cancelled", }) } - -// RespondToRequest lets the admin approve or reject a lobby request (HTTP fallback). -// POST /api/v1/lobby/respond -func (h *LobbyHandler) RespondToRequest(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(uint) - if !ok { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "error": "Unauthorized", - }) - } - - var req types.LobbyRespondRequest - if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request body", - }) - } - - if req.MeetingCode == "" || req.RequestID == "" || req.Action == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "meeting_code, request_id, and action are required", - }) - } - - if req.Action != "approve" && req.Action != "reject" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "action must be 'approve' or 'reject'", - }) - } - - // Verify the user is the meeting creator - meeting, err := h.meetingService.GetMeetingByCode(req.MeetingCode) - if err != nil { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ - "error": "Meeting not found", - }) - } - - if meeting.CreatorID != userID { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ - "error": "Only meeting creator can manage lobby requests", - }) - } - - // Get the lobby request - lobbyReq, err := cache.GetLobbyRequest(req.RequestID) - if err != nil { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ - "error": "Request not found or expired", - }) - } - - if lobbyReq.MeetingCode != req.MeetingCode { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Request does not belong to this meeting", - }) - } - - if req.Action == "reject" { - if err := cache.UpdateLobbyRequestStatus(req.RequestID, cache.LobbyStatusRejected); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Failed to reject request", - }) - } - - // Notify visitor via WebSocket - cache.Hub.NotifyVisitor(req.RequestID, map[string]string{"type": "rejected"}) - cache.Hub.NotifyAdmins(req.MeetingCode, map[string]string{ - "type": "request_resolved", - "request_id": req.RequestID, - }) - go cache.CleanupLobbyRequest(req.RequestID) - - return c.JSON(fiber.Map{ - "message": "Request rejected", - }) - } - - // Approve: generate token and store it - metadata := fmt.Sprintf(`{"name":"%s","avatar":"%s","role":"%s"}`, lobbyReq.Name, lobbyReq.AvatarURL, lobbyReq.Role) - - token, err := h.livekitService.CreateJoinToken( - lobbyReq.MeetingCode, - lobbyReq.Identity, - lobbyReq.Name, - lobbyReq.Role, - metadata, - ) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Failed to generate token", - }) - } - - // Update status to approved - if err := cache.UpdateLobbyRequestStatus(req.RequestID, cache.LobbyStatusApproved); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Failed to approve request", - }) - } - - // Notify visitor via WebSocket - cache.Hub.NotifyVisitor(req.RequestID, map[string]interface{}{ - "type": "approved", - "token": token, - "url": h.livekitService.GetURL(), - "room_code": lobbyReq.MeetingCode, - "identity": lobbyReq.Identity, - "user_name": lobbyReq.Name, - }) - cache.Hub.NotifyAdmins(req.MeetingCode, map[string]string{ - "type": "request_resolved", - "request_id": req.RequestID, - }) - go func() { - // give visitor time to receive the token - time.Sleep(5 * time.Second) - cache.CleanupLobbyRequest(req.RequestID) - }() - - return c.JSON(fiber.Map{ - "message": "Request approved", - }) -} diff --git a/backend/internal/handlers/meeting_handler.go b/backend/internal/handlers/meeting_handler.go index 92ab6ed..2e7cf81 100644 --- a/backend/internal/handlers/meeting_handler.go +++ b/backend/internal/handlers/meeting_handler.go @@ -2,8 +2,8 @@ package handlers import ( "mini-meeting/internal/config" + "mini-meeting/internal/handlers/dto" "mini-meeting/internal/services" - "mini-meeting/internal/types" "strconv" "github.com/gofiber/fiber/v2" @@ -45,7 +45,7 @@ func (h *MeetingHandler) CreateMeeting(c *fiber.Ctx) error { return c.Status(fiber.StatusCreated).JSON(fiber.Map{ "message": "Meeting created successfully", - "data": types.ToMeetingResponse(meeting, baseURL), + "data": dto.ToMeetingResponse(meeting, baseURL), }) } @@ -77,7 +77,7 @@ func (h *MeetingHandler) GetMeeting(c *fiber.Ctx) error { } return c.JSON(fiber.Map{ - "data": types.ToMeetingResponse(meeting, baseURL), + "data": dto.ToMeetingResponse(meeting, baseURL), }) } @@ -109,7 +109,7 @@ func (h *MeetingHandler) GetMeetingByCode(c *fiber.Ctx) error { } return c.JSON(fiber.Map{ - "data": types.ToMeetingResponse(meeting, baseURL), + "data": dto.ToMeetingResponse(meeting, baseURL), }) } @@ -135,9 +135,9 @@ func (h *MeetingHandler) GetMyMeetings(c *fiber.Ctx) error { baseURL = "http://localhost:5173" } - responses := make([]types.MeetingResponse, len(meetings)) + responses := make([]dto.MeetingResponse, len(meetings)) for i, meeting := range meetings { - responses[i] = types.ToMeetingResponse(&meeting, baseURL) + responses[i] = dto.ToMeetingResponse(&meeting, baseURL) } return c.JSON(fiber.Map{ @@ -160,9 +160,9 @@ func (h *MeetingHandler) GetAllMeetings(c *fiber.Ctx) error { baseURL = "http://localhost:5173" } - responses := make([]types.MeetingResponse, len(meetings)) + responses := make([]dto.MeetingResponse, len(meetings)) for i, meeting := range meetings { - responses[i] = types.ToMeetingResponse(&meeting, baseURL) + responses[i] = dto.ToMeetingResponse(&meeting, baseURL) } return c.JSON(fiber.Map{ diff --git a/backend/internal/handlers/user_handler.go b/backend/internal/handlers/user_handler.go index c5eb810..b098834 100644 --- a/backend/internal/handlers/user_handler.go +++ b/backend/internal/handlers/user_handler.go @@ -1,8 +1,8 @@ package handlers import ( + "mini-meeting/internal/handlers/dto" "mini-meeting/internal/services" - "mini-meeting/internal/types" "strconv" "github.com/gofiber/fiber/v2" @@ -110,7 +110,7 @@ func (h *UserHandler) UpdateMe(c *fiber.Ctx) error { }) } - var req types.UpdateUserRequest + var req dto.UpdateUserRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request body", diff --git a/backend/internal/repositories/audio_chunk_repository.go b/backend/internal/repositories/audio_chunk_repository.go new file mode 100644 index 0000000..5be9251 --- /dev/null +++ b/backend/internal/repositories/audio_chunk_repository.go @@ -0,0 +1,45 @@ +package repositories + +import ( + "mini-meeting/internal/models" + + "gorm.io/gorm" +) + +type AudioChunkRepository struct { + db *gorm.DB +} + +func NewAudioChunkRepository(db *gorm.DB) *AudioChunkRepository { + return &AudioChunkRepository{db: db} +} + +func (r *AudioChunkRepository) Create(chunk *models.AudioChunk) error { + return r.db.Create(chunk).Error +} + +func (r *AudioChunkRepository) FindBySessionID(sessionID uint) ([]models.AudioChunk, error) { + var chunks []models.AudioChunk + err := r.db.Where("session_id = ?", sessionID). + Order("user_identity ASC, chunk_index ASC"). + Find(&chunks).Error + return chunks, err +} + +func (r *AudioChunkRepository) FindBySessionAndUser(sessionID uint, userIdentity string) ([]models.AudioChunk, error) { + var chunks []models.AudioChunk + err := r.db.Where("session_id = ? AND user_identity = ?", sessionID, userIdentity). + Order("chunk_index ASC"). + Find(&chunks).Error + return chunks, err +} + +func (r *AudioChunkRepository) CountBySessionID(sessionID uint) (int64, error) { + var count int64 + err := r.db.Model(&models.AudioChunk{}).Where("session_id = ?", sessionID).Count(&count).Error + return count, err +} + +func (r *AudioChunkRepository) DeleteBySessionID(sessionID uint) error { + return r.db.Where("session_id = ?", sessionID).Delete(&models.AudioChunk{}).Error +} diff --git a/backend/internal/repositories/summarizer_repository.go b/backend/internal/repositories/summarizer_repository.go deleted file mode 100644 index 0443409..0000000 --- a/backend/internal/repositories/summarizer_repository.go +++ /dev/null @@ -1,144 +0,0 @@ -package repositories - -import ( - "mini-meeting/internal/models" - "time" - - "gorm.io/gorm" -) - -type SummarizerRepository struct { - db *gorm.DB -} - -func NewSummarizerRepository(db *gorm.DB) *SummarizerRepository { - return &SummarizerRepository{db: db} -} - -// Session operations - -func (r *SummarizerRepository) CreateSession(session *models.SummarizerSession) error { - return r.db.Create(session).Error -} - -func (r *SummarizerRepository) FindSessionByID(id uint) (*models.SummarizerSession, error) { - var session models.SummarizerSession - err := r.db.Preload("Meeting").First(&session, id).Error - if err != nil { - return nil, err - } - return &session, nil -} - -func (r *SummarizerRepository) FindActiveSessionByMeetingID(meetingID uint) (*models.SummarizerSession, error) { - var session models.SummarizerSession - err := r.db.Where("meeting_id = ? AND status = ?", meetingID, models.StatusStarted). - First(&session).Error - if err != nil { - return nil, err - } - return &session, nil -} - -func (r *SummarizerRepository) UpdateSessionStatus(id uint, status models.SummarizerSessionStatus, error *string, endedAt *time.Time) error { - updates := map[string]interface{}{ - "status": status, - "updated_at": time.Now(), - } - if error != nil { - updates["error"] = error - } - if endedAt != nil { - updates["ended_at"] = endedAt - } - return r.db.Model(&models.SummarizerSession{}).Where("id = ?", id).Updates(updates).Error -} - -func (r *SummarizerRepository) FindSessionsByMeetingID(meetingID uint) ([]models.SummarizerSession, error) { - var sessions []models.SummarizerSession - err := r.db.Where("meeting_id = ?", meetingID).Order("created_at DESC").Find(&sessions).Error - return sessions, err -} - -func (r *SummarizerRepository) FindStuckSessions(status models.SummarizerSessionStatus, cutoffTime time.Time) ([]models.SummarizerSession, error) { - var sessions []models.SummarizerSession - err := r.db.Where("status = ? AND updated_at < ?", status, cutoffTime).Find(&sessions).Error - return sessions, err -} - -func (r *SummarizerRepository) FindAllByUserIDPaginated(userID uint, page, pageSize int) ([]models.SummarizerSession, int64, error) { - var sessions []models.SummarizerSession - var total int64 - - query := r.db.Model(&models.SummarizerSession{}).Where("user_id = ?", userID) - - if err := query.Count(&total).Error; err != nil { - return nil, 0, err - } - - offset := (page - 1) * pageSize - err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&sessions).Error - if err != nil { - return nil, 0, err - } - - return sessions, total, nil -} - -func (r *SummarizerRepository) DeleteSession(id uint) error { - return r.db.Delete(&models.SummarizerSession{}, id).Error -} - -func (r *SummarizerRepository) CreateAudioChunk(chunk *models.AudioChunk) error { - return r.db.Create(chunk).Error -} - -func (r *SummarizerRepository) FindChunksBySessionID(sessionID uint) ([]models.AudioChunk, error) { - var chunks []models.AudioChunk - err := r.db.Where("session_id = ?", sessionID). - Order("user_identity ASC, chunk_index ASC"). - Find(&chunks).Error - return chunks, err -} - -func (r *SummarizerRepository) FindChunksBySessionAndUser(sessionID uint, userIdentity string) ([]models.AudioChunk, error) { - var chunks []models.AudioChunk - err := r.db.Where("session_id = ? AND user_identity = ?", sessionID, userIdentity). - Order("chunk_index ASC"). - Find(&chunks).Error - return chunks, err -} - -func (r *SummarizerRepository) CountChunksBySessionID(sessionID uint) (int64, error) { - var count int64 - err := r.db.Model(&models.AudioChunk{}).Where("session_id = ?", sessionID).Count(&count).Error - return count, err -} - -func (r *SummarizerRepository) DeleteChunksBySessionID(sessionID uint) error { - return r.db.Where("session_id = ?", sessionID).Delete(&models.AudioChunk{}).Error -} - -// Transcript operations - -func (r *SummarizerRepository) CreateTranscript(transcript *models.Transcript) error { - return r.db.Create(transcript).Error -} - -func (r *SummarizerRepository) FindTranscriptsBySessionID(sessionID uint) ([]models.Transcript, error) { - var transcripts []models.Transcript - err := r.db.Where("session_id = ?", sessionID).Order("start_time ASC").Find(&transcripts).Error - return transcripts, err -} - -func (r *SummarizerRepository) DeleteTranscriptsBySessionID(sessionID uint) error { - return r.db.Where("session_id = ?", sessionID).Delete(&models.Transcript{}).Error -} - -func (r *SummarizerRepository) UpdateSessionTranscript(id uint, transcript string) error { - return r.db.Model(&models.SummarizerSession{}).Where("id = ?", id).Update("transcript", transcript).Error -} - -func (r *SummarizerRepository) UpdateSessionSummary(id uint, summary string) error { - return r.db.Model(&models.SummarizerSession{}).Where("id = ?", id).Update("summary", summary).Error -} diff --git a/backend/internal/repositories/summarizer_session_repository.go b/backend/internal/repositories/summarizer_session_repository.go new file mode 100644 index 0000000..55584a9 --- /dev/null +++ b/backend/internal/repositories/summarizer_session_repository.go @@ -0,0 +1,96 @@ +package repositories + +import ( + "mini-meeting/internal/models" + "time" + + "gorm.io/gorm" +) + +type SummarizerSessionRepository struct { + db *gorm.DB +} + +func NewSummarizerSessionRepository(db *gorm.DB) *SummarizerSessionRepository { + return &SummarizerSessionRepository{db: db} +} + +func (r *SummarizerSessionRepository) Create(session *models.SummarizerSession) error { + return r.db.Create(session).Error +} + +func (r *SummarizerSessionRepository) FindByID(id uint) (*models.SummarizerSession, error) { + var session models.SummarizerSession + err := r.db.Preload("Meeting").First(&session, id).Error + if err != nil { + return nil, err + } + return &session, nil +} + +func (r *SummarizerSessionRepository) FindActiveByMeetingID(meetingID uint) (*models.SummarizerSession, error) { + var session models.SummarizerSession + err := r.db.Where("meeting_id = ? AND status = ?", meetingID, models.StatusStarted). + First(&session).Error + if err != nil { + return nil, err + } + return &session, nil +} + +func (r *SummarizerSessionRepository) UpdateStatus(id uint, status models.SummarizerSessionStatus, sessionError *string, endedAt *time.Time) error { + updates := map[string]interface{}{ + "status": status, + "updated_at": time.Now(), + } + if sessionError != nil { + updates["error"] = sessionError + } + if endedAt != nil { + updates["ended_at"] = endedAt + } + return r.db.Model(&models.SummarizerSession{}).Where("id = ?", id).Updates(updates).Error +} + +func (r *SummarizerSessionRepository) FindByMeetingID(meetingID uint) ([]models.SummarizerSession, error) { + var sessions []models.SummarizerSession + err := r.db.Where("meeting_id = ?", meetingID).Order("created_at DESC").Find(&sessions).Error + return sessions, err +} + +func (r *SummarizerSessionRepository) FindStuck(status models.SummarizerSessionStatus, cutoffTime time.Time) ([]models.SummarizerSession, error) { + var sessions []models.SummarizerSession + err := r.db.Where("status = ? AND updated_at < ?", status, cutoffTime).Find(&sessions).Error + return sessions, err +} + +func (r *SummarizerSessionRepository) FindAllByUserIDPaginated(userID uint, page, pageSize int) ([]models.SummarizerSession, int64, error) { + var sessions []models.SummarizerSession + var total int64 + + query := r.db.Model(&models.SummarizerSession{}).Where("user_id = ?", userID) + + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + offset := (page - 1) * pageSize + err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&sessions).Error + if err != nil { + return nil, 0, err + } + + return sessions, total, nil +} + +func (r *SummarizerSessionRepository) Delete(id uint) error { + return r.db.Delete(&models.SummarizerSession{}, id).Error +} + +func (r *SummarizerSessionRepository) UpdateTranscript(id uint, transcript string) error { + return r.db.Model(&models.SummarizerSession{}).Where("id = ?", id).Update("transcript", transcript).Error +} + +func (r *SummarizerSessionRepository) UpdateSummary(id uint, summary string) error { + return r.db.Model(&models.SummarizerSession{}).Where("id = ?", id).Update("summary", summary).Error +} diff --git a/backend/internal/repositories/transcript_repository.go b/backend/internal/repositories/transcript_repository.go new file mode 100644 index 0000000..1a0c8e9 --- /dev/null +++ b/backend/internal/repositories/transcript_repository.go @@ -0,0 +1,29 @@ +package repositories + +import ( + "mini-meeting/internal/models" + + "gorm.io/gorm" +) + +type TranscriptRepository struct { + db *gorm.DB +} + +func NewTranscriptRepository(db *gorm.DB) *TranscriptRepository { + return &TranscriptRepository{db: db} +} + +func (r *TranscriptRepository) Create(transcript *models.Transcript) error { + return r.db.Create(transcript).Error +} + +func (r *TranscriptRepository) FindBySessionID(sessionID uint) ([]models.Transcript, error) { + var transcripts []models.Transcript + err := r.db.Where("session_id = ?", sessionID).Order("start_time ASC").Find(&transcripts).Error + return transcripts, err +} + +func (r *TranscriptRepository) DeleteBySessionID(sessionID uint) error { + return r.db.Where("session_id = ?", sessionID).Delete(&models.Transcript{}).Error +} diff --git a/backend/internal/routes/auth.go b/backend/internal/routes/auth.go new file mode 100644 index 0000000..c0cd475 --- /dev/null +++ b/backend/internal/routes/auth.go @@ -0,0 +1,13 @@ +package routes + +import ( + "mini-meeting/internal/handlers" + + "github.com/gofiber/fiber/v2" +) + +func setupAuthRoutes(api fiber.Router, authHandler *handlers.AuthHandler) { + auth := api.Group("/auth") + auth.Get("/:provider", authHandler.OAuthLogin) + auth.Get("/:provider/callback", authHandler.OAuthCallback) +} diff --git a/backend/internal/routes/livekit.go b/backend/internal/routes/livekit.go new file mode 100644 index 0000000..be086a4 --- /dev/null +++ b/backend/internal/routes/livekit.go @@ -0,0 +1,23 @@ +package routes + +import ( + "mini-meeting/internal/config" + "mini-meeting/internal/handlers" + "mini-meeting/internal/middleware" + + "github.com/gofiber/fiber/v2" +) + +func setupLiveKitRoutes(api fiber.Router, livekitHandler *handlers.LiveKitHandler, cfg *config.Config) { + // Public — needed for guests joining a meeting + publicLiveKit := api.Group("/livekit") + publicLiveKit.Post("/token", livekitHandler.GenerateToken) + publicLiveKit.Get("/participants/count", livekitHandler.GetParticipantCount) + + // Protected — host/admin controls + livekit := api.Group("/livekit", middleware.AuthMiddleware(cfg)) + livekit.Get("/participants", livekitHandler.ListParticipants) + livekit.Post("/remove-participant", livekitHandler.RemoveParticipant) + livekit.Post("/mute-participant", livekitHandler.MuteParticipant) + livekit.Post("/end-meeting", livekitHandler.EndMeeting) +} diff --git a/backend/internal/routes/lobby.go b/backend/internal/routes/lobby.go new file mode 100644 index 0000000..bfebb0d --- /dev/null +++ b/backend/internal/routes/lobby.go @@ -0,0 +1,32 @@ +package routes + +import ( + "mini-meeting/internal/handlers" + + "github.com/gofiber/contrib/websocket" + "github.com/gofiber/fiber/v2" +) + +func setupLobbyRoutes( + app *fiber.App, + api fiber.Router, + lobbyHandler *handlers.LobbyHandler, + lobbyWSHandler *handlers.LobbyWSHandler, +) { + // Public lobby routes (guests request / cancel) + publicLobby := api.Group("/lobby") + publicLobby.Post("/request", lobbyHandler.RequestToJoin) + publicLobby.Delete("/request", lobbyHandler.CancelRequest) + + // WebSocket upgrade middleware + app.Use("/ws", func(c *fiber.Ctx) error { + if websocket.IsWebSocketUpgrade(c) { + return c.Next() + } + return fiber.ErrUpgradeRequired + }) + + // WebSocket lobby routes + app.Get("/ws/lobby/visitor", websocket.New(lobbyWSHandler.HandleVisitor)) + app.Get("/ws/lobby/admin", websocket.New(lobbyWSHandler.HandleAdmin)) +} diff --git a/backend/internal/routes/meeting.go b/backend/internal/routes/meeting.go new file mode 100644 index 0000000..aaf86d1 --- /dev/null +++ b/backend/internal/routes/meeting.go @@ -0,0 +1,40 @@ +package routes + +import ( + "mini-meeting/internal/config" + "mini-meeting/internal/handlers" + "mini-meeting/internal/middleware" + + "github.com/gofiber/fiber/v2" +) + +func setupMeetingRoutes( + api fiber.Router, + meetingHandler *handlers.MeetingHandler, + summarizerHandler *handlers.SummarizerHandler, + cfg *config.Config, +) { + // Public — guest accessible + publicMeetings := api.Group("/meetings") + publicMeetings.Get("/code/:code", meetingHandler.GetMeetingByCode) + + // Protected meeting routes + meetings := api.Group("/meetings", middleware.AuthMiddleware(cfg)) + meetings.Post("/", meetingHandler.CreateMeeting) + meetings.Get("/my", meetingHandler.GetMyMeetings) + meetings.Get("/:id", meetingHandler.GetMeeting) + meetings.Delete("/:id", meetingHandler.DeleteMeeting) + + // Summarizer sub-routes (under meetings) + meetings.Post("/:id/summarizer/start", summarizerHandler.StartSummarizer) + meetings.Post("/:id/summarizer/stop", summarizerHandler.StopSummarizer) + + // Admin-only + meetings.Get("/", middleware.AdminMiddleware(), meetingHandler.GetAllMeetings) + + // Session management routes (authenticated) + sessions := api.Group("/sessions", middleware.AuthMiddleware(cfg)) + sessions.Get("/", summarizerHandler.GetSessions) + sessions.Get("/:id", summarizerHandler.GetSession) + sessions.Delete("/:id", summarizerHandler.DeleteSession) +} diff --git a/backend/internal/routes/routes.go b/backend/internal/routes/routes.go index 4c9793a..ec11418 100644 --- a/backend/internal/routes/routes.go +++ b/backend/internal/routes/routes.go @@ -3,83 +3,26 @@ package routes import ( "mini-meeting/internal/config" "mini-meeting/internal/handlers" - "mini-meeting/internal/middleware" - "github.com/gofiber/contrib/websocket" "github.com/gofiber/fiber/v2" ) -func SetupRoutes(app *fiber.App, userHandler *handlers.UserHandler, authHandler *handlers.AuthHandler, meetingHandler *handlers.MeetingHandler, livekitHandler *handlers.LiveKitHandler, lobbyHandler *handlers.LobbyHandler, lobbyWSHandler *handlers.LobbyWSHandler, summarizerHandler *handlers.SummarizerHandler, cfg *config.Config) { - // API routes +func SetupRoutes( + app *fiber.App, + userHandler *handlers.UserHandler, + authHandler *handlers.AuthHandler, + meetingHandler *handlers.MeetingHandler, + livekitHandler *handlers.LiveKitHandler, + lobbyHandler *handlers.LobbyHandler, + lobbyWSHandler *handlers.LobbyWSHandler, + summarizerHandler *handlers.SummarizerHandler, + cfg *config.Config, +) { api := app.Group("/api/v1") - // Auth routes (public) - OAuth only - auth := api.Group("/auth") - auth.Get("/:provider", authHandler.OAuthLogin) - auth.Get("/:provider/callback", authHandler.OAuthCallback) - - // User routes (protected) - users := api.Group("/users", middleware.AuthMiddleware(cfg)) - users.Get("/me", userHandler.GetMe) - users.Patch("/me", userHandler.UpdateMe) - - // Admin-only routes - users.Get("/", middleware.AdminMiddleware(), userHandler.GetAllUsers) - users.Get("/:id", middleware.AdminMiddleware(), userHandler.GetUser) - users.Delete("/:id", middleware.AdminMiddleware(), userHandler.DeleteUser) - - // Public meeting routes (accessible to guests) - publicMeetings := api.Group("/meetings") - publicMeetings.Get("/code/:code", meetingHandler.GetMeetingByCode) - - // Protected meeting routes (require authentication) - meetings := api.Group("/meetings", middleware.AuthMiddleware(cfg)) - meetings.Post("/", meetingHandler.CreateMeeting) - meetings.Get("/my", meetingHandler.GetMyMeetings) - meetings.Get("/:id", meetingHandler.GetMeeting) - meetings.Delete("/:id", meetingHandler.DeleteMeeting) - - // Summarizer routes (protected) - meetings.Post("/:id/summarizer/start", summarizerHandler.StartSummarizer) - meetings.Post("/:id/summarizer/stop", summarizerHandler.StopSummarizer) - - // Session management routes (protected) - sessions := api.Group("/sessions", middleware.AuthMiddleware(cfg)) - sessions.Get("/", summarizerHandler.GetSessions) - sessions.Get("/:id", summarizerHandler.GetSession) - sessions.Delete("/:id", summarizerHandler.DeleteSession) - - // Admin-only meeting routes - meetings.Get("/", middleware.AdminMiddleware(), meetingHandler.GetAllMeetings) - - // Public LiveKit routes (accessible to guests for joining meetings) - publicLiveKit := api.Group("/livekit") - publicLiveKit.Post("/token", livekitHandler.GenerateToken) - publicLiveKit.Get("/participants/count", livekitHandler.GetParticipantCount) - - // Protected LiveKit routes (require authentication) - livekit := api.Group("/livekit", middleware.AuthMiddleware(cfg)) - livekit.Get("/participants", livekitHandler.ListParticipants) - livekit.Post("/remove-participant", livekitHandler.RemoveParticipant) - livekit.Post("/mute-participant", livekitHandler.MuteParticipant) - livekit.Post("/end-meeting", livekitHandler.EndMeeting) - - // Public lobby routes (HTTP — request to join + cancel) - publicLobby := api.Group("/lobby") - publicLobby.Post("/request", lobbyHandler.RequestToJoin) - publicLobby.Delete("/request", lobbyHandler.CancelRequest) - - // Protected lobby routes (admin only — approve/reject via HTTP fallback) - lobby := api.Group("/lobby", middleware.AuthMiddleware(cfg)) - lobby.Post("/respond", lobbyHandler.RespondToRequest) - - // WebSocket lobby routes - app.Use("/ws", func(c *fiber.Ctx) error { - if websocket.IsWebSocketUpgrade(c) { - return c.Next() - } - return fiber.ErrUpgradeRequired - }) - app.Get("/ws/lobby/visitor", websocket.New(lobbyWSHandler.HandleVisitor)) - app.Get("/ws/lobby/admin", websocket.New(lobbyWSHandler.HandleAdmin)) + setupAuthRoutes(api, authHandler) + setupUserRoutes(api, userHandler, cfg) + setupMeetingRoutes(api, meetingHandler, summarizerHandler, cfg) + setupLiveKitRoutes(api, livekitHandler, cfg) + setupLobbyRoutes(app, api, lobbyHandler, lobbyWSHandler) } diff --git a/backend/internal/routes/user.go b/backend/internal/routes/user.go new file mode 100644 index 0000000..d8cfbe0 --- /dev/null +++ b/backend/internal/routes/user.go @@ -0,0 +1,22 @@ +package routes + +import ( + "mini-meeting/internal/config" + "mini-meeting/internal/handlers" + "mini-meeting/internal/middleware" + + "github.com/gofiber/fiber/v2" +) + +func setupUserRoutes(api fiber.Router, userHandler *handlers.UserHandler, cfg *config.Config) { + users := api.Group("/users", middleware.AuthMiddleware(cfg)) + + // Self-service routes + users.Get("/me", userHandler.GetMe) + users.Patch("/me", userHandler.UpdateMe) + + // Admin-only routes + users.Get("/", middleware.AdminMiddleware(), userHandler.GetAllUsers) + users.Get("/:id", middleware.AdminMiddleware(), userHandler.GetUser) + users.Delete("/:id", middleware.AdminMiddleware(), userHandler.DeleteUser) +} diff --git a/backend/internal/services/meeting_service.go b/backend/internal/services/meeting_service.go index 46a1cb5..ef8bbdc 100644 --- a/backend/internal/services/meeting_service.go +++ b/backend/internal/services/meeting_service.go @@ -125,3 +125,7 @@ func (s *MeetingService) DeleteMeeting(id uint, userID uint) error { return s.repo.Delete(id) } + +func (s *MeetingService) DeleteByCreatorID(creatorID uint) error { + return s.repo.DeleteByCreatorID(creatorID) +} diff --git a/backend/internal/services/normalization_service.go b/backend/internal/services/normalization_service.go index f5dbd4a..b83f87b 100644 --- a/backend/internal/services/normalization_service.go +++ b/backend/internal/services/normalization_service.go @@ -9,13 +9,15 @@ import ( ) type NormalizationService struct { - repo *repositories.SummarizerRepository + sessionRepo *repositories.SummarizerSessionRepository + transcriptRepo *repositories.TranscriptRepository summarizationService *SummarizationService } -func NewNormalizationService(repo *repositories.SummarizerRepository, summarizationService *SummarizationService) *NormalizationService { +func NewNormalizationService(sessionRepo *repositories.SummarizerSessionRepository, transcriptRepo *repositories.TranscriptRepository, summarizationService *SummarizationService) *NormalizationService { return &NormalizationService{ - repo: repo, + sessionRepo: sessionRepo, + transcriptRepo: transcriptRepo, summarizationService: summarizationService, } } @@ -31,7 +33,7 @@ type MergedSegment struct { // ProcessSession normalizes all transcripts for a given session func (s *NormalizationService) ProcessSession(sessionID uint) error { // 1. Get session and validate status - session, err := s.repo.FindSessionByID(sessionID) + session, err := s.sessionRepo.FindByID(sessionID) if err != nil { return fmt.Errorf("failed to get session: %w", err) } @@ -45,7 +47,7 @@ func (s *NormalizationService) ProcessSession(sessionID uint) error { } // 2. Get all transcripts for this session - transcripts, err := s.repo.FindTranscriptsBySessionID(sessionID) + transcripts, err := s.transcriptRepo.FindBySessionID(sessionID) if err != nil { return fmt.Errorf("failed to get transcripts: %w", err) } @@ -64,17 +66,17 @@ func (s *NormalizationService) ProcessSession(sessionID uint) error { // 5. Update session status to NORMALIZED now := time.Now() - if err := s.repo.UpdateSessionStatus(sessionID, models.StatusNormalized, nil, &now); err != nil { + if err := s.sessionRepo.UpdateStatus(sessionID, models.StatusNormalized, nil, &now); err != nil { return fmt.Errorf("failed to update session status: %w", err) } // 6. Save to session - if err := s.repo.UpdateSessionTranscript(sessionID, normalizedText); err != nil { + if err := s.sessionRepo.UpdateTranscript(sessionID, normalizedText); err != nil { return fmt.Errorf("failed to update session transcript: %w", err) } // 7. Delete individual transcript records - if err := s.repo.DeleteTranscriptsBySessionID(sessionID); err != nil { + if err := s.transcriptRepo.DeleteBySessionID(sessionID); err != nil { fmt.Printf("Warning: Failed to cleanup transcripts for session %d: %v\n", sessionID, err) } else { fmt.Printf("Cleaned up %d transcript records for session %d\n", len(transcripts), sessionID) diff --git a/backend/internal/services/summarization_service.go b/backend/internal/services/summarization_service.go index 340a3da..58466cc 100644 --- a/backend/internal/services/summarization_service.go +++ b/backend/internal/services/summarization_service.go @@ -25,23 +25,23 @@ type SummarizationResponse struct { } type SummarizationService struct { - repo *repositories.SummarizerRepository - userRepo *repositories.UserRepository + sessionRepo *repositories.SummarizerSessionRepository + userService *UserService openRouterService *OpenRouterService emailService *EmailService cfg *config.Config } func NewSummarizationService( - repo *repositories.SummarizerRepository, - userRepo *repositories.UserRepository, + sessionRepo *repositories.SummarizerSessionRepository, + userService *UserService, openRouterService *OpenRouterService, emailService *EmailService, cfg *config.Config, ) *SummarizationService { return &SummarizationService{ - repo: repo, - userRepo: userRepo, + sessionRepo: sessionRepo, + userService: userService, openRouterService: openRouterService, emailService: emailService, cfg: cfg, @@ -51,7 +51,7 @@ func NewSummarizationService( // ProcessSummarization generates an AI-powered summary from a normalized transcript func (s *SummarizationService) ProcessSummarization(sessionID uint) error { // 1. Get session and validate status - session, err := s.repo.FindSessionByID(sessionID) + session, err := s.sessionRepo.FindByID(sessionID) if err != nil { return fmt.Errorf("failed to get session: %w", err) } @@ -84,7 +84,7 @@ func (s *SummarizationService) ProcessSummarization(sessionID uint) error { if err != nil { // Update session with error errMsg := fmt.Sprintf("Failed to generate summary: %v", err) - s.repo.UpdateSessionStatus(sessionID, models.StatusNormalized, &errMsg, nil) + s.sessionRepo.UpdateStatus(sessionID, models.StatusNormalized, &errMsg, nil) return fmt.Errorf("failed to generate summary: %w", err) } @@ -93,12 +93,12 @@ func (s *SummarizationService) ProcessSummarization(sessionID uint) error { // 4. Update session status to SUMMARIZED now := time.Now() - if err := s.repo.UpdateSessionStatus(sessionID, models.StatusSummarized, nil, &now); err != nil { + if err := s.sessionRepo.UpdateStatus(sessionID, models.StatusSummarized, nil, &now); err != nil { return fmt.Errorf("failed to update session status: %w", err) } // 5. Save the summary to the session - if err := s.repo.UpdateSessionSummary(sessionID, resp.Summary); err != nil { + if err := s.sessionRepo.UpdateSummary(sessionID, resp.Summary); err != nil { return fmt.Errorf("failed to update session summary: %w", err) } @@ -107,7 +107,7 @@ func (s *SummarizationService) ProcessSummarization(sessionID uint) error { // 6. Fire email notification (non-blocking) go func() { - user, err := s.userRepo.FindByID(session.UserID) + user, err := s.userService.GetUserByID(session.UserID) if err != nil { fmt.Printf("EmailNotification: failed to fetch user for session %d: %v\n", sessionID, err) return diff --git a/backend/internal/services/summarizer_service.go b/backend/internal/services/summarizer_service.go index c017d02..3c4a830 100644 --- a/backend/internal/services/summarizer_service.go +++ b/backend/internal/services/summarizer_service.go @@ -5,9 +5,9 @@ import ( "fmt" "io" "mini-meeting/internal/config" + "mini-meeting/internal/handlers/dto" "mini-meeting/internal/models" "mini-meeting/internal/repositories" - "mini-meeting/internal/types" "os" "path/filepath" "sync" @@ -19,9 +19,10 @@ import ( ) type SummarizerService struct { - repo *repositories.SummarizerRepository - meetingRepo *repositories.MeetingRepository - userRepo *repositories.UserRepository + sessionRepo *repositories.SummarizerSessionRepository + chunkRepo *repositories.AudioChunkRepository + transcriptRepo *repositories.TranscriptRepository + meetingService *MeetingService livekitService *LiveKitService transcriptionService *TranscriptionService cfg *config.Config @@ -35,17 +36,19 @@ type SummarizerService struct { } func NewSummarizerService( - repo *repositories.SummarizerRepository, - meetingRepo *repositories.MeetingRepository, - userRepo *repositories.UserRepository, + sessionRepo *repositories.SummarizerSessionRepository, + chunkRepo *repositories.AudioChunkRepository, + transcriptRepo *repositories.TranscriptRepository, + meetingService *MeetingService, livekitService *LiveKitService, transcriptionService *TranscriptionService, cfg *config.Config, ) *SummarizerService { return &SummarizerService{ - repo: repo, - meetingRepo: meetingRepo, - userRepo: userRepo, + sessionRepo: sessionRepo, + chunkRepo: chunkRepo, + transcriptRepo: transcriptRepo, + meetingService: meetingService, livekitService: livekitService, transcriptionService: transcriptionService, cfg: cfg, @@ -56,7 +59,7 @@ func NewSummarizerService( // StartSummarizer starts a new summarizer session for a meeting func (s *SummarizerService) StartSummarizer(meetingID uint, userID uint) (*models.SummarizerSession, error) { // Validate meeting exists and user is the creator - meeting, err := s.meetingRepo.FindByID(meetingID) + meeting, err := s.meetingService.GetMeetingByID(meetingID) if err != nil { return nil, fmt.Errorf("meeting not found: %w", err) } @@ -66,7 +69,7 @@ func (s *SummarizerService) StartSummarizer(meetingID uint, userID uint) (*model } // Check if there's already an active session - existingSession, err := s.repo.FindActiveSessionByMeetingID(meetingID) + existingSession, err := s.sessionRepo.FindActiveByMeetingID(meetingID) if err == nil && existingSession != nil { return nil, fmt.Errorf("summarizer already running for this meeting") } @@ -79,7 +82,7 @@ func (s *SummarizerService) StartSummarizer(meetingID uint, userID uint) (*model StartedAt: time.Now(), } - if err := s.repo.CreateSession(session); err != nil { + if err := s.sessionRepo.Create(session); err != nil { return nil, fmt.Errorf("failed to create session: %w", err) } @@ -89,7 +92,7 @@ func (s *SummarizerService) StartSummarizer(meetingID uint, userID uint) (*model // Update session status with error now := time.Now() errMsg := fmt.Sprintf("Failed to join LiveKit room: %v", err) - s.repo.UpdateSessionStatus(session.ID, models.StatusStarted, &errMsg, &now) + s.sessionRepo.UpdateStatus(session.ID, models.StatusStarted, &errMsg, &now) fmt.Printf("%s\n", errMsg) } }() @@ -100,13 +103,13 @@ func (s *SummarizerService) StartSummarizer(meetingID uint, userID uint) (*model // StopSummarizer stops an active summarizer session func (s *SummarizerService) StopSummarizer(sessionID uint, userID uint) (int64, error) { // Get session - session, err := s.repo.FindSessionByID(sessionID) + session, err := s.sessionRepo.FindByID(sessionID) if err != nil { return 0, fmt.Errorf("session not found: %w", err) } // Validate user is meeting creator - meeting, err := s.meetingRepo.FindByID(session.MeetingID) + meeting, err := s.meetingService.GetMeetingByID(session.MeetingID) if err != nil { return 0, fmt.Errorf("meeting not found: %w", err) } @@ -133,12 +136,12 @@ func (s *SummarizerService) StopSummarizer(sessionID uint, userID uint) (int64, // Update session status to CAPTURED now := time.Now() - if err := s.repo.UpdateSessionStatus(sessionID, models.StatusCaptured, nil, &now); err != nil { + if err := s.sessionRepo.UpdateStatus(sessionID, models.StatusCaptured, nil, &now); err != nil { return 0, fmt.Errorf("failed to update session status: %w", err) } // Count total chunks - totalChunks, err := s.repo.CountChunksBySessionID(sessionID) + totalChunks, err := s.chunkRepo.CountBySessionID(sessionID) if err != nil { return 0, fmt.Errorf("failed to count chunks: %w", err) } @@ -326,7 +329,7 @@ func (s *SummarizerService) createChunkMetadata(sessionID uint, userIdentity str DurationSeconds: duration, } - if err := s.repo.CreateAudioChunk(chunk); err != nil { + if err := s.chunkRepo.Create(chunk); err != nil { return fmt.Errorf("failed to create audio chunk metadata: %w", err) } @@ -336,7 +339,7 @@ func (s *SummarizerService) createChunkMetadata(sessionID uint, userIdentity str // GetActiveSession returns the active summarizer session for a meeting func (s *SummarizerService) GetActiveSession(meetingID uint) (*models.SummarizerSession, error) { - session, err := s.repo.FindActiveSessionByMeetingID(meetingID) + session, err := s.sessionRepo.FindActiveByMeetingID(meetingID) if err != nil { return nil, fmt.Errorf("no active summarizer session found") } @@ -345,11 +348,11 @@ func (s *SummarizerService) GetActiveSession(meetingID uint) (*models.Summarizer // GetSessionByID returns a session by its ID func (s *SummarizerService) GetSessionByID(sessionID uint) (*models.SummarizerSession, error) { - return s.repo.FindSessionByID(sessionID) + return s.sessionRepo.FindByID(sessionID) } // GetSessions returns a paginated list of sessions for a user -func (s *SummarizerService) GetSessions(userID uint, page, pageSize int) (*types.PaginatedSessionsResponse, error) { +func (s *SummarizerService) GetSessions(userID uint, page, pageSize int) (*dto.PaginatedSessionsResponse, error) { // Set default values if page < 1 { page = 1 @@ -358,15 +361,15 @@ func (s *SummarizerService) GetSessions(userID uint, page, pageSize int) (*types pageSize = 10 } - sessions, total, err := s.repo.FindAllByUserIDPaginated(userID, page, pageSize) + sessions, total, err := s.sessionRepo.FindAllByUserIDPaginated(userID, page, pageSize) if err != nil { return nil, fmt.Errorf("failed to fetch sessions: %w", err) } // Map models to response type - sessionList := make([]types.SessionsList, len(sessions)) + sessionList := make([]dto.SessionsList, len(sessions)) for i, session := range sessions { - sessionList[i] = types.SessionsList{ + sessionList[i] = dto.SessionsList{ ID: session.ID, Status: session.Status, Error: session.Error, @@ -380,7 +383,7 @@ func (s *SummarizerService) GetSessions(userID uint, page, pageSize int) (*types totalPages++ } - return &types.PaginatedSessionsResponse{ + return &dto.PaginatedSessionsResponse{ Data: sessionList, Total: total, Page: page, @@ -390,8 +393,8 @@ func (s *SummarizerService) GetSessions(userID uint, page, pageSize int) (*types } // GetSession retrieves a specific session for a user (verifying ownership) -func (s *SummarizerService) GetSession(sessionID, userID uint) (*types.SessionResponse, error) { - session, err := s.repo.FindSessionByID(sessionID) +func (s *SummarizerService) GetSession(sessionID, userID uint) (*dto.SessionResponse, error) { + session, err := s.sessionRepo.FindByID(sessionID) if err != nil { return nil, fmt.Errorf("session not found: %w", err) } @@ -400,7 +403,7 @@ func (s *SummarizerService) GetSession(sessionID, userID uint) (*types.SessionRe return nil, errors.New("unauthorized: session does not belong to user") } - return &types.SessionResponse{ + return &dto.SessionResponse{ ID: session.ID, Status: session.Status, Error: session.Error, @@ -413,7 +416,7 @@ func (s *SummarizerService) GetSession(sessionID, userID uint) (*types.SessionRe // DeleteSession deletes a session (verifying ownership) func (s *SummarizerService) DeleteSession(sessionID, userID uint) error { - session, err := s.repo.FindSessionByID(sessionID) + session, err := s.sessionRepo.FindByID(sessionID) if err != nil { return fmt.Errorf("session not found: %w", err) } @@ -429,17 +432,17 @@ func (s *SummarizerService) DeleteSession(sessionID, userID uint) error { } // 2. Delete chunks for this session in database - if err := s.repo.DeleteChunksBySessionID(sessionID); err != nil { + if err := s.chunkRepo.DeleteBySessionID(sessionID); err != nil { return fmt.Errorf("failed to delete chunks: %w", err) } // 3. Delete transcripts for this session in database - if err := s.repo.DeleteTranscriptsBySessionID(sessionID); err != nil { + if err := s.transcriptRepo.DeleteBySessionID(sessionID); err != nil { return fmt.Errorf("failed to delete transcripts: %w", err) } // 4. Finally delete the session record itself - if err := s.repo.DeleteSession(sessionID); err != nil { + if err := s.sessionRepo.Delete(sessionID); err != nil { return fmt.Errorf("failed to delete session: %w", err) } diff --git a/backend/internal/services/transcription_service.go b/backend/internal/services/transcription_service.go index d389ad1..ff07ac8 100644 --- a/backend/internal/services/transcription_service.go +++ b/backend/internal/services/transcription_service.go @@ -16,14 +16,18 @@ import ( ) type TranscriptionService struct { - repo *repositories.SummarizerRepository + sessionRepo *repositories.SummarizerSessionRepository + chunkRepo *repositories.AudioChunkRepository + transcriptRepo *repositories.TranscriptRepository normalizationService *NormalizationService cfg *config.Config } -func NewTranscriptionService(repo *repositories.SummarizerRepository, normalizationService *NormalizationService, cfg *config.Config) *TranscriptionService { +func NewTranscriptionService(sessionRepo *repositories.SummarizerSessionRepository, chunkRepo *repositories.AudioChunkRepository, transcriptRepo *repositories.TranscriptRepository, normalizationService *NormalizationService, cfg *config.Config) *TranscriptionService { return &TranscriptionService{ - repo: repo, + sessionRepo: sessionRepo, + chunkRepo: chunkRepo, + transcriptRepo: transcriptRepo, normalizationService: normalizationService, cfg: cfg, } @@ -101,7 +105,7 @@ func (s *TranscriptionService) TranscribeChunk(filePath string) (string, error) // ProcessSession transcribes all chunks for a given session func (s *TranscriptionService) ProcessSession(sessionID uint) error { // 1. Get session and validate status - session, err := s.repo.FindSessionByID(sessionID) + session, err := s.sessionRepo.FindByID(sessionID) if err != nil { return fmt.Errorf("failed to get session: %w", err) } @@ -115,7 +119,7 @@ func (s *TranscriptionService) ProcessSession(sessionID uint) error { } // 2. Get all audio chunks - chunks, err := s.repo.FindChunksBySessionID(sessionID) + chunks, err := s.chunkRepo.FindBySessionID(sessionID) if err != nil { return fmt.Errorf("failed to get chunks: %w", err) } @@ -123,14 +127,14 @@ func (s *TranscriptionService) ProcessSession(sessionID uint) error { if len(chunks) == 0 { errMsg := "Session stopped immediately — no audio chunks were captured" now := time.Now() - s.repo.UpdateSessionStatus(sessionID, models.StatusCaptured, &errMsg, &now) + s.sessionRepo.UpdateStatus(sessionID, models.StatusCaptured, &errMsg, &now) return fmt.Errorf("%s", errMsg) } if len(chunks) <= 2 { errMsg := fmt.Sprintf("Session stopped too quickly — only %d audio chunk(s) captured, not enough speech to transcribe", len(chunks)) now := time.Now() - s.repo.UpdateSessionStatus(sessionID, models.StatusCaptured, &errMsg, &now) + s.sessionRepo.UpdateStatus(sessionID, models.StatusCaptured, &errMsg, &now) return fmt.Errorf("%s", errMsg) } @@ -164,7 +168,7 @@ func (s *TranscriptionService) ProcessSession(sessionID uint) error { EndTime: chunk.DurationSeconds * float64(chunk.ChunkIndex+1), } - if err := s.repo.CreateTranscript(transcript); err != nil { + if err := s.transcriptRepo.Create(transcript); err != nil { fmt.Printf("Failed to save transcript for chunk %d: %v\n", chunk.ID, err) continue } @@ -176,12 +180,12 @@ func (s *TranscriptionService) ProcessSession(sessionID uint) error { if successCount == 0 { errMsg := "Failed to transcribe any chunks — all audio segments were empty or unreadable" now := time.Now() - s.repo.UpdateSessionStatus(sessionID, models.StatusCaptured, &errMsg, &now) + s.sessionRepo.UpdateStatus(sessionID, models.StatusCaptured, &errMsg, &now) return fmt.Errorf("%s", errMsg) } now := time.Now() - if err := s.repo.UpdateSessionStatus(sessionID, models.StatusTranscribed, nil, &now); err != nil { + if err := s.sessionRepo.UpdateStatus(sessionID, models.StatusTranscribed, nil, &now); err != nil { return fmt.Errorf("failed to update session status: %w", err) } @@ -195,7 +199,7 @@ func (s *TranscriptionService) ProcessSession(sessionID uint) error { fmt.Printf("Cleaned up audio files for session %d\n", sessionID) } - if err := s.repo.DeleteChunksBySessionID(sessionID); err != nil { + if err := s.chunkRepo.DeleteBySessionID(sessionID); err != nil { fmt.Printf("Warning: Failed to cleanup session chunks from database %d: %v\n", sessionID, err) } else { fmt.Printf("Cleaned up audio chunks records from database for session %d\n", sessionID) diff --git a/backend/internal/services/user_service.go b/backend/internal/services/user_service.go index 6151655..38d878d 100644 --- a/backend/internal/services/user_service.go +++ b/backend/internal/services/user_service.go @@ -2,23 +2,23 @@ package services import ( "errors" + "mini-meeting/internal/handlers/dto" "mini-meeting/internal/models" "mini-meeting/internal/repositories" - "mini-meeting/internal/types" "strings" "gorm.io/gorm" ) type UserService struct { - repo *repositories.UserRepository - meetingRepo *repositories.MeetingRepository + repo *repositories.UserRepository + meetingService *MeetingService } -func NewUserService(repo *repositories.UserRepository, meetingRepo *repositories.MeetingRepository) *UserService { +func NewUserService(repo *repositories.UserRepository, meetingService *MeetingService) *UserService { return &UserService{ - repo: repo, - meetingRepo: meetingRepo, + repo: repo, + meetingService: meetingService, } } @@ -45,7 +45,7 @@ func (s *UserService) GetUserByEmail(email string) (*models.User, error) { return user, nil } -func (s *UserService) GetAllUsersPaginated(page, pageSize int, search string) (*types.PaginatedUsersResponse, error) { +func (s *UserService) GetAllUsersPaginated(page, pageSize int, search string) (*dto.PaginatedUsersResponse, error) { // Set default values if page < 1 { page = 1 @@ -65,7 +65,7 @@ func (s *UserService) GetAllUsersPaginated(page, pageSize int, search string) (* totalPages++ } - return &types.PaginatedUsersResponse{ + return &dto.PaginatedUsersResponse{ Data: users, Total: total, Page: page, @@ -74,7 +74,7 @@ func (s *UserService) GetAllUsersPaginated(page, pageSize int, search string) (* }, nil } -func (s *UserService) UpdateUser(id uint, req *types.UpdateUserRequest) (*models.User, error) { +func (s *UserService) UpdateUser(id uint, req *dto.UpdateUserRequest) (*models.User, error) { user, err := s.repo.FindByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -117,7 +117,7 @@ func (s *UserService) DeleteUser(id uint) error { } // Delete all meetings created by this user - if err := s.meetingRepo.DeleteByCreatorID(id); err != nil { + if err := s.meetingService.DeleteByCreatorID(id); err != nil { return err } diff --git a/backend/internal/workers/normalization_worker.go b/backend/internal/workers/normalization_worker.go index b0b3627..d2d1f84 100644 --- a/backend/internal/workers/normalization_worker.go +++ b/backend/internal/workers/normalization_worker.go @@ -9,14 +9,14 @@ import ( ) type NormalizationWorker struct { - repo *repositories.SummarizerRepository + repo *repositories.SummarizerSessionRepository normalizationService *services.NormalizationService interval time.Duration stuckThreshold time.Duration } func NewNormalizationWorker( - repo *repositories.SummarizerRepository, + repo *repositories.SummarizerSessionRepository, normalizationService *services.NormalizationService, interval time.Duration, stuckThreshold time.Duration, @@ -44,7 +44,7 @@ func (w *NormalizationWorker) processStuckSessions() { cutoffTime := time.Now().Add(-w.stuckThreshold) // Find sessions that are TRANSCRIBED but haven't been updated recently - sessions, err := w.repo.FindStuckSessions(models.StatusTranscribed, cutoffTime) + sessions, err := w.repo.FindStuck(models.StatusTranscribed, cutoffTime) if err != nil { fmt.Printf("NormalizationWorker: Error finding stuck sessions: %v\n", err) return diff --git a/backend/internal/workers/summarization_worker.go b/backend/internal/workers/summarization_worker.go index 9c4f9e0..716ef76 100644 --- a/backend/internal/workers/summarization_worker.go +++ b/backend/internal/workers/summarization_worker.go @@ -9,14 +9,14 @@ import ( ) type SummarizationWorker struct { - repo *repositories.SummarizerRepository + repo *repositories.SummarizerSessionRepository summarizationService *services.SummarizationService interval time.Duration stuckThreshold time.Duration } func NewSummarizationWorker( - repo *repositories.SummarizerRepository, + repo *repositories.SummarizerSessionRepository, summarizationService *services.SummarizationService, interval time.Duration, stuckThreshold time.Duration, @@ -44,7 +44,7 @@ func (w *SummarizationWorker) processStuckSessions() { cutoffTime := time.Now().Add(-w.stuckThreshold) // Find sessions that are NORMALIZED but haven't been updated recently - sessions, err := w.repo.FindStuckSessions(models.StatusNormalized, cutoffTime) + sessions, err := w.repo.FindStuck(models.StatusNormalized, cutoffTime) if err != nil { fmt.Printf("SummarizationWorker: Error finding stuck sessions: %v\n", err) return diff --git a/backend/internal/workers/transcription_worker.go b/backend/internal/workers/transcription_worker.go index 009b45d..800b6f8 100644 --- a/backend/internal/workers/transcription_worker.go +++ b/backend/internal/workers/transcription_worker.go @@ -9,14 +9,14 @@ import ( ) type TranscriptionWorker struct { - repo *repositories.SummarizerRepository + repo *repositories.SummarizerSessionRepository transcriptionService *services.TranscriptionService interval time.Duration stuckThreshold time.Duration } func NewTranscriptionWorker( - repo *repositories.SummarizerRepository, + repo *repositories.SummarizerSessionRepository, transcriptionService *services.TranscriptionService, interval time.Duration, stuckThreshold time.Duration, @@ -47,7 +47,7 @@ func (w *TranscriptionWorker) Start() { func (w *TranscriptionWorker) processStuckSessions() { cutoffTime := time.Now().Add(-w.stuckThreshold) - sessions, err := w.repo.FindStuckSessions(models.StatusCaptured, cutoffTime) + sessions, err := w.repo.FindStuck(models.StatusCaptured, cutoffTime) if err != nil { log.Printf("Worker: Failed to fetch stuck sessions: %v", err) return diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e067f33..406e652 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "@hookform/resolvers": "^5.2.2", "@livekit/components-react": "^2.9.19", "@livekit/components-styles": "^1.2.0", + "@vercel/analytics": "^1.6.1", "axios": "^1.13.4", "livekit-client": "^2.17.0", "lucide-react": "^0.564.0", @@ -85,6 +86,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1833,8 +1835,7 @@ "version": "1.0.22", "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz", "integrity": "sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", @@ -1856,6 +1857,7 @@ "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1866,6 +1868,7 @@ "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1925,6 +1928,7 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -2149,6 +2153,44 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vercel/analytics": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.6.1.tgz", + "integrity": "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==", + "license": "MPL-2.0", + "peerDependencies": { + "@remix-run/react": "^2", + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "react": "^18 || ^19 || ^19.0.0-rc", + "svelte": ">= 4", + "vue": "^3", + "vue-router": "^4" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, "node_modules/@vitejs/plugin-react": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", @@ -2176,6 +2218,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2335,6 +2378,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2695,6 +2739,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3647,6 +3692,7 @@ "resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.17.0.tgz", "integrity": "sha512-BD1QUS44ancVTBdnAher0aO7DV5holFYH2lYradYT/HgXtn6R8xPyvtDAH3UH40jGcesDo9fEopCFwEdOgrIhg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@livekit/mutex": "1.1.1", "@livekit/protocol": "1.42.2", @@ -3926,6 +3972,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3953,6 +4000,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -4000,6 +4048,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4009,6 +4058,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -4021,6 +4071,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -4307,7 +4358,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/type-check": { "version": "0.4.0", @@ -4337,6 +4389,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4438,6 +4491,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -4571,6 +4625,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/package.json b/frontend/package.json index 406f98d..e367490 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "@hookform/resolvers": "^5.2.2", "@livekit/components-react": "^2.9.19", "@livekit/components-styles": "^1.2.0", + "@vercel/analytics": "^1.6.1", "axios": "^1.13.4", "livekit-client": "^2.17.0", "lucide-react": "^0.564.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a211ed1..5fa6c37 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,3 +1,4 @@ +import { Analytics } from "@vercel/analytics/react"; import AppRouter from "./router"; /** @@ -5,7 +6,12 @@ import AppRouter from "./router"; * Delegates all routing logic to AppRouter for better organization */ function App() { - return ; + return ( + <> + + + + ); } export default App; diff --git a/frontend/src/services/api/user/index.ts b/frontend/src/services/api/user/index.ts index 8943687..2de7398 100644 --- a/frontend/src/services/api/user/index.ts +++ b/frontend/src/services/api/user/index.ts @@ -11,8 +11,6 @@ export const userService = { updateCurrentUser: userProfileService.updateCurrentUser, getAllUsers: userAdminService.getAllUsers, getUserById: userAdminService.getUserById, - createUser: userAdminService.createUser, - updateUser: userAdminService.updateUser, deleteUser: userAdminService.deleteUser, getSessions: sessionService.getSessions, getSession: sessionService.getSession, diff --git a/frontend/src/services/api/user/user-admin.service.ts b/frontend/src/services/api/user/user-admin.service.ts index 2f42b0d..004d7eb 100644 --- a/frontend/src/services/api/user/user-admin.service.ts +++ b/frontend/src/services/api/user/user-admin.service.ts @@ -3,9 +3,6 @@ import type { User, UserResponse, PaginatedUsersResponse, - UpdateUserRequest, - UserUpdateResponse, - CreateUserRequest, UserDeleteResponse, } from "../../../types/user.types"; @@ -36,25 +33,6 @@ export const userAdminService = { return response.data.data; }, - /** - * Create new user (Admin only) - */ - createUser: async (data: CreateUserRequest): Promise => { - const response = await apiClient.post("/users", data); - return response.data.data; - }, - - /** - * Update user by ID (Admin only) - */ - updateUser: async (id: number, data: UpdateUserRequest): Promise => { - const response = await apiClient.patch( - `/users/${id}`, - data, - ); - return response.data.data; - }, - /** * Delete user (Admin only) */ diff --git a/frontend/src/store/auth/authActions.ts b/frontend/src/store/auth/authActions.ts index aca337a..36c30a2 100644 --- a/frontend/src/store/auth/authActions.ts +++ b/frontend/src/store/auth/authActions.ts @@ -43,11 +43,6 @@ export const createAuthActions = ( }, setAuthData: async (token: string) => { - if (userFetcher.isLocked()) { - console.warn("⚠️ User fetch already in progress"); - return; - } - try { console.log("🔐 Storing token..."); tokenStorage.set(token); @@ -88,7 +83,7 @@ export const createAuthActions = ( initialize: async () => { const token = get().token; - if (!token || userFetcher.isLocked()) { + if (!token) { set({ isLoading: false }); return; } diff --git a/frontend/src/store/auth/userFetcher.ts b/frontend/src/store/auth/userFetcher.ts index 52e2698..04201f9 100644 --- a/frontend/src/store/auth/userFetcher.ts +++ b/frontend/src/store/auth/userFetcher.ts @@ -1,33 +1,36 @@ import { userService } from "../../services/api/user"; import type { User } from "../../types/user.types"; -let isFetchingUser = false; +let fetchPromise: Promise | null = null; /** * User data fetching with lock mechanism - * Prevents duplicate concurrent requests + * Prevents duplicate concurrent requests by returning the existing promise */ export const userFetcher = { - isLocked: () => isFetchingUser, + isLocked: () => fetchPromise !== null, fetchWithLock: async (): Promise => { - if (isFetchingUser) { - console.warn("⚠️ User fetch already in progress"); - throw new Error("User fetch already in progress"); + if (fetchPromise) { + console.warn("⚠️ User fetch already in progress, returning existing promise"); + return fetchPromise; } - isFetchingUser = true; console.log("🔒 User fetch locked, making API call..."); - try { - const userData = await userService.getCurrentUser(); - console.log("✅ User data received from API:", userData.email); - return userData; - } catch (error) { - console.error("❌ Failed to fetch user from API:", error); - throw error; - } finally { - isFetchingUser = false; - console.log("🔓 User fetch unlocked"); - } + fetchPromise = (async () => { + try { + const userData = await userService.getCurrentUser(); + console.log("✅ User data received from API:", userData.email); + return userData; + } catch (error) { + console.error("❌ Failed to fetch user from API:", error); + throw error; + } finally { + fetchPromise = null; + console.log("🔓 User fetch unlocked"); + } + })(); + + return fetchPromise; }, }; diff --git a/frontend/src/types/user.types.ts b/frontend/src/types/user.types.ts index a3c0597..d8a498a 100644 --- a/frontend/src/types/user.types.ts +++ b/frontend/src/types/user.types.ts @@ -24,27 +24,10 @@ export interface User { avatar_url?: string; created_at: string; } - -export interface UpdateUserRequest { - name?: string; - email?: string; - password?: string; -} - -export interface CreateUserRequest { - email: string; - password: string; - name: string; -} - export interface UserResponse { data: User; } -export interface UsersResponse { - data: User[]; -} - export interface PaginatedUsersResponse { data: User[]; total: number; @@ -53,10 +36,6 @@ export interface PaginatedUsersResponse { total_pages: number; } -export interface UserUpdateResponse { - message: string; - data: User; -} export interface PaginatedSessionsResponse { data: SummarizerSessionList[]; @@ -69,3 +48,11 @@ export interface PaginatedSessionsResponse { export interface UserDeleteResponse { message: string; } + +export interface UpdateUserRequest { + name?: string; +} + +export interface UserUpdateResponse { + data: User; +}