Add performance optimizations and database indexes
Database Indexes (migrations 006-009): - Add case-insensitive indexes for auth lookups (email, username) - Add composite indexes for task kanban queries - Add indexes for notification, document, and completion queries - Add unique index for active share codes - Remove redundant idx_share_code_active and idx_notification_user_sent Repository Optimizations: - Add FindResidenceIDsByUser() lightweight method (IDs only, no preloads) - Optimize GetResidenceUsers() with single UNION query (was 2 queries) - Optimize kanban completion preloads to minimal columns (id, task_id, completed_at) Service Optimizations: - Remove Category/Priority/Frequency preloads from task queries - Remove summary calculations from CRUD responses (client calculates) - Use lightweight FindResidenceIDsByUser() instead of full FindByUser() These changes reduce database load and response times for common operations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,17 +55,13 @@ func (s *TaskService) SetResidenceService(rs *ResidenceService) {
|
||||
s.residenceService = rs
|
||||
}
|
||||
|
||||
// getSummaryForUser gets the total summary for a user (helper for CRUD responses).
|
||||
// Uses UTC time. For timezone-aware summary, call residence service directly.
|
||||
func (s *TaskService) getSummaryForUser(userID uint) responses.TotalSummary {
|
||||
if s.residenceService == nil {
|
||||
return responses.TotalSummary{}
|
||||
}
|
||||
summary, err := s.residenceService.GetSummary(userID, time.Now().UTC())
|
||||
if err != nil || summary == nil {
|
||||
return responses.TotalSummary{}
|
||||
}
|
||||
return *summary
|
||||
// getSummaryForUser returns an empty summary placeholder.
|
||||
// DEPRECATED: Summary calculation has been removed from CRUD responses for performance.
|
||||
// Clients should calculate summary from kanban data instead (which already includes all tasks).
|
||||
// The summary field is kept in responses for backward compatibility but will always be empty.
|
||||
func (s *TaskService) getSummaryForUser(_ uint) responses.TotalSummary {
|
||||
// Return empty summary - clients should calculate from kanban data
|
||||
return responses.TotalSummary{}
|
||||
}
|
||||
|
||||
// === Task CRUD ===
|
||||
@@ -96,17 +92,12 @@ func (s *TaskService) GetTask(taskID, userID uint) (*responses.TaskResponse, err
|
||||
// ListTasks lists all tasks accessible to a user as a kanban board.
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
|
||||
func (s *TaskService) ListTasks(userID uint, now time.Time) (*responses.KanbanBoardResponse, error) {
|
||||
// Get all residence IDs accessible to user
|
||||
residences, err := s.residenceRepo.FindByUser(userID)
|
||||
// Get all residence IDs accessible to user (lightweight - no preloads)
|
||||
residenceIDs, err := s.residenceRepo.FindResidenceIDsByUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
residenceIDs := make([]uint, len(residences))
|
||||
for i, r := range residences {
|
||||
residenceIDs[i] = r.ID
|
||||
}
|
||||
|
||||
if len(residenceIDs) == 0 {
|
||||
// Return empty kanban board
|
||||
return &responses.KanbanBoardResponse{
|
||||
@@ -541,13 +532,20 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
|
||||
// - If frequency is "Custom", use task.CustomIntervalDays for recurrence
|
||||
// - If frequency is recurring, calculate next_due_date = completion_date + frequency_days
|
||||
// and reset in_progress to false so task shows in correct kanban column
|
||||
//
|
||||
// Note: Frequency is no longer preloaded for performance, so we load it separately if needed
|
||||
var intervalDays *int
|
||||
if task.Frequency != nil && task.Frequency.Name == "Custom" {
|
||||
// Custom frequency - use task's custom_interval_days
|
||||
intervalDays = task.CustomIntervalDays
|
||||
} else if task.Frequency != nil {
|
||||
// Standard frequency - use frequency's days
|
||||
intervalDays = task.Frequency.Days
|
||||
if task.FrequencyID != nil {
|
||||
frequency, err := s.taskRepo.GetFrequencyByID(*task.FrequencyID)
|
||||
if err == nil && frequency != nil {
|
||||
if frequency.Name == "Custom" {
|
||||
// Custom frequency - use task's custom_interval_days
|
||||
intervalDays = task.CustomIntervalDays
|
||||
} else {
|
||||
// Standard frequency - use frequency's days
|
||||
intervalDays = frequency.Days
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if intervalDays == nil || *intervalDays == 0 {
|
||||
@@ -645,28 +643,33 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error {
|
||||
|
||||
// Update next_due_date and in_progress based on frequency
|
||||
// Determine interval days: Custom frequency uses task.CustomIntervalDays, otherwise use frequency.Days
|
||||
// Note: Frequency is no longer preloaded for performance, so we load it separately if needed
|
||||
var quickIntervalDays *int
|
||||
if task.Frequency != nil && task.Frequency.Name == "Custom" {
|
||||
quickIntervalDays = task.CustomIntervalDays
|
||||
} else if task.Frequency != nil {
|
||||
quickIntervalDays = task.Frequency.Days
|
||||
var frequencyName = "unknown"
|
||||
if task.FrequencyID != nil {
|
||||
frequency, err := s.taskRepo.GetFrequencyByID(*task.FrequencyID)
|
||||
if err == nil && frequency != nil {
|
||||
frequencyName = frequency.Name
|
||||
if frequency.Name == "Custom" {
|
||||
quickIntervalDays = task.CustomIntervalDays
|
||||
} else {
|
||||
quickIntervalDays = frequency.Days
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if quickIntervalDays == nil || *quickIntervalDays == 0 {
|
||||
// One-time task - clear next_due_date (completion is determined by NextDueDate == nil + has completions)
|
||||
log.Info().
|
||||
Uint("task_id", task.ID).
|
||||
Bool("frequency_nil", task.Frequency == nil).
|
||||
Bool("has_frequency", task.FrequencyID != nil).
|
||||
Msg("QuickComplete: One-time task, clearing next_due_date")
|
||||
task.NextDueDate = nil
|
||||
task.InProgress = false
|
||||
} else {
|
||||
// Recurring task - calculate next due date from completion date + interval
|
||||
nextDue := completedAt.AddDate(0, 0, *quickIntervalDays)
|
||||
frequencyName := "unknown"
|
||||
if task.Frequency != nil {
|
||||
frequencyName = task.Frequency.Name
|
||||
}
|
||||
// frequencyName was already set when loading frequency above
|
||||
log.Info().
|
||||
Uint("task_id", task.ID).
|
||||
Str("frequency_name", frequencyName).
|
||||
@@ -780,17 +783,12 @@ func (s *TaskService) GetCompletion(completionID, userID uint) (*responses.TaskC
|
||||
|
||||
// ListCompletions lists all task completions for a user
|
||||
func (s *TaskService) ListCompletions(userID uint) ([]responses.TaskCompletionResponse, error) {
|
||||
// Get all residence IDs
|
||||
residences, err := s.residenceRepo.FindByUser(userID)
|
||||
// Get all residence IDs (lightweight - no preloads)
|
||||
residenceIDs, err := s.residenceRepo.FindResidenceIDsByUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
residenceIDs := make([]uint, len(residences))
|
||||
for i, r := range residences {
|
||||
residenceIDs[i] = r.ID
|
||||
}
|
||||
|
||||
if len(residenceIDs) == 0 {
|
||||
return []responses.TaskCompletionResponse{}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user