Dashboard Architecture Deep-Dive
Comprehensive architectural documentation for the Oter Dashboard feature.
Table of Contents
- Overview
- Layered Architecture
- Data Flow
- Service Dependencies
- Algorithm Implementations
- Database Schema
- Testing Architecture
- 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 foundConfiguration 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
| Service | Methods Called | Purpose |
|---|---|---|
| TaskService | fetchAllByDateRange(userId, "", weekStart, weekEnd, 100, 0) | Get all tasks for the current week |
| HabitService | fetchAll(userId, "", 100, 0) | Get all habits (for overdue detection) |
fetchAllByDateRange(userId, "", weekStart, weekEnd, 100, 0) | Get habits for current week (for next habit) | |
| TransactionService | getTransactionsByUser(userId, limit=3) | Get 3 most recent transactions |
| AccountService | getTotalBalance(userId) | Calculate total across all accounts |
| NutritionService | getRecipesByDay(userId, dayOfWeek) | Get meals planned for today |
getRecipesNotAssignedToDay(userId, "", 100, 0) | Get unexpected meals | |
| WorkoutService | getWorkoutDayById(userId, dayOfWeek) | Get today’s workout plan |
getWorkoutDaysByDay(userId, dayOfWeek, date) | Get workout completion status per day | |
| DailyJournalService | getByUserId(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 tableAlgorithm 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 nullTime Complexity: O(n) where n = number of tasks
Key Logic Points:
- Uses
dueDateTimeprimarily, falls back toscheduledDateTime - 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 habitTime 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 listTime 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 FloatTime 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 listTime 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:
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | UUID | PRIMARY KEY | Auto-generated |
| user_id | INT | FOREIGN KEY → Users(id), CASCADE | Owner of configuration |
| platform | VARCHAR(20) | NOT NULL | ANDROID, DESKTOP, WEB, IOS |
| configuration_json | TEXT | NOT NULL | Serialized DashboardConfigurationDTO |
| created_at | DATETIME | NOT NULL | Creation timestamp |
| updated_at | DATETIME | NOT NULL | Last update timestamp |
Indexes:
- PRIMARY KEY on
id - UNIQUE INDEX on
(user_id, platform)- one config per user per platform - FOREIGN KEY on
user_idreferencingUsers(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"
}Related Track Tables
TaskTracks:
doneDateTime: When task was completed (used for statistics)
HabitTracks:
doneDateTime: When habit was completed (used for statistics)
RecipeTracks:
consumedDateTime: When meal was loggedskipped: 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