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;
+}