Skip to Content
FeaturesDashboardOverview

Dashboard Feature Guide

Guide to the Dashboard / Home feature — a single /dashboard endpoint 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:

ValueMeaning (per domain heuristics)
goodFew/no pending items, active streak, or budgets well under limit
mediumModerate pending load, partial streak, or budget ≥ 80% used (default)
badHigh pending load, no streak, budget over limit, or overdue book deadline

Widget Enums

DashboardWidgetTypeWidgetSizePlatform
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, CUSTOMSMALL, MEDIUM, LARGE, FULLANDROID, SERVER, DESKTOP, WEB, IOS

API Endpoints

All routes are mounted under /api/v1. They require authentication.

MethodPathKey ParamsDescription
GET/dashboarddateTime (required, dd/MM/yyyy HH:mm)Aggregated, platform-filtered dashboard for the reference moment
GET/dashboard/configurationplatform query (default DESKTOP)User’s saved widget layout for that platform (404 if none)
POST/dashboard/configurationbody: SaveDashboardConfigurationRequestPersist widget layout preferences

Notes:

  • GET /dashboard returns 400 if dateTime is missing (DASHBOARD_DATETIME_REQUIRED) or malformed (DASHBOARD_DATETIME_INVALID).
  • The X-Platform header (ANDROID, IOS, WEB, DESKTOP; default DESKTOP) is read on GET /dashboard and passed to FeatureFlagRepository.getEffectiveFlagsForPlatform(platform, userId). The resulting Map<String, Boolean> of flags (TASKS, HABITS, FINANCE, MEALS, WORKOUT, JOURNAL, BOOKS, PROJECTS) gates which domain slices are populated.
  • GET /dashboard/configuration selects the layout via a platform query 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 /dashboard endpoint — 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

FilePurpose
server/.../routing/DashboardRouting.kt/dashboard + /dashboard/configuration endpoints; dateTime validation; X-Platform → feature flags
server/.../service/DashboardService.ktParallel aggregation of all domain slices into DashboardResponseDTO; next/overdue/priority/card-status logic
server/.../models/dashboard/DashboardResponseDTO.ktServer response DTO + TaskStatsDTO / HabitStatsDTO / DashboardMealAgendaItemDTO
server/.../models/dashboard/DashboardConfigurationDTO.ktConfiguration DTO + DashboardWidgetDTO + SaveDashboardConfigurationRequest
server/.../repository/DashboardConfigurationRepository.ktPersistence of per-user/per-platform widget layout
shared/.../models/dashboard/DashboardModels.ktShared DashboardResponse and child models (KMP)
shared/.../dashboard/DashboardConfiguration.ktDashboardConfiguration, builder, DefaultDashboardConfiguration
shared/.../dashboard/DashboardWidget.ktDashboardWidget, DashboardWidgetType, WidgetSize, Platform enums
shared/.../services/dashboard/DashboardService.ktKtor HTTP client for dashboard + configuration
home/home_domain/.../use_cases/DashboardUseCases.ktBundles GetDashboard, RefreshDashboard, settings use cases
home/home_domain/.../repository/DashboardRepository.ktDomain repository interface
home/home_data/.../repository/DashboardRepositoryImpl.ktRepository implementation (network-aware)
home/home_data/.../datasources/DashboardRemoteDataSource.ktCalls shared DashboardService
home/home_presentation/.../viewmodel/DashboardViewModel.ktMVI ViewModel; auto-fetch + event-bus refresh
home/home_presentation/.../intent/DashboardState.ktDashboardState + fromDashboardResponse mapper
home/home_presentation/.../intent/DashboardIntent.ktMVI intents (fetch, refresh, mark done, navigate)
composeApp/src/androidMain/.../widget/OterTodayWidget.ktJetpack Glance today’s-actions home-screen widget

Testing

  • Server unit (server/.../service/DashboardServiceTest.kt): overdue habit logic across DAILY / WEEKLY / MONTHLY / YEARLY frequencies, done habits excluded from overdue, mixed-habit overdue counts, and getNextHabit selection 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 dateTime400; per-day chart arrays always length 7; card-status thresholds at the good/medium/bad boundaries.

Troubleshooting

Dashboard returns 400

  • dateTime query param is required and must be dd/MM/yyyy HH:mm. A missing param maps to DASHBOARD_DATETIME_REQUIRED; a malformed one to DASHBOARD_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 the X-Platform header; 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. Ensure dateTime uses the device’s local time.

Home screen not refreshing after completing an item elsewhere

  • The ViewModel only refreshes on DomainEvent.RemoteAgendaStale and DomainEvent.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 null configuration; fall back to DefaultDashboardConfiguration.create(platform). Note this endpoint reads platform from a query param, not the X-Platform header.

Glance widget shows nothing / stale data

  • The widget builds its list from GetTodayActionsUseCase (not /dashboard) and updates only when refreshed. Call refreshOterTodayWidget(context) after task/habit mutations; habit frequency filtering in the widget is simplified relative to the server’s overdue logic.