feat: Activity feed — cron deliveries with output (#420)
* feat: add Activity feed showing cron jobs that produced output - New ActivityItem data model for unified notification feed - SessionInfo extended with preview/started_at fields - GET /api/sessions?source=cron endpoint added with source param - ActivityViewModel fetches cron run sessions, filters for output - ActivityScreen shows chronological feed with status icons - Wired into navigation (AUTOMATE drawer section) - Only shows runs with message_count > 0 (signal-driven) Closes #402 * fix: add @Composable to statusColor helper (MaterialTheme access) * fix: Double type for started_at (backend returns float timestamps) * fix: use GET /api/cron/jobs for Activity feed (no-agent jobs don't create sessions) Root cause: no-agent cron jobs (jellyfin, battery monitor, etc.) run scripts directly and NEVER create DB sessions. The previous approach using GET /api/sessions?source=cron only showed 3 old agent-driven sessions. Fix: use GET /api/cron/jobs which tracks last_run_at + last_status for ALL job types. Filter last_status == "ok" (job ran and delivered). * fix: match codebase patterns for ViewModel, Scaffold, and NavIcon - safeApiCall returns NetworkResult, use when(is) not onSuccess/onFailure - HermesScaffold navigationIcon expects NavIcon.Menu/Back sealed class - Screen composable param must be onOpenDrawer (capital O) * fix: correct ActivityScreen API mismatches - NavIcon.Menu param is onOpen, not onClick - EmptyState param is subtitle, not description - Add missing viewModel import * fix: padding, clickable cards, and navigation on Activity screen - Fix double padding by using listContentPadding + listItemSpacing matching the codebase pattern - Add clickable cards that navigate to CronJobsScreen - Remove unused scroll and spacer workarounds * refactor: replace separate Activity feed with delivered-only filter in Cron tab - Remove ActivityScreen, ActivityViewModel, ActivityItem - Remove ActivityScreen from navigation keys and registry - Add showDeliveredOnly toggle to CronJobsUiState - Add TaskAlt icon button in cron top bar toggles filter - Jobs with lastRunStatus != "ok" hidden when filter is on * feat: clickable cron job cards show run-details dialog - Clicking a job card opens an AlertDialog with last run info - Shows status, last run time, schedule, error (if any), script - RunDetailRow helper composable for label/value pairs * fix: add missing CronJob import in CronJobsScreen * fix: use snake_case field names for CronJob model - lastRunAt -> last_run_at, lastError -> last_error - These are data class constructor params, not computed properties * refactor: remove delivered-only filter toggle, keep card-click dialog - Remove TaskAlt filter icon button from Cron tab top bar - Remove showDeliveredOnly from state and toggleDeliveredFilter() method - Simplify cron list back to direct state.jobs with no filter layer * chore: remove dead activity_empty strings --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
c91651302a
commit
58bd78bd79
3 files changed
+59
-2
No files matched your search
@@ -15,6 +15,9 @@ data class CronJob(
|
|||||||
val schedule_display: String? = null,
|
val schedule_display: String? = null,
|
||||||
val last_status: String? = null,
|
val last_status: String? = null,
|
||||||
val next_run_at: String? = null,
|
val next_run_at: String? = null,
|
||||||
|
val last_run_at: String? = null,
|
||||||
|
val last_error: String? = null,
|
||||||
|
val last_delivery_error: String? = null,
|
||||||
// Full editor fields — all optional with defaults for backward compat
|
// Full editor fields — all optional with defaults for backward compat
|
||||||
val enabled: Boolean? = null,
|
val enabled: Boolean? = null,
|
||||||
val prompt: String? = null,
|
val prompt: String? = null,
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ data class SessionInfo(
|
|||||||
val created_at: String?,
|
val created_at: String?,
|
||||||
val message_count: Int?,
|
val message_count: Int?,
|
||||||
val status: String?,
|
val status: String?,
|
||||||
|
val preview: String? = null,
|
||||||
|
val started_at: Double? = null,
|
||||||
)
|
)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.m57.hermescontrol.ui.cron
|
package com.m57.hermescontrol.ui.cron
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -49,6 +50,7 @@ import androidx.compose.ui.window.DialogProperties
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.data.model.CronJob
|
||||||
import com.m57.hermescontrol.theme.LocalSpacing
|
import com.m57.hermescontrol.theme.LocalSpacing
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
@@ -70,6 +72,7 @@ fun CronJobsScreen(
|
|||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val spacing = LocalSpacing.current
|
val spacing = LocalSpacing.current
|
||||||
|
var selectedJob by remember { mutableStateOf<CronJob?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.loadCronJobs()
|
viewModel.loadCronJobs()
|
||||||
@@ -105,9 +108,9 @@ fun CronJobsScreen(
|
|||||||
|
|
||||||
state.jobs.isEmpty() -> {
|
state.jobs.isEmpty() -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
|
icon = Icons.Filled.Schedule,
|
||||||
title = stringResource(R.string.cron_empty_title),
|
title = stringResource(R.string.cron_empty_title),
|
||||||
subtitle = stringResource(R.string.cron_empty_desc),
|
subtitle = stringResource(R.string.cron_empty_desc),
|
||||||
icon = Icons.Filled.Schedule,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +122,10 @@ fun CronJobsScreen(
|
|||||||
) {
|
) {
|
||||||
items(state.jobs, key = { it.id }) { job ->
|
items(state.jobs, key = { it.id }) { job ->
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = { selectedJob = job }),
|
||||||
colors =
|
colors =
|
||||||
CardDefaults.cardColors(
|
CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
@@ -237,6 +243,28 @@ fun CronJobsScreen(
|
|||||||
onClearToast = { viewModel.clearEditorToast() },
|
onClearToast = { viewModel.clearEditorToast() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Run Details Dialog ──
|
||||||
|
selectedJob?.let { job ->
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { selectedJob = null },
|
||||||
|
title = { Text(job.name, style = MaterialTheme.typography.titleLarge) },
|
||||||
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(spacing.sm)) {
|
||||||
|
RunDetailRow("Status", job.lastRunStatus.ifEmpty { "unknown" })
|
||||||
|
job.last_run_at?.let { if (it.isNotBlank()) RunDetailRow("Last run", it) }
|
||||||
|
RunDetailRow("Schedule", CronExpressionFormatter.cronToHumanReadable(job.scheduleText))
|
||||||
|
if (job.last_error != null && job.last_error.isNotBlank()) {
|
||||||
|
RunDetailRow("Error", job.last_error)
|
||||||
|
}
|
||||||
|
job.script?.let { if (it.isNotBlank()) RunDetailRow("Script", it) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = { selectedJob = null }) { Text("Close") }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -483,3 +511,27 @@ fun CronJobEditorDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RunDetailRow(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.weight(0.35f),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.weight(0.65f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user