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 last_status: 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
|
||||
val enabled: Boolean? = null,
|
||||
val prompt: String? = null,
|
||||
|
||||
@@ -13,4 +13,6 @@ data class SessionInfo(
|
||||
val created_at: String?,
|
||||
val message_count: Int?,
|
||||
val status: String?,
|
||||
val preview: String? = null,
|
||||
val started_at: Double? = null,
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.m57.hermescontrol.ui.cron
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -49,6 +50,7 @@ import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.model.CronJob
|
||||
import com.m57.hermescontrol.theme.LocalSpacing
|
||||
import com.m57.hermescontrol.ui.common.EmptyState
|
||||
import com.m57.hermescontrol.ui.common.ErrorState
|
||||
@@ -70,6 +72,7 @@ fun CronJobsScreen(
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val spacing = LocalSpacing.current
|
||||
var selectedJob by remember { mutableStateOf<CronJob?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.loadCronJobs()
|
||||
@@ -105,9 +108,9 @@ fun CronJobsScreen(
|
||||
|
||||
state.jobs.isEmpty() -> {
|
||||
EmptyState(
|
||||
icon = Icons.Filled.Schedule,
|
||||
title = stringResource(R.string.cron_empty_title),
|
||||
subtitle = stringResource(R.string.cron_empty_desc),
|
||||
icon = Icons.Filled.Schedule,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -119,7 +122,10 @@ fun CronJobsScreen(
|
||||
) {
|
||||
items(state.jobs, key = { it.id }) { job ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = { selectedJob = job }),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
@@ -237,6 +243,28 @@ fun CronJobsScreen(
|
||||
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
|
||||
@@ -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