Skip to Content
AgentsDashboard architecture

Dashboard Architecture Deep-Dive

Comprehensive architectural documentation for the Oter Dashboard feature.

Table of Contents

  1. Overview
  2. Layered Architecture
  3. Data Flow
  4. Service Dependencies
  5. Algorithm Implementations
  6. Database Schema
  7. Testing Architecture
  8. Design Patterns

Overview

The Dashboard feature provides a unified view of user activity across all Oter modules (tasks, habits, finance, nutrition, workouts, journal). It aggregates data from 7 domain services and applies intelligent algorithms to surface the most relevant information.

Key Characteristics:

  • Multi-domain aggregation: Combines data from tasks, habits, finance, nutrition, workout, and journal modules
  • Intelligent prioritization: Uses algorithms to determine next task/habit
  • Frequency-aware: Handles DAILY, WEEKLY, MONTHLY, YEARLY habit frequencies
  • Customizable: Per-platform widget configurations
  • Analytics-ready: Provides weekly statistics and per-day breakdowns

Layered Architecture

The dashboard follows a clean 4-layer architecture:

┌─────────────────────────────────────────┐ │ Routing Layer (HTTP) │ │ DashboardRouting.kt │ │ - Endpoint definitions │ │ - Request validation │ │ - Authentication │ │ - Response serialization │ └────────────────┬────────────────────────┘ ┌────────────────▼────────────────────────┐ │ Service Layer (Business Logic) │ │ DashboardService.kt │ │ - Data aggregation │ │ - Algorithm execution │ │ - Statistics calculation │ │ │ │ DashboardConfigurationService.kt │ │ - Configuration CRUD │ │ - JSON serialization │ └────────────────┬────────────────────────┘ ┌────────────────▼────────────────────────┐ │ Repository Layer (Data Access) │ │ DashboardConfigurationRepository.kt │ │ - Service wrapper │ │ - Abstraction layer │ └────────────────┬────────────────────────┘ ┌────────────────▼────────────────────────┐ │ Database Layer (Persistence) │ │ DashboardConfigurations (Entity) │ │ - Exposed ORM mapping │ │ - Transaction management │ └─────────────────────────────────────────┘

Layer Responsibilities

Routing Layer (/server/src/main/kotlin/com/esteban/ruano/routing/DashboardRouting.kt):

  • HTTP endpoint definitions (GET /dashboard, GET /configuration, POST /configuration)
  • JWT authentication enforcement
  • Request parameter extraction and validation
  • Response formatting and error handling
  • HTTP status code mapping

Service Layer (/server/src/main/kotlin/com/esteban/ruano/service/DashboardService.kt):

  • Core business logic implementation
  • Multi-service data aggregation
  • Algorithm execution (next task, next habit, overdue detection)
  • Weekly statistics computation
  • Per-day breakdown calculation
  • Null handling and data transformation

Repository Layer (/server/src/main/kotlin/com/esteban/ruano/repository/DashboardConfigurationRepository.kt):

  • Thin wrapper around service layer
  • Provides interface for routing layer
  • Enables future repository pattern expansion

Database Layer:

  • ORM entity definitions using Exposed
  • Transaction boundary management
  • Database query execution
  • JSON serialization for configuration storage

Data Flow

Dashboard Data Request Flow

1. HTTP Request GET /api/v1/dashboard?dateTime=2024-01-16T14:30:00 Authorization: Bearer <token> 2. DashboardRouting.kt (Line 21-48) - Extract JWT principal → userId - Validate dateTime parameter - Check datetime format 3. DashboardService.getDashboardData(userId, dateTime) (Line 42-156) - Parse dateTime to LocalDateTime - Calculate week range (Mon-Sun) - Fetch data from 7 services in parallel: * TaskService → tasks for week * HabitService → habits for week + all habits * TransactionService → last 3 transactions * AccountService → total balance * NutritionService → today's meals + unexpected * WorkoutService → today's workout + streak * DailyJournalService → recent entries + streak 4. Algorithm Execution - getNextTask(tasks, currentDateTime) → Line 232-295 - getNextHabit(habits, currentDateTime) → Line 297-399 - calculateTaskStats(tasks) → Line 415-419 - calculateHabitStats(habits) → Line 421-425 - calculateOverdueTasks(tasks, now) → Line 401-409 - calculateOverdueHabits(allHabits, now) → Line 411-413 - getOverdueTasksList(tasks, now) → Line 501-509 - getOverdueHabitsList(habits, now) → Line 511-656 5. Statistics Calculation - calculateTaskCompletionFromTracks(weekStart) → Line 174-183 - calculateHabitCompletionFromTracks(weekStart) → Line 185-194 - calculateRecipeCompletionFromTracks(weekStart) → Line 196-205 - getTrackCountsPerDay(weekStart) → Line 158-172 - getWorkoutTrackCountsPerDay(userId, weekStart) → Line 427-435 - getPlannedMealsPerDay(userId, weekStart) → Line 437-468 - getUnexpectedMealsByDateRange(userId, start, end) → Line 470-499 6. Response Construction - Assemble DashboardResponseDTO with 40+ fields - Include all calculated data 7. HTTP Response 200 OK Content-Type: application/json { DashboardResponseDTO }

