Skip to Content
AgentsCommon tasks

Dashboard Common Tasks Guide

Developer cookbook for common modifications and enhancements to the Dashboard feature.

Table of Contents

  1. Adding New Metrics to Dashboard Response
  2. Modifying Next Task Selection Logic
  3. Modifying Next Habit Selection Logic
  4. Adding New Widget Types
  5. Implementing New Frequency Types
  6. Debugging Overdue Calculation Issues
  7. Optimizing Dashboard Query Performance
  8. Adding New Platform Support
  9. Testing Dashboard Functionality

Adding New Metrics to Dashboard Response

Use Case: You want to add a new metric like “total steps walked today” to the dashboard.

Step 1: Add Field to DashboardResponseDTO

File: /shared/src/commonMain/kotlin/models/dashboard/DashboardResponseDTO.kt

@Serializable data class DashboardResponseDTO( // ... existing fields ... val weeklyMealLogging: Float, // NEW FIELD val todaySteps: Int = 0, val weeklySteps: Int = 0, // ... remaining fields ... )

Step 2: Add Service Dependency (if needed)

File: /server/src/main/kotlin/com/esteban/ruano/service/DashboardService.kt

class DashboardService( private val taskService: TaskService, private val habitService: HabitService, // ... existing services ... private val activityService: ActivityService // NEW ) {

Step 3: Update KoinConfig

File: /server/src/main/kotlin/com/esteban/ruano/plugins/KoinConfig.kt

single { DashboardService( get(), get(), get(), get(), get(), get(), get(), get() // Add new service here ) }

Step 4: Fetch Data in getDashboardData

File: /server/src/main/kotlin/com/esteban/ruano/service/DashboardService.kt

Location: Around line 95 (after existing service calls)

fun getDashboardData(userId: Int, dateTime: String): DashboardResponseDTO { // ... existing code ... // NEW: Fetch activity data val todaySteps = activityService.getStepsForDate(userId, today) val weeklySteps = activityService.getStepsForWeek(userId, weekStart, weekEnd) return DashboardResponseDTO( // ... existing fields ... todaySteps = todaySteps, weeklySteps = weeklySteps, // ... remaining fields ... ) }

Step 5: Test the New Metric

// In DashboardServiceTest.kt @Test fun `dashboard includes activity metrics`() { // Given val userId = 1 val dateTime = "2024-01-16T14:30:00" // Mock activityService response every { activityService.getStepsForDate(userId, any()) } returns 8500 every { activityService.getStepsForWeek(userId, any(), any()) } returns 45000 // When val result = dashboardService.getDashboardData(userId, dateTime) // Then assertEquals(8500, result.todaySteps) assertEquals(45000, result.weeklySteps) }

Modifying Next Task Selection Logic

Use Case: Change priority threshold or add new prioritization criteria.

Example: Change High Priority Threshold

File: /server/src/main/kotlin/com/esteban/ruano/service/DashboardService.kt

Location: Line 268

Current Code:

val highPriorityTasks = todaysTasks.filter { it.priority >= 4 }

Modified Code (lower threshold to 3):

val highPriorityTasks = todaysTasks.filter { it.priority >= 3 }

Example: Add Tag-Based Prioritization

Location: Line 232-295 (entire getNextTask function)

Add After Line 245:

// Separate tasks into categories val overdueTasks = pendingTasks.filter { /* ... */ } // NEW: Further prioritize by tag val urgentOverdueTasks = overdueTasks.filter { it.tags?.contains("urgent") == true } if (urgentOverdueTasks.isNotEmpty()) { return urgentOverdueTasks.maxByOrNull { it.priority } } val todaysTasks = pendingTasks.filter { /* ... */ }

Example: Consider Estimated Duration

Add Before Return Statement (around line 276):

// Return by priority first, then by duration (shorter tasks first) return todaysTasks.maxByOrNull { it.priority }?.let { highPriorityTask -> val samePriorityTasks = todaysTasks.filter { it.priority == highPriorityTask.priority } samePriorityTasks.minByOrNull { it.estimatedDuration ?: Int.MAX_VALUE // Prefer tasks with shorter duration } }

Modifying Next Habit Selection Logic

Use Case: Change how the next habit is selected.

Example: Prioritize Habits by Streak

File: /server/src/main/kotlin/com/esteban/ruano/service/DashboardService.kt

Location: Line 389-394 (selection logic)

Current Code:

val result = when { futureTimesToday.isNotEmpty() -> futureTimesToday.first().habit pastTimesToday.isNotEmpty() -> pastTimesToday.first().habit futureHabits.isNotEmpty() -> futureHabits.minByOrNull { it.nextOccurrenceDateTime }?.habit else -> nonOverdueHabitsWithOccurrence.firstOrNull()?.habit }

Modified Code (prioritize by streak):

val result = when { futureTimesToday.isNotEmpty() -> { // Prioritize habits with longer streaks to maintain momentum futureTimesToday.maxByOrNull { it.habit.streak }?.habit } pastTimesToday.isNotEmpty() -> { pastTimesToday.maxByOrNull { it.habit.streak }?.habit } futureHabits.isNotEmpty() -> { futureHabits.maxByOrNull { it.habit.streak }?.habit } else -> nonOverdueHabitsWithOccurrence.firstOrNull()?.habit }

Example: Filter Habits by Category

Add After Line 301 (filtering pending habits):

val pendingHabits = habits.filter { !it.done } // NEW: Only consider habits from specific categories val categoryFilter = listOf("health", "productivity") val filteredHabits = pendingHabits.filter { habit -> habit.category in categoryFilter } if (filteredHabits.isEmpty()) return null

Adding New Widget Types

Use Case: Create a new dashboard widget for displaying custom data.

Step 1: Define Widget Type Constant

File: Create /shared/src/commonMain/kotlin/models/dashboard/WidgetTypes.kt (if doesn’t exist)

object WidgetTypes { const val TASK_OVERVIEW = "TASK_OVERVIEW" const val HABIT_OVERVIEW = "HABIT_OVERVIEW" const val FINANCE_OVERVIEW = "FINANCE_OVERVIEW" // NEW WIDGET TYPE const val GOAL_TRACKER = "GOAL_TRACKER" }

Step 2: Update Default Configuration

File: /server/src/main/kotlin/com/esteban/ruano/service/DashboardConfigurationService.kt

Add to default widgets:

fun createDefaultConfiguration(platform: String): DashboardConfigurationDTO { return DashboardConfigurationDTO( widgets = listOf( // ... existing widgets ... DashboardWidgetDTO( id = "goal-tracker", type = WidgetTypes.GOAL_TRACKER, title = "Goal Progress", size = "MEDIUM", enabled = true, order = 3, platforms = listOf("DESKTOP", "WEB"), config = mapOf( "showProgress" to "true", "maxGoals" to "5" ) ) ), gridColumns = 3, platform = platform ) }

Step 3: Handle in Frontend

Frontend code would read the widget configuration and render accordingly based on type.


Implementing New Frequency Types

Use Case: Add support for “BI_WEEKLY” or “QUARTERLY” habit frequencies.

Step 1: Add Frequency Constant

File: /shared/src/commonMain/kotlin/models/habits/HabitFrequency.kt (if exists)

enum class HabitFrequency { DAILY, WEEKLY, MONTHLY, YEARLY, BI_WEEKLY, // NEW QUARTERLY // NEW }

Step 2: Update Next Habit Calculation

File: /server/src/main/kotlin/com/esteban/ruano/service/DashboardService.kt

Location: Line 315-352 (frequency switch in getNextHabit)

Add Cases:

val nextOccurrence = when (habit.frequency.uppercase()) { "DAILY" -> { /* existing */ } "WEEKLY" -> { /* existing */ } "MONTHLY" -> { /* existing */ } "YEARLY" -> { /* existing */ } // NEW: Bi-weekly (every 2 weeks) "BI_WEEKLY" -> { val targetDayOfWeek = baseDate.dayOfWeek val daysSinceBase = ChronoUnit.DAYS.between(baseDate, today).toInt() val weeksSinceBase = daysSinceBase / 7 // Calculate next occurrence (every 2 weeks) if (weeksSinceBase % 2 == 0 && today.dayOfWeek == targetDayOfWeek) { today } else { var nextDate = today while (true) { nextDate = nextDate.plus(DatePeriod(days = 1)) val newDaysSince = ChronoUnit.DAYS.between(baseDate, nextDate).toInt() val newWeeksSince = newDaysSince / 7 if (newWeeksSince % 2 == 0 && nextDate.dayOfWeek == targetDayOfWeek) { break } } nextDate } } // NEW: Quarterly (every 3 months) "QUARTERLY" -> { val targetDayOfMonth = baseDate.dayOfMonth val monthsSinceBase = ChronoUnit.MONTHS.between(baseDate, today).toInt() if (monthsSinceBase % 3 == 0 && today.dayOfMonth == targetDayOfMonth) { today } else { var nextMonth = today.month.value var nextYear = today.year while (true) { nextMonth++ if (nextMonth > 12) { nextMonth = 1 nextYear++ } val testDate = LocalDate(nextYear, nextMonth, 1) val testMonthsSince = ChronoUnit.MONTHS.between(baseDate, testDate).toInt() if (testMonthsSince % 3 == 0) { return LocalDate(nextYear, nextMonth, targetDayOfMonth) } } } } else -> baseDate }

Step 3: Update Overdue Detection

File: Same file, location: Line 538-643 (frequency switch in getOverdueHabitsList)

Add Cases:

val shouldBeOverdue = when (habit.frequency.uppercase()) { "DAILY" -> { /* existing */ } "WEEKLY" -> { /* existing */ } "MONTHLY" -> { /* existing */ } "YEARLY" -> { /* existing */ } // NEW: Bi-weekly overdue logic "BI_WEEKLY" -> { val targetDayOfWeek = baseDate.dayOfWeek val daysSinceBase = ChronoUnit.DAYS.between(baseDate, today).toInt() val weeksSinceBase = daysSinceBase / 7 // Check if we're in a bi-weekly cycle if (weeksSinceBase % 2 == 0 && today.dayOfWeek == targetDayOfWeek) { currentTime > baseTime } else if (weeksSinceBase % 2 == 1) { // We're in an off-week, check if last occurrence was missed val lastOccurrenceWeeksAgo = if (today.dayOfWeek.ordinal < targetDayOfWeek.ordinal) 2 else 1 val lastOccurrenceDate = today.minus(DatePeriod(days = lastOccurrenceWeeksAgo * 7 + (today.dayOfWeek.ordinal - targetDayOfWeek.ordinal))) val lastOccurrenceDateTime = LocalDateTime(lastOccurrenceDate, baseTime) now > lastOccurrenceDateTime } else { false } } // NEW: Quarterly overdue logic "QUARTERLY" -> { // Similar to monthly but every 3 months val targetDayOfMonth = baseDate.dayOfMonth val monthsSinceBase = ChronoUnit.MONTHS.between(baseDate, today).toInt() if (monthsSinceBase % 3 == 0 && today.dayOfMonth == targetDayOfMonth) { currentTime > baseTime } else if (monthsSinceBase % 3 > 0) { // Calculate last quarterly occurrence val lastQuarterMonthsAgo = monthsSinceBase % 3 val lastOccurrenceDate = today.minus(DatePeriod(months = lastQuarterMonthsAgo)) .let { LocalDate(it.year, it.month, targetDayOfMonth) } val lastOccurrenceDateTime = LocalDateTime(lastOccurrenceDate, baseTime) now > lastOccurrenceDateTime } else { false } } else -> { /* existing */ } }

Step 4: Add Tests

File: /server/src/test/kotlin/com/esteban/ruano/service/DashboardServiceTest.kt

@Test fun `bi-weekly habit shows as overdue after 2 weeks`() { val habit = createMockHabit( frequency = "BI_WEEKLY", baseDateTime = "2024-01-01T10:00:00", // Monday done = false ) // 2 weeks + 1 day later, same day of week, time passed val now = "2024-01-15T11:00:00".toLocalDateTime() // Monday, time passed val overdueHabits = service.getOverdueHabitsList(listOf(habit), now) assertEquals(1, overdueHabits.size) } @Test fun `quarterly habit calculates next occurrence correctly`() { val habit = createMockHabit( frequency = "QUARTERLY", baseDateTime = "2024-01-15T10:00:00", done = false ) val now = "2024-02-16T09:00:00".toLocalDateTime() val nextHabit = service.getNextHabit(listOf(habit), now) assertNotNull(nextHabit) assertEquals(habit.id, nextHabit.id) }

Debugging Overdue Calculation Issues

Use Case: Habit is incorrectly showing as overdue (or not overdue when it should be).

Step 1: Enable Debug Logging

Debug logging is already enabled in DashboardService.kt. Check server logs for output.

Look for:

=== OVERDUE HABITS DEBUG === Time: 2024-01-16T14:30:00, Total habits: 5 Today: 2024-01-16, Current time: 14:30 Checking: Morning workout (DAILY) - Base: 2024-01-16 07:00 DAILY: next occurrence: 2024-01-16T07:00, has passed: true Final result for Morning workout: true Overdue habits found: 1 - Morning workout (DAILY) === END OVERDUE HABITS ===

Step 2: Verify Habit Data

Check the habit’s dateTime field in the database:

SELECT id, name, frequency, date_time, done FROM habits WHERE user_id = ? AND name = 'Morning workout';

Ensure:

  • date_time is not null
  • frequency matches expected value
  • done is false

Step 3: Manually Test Calculation

Create a unit test with the exact scenario:

@Test fun `debug specific habit overdue calculation`() { val habit = HabitDTO( id = "test-id", name = "Morning workout", frequency = "DAILY", dateTime = "2024-01-16T07:00:00", done = false, streak = 5 ) val now = "2024-01-16T14:30:00".toLocalDateTime() val overdueHabits = dashboardService.getOverdueHabitsList(listOf(habit), now) // Add breakpoint here to step through logic println("Overdue count: ${overdueHabits.size}") overdueHabits.forEach { println("Overdue: ${it.name}") } }

Step 4: Check Time Zones

Ensure server and client use same timezone:

// In DashboardService.kt println("Server timezone: ${TimeZone.currentSystemDefault()}") println("Current time: ${Clock.System.now()}")

Optimizing Dashboard Query Performance

Use Case: Dashboard endpoint is slow with large datasets.

Strategy 1: Add Caching

File: Create /server/src/main/kotlin/com/esteban/ruano/service/DashboardCacheService.kt

class DashboardCacheService { private val cache = ConcurrentHashMap<String, CachedDashboard>() private val cacheDuration = 5.minutes data class CachedDashboard( val data: DashboardResponseDTO, val timestamp: Instant ) fun get(userId: Int, dateTime: String): DashboardResponseDTO? { val key = "$userId:$dateTime" val cached = cache[key] ?: return null if (Clock.System.now() - cached.timestamp > cacheDuration) { cache.remove(key) return null } return cached.data } fun put(userId: Int, dateTime: String, data: DashboardResponseDTO) { val key = "$userId:$dateTime" cache[key] = CachedDashboard(data, Clock.System.now()) } }

Update DashboardService:

class DashboardService( private val taskService: TaskService, // ... other services ... private val cacheService: DashboardCacheService ) { fun getDashboardData(userId: Int, dateTime: String): DashboardResponseDTO { // Check cache first cacheService.get(userId, dateTime)?.let { return it } // ... existing logic ... val result = DashboardResponseDTO(/* ... */) // Cache result cacheService.put(userId, dateTime, result) return result } }

Strategy 2: Parallelize Service Calls

Use Kotlin Coroutines:

suspend fun getDashboardData(userId: Int, dateTime: String): DashboardResponseDTO = coroutineScope { val currentDateTime = dateTime.toLocalDateTime() val today = currentDateTime.date val weekStart = today.minus(DatePeriod(days = today.dayOfWeek.ordinal)) val weekEnd = weekStart.plus(DatePeriod(days = 6)) // Fetch all data in parallel val tasksDeferred = async { taskService.fetchAllByDateRange(userId, "", weekStart, weekEnd, 100, 0) } val habitsDeferred = async { habitService.fetchAllByDateRange(userId, "", weekStart, weekEnd, 100, 0) } val transactionsDeferred = async { transactionService.getTransactionsByUser(userId, limit = 3) } val balanceDeferred = async { accountService.getTotalBalance(userId) } // ... more async calls ... val tasks = tasksDeferred.await() val habits = habitsDeferred.await() val transactions = transactionsDeferred.await() val balance = balanceDeferred.await() // ... rest of logic ... }

Strategy 3: Add Database Indexes

-- Index on track tables for date range queries CREATE INDEX idx_task_tracks_done_datetime ON task_tracks(done_date_time); CREATE INDEX idx_habit_tracks_done_datetime ON habit_tracks(done_date_time); CREATE INDEX idx_recipe_tracks_consumed_datetime ON recipe_tracks(consumed_date_time); -- Composite index for user-specific queries CREATE INDEX idx_tasks_user_date ON tasks(user_id, due_date_time); CREATE INDEX idx_habits_user_date ON habits(user_id, date_time);

Strategy 4: Reduce Data Fetched

Add pagination support:

// Instead of fetching 100 items val tasks = taskService.fetchAllByDateRange(userId, "", weekStart, weekEnd, 100, 0) // Fetch only what's needed val tasks = taskService.fetchAllByDateRange(userId, "", weekStart, weekEnd, 20, 0)

Adding New Platform Support

Use Case: Add support for a new platform like “TABLET” or “WATCH”.

Step 1: Define Platform Constant

File: /shared/src/commonMain/kotlin/models/dashboard/Platforms.kt (if exists)

object Platforms { const val ANDROID = "ANDROID" const val DESKTOP = "DESKTOP" const val WEB = "WEB" const val IOS = "IOS" const val TABLET = "TABLET" // NEW }

Step 2: No Server Changes Needed

The server already supports any platform string. Configuration is stored per (userId, platform) pair.

Step 3: Create Default Configuration

File: Frontend configuration initialization

// When user first accesses dashboard from tablet val defaultConfig = DashboardConfigurationDTO( widgets = listOf( DashboardWidgetDTO( id = "quick-overview", type = "COMPACT_VIEW", title = "Quick Overview", size = "SMALL", enabled = true, order = 0, platforms = listOf("TABLET"), config = mapOf("compact" to "true") ) ), gridColumns = 2, // Smaller grid for tablets platform = "TABLET" ) // Save to server dashboardApi.saveConfiguration(defaultConfig)

Testing Dashboard Functionality

Unit Testing

Test Next Task Selection:

@Test fun `returns highest priority overdue task`() { val tasks = listOf( createTask(priority = 3, dueDate = yesterday), createTask(priority = 5, dueDate = yesterday), createTask(priority = 4, dueDate = today) ) val result = dashboardService.getNextTask(tasks, now) assertEquals(5, result?.priority) }

Test Next Habit Calculation:

@Test fun `returns next habit with future time today`() { val habits = listOf( createHabit(frequency = "DAILY", baseTime = "18:00"), createHabit(frequency = "DAILY", baseTime = "20:00") ) val now = "2024-01-16T17:00:00".toLocalDateTime() val result = dashboardService.getNextHabit(habits, now) assertEquals("18:00", result?.dateTime?.substring(11, 16)) }

Integration Testing

Test Full Dashboard Endpoint:

@Test fun `dashboard endpoint returns complete data`() = testApplication { application { configureSerialization() configureRouting() } val response = client.get("/api/v1/dashboard?dateTime=2024-01-16T14:30:00") { header("Authorization", "Bearer $validToken") } assertEquals(HttpStatusCode.OK, response.status) val dashboard = response.body<DashboardResponseDTO>() assertNotNull(dashboard.taskStats) assertNotNull(dashboard.habitStats) }

Manual Testing

Using cURL:

# Get dashboard data curl -X GET "http://localhost:8080/api/v1/dashboard?dateTime=2024-01-16T14:30:00" \ -H "Authorization: Bearer YOUR_TOKEN" # Get configuration curl -X GET "http://localhost:8080/api/v1/dashboard/configuration?platform=DESKTOP" \ -H "Authorization: Bearer YOUR_TOKEN" # Save configuration curl -X POST "http://localhost:8080/api/v1/dashboard/configuration" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"configuration":{"widgets":[],"gridColumns":3,"platform":"DESKTOP"}}'