From fda70ecf9ebeaa8205ce680dae5906391a166fa7 Mon Sep 17 00:00:00 2001 From: Draco-Lunaris-Echo Date: Thu, 4 Jun 2026 12:49:53 -0500 Subject: [PATCH] feat(jobs): add host_names to job list API and UI (#24) * feat(jobs): add host_names to job list API and UI Closes #23 * fix(jobs): add mut for host_names merge loop --- crates/pm-core/src/models.rs | 4 +++ crates/pm-web/src/routes/jobs.rs | 44 +++++++++++++++++++++++++++++++- docs/REST_API.md | 6 ++++- frontend/src/pages/JobsPage.tsx | 10 ++++++-- frontend/src/types/index.ts | 1 + 5 files changed, 61 insertions(+), 4 deletions(-) mode change 100755 => 100644 crates/pm-web/src/routes/jobs.rs diff --git a/crates/pm-core/src/models.rs b/crates/pm-core/src/models.rs index 2eb9c05..81a2bfd 100644 --- a/crates/pm-core/src/models.rs +++ b/crates/pm-core/src/models.rs @@ -505,6 +505,10 @@ pub struct PatchJobSummary { pub status: JobStatus, pub immediate: bool, pub host_count: i64, + /// Display names of hosts targeted by this job (falls back to fqdn). + #[serde(default)] + #[sqlx(skip)] + pub host_names: Vec, pub succeeded_count: i64, pub failed_count: i64, pub notes: String, diff --git a/crates/pm-web/src/routes/jobs.rs b/crates/pm-web/src/routes/jobs.rs old mode 100755 new mode 100644 index a24e4b6..4e9e370 --- a/crates/pm-web/src/routes/jobs.rs +++ b/crates/pm-web/src/routes/jobs.rs @@ -20,6 +20,7 @@ use pm_core::{ }; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; +use std::collections::HashMap; use uuid::Uuid; use crate::AppState; @@ -52,6 +53,13 @@ struct JobListResponse { offset: i64, } +/// Helper struct for the host_names aggregation query. +#[derive(Debug, sqlx::FromRow)] +struct JobHostNames { + id: Uuid, + host_names: Vec, +} + /// Per-host row included in `GET /api/v1/jobs/{id}` response. #[derive(Debug, Clone, Serialize, sqlx::FromRow)] struct JobHostRow { @@ -229,7 +237,7 @@ async fn list_jobs( let limit = q.limit.unwrap_or(50).min(200); let offset = q.offset.unwrap_or(0); - let jobs: Vec = if auth.role.is_admin() { + let mut jobs: Vec = if auth.role.is_admin() { // Admins see every job. sqlx::query_as( r#" @@ -298,6 +306,40 @@ async fn list_jobs( err(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", "Database error") })?; + // Fetch host names for all jobs in this page. + let job_ids: Vec = jobs.iter().map(|j| j.id).collect(); + let host_names_rows: Vec = if job_ids.is_empty() { + Vec::new() + } else { + sqlx::query_as( + r#" + SELECT pjh.job_id AS id, + array_agg(COALESCE(NULLIF(h.display_name, ''), h.fqdn) + ORDER BY h.fqdn) AS host_names + FROM patch_job_hosts pjh + JOIN hosts h ON h.id = pjh.host_id + WHERE pjh.job_id = ANY($1) + GROUP BY pjh.job_id + "#, + ) + .bind(&job_ids) + .fetch_all(&state.db) + .await + .unwrap_or_else(|e| { + tracing::warn!(error = %e, "list_jobs: host_names query failed, using empty defaults"); + Vec::new() + }) + }; + + // Merge host_names into summaries. + let mut host_names_map: HashMap> = host_names_rows + .into_iter() + .map(|r| (r.id, r.host_names)) + .collect(); + for job in &mut jobs { + job.host_names = host_names_map.remove(&job.id).unwrap_or_default(); + } + // Total count for pagination metadata. let total: i64 = if auth.role.is_admin() { sqlx::query_scalar("SELECT COUNT(*) FROM patch_jobs") diff --git a/docs/REST_API.md b/docs/REST_API.md index 69adb39..a21425f 100644 --- a/docs/REST_API.md +++ b/docs/REST_API.md @@ -70,12 +70,16 @@ Security: JWT Bearer Token (except Public Endpoints) ## 7. Jobs & Patch Deployment | Method | Endpoint | Description | |--------|----------|-------------| -| GET | `/jobs` | List patch jobs | +| GET | `/jobs` | List patch jobs (includes `host_names` per job) | | POST | `/jobs` | Create new patch job | | GET | `/jobs/{id}` | Get job status/details | | POST | `/jobs/{id}/cancel` | Cancel running job | | POST | `/jobs/{id}/rollback` | Rollback completed job | +### GET /jobs Response Fields +Each job summary object includes: +- `host_names`: Array of display names for hosts targeted by this job. Falls back to `fqdn` when `display_name` is empty. Single-host jobs show one name; multi-host jobs show all names sorted alphabetically. + ## 8. Maintenance Windows *Scoped to host.* | Method | Endpoint | Description | diff --git a/frontend/src/pages/JobsPage.tsx b/frontend/src/pages/JobsPage.tsx index 74570cc..760f56f 100644 --- a/frontend/src/pages/JobsPage.tsx +++ b/frontend/src/pages/JobsPage.tsx @@ -194,7 +194,13 @@ function JobRow({ - {job.host_count} + + {job.host_names.length === 1 + ? job.host_names[0] + : job.host_names.length > 1 + ? {job.host_names[0]} +{job.host_names.length - 1} + : '—'} + {job.succeeded_count} @@ -512,7 +518,7 @@ export default function JobsPage() { Created Kind Status - Hosts + Hosts Succeeded Failed Schedule diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index d15a81e..0360095 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -144,6 +144,7 @@ export interface PatchJobSummary { status: JobStatus immediate: boolean host_count: number + host_names: string[] succeeded_count: number failed_count: number notes: string