Configuration Request Flow

GET /api/v1/dashboard/configuration?platform=DESKTOP DashboardRouting.kt (Line 52-71) DashboardConfigurationRepository.getConfiguration(userId, platform) DashboardConfigurationService.getConfiguration(userId, platform) Database query on DashboardConfigurations table WHERE userId = ? AND platform = ? Deserialize JSON → DashboardConfigurationDTO Return to client (200) or 404 if not found

Configuration Save Flow

POST /api/v1/dashboard/configuration Body: { configuration: DashboardConfigurationDTO } DashboardRouting.kt (Line 74-93) DashboardConfigurationRepository.saveConfiguration(userId, config) DashboardConfigurationService.saveConfiguration(userId, config) Serialize config → JSON string Upsert into DashboardConfigurations table ON CONFLICT (userId, platform) DO UPDATE Return success (200) or failure (500)

Service Dependencies

DashboardService depends on 7 domain services (constructor injection):

class DashboardService( private val taskService: TaskService, private val habitService: HabitService, private val transactionService: TransactionService, private val accountService: AccountService, private val nutritionService: NutritionService, private val workoutService: WorkoutService, private val journalService: DailyJournalService )

Service Usage Breakdown

ServiceMethods CalledPurpose
TaskServicefetchAllByDateRange(userId, "", weekStart, weekEnd, 100, 0)Get all tasks for the current week
HabitServicefetchAll(userId, "", 100, 0)Get all habits (for overdue detection)
fetchAllByDateRange(userId, "", weekStart, weekEnd, 100, 0)Get habits for current week (for next habit)
TransactionServicegetTransactionsByUser(userId, limit=3)Get 3 most recent transactions
AccountServicegetTotalBalance(userId)Calculate total across all accounts
NutritionServicegetRecipesByDay(userId, dayOfWeek)Get meals planned for today
getRecipesNotAssignedToDay(userId, "", 100, 0)Get unexpected meals
WorkoutServicegetWorkoutDayById(userId, dayOfWeek)Get today’s workout plan
getWorkoutDaysByDay(userId, dayOfWeek, date)Get workout completion status per day
DailyJournalServicegetByUserId(userId, limit=3, offset=0)Get 3 most recent journal entries

Dependency Graph

DashboardService ├── TaskService │ └── TaskRepository → Tasks table │ └── TaskTrack table (for statistics) ├── HabitService │ └── HabitRepository → Habits table │ └── HabitTrack table (for statistics) ├── TransactionService │ └── TransactionRepository → Transactions table ├── AccountService │ └── AccountRepository → Accounts table ├── NutritionService │ └── RecipeRepository → Recipes table │ └── RecipeDay table (meal planning) │ └── RecipeTrack table (meal logging) ├── WorkoutService │ └── WorkoutRepository → WorkoutDays table │ └── WorkoutExercises table └── DailyJournalService └── JournalRepository → DailyJournals table

Algorithm Implementations

Next Task Selection Algorithm

Location: DashboardService.kt:232-295

Input: tasks: List<TaskDTO>, now: LocalDateTime

Output: TaskDTO? (null if no pending tasks)

Algorithm:

1. Filter: Keep only tasks where done != true 2. If empty, return null 3. Categorize tasks into 3 groups: a. Overdue: dueDate < today b. Today's: dueDate == today c. Future: dueDate > today 4. Priority selection: a. If overdueTasks.isNotEmpty(): - Return task with highest priority (maxByOrNull) b. Else if todaysTasks.isNotEmpty(): - Filter high priority (priority >= 4) - If high priority exists, return earliest by time - Otherwise, return highest priority, then earliest time c. Else if futureTasks.isNotEmpty(): - Return highest priority, then earliest date/time d. Else return null

