Dashboard Feature Guide
Guide to the Dashboard / Home feature — a single
/dashboardendpoint that aggregates tasks, habits, finance, meals, workout, journal, books, and projects into one platform-aware response, plus per-platform widget layout configuration and an Android home-screen widget.
Overview
The Dashboard is the home screen of Oter. Unlike most modules, it owns no domain data of its own — the server’s DashboardService fans out to every feature service (TaskService, HabitService, TransactionService, NutritionService, WorkoutService, DailyJournalService, BudgetService, BooksService, PrincipleService, …), runs them in parallel for one reference dateTime, and returns a single DashboardResponseDTO. The server also computes derived values clients never recalculate: the “next” task/habit, ranked priority carousels, overdue lists, per-day completion arrays for charts, weekly completion ratios, and a simplified "good" | "medium" | "bad" card status per domain.
Which domains appear in the response is gated by effective feature flags for the requesting platform — the same X-Platform header used elsewhere in the app. A disabled feature returns an empty/zeroed slice rather than being omitted from the JSON.
Separately, a /dashboard/configuration endpoint stores a per-user, per-platform widget layout (which widgets show, their order, size, and grid columns). The Android app additionally ships a Jetpack Glance home-screen widget that surfaces today’s tasks and habits.
Architecture
The server DashboardService is the aggregation point: it loads a weekly track summary, then launches one async slice per domain (tasks, habits, finance, meals, workout, journal, books, projects) inside a single coroutineScope, and assembles all of them into DashboardResponseDTO. Disabled flags short-circuit each slice to an empty default.
Data Models
DashboardResponse (shared KMP model)
The shared DashboardResponse mirrors the server’s DashboardResponseDTO. Selected fields:
data class DashboardResponse(
val nextTask: Task?,
val nextHabit: Habit?,
val priorityTaskCandidates: List<Task> = emptyList(), // ranked carousel, max 5
val priorityHabitCandidates: List<Habit> = emptyList(), // ranked carousel, max 5
val taskStats: TaskStats,
val habitStats: HabitStats,
val overdueTasksList: List<Task> = emptyList(),
val overdueHabitsList: List<Habit> = emptyList(),
val pendingTasksToday: List<Task> = emptyList(),
val pendingHabitsToday: List<Habit> = emptyList(),
// Finance
val recentTransactions: List<TransactionDTO>? = emptyList(),
val accountBalance: Double = 0.0,
val budgetProgress: List<BudgetProgressDTO>? = emptyList(),
// Meals
val todayCalories: Int = 0,
val nextMeal: MealDTO? = null,
val overdueMealsToday: List<DashboardMealAgendaItem> = emptyList(),
val pendingMealsToday: List<DashboardMealAgendaItem> = emptyList(),
val consumedMealsToday: List<DashboardMealAgendaItem> = emptyList(),
// Workout
val todayWorkout: WorkoutDTO? = null,
val workoutScheduledTime: String? = null,
val workoutCompletedToday: Boolean = false,
val workoutOverdueToday: Boolean = false,
val workoutStreak: Int = 0,
// Journal / Books / Projects / Principles
val journalCompleted: Boolean = false,
val journalStreak: Int = 0,
val booksReadingStreak: Int = 0,
val currentlyReadingBooks: List<DashboardReadingBook> = emptyList(),
val projectsStatus: List<ProjectDashboardItem> = emptyList(),
val principles: List<Principle>? = null,
// Weekly completion ratios + per-day chart arrays (7 ints each)
val weeklyTaskCompletion: Float = 0f,
val tasksCompletedPerDayThisWeek: List<Int>? = emptyList(),
// Server-computed card status: "good" | "medium" | "bad"
val tasksCardStatus: String? = "medium",
val habitsCardStatus: String? = "medium",
// ... mealsCardStatus, workoutCardStatus, financesCardStatus, journalCardStatus, booksCardStatus
)DashboardConfiguration (widget layout)
data class DashboardConfiguration(
val widgets: List<DashboardWidget> = emptyList(),
val gridColumns: Int = 3,
val platform: Platform = Platform.DESKTOP
)
data class DashboardWidget(
val id: String,
val type: DashboardWidgetType,
val title: String,
val size: WidgetSize = WidgetSize.MEDIUM,
val enabled: Boolean = true,
val order: Int = 0,
val platforms: Set<Platform> = setOf(Platform.ANDROID, Platform.DESKTOP, Platform.WEB, Platform.IOS),
val config: Map<String, Any> = emptyMap()
)DefaultDashboardConfiguration.create(platform) builds the starter layout (overdue tasks, overdue habits, today’s tasks/habits, timers, next task/habit, finance, statistics) and varies gridColumns by platform (Desktop/Web 3, Android/iOS 2).
Card Status Enum (string-encoded)
The server emits a per-domain status string rather than a Kotlin enum:
| Value | Meaning (per domain heuristics) |
|---|---|
good | Few/no pending items, active streak, or budgets well under limit |
medium | Moderate pending load, partial streak, or budget ≥ 80% used (default) |
bad | High pending load, no streak, budget over limit, or overdue book deadline |
Widget Enums
DashboardWidgetType | WidgetSize | Platform |
|---|---|---|
| TASKS, HABITS, TIMERS, FINANCE, NUTRITION, WORKOUT, JOURNAL, STUDY, STATISTICS, OVERDUE_TASKS, OVERDUE_HABITS, NEXT_TASK, NEXT_HABIT, RECENT_TRANSACTIONS, ACCOUNT_BALANCE, TODAY_CALORIES, WORKOUT_STREAK, JOURNAL_STREAK, WEEKLY_PROGRESS, CUSTOM | SMALL, MEDIUM, LARGE, FULL | ANDROID, SERVER, DESKTOP, WEB, IOS |
API Endpoints
All routes are mounted under /api/v1. They require authentication.
| Method | Path | Key Params | Description |
|---|---|---|---|
GET | /dashboard | dateTime (required, dd/MM/yyyy HH:mm) | Aggregated, platform-filtered dashboard for the reference moment |
GET | /dashboard/configuration | platform query (default DESKTOP) | User’s saved widget layout for that platform (404 if none) |
POST | /dashboard/configuration | body: SaveDashboardConfigurationRequest | Persist widget layout preferences |
Notes:
GET /dashboardreturns400ifdateTimeis missing (DASHBOARD_DATETIME_REQUIRED) or malformed (DASHBOARD_DATETIME_INVALID).- The
X-Platformheader (ANDROID,IOS,WEB,DESKTOP; defaultDESKTOP) is read onGET /dashboardand passed toFeatureFlagRepository.getEffectiveFlagsForPlatform(platform, userId). The resultingMap<String, Boolean>of flags (TASKS,HABITS,FINANCE,MEALS,WORKOUT,JOURNAL,BOOKS,PROJECTS) gates which domain slices are populated. GET /dashboard/configurationselects the layout via aplatformquery parameter, not the header.
Client Behavior
The ViewModel auto-listens to the DomainEventBus and re-fetches whenever a DomainEvent.RemoteAgendaStale or DomainEvent.FinanceRemoteMutation fires, so completing a task/habit or a finance change elsewhere refreshes the home screen without a manual pull. RefreshDashboard (pull-to-refresh) and MarkTaskDone / MarkHabitDone / MarkHabitUndone / MarkRecipeConsumed intents delegate to the respective feature use cases; MarkRecipeConsumed triggers an explicit refresh on success.
Widgets
Android home-screen widget (Jetpack Glance)
OterTodayWidget (composeApp/src/androidMain/.../widget/OterTodayWidget.kt) is a GlanceAppWidget that displays today’s tasks and habits in a unified list. Because Glance widgets cannot use @AndroidEntryPoint, it pulls the shared KMP GetTodayActionsUseCase through a Hilt EntryPoint, which combines GetTasksWithSmartFiltering and GetHabitsByRangeDate, sorts incomplete items first, and takes up to DASHBOARD_PRIORITY_TASK_CAROUSEL_MAX / DASHBOARD_PRIORITY_HABIT_CAROUSEL_MAX (5 each). OterTodayWidgetReceiver handles widget lifecycle, and refreshOterTodayWidget(context) forces an update. Tapping the widget opens MainActivity.
Note: this widget does not call the
/dashboardendpoint — it builds its list directly from the shared task/habit use cases. See Widget implementation for the full file inventory.
In-app dashboard widgets (layout config)
Within the app, “widgets” refer to the configurable cards described by DashboardConfiguration / DashboardWidget. The layout is persisted per platform via /dashboard/configuration and rendered on the home screen; the underlying data still comes from the single /dashboard response.
Key Files
| File | Purpose |
|---|---|
server/.../routing/DashboardRouting.kt | /dashboard + /dashboard/configuration endpoints; dateTime validation; X-Platform → feature flags |
server/.../service/DashboardService.kt | Parallel aggregation of all domain slices into DashboardResponseDTO; next/overdue/priority/card-status logic |
server/.../models/dashboard/DashboardResponseDTO.kt | Server response DTO + TaskStatsDTO / HabitStatsDTO / DashboardMealAgendaItemDTO |
server/.../models/dashboard/DashboardConfigurationDTO.kt | Configuration DTO + DashboardWidgetDTO + SaveDashboardConfigurationRequest |
server/.../repository/DashboardConfigurationRepository.kt | Persistence of per-user/per-platform widget layout |
shared/.../models/dashboard/DashboardModels.kt | Shared DashboardResponse and child models (KMP) |
shared/.../dashboard/DashboardConfiguration.kt | DashboardConfiguration, builder, DefaultDashboardConfiguration |
shared/.../dashboard/DashboardWidget.kt | DashboardWidget, DashboardWidgetType, WidgetSize, Platform enums |
shared/.../services/dashboard/DashboardService.kt | Ktor HTTP client for dashboard + configuration |
home/home_domain/.../use_cases/DashboardUseCases.kt | Bundles GetDashboard, RefreshDashboard, settings use cases |
home/home_domain/.../repository/DashboardRepository.kt | Domain repository interface |
home/home_data/.../repository/DashboardRepositoryImpl.kt | Repository implementation (network-aware) |
home/home_data/.../datasources/DashboardRemoteDataSource.kt | Calls shared DashboardService |
home/home_presentation/.../viewmodel/DashboardViewModel.kt | MVI ViewModel; auto-fetch + event-bus refresh |
home/home_presentation/.../intent/DashboardState.kt | DashboardState + fromDashboardResponse mapper |
home/home_presentation/.../intent/DashboardIntent.kt | MVI intents (fetch, refresh, mark done, navigate) |
composeApp/src/androidMain/.../widget/OterTodayWidget.kt | Jetpack Glance today’s-actions home-screen widget |
Testing
- Server unit (
server/.../service/DashboardServiceTest.kt): overdue habit logic acrossDAILY/WEEKLY/MONTHLY/YEARLYfrequencies, done habits excluded from overdue, mixed-habit overdue counts, andgetNextHabitselection with multiple overdue habits. - Integration:
GET /dashboard?dateTime=...for a moment spanning a full week; verify each enabled slice (tasks, habits, finance, meals, workout, journal, books, projects) is populated and disabled flags yield zeroed/empty slices. - Edge cases: missing/invalid
dateTime→400; per-day chart arrays always length 7; card-status thresholds at thegood/medium/badboundaries.
Troubleshooting
Dashboard returns 400
dateTimequery param is required and must bedd/MM/yyyy HH:mm. A missing param maps toDASHBOARD_DATETIME_REQUIRED; a malformed one toDASHBOARD_DATETIME_INVALID.
A whole section (finance, books, projects, …) is empty
- That domain’s feature flag is disabled for the current platform. The server resolves flags via
getEffectiveFlagsForPlatform(platform, userId)from theX-Platformheader; a disabled flag short-circuits the slice to its empty default rather than erroring.
Wrong next task / habit or wrong overdue list
- These are computed server-side from the supplied
dateTime. A client-server timezone mismatch shifts “today” and the overdue window. EnsuredateTimeuses the device’s local time.
Home screen not refreshing after completing an item elsewhere
- The ViewModel only refreshes on
DomainEvent.RemoteAgendaStaleandDomainEvent.FinanceRemoteMutation. If a feature mutation does not emit one of these events, the dashboard will not auto-refresh until the next fetch or pull-to-refresh.
GET /dashboard/configuration returns 404
- No saved layout exists for that user/platform yet. The shared client maps 404 to a
nullconfiguration; fall back toDefaultDashboardConfiguration.create(platform). Note this endpoint readsplatformfrom a query param, not theX-Platformheader.
Glance widget shows nothing / stale data
- The widget builds its list from
GetTodayActionsUseCase(not/dashboard) and updates only when refreshed. CallrefreshOterTodayWidget(context)after task/habit mutations; habit frequency filtering in the widget is simplified relative to the server’s overdue logic.
Related Docs
- Calendar — sibling presentation-aggregator that also consumes
/dashboard - Habits — habit recurrence + overdue rules reused by the dashboard
- Mobile integration — Android Clean Architecture + MVI integration details
- Widget implementation — Jetpack Glance today widget file inventory
- Domain & models — shared domain model reference