Time Complexity: O(n) where n = number of tasks

Key Logic Points:

  • Uses dueDateTime primarily, falls back to scheduledDateTime
  • Compares dates only for categorization (time is secondary)
  • High priority threshold = 4
  • Within same priority, earliest time wins

Next Habit Selection Algorithm

Location: DashboardService.kt:297-399

Input: habits: List<HabitDTO>, now: LocalDateTime

Output: HabitDTO? (null if no pending non-overdue habits)

Algorithm:

1. Filter: Keep only habits where done == false 2. If empty, return null 3. For each pending habit, calculate next occurrence: - DAILY: If baseTime > currentTime then today, else tomorrow - WEEKLY: Find next date matching baseDate.dayOfWeek - MONTHLY: Find next date matching baseDate.dayOfMonth - YEARLY: Find next date matching baseDate.dayOfYear 4. Create HabitOccurrence objects with nextOccurrenceDateTime 5. Filter: Remove habits where now > nextOccurrenceDateTime (overdue) 6. If no non-overdue habits, return null 7. Categorize remaining habits: a. Today's habits with future times (effectiveTime >= currentTime) b. Today's habits with past times (effectiveTime < currentTime) c. Future date habits 8. Priority selection: a. If futureTimesToday.isNotEmpty(): return first (earliest) b. Else if pastTimesToday.isNotEmpty(): return first (earliest) c. Else if futureHabits.isNotEmpty(): return earliest by datetime d. Else return first remaining habit

Time Complexity: O(n * m) where n = number of habits, m = days to find next occurrence

Key Logic Points:

  • Calculates next occurrence for ALL pending habits first
  • Filters out overdue habits to prevent showing them as “next”
  • Prioritizes future times today over past times today
  • Uses baseDateTime from habit.dateTime field

Overdue Habits Detection Algorithm

Location: DashboardService.kt:511-656

Input: habits: List<HabitDTO>, now: LocalDateTime

Output: List<HabitDTO> (habits past their scheduled occurrence)

Algorithm:

1. For each habit: a. Skip if done == true b. Skip if baseDateTime is null c. Extract baseDate and baseTime from baseDateTime 2. Determine if overdue based on frequency: DAILY: - nextOccurrenceDate = if (baseDate <= today) today else baseDate - Overdue if now > LocalDateTime(nextOccurrenceDate, baseTime) WEEKLY: - If baseDate > today: Not overdue - If today.dayOfWeek == targetDayOfWeek: - Overdue if currentTime > baseTime - Else if we've passed target day this week: - Calculate lastOccurrenceDateTime - Overdue if now > lastOccurrenceDateTime - Else: Not overdue (target day is future this week) MONTHLY: - If baseDate > today: Not overdue - If today.dayOfMonth == targetDay: - Overdue if currentTime > baseTime - Else if today.dayOfMonth > targetDay: - Calculate lastOccurrenceDateTime this month - Overdue if now > lastOccurrenceDateTime - Else: Not overdue YEARLY: - If baseDate > today: Not overdue - If today.dayOfYear == targetDay: - Overdue if currentTime > baseTime - Else if today.dayOfYear > targetDay: - Calculate lastOccurrenceDateTime this year - Overdue if now > lastOccurrenceDateTime - Else: Not overdue 3. Collect all habits where shouldBeOverdue == true 4. Return filtered list

Time Complexity: O(n) where n = number of habits

Key Logic Points:

  • Extensive debug logging (println statements)
  • Handles future base dates (habit not yet started)
  • Compares both date AND time for precision
  • Different logic per frequency type

Weekly Statistics Calculation

Location: DashboardService.kt:174-205

Input: weekStart: LocalDate, column: Column<LocalDateTime>, finder: (Op<Boolean>) -> Iterable<*>

Output: Float (average per day)

Algorithm:

1. Define week range: - start = weekStart at 00:00 - end = weekStart + 6 days at 23:59 2. Query track table where column between start and end 3. Count results 4. Divide by 7 5. Return as Float

Time Complexity: O(1) database query

Used for: Tasks, habits, recipes (meals)

Per-Day Counts Calculation

Location: DashboardService.kt:158-172

Input: weekStart: LocalDate, column: Column<LocalDateTime>, finder

Output: List<Int> (7 elements for Mon-Sun)

Algorithm:

1. For i = 0 to 6: a. Calculate day = weekStart + i days b. Define range: day at 00:00 to day at 23:59 c. Query track table where column between range d. Count results e. Add to list 2. Return 7-element list

Time Complexity: O(7) = O(1) database queries

Used for: Tasks, habits, meals logged


Database Schema

DashboardConfigurations Table

object DashboardConfigurations : UUIDTable() { val userId = reference("user_id", Users, onDelete = ReferenceOption.CASCADE) val platform = varchar("platform", 20) val configurationJson = text("configuration_json") val createdAt = datetime("created_at") val updatedAt = datetime("updated_at") init { uniqueIndex(userId, platform) } }

Schema:

ColumnTypeConstraintsDescription
idUUIDPRIMARY KEYAuto-generated
user_idINTFOREIGN KEY → Users(id), CASCADEOwner of configuration
platformVARCHAR(20)NOT NULLANDROID, DESKTOP, WEB, IOS
configuration_jsonTEXTNOT NULLSerialized DashboardConfigurationDTO
created_atDATETIMENOT NULLCreation timestamp
updated_atDATETIMENOT NULLLast update timestamp

Indexes:

  • PRIMARY KEY on id
  • UNIQUE INDEX on (user_id, platform) - one config per user per platform
  • FOREIGN KEY on user_id referencing Users(id) with CASCADE delete

Example Data:

{ "id": "123e4567-e89b-12d3-a456-426614174000", "user_id": 42, "platform": "DESKTOP", "configuration_json": "{\"widgets\":[...],\"gridColumns\":3,\"platform\":\"DESKTOP\"}", "created_at": "2024-01-15T10:30:00", "updated_at": "2024-01-16T14:20:00" }

TaskTracks:

  • doneDateTime: When task was completed (used for statistics)

HabitTracks:

  • doneDateTime: When habit was completed (used for statistics)

RecipeTracks:

  • consumedDateTime: When meal was logged
  • skipped: Boolean indicating unexpected meal

WorkoutDays:

  • isCompleted: Boolean flag for completion status

Testing Architecture

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

Test Strategy:

  • Unit tests for algorithms (next habit, overdue detection)
  • Mock service dependencies using test data
  • Test edge cases (empty lists, all overdue, mixed frequencies)
  • Verify correct prioritization and filtering

Example Test Pattern:

@Test fun `test daily habit overdue detection`() { // Given: Habit with baseDateTime yesterday at 10:00 val habit = createMockHabit( frequency = "DAILY", baseDateTime = "2024-01-15T10:00:00", done = false ) // When: Current time is today at 11:00 val now = "2024-01-16T11:00:00".toLocalDateTime() val overdueHabits = service.getOverdueHabitsList(listOf(habit), now) // Then: Habit should be overdue assertEquals(1, overdueHabits.size) assertEquals(habit.id, overdueHabits[0].id) }

Design Patterns

1. Service Aggregator Pattern

DashboardService acts as an aggregator, orchestrating calls to multiple domain services and combining their results.

2. Repository Pattern

DashboardConfigurationRepository provides an abstraction layer over the service, enabling future swapping of implementations.

3. Strategy Pattern

Overdue detection and next occurrence calculation use different strategies based on habit frequency (DAILY, WEEKLY, MONTHLY, YEARLY).

4. Data Transfer Object (DTO) Pattern

All API communication uses DTOs (DashboardResponseDTO, DashboardConfigurationDTO) separate from database entities.

5. Dependency Injection

All services are injected via constructor (Koin framework), enabling testability and loose coupling.

6. Extension Function Pattern (Ktor)

Routing is defined as extension function on Route for clean DSL-style API definitions.

7. Transaction Script Pattern

Business logic encapsulated in service methods that orchestrate database transactions and calculations.


Performance Considerations

Current Bottlenecks:

  • No caching - every request queries database
  • Sequential service calls in getDashboardData
  • No pagination (fetches all weekly data)
  • Debug logging enabled (println statements)

Optimization Strategies:

  • Implement Redis caching for dashboard data (TTL: 5 minutes)
  • Use Kotlin coroutines to parallelize service calls
  • Add pagination to limit result sets
  • Disable debug logging in production
  • Consider materialized views for weekly statistics

Scalability:

  • Stateless service design enables horizontal scaling
  • Database connection pooling via HikariCP
  • Consider read replicas for analytics queries