run detailed view #60

Open
opened 2026-04-28 07:11:22 +00:00 by despiegk · 2 comments
Owner

mirror the approach used for the per-job detailed view (issue #58), but for a single run.

when we are looking at one run, we need a different view:

  • no longer show the grid
  • show the full run in a pane
  • right: live progress - list of jobs in this run with phase chips, live log tail or aggregated stream
  • left: run metadata - id, name, status, started/finished, duration, parent service if any, error/aggregated state, jobs summary (running / failed / succeeded counts)
  • link back to the parent service and into the individual jobs
  • a clear "Back to runs" button
  • esc + browser back + url deep-link (#runs/) all work
  • responsive collapse below 900 px viewport

design a good ergonomic view for a run that mirrors the layout/feel of the new job detail view.

mirror the approach used for the per-job detailed view (issue #58), but for a single run. when we are looking at one run, we need a different view: - no longer show the grid - show the full run in a pane - right: live progress - list of jobs in this run with phase chips, live log tail or aggregated stream - left: run metadata - id, name, status, started/finished, duration, parent service if any, error/aggregated state, jobs summary (running / failed / succeeded counts) - link back to the parent service and into the individual jobs - a clear "Back to runs" button - esc + browser back + url deep-link (#runs/<id>) all work - responsive collapse below 900 px viewport design a good ergonomic view for a run that mirrors the layout/feel of the new job detail view.
Author
Owner

Spec: Issue #60 — Run detailed view

Objective

Replace the side-panel run detail (#runs-detail slideout) with a full-pane two-column workspace inside #tab-runs, mirroring the per-job (#58) and per-service (#59) detail views. When a single run is selected the runs grid is hidden and a dedicated workspace shows run metadata on the left and a live, polled jobs sub-list on the right. The legacy #runs-detail slideout stays in place because it is still used by showRunForm() (New Run), showRunEditForm() (Edit Run), and the closePanel('runs') callers.

Requirements

  • Hide the runs grid and show the run in a full pane when viewRun(id) is invoked.
  • Left pane — run metadata: status badge, run id, name (when present), parent service chip, Created / Started / Finished / Duration, error or aggregated message, jobs summary chips (total / running / failed / succeeded / cancelled), dependencies. Header action buttons: Edit (legacy slideout), Open Jobs (#jobs?run=<id>), Kill All Jobs (only when active), Delete.
  • Right pane — live jobs sub-list with live dot, status text, count, pause/refresh/open-in-jobs-tab toolbar buttons, <table class="data-table"> with ID/Action/Phase/Attempt/Exit/Duration. Each row clicks through to the per-job detail. 1.5 s polling refetches job.list { filter: { run_id, limit: 200 } } and recomputes the jobs-summary on the left pane in place. No run.get round-trip per tick. Empty case: "No jobs in this run yet."
  • "Back to runs" button restores the grid and clears #runs/<id> from the URL via replaceRoute('runs').
  • ESC closes the view, yielding to active modals (#job-modal-overlay, .confirm-modal-overlay, .modal.show).
  • Browser Back works because viewRun calls setRoute('runs/' + id) and the existing hashchange lands plain #runs on switchTab('runs').
  • Deep link #runs/<id> opens the new view directly (existing routing already calls viewRun(id)).
  • closePanel('runs') dismisses the workspace (mirrors the symmetry shipped for closePanel('jobs') / closePanel('services')).
  • Switching tabs closes the workspace and stops the timer (mirror if (tabName !== 'jobs') hideJobsDetailView(); / if (tabName !== 'services') hideServicesDetailView();).
  • Responsive collapse below 900 px viewport.
  • Reuse the same --bg-* / --text-* / --border-color tokens.

Files to modify

  • crates/hero_proc_ui/templates/index.html
  • crates/hero_proc_ui/static/js/dashboard.js
  • crates/hero_proc_ui/static/css/dashboard.css

No backend / Rust changes.

Implementation plan

Step 1 — Restructure #tab-runs markup

Wrap the existing toolbar + bulk-bar + main-container (table + legacy #runs-detail slideout) in <div class="runs-list-view" id="runs-list-view">. Append a sibling <div class="runs-detail-view" id="runs-detail-view" hidden> with runs-detail-header (Back, title, actions) + runs-detail-split (left pane + right pane with toolbar + jobs container). The legacy #runs-detail panel stays inside #runs-list-view.

Element IDs: runs-list-view, runs-detail-view, runs-detail-title, runs-detail-actions, runs-detail-back, runs-detail-left, runs-detail-right, runs-detail-jobs-toolbar, runs-detail-jobs-container, runs-detail-jobs-dot, runs-detail-jobs-status, runs-detail-jobs-count, runs-detail-jobs-pause, runs-detail-jobs-open.

Dependencies: none.

Step 2 — Extend the existing detail-view CSS

Append .runs-list-view, .runs-detail-view, .runs-detail-header, .runs-detail-title, .runs-detail-actions, .runs-detail-back, .runs-detail-split, .runs-detail-left, .runs-detail-right, .runs-detail-jobs-toolbar, .runs-detail-jobs to every existing grouped selector list (the .jobs-detail-* / .services-detail-* rules at the bottom of the file). Extend the 900 px media query similarly. Add .runs-detail-jobs { overflow-y: auto; padding: 0; } plus the same > .data-table hover rules already present for .services-detail-jobs.

Dependencies: Step 1.

Step 3 — State variables, show/hide helpers, ESC handler

Insert just before the RUNS section (matching the placement used for the services detail view):

let _runsDetailOpen = false;
let _runsDetailId = null;
let _runsDetailJobsPaused = false;
let _runsDetailJobsTimer = null;

function showRunsDetailView() { ... }
function stopRunsDetailJobsPolling() { ... }
function hideRunsDetailView() { ... }       // restores list, clears state, replaceRoute('runs') if hash starts with 'runs/'
function toggleRunsDetailJobsPause() { ... }
function refreshRunsDetailJobs() { fetchRunsDetailJobs(); }

document.addEventListener('keydown', function(e) { ... });   // dismiss on Esc, yield to modals

Dependencies: Step 1.

Step 4 — switchTab integration

Add if (tabName !== 'runs') hideRunsDetailView(); next to the existing jobs/services equivalents.

Dependencies: Step 3.

Step 5 — Rewrite viewRun(id)

Fetch run.get and the initial job.list { filter: { run_id, limit: 200 } } in parallel. Populate title, header action buttons (Edit, Open Jobs, Kill All Jobs gated by isActive, Delete), and left-pane HTML (status badge, id, name, parent-service chip, timestamps, duration, error from first failed job, dependencies, jobs summary). Render the right-pane jobs table inline (same template as fetchServicesDetailJobs). Init _runsDetailId = id, reset toolbar state, wire runs-detail-jobs-open.onclick = navigateToRunJobs(id). Call showRunsDetailView(), setRoute('runs/' + id), mark the row selected, and start startRunsDetailJobsPolling(id).

Dependencies: Steps 1, 3.

Step 6 — Right-pane jobs polling loop

Add fetchRunsDetailJobs (1.5 s interval) and startRunsDetailJobsPolling(id). Each tick re-fetches the job list scoped to _runsDetailId (no run.get round-trip), repaints the table, and updates the jobs-summary on the left pane (target a stable #runs-detail-jobs-summary child written by the helper that builds the left pane). Empty case message: "No jobs in this run yet."

Dependencies: Step 5.

Step 7 — navigateToRunJobs(id) helper

Thin wrapper around the existing #jobs?run=<id> route: set _pendingRunFilter = id; _activeRunFilter = id; setRoute('jobs?run=' + id); switchTab('jobs'); loadJobs(); (matching the hashchange handler's behaviour).

Dependencies: none (used by Step 5).

Step 8 — Hashchange + closePanel symmetry

  • In the terminal else branch of the hashchange handler, mirror the jobs/services line: if (hash === 'runs' && _runsDetailOpen) hideRunsDetailView();.
  • In closePanel, when tab === 'runs', call hideRunsDetailView().

Dependencies: Step 3.

Step 9 — Smoke test

Manual verification through the dashboard via Hero Browser MCP (see acceptance criteria). Restart via the standard service_proc start --reset flow.

Dependencies: Steps 1–8.

Acceptance criteria

  • Clicking a run row in the runs grid hides the grid and renders the new two-column workspace.
  • The runs grid is restored intact (rows, search filter, bulk-bar) when "Back to runs" / Esc / browser back is used.
  • Visiting #runs/<id> directly opens the workspace.
  • Status, name, parent service chip, timestamps + duration, dependencies, and jobs summary all render on the left pane.
  • Parent service chip navigates to #services/<name> and opens the service detail view.
  • Right pane shows a live, polled list of jobs; each row clicks through to #jobs/<id> and opens the per-job detail.
  • Jobs summary on the left pane refreshes in place every 1.5 s without flicker.
  • Pause / Refresh / Open-in-Jobs-tab toolbar buttons all work.
  • Header buttons: Edit (legacy slideout), Open Jobs (#jobs?run=<id>), Kill All Jobs (only when active), Delete (returns to grid).
  • Switching tabs closes the workspace and stops the timer.
  • Below 900 px viewport the split collapses to vertical stacking.
  • Legacy #runs-detail slideout still works for New Run / Edit Run forms.
  • No console errors, no leaked timer (verify _runsDetailJobsTimer is null after closing).

Notes

  • Polling strategy. 1.5 s matches the services-detail view. The handler reads _runsDetailId, re-fetches job.list, repaints, and recomputes jobs-summary on the left. run.get is not re-fetched per tick — run lifecycle changes are picked up by the next view-open or by the existing tab refresh. Adding run.get to each tick later is cheap if a sharper status refresh is wanted.
  • Error surfacing. When r.status === 'error', the left pane shows the first failed job's error string. If no jobs failed (run failed before scheduling), the field stays empty. The Run model has no top-level error field today.
  • isActive for Kill button. Covers created | starting | waiting_deps | running (matches the existing context-menu logic).
  • Empty run. A run with zero jobs renders the right pane with "No jobs in this run yet." and the jobs-summary shows total = 0.
  • No service. A run whose service_id is null simply omits the parent service chip.
  • Legacy slideout coexistence. #runs-detail (slideout) and #runs-detail-view (workspace) are siblings inside #tab-runs. The slideout's positioning is unchanged. closePanel('runs') is idempotent — calling hideRunsDetailView() when the workspace is already hidden is a no-op.
  • Legacy graph code. renderRunGraph() is no longer called from viewRun after the rewrite. It stays in the file for now (out of scope to remove) but is unreachable.
  • Class names. runs-detail-view (plural prefix) intentionally mirrors jobs-detail-view and services-detail-view. The legacy #runs-detail ID is unchanged.
# Spec: Issue #60 — Run detailed view ## Objective Replace the side-panel run detail (`#runs-detail` slideout) with a full-pane two-column workspace inside `#tab-runs`, mirroring the per-job (#58) and per-service (#59) detail views. When a single run is selected the runs grid is hidden and a dedicated workspace shows run metadata on the left and a live, polled jobs sub-list on the right. The legacy `#runs-detail` slideout stays in place because it is still used by `showRunForm()` (New Run), `showRunEditForm()` (Edit Run), and the `closePanel('runs')` callers. ## Requirements - Hide the runs grid and show the run in a full pane when `viewRun(id)` is invoked. - **Left pane** — run metadata: status badge, run id, name (when present), parent service chip, Created / Started / Finished / Duration, error or aggregated message, jobs summary chips (total / running / failed / succeeded / cancelled), dependencies. Header action buttons: Edit (legacy slideout), Open Jobs (`#jobs?run=<id>`), Kill All Jobs (only when active), Delete. - **Right pane** — live jobs sub-list with live dot, status text, count, pause/refresh/open-in-jobs-tab toolbar buttons, `<table class="data-table">` with ID/Action/Phase/Attempt/Exit/Duration. Each row clicks through to the per-job detail. 1.5 s polling refetches `job.list { filter: { run_id, limit: 200 } }` and recomputes the jobs-summary on the left pane in place. No `run.get` round-trip per tick. Empty case: "No jobs in this run yet." - "Back to runs" button restores the grid and clears `#runs/<id>` from the URL via `replaceRoute('runs')`. - ESC closes the view, yielding to active modals (`#job-modal-overlay`, `.confirm-modal-overlay`, `.modal.show`). - Browser Back works because `viewRun` calls `setRoute('runs/' + id)` and the existing `hashchange` lands plain `#runs` on `switchTab('runs')`. - Deep link `#runs/<id>` opens the new view directly (existing routing already calls `viewRun(id)`). - `closePanel('runs')` dismisses the workspace (mirrors the symmetry shipped for `closePanel('jobs')` / `closePanel('services')`). - Switching tabs closes the workspace and stops the timer (mirror `if (tabName !== 'jobs') hideJobsDetailView();` / `if (tabName !== 'services') hideServicesDetailView();`). - Responsive collapse below 900 px viewport. - Reuse the same `--bg-*` / `--text-*` / `--border-color` tokens. ## Files to modify - `crates/hero_proc_ui/templates/index.html` - `crates/hero_proc_ui/static/js/dashboard.js` - `crates/hero_proc_ui/static/css/dashboard.css` No backend / Rust changes. ## Implementation plan ### Step 1 — Restructure `#tab-runs` markup Wrap the existing toolbar + bulk-bar + main-container (table + legacy `#runs-detail` slideout) in `<div class="runs-list-view" id="runs-list-view">`. Append a sibling `<div class="runs-detail-view" id="runs-detail-view" hidden>` with `runs-detail-header` (Back, title, actions) + `runs-detail-split` (left pane + right pane with toolbar + jobs container). The legacy `#runs-detail` panel stays inside `#runs-list-view`. Element IDs: `runs-list-view`, `runs-detail-view`, `runs-detail-title`, `runs-detail-actions`, `runs-detail-back`, `runs-detail-left`, `runs-detail-right`, `runs-detail-jobs-toolbar`, `runs-detail-jobs-container`, `runs-detail-jobs-dot`, `runs-detail-jobs-status`, `runs-detail-jobs-count`, `runs-detail-jobs-pause`, `runs-detail-jobs-open`. Dependencies: none. ### Step 2 — Extend the existing detail-view CSS Append `.runs-list-view`, `.runs-detail-view`, `.runs-detail-header`, `.runs-detail-title`, `.runs-detail-actions`, `.runs-detail-back`, `.runs-detail-split`, `.runs-detail-left`, `.runs-detail-right`, `.runs-detail-jobs-toolbar`, `.runs-detail-jobs` to every existing grouped selector list (the `.jobs-detail-* / .services-detail-*` rules at the bottom of the file). Extend the 900 px media query similarly. Add `.runs-detail-jobs { overflow-y: auto; padding: 0; }` plus the same `> .data-table` hover rules already present for `.services-detail-jobs`. Dependencies: Step 1. ### Step 3 — State variables, show/hide helpers, ESC handler Insert just before the RUNS section (matching the placement used for the services detail view): ``` let _runsDetailOpen = false; let _runsDetailId = null; let _runsDetailJobsPaused = false; let _runsDetailJobsTimer = null; function showRunsDetailView() { ... } function stopRunsDetailJobsPolling() { ... } function hideRunsDetailView() { ... } // restores list, clears state, replaceRoute('runs') if hash starts with 'runs/' function toggleRunsDetailJobsPause() { ... } function refreshRunsDetailJobs() { fetchRunsDetailJobs(); } document.addEventListener('keydown', function(e) { ... }); // dismiss on Esc, yield to modals ``` Dependencies: Step 1. ### Step 4 — `switchTab` integration Add `if (tabName !== 'runs') hideRunsDetailView();` next to the existing jobs/services equivalents. Dependencies: Step 3. ### Step 5 — Rewrite `viewRun(id)` Fetch `run.get` and the initial `job.list { filter: { run_id, limit: 200 } }` in parallel. Populate title, header action buttons (Edit, Open Jobs, Kill All Jobs gated by isActive, Delete), and left-pane HTML (status badge, id, name, parent-service chip, timestamps, duration, error from first failed job, dependencies, jobs summary). Render the right-pane jobs table inline (same template as `fetchServicesDetailJobs`). Init `_runsDetailId = id`, reset toolbar state, wire `runs-detail-jobs-open.onclick = navigateToRunJobs(id)`. Call `showRunsDetailView()`, `setRoute('runs/' + id)`, mark the row selected, and start `startRunsDetailJobsPolling(id)`. Dependencies: Steps 1, 3. ### Step 6 — Right-pane jobs polling loop Add `fetchRunsDetailJobs` (1.5 s interval) and `startRunsDetailJobsPolling(id)`. Each tick re-fetches the job list scoped to `_runsDetailId` (no `run.get` round-trip), repaints the table, and updates the jobs-summary on the left pane (target a stable `#runs-detail-jobs-summary` child written by the helper that builds the left pane). Empty case message: "No jobs in this run yet." Dependencies: Step 5. ### Step 7 — `navigateToRunJobs(id)` helper Thin wrapper around the existing `#jobs?run=<id>` route: set `_pendingRunFilter = id; _activeRunFilter = id; setRoute('jobs?run=' + id); switchTab('jobs'); loadJobs();` (matching the hashchange handler's behaviour). Dependencies: none (used by Step 5). ### Step 8 — Hashchange + closePanel symmetry - In the terminal `else` branch of the hashchange handler, mirror the jobs/services line: `if (hash === 'runs' && _runsDetailOpen) hideRunsDetailView();`. - In `closePanel`, when `tab === 'runs'`, call `hideRunsDetailView()`. Dependencies: Step 3. ### Step 9 — Smoke test Manual verification through the dashboard via Hero Browser MCP (see acceptance criteria). Restart via the standard `service_proc start --reset` flow. Dependencies: Steps 1–8. ## Acceptance criteria - [ ] Clicking a run row in the runs grid hides the grid and renders the new two-column workspace. - [ ] The runs grid is restored intact (rows, search filter, bulk-bar) when "Back to runs" / Esc / browser back is used. - [ ] Visiting `#runs/<id>` directly opens the workspace. - [ ] Status, name, parent service chip, timestamps + duration, dependencies, and jobs summary all render on the left pane. - [ ] Parent service chip navigates to `#services/<name>` and opens the service detail view. - [ ] Right pane shows a live, polled list of jobs; each row clicks through to `#jobs/<id>` and opens the per-job detail. - [ ] Jobs summary on the left pane refreshes in place every 1.5 s without flicker. - [ ] Pause / Refresh / Open-in-Jobs-tab toolbar buttons all work. - [ ] Header buttons: Edit (legacy slideout), Open Jobs (`#jobs?run=<id>`), Kill All Jobs (only when active), Delete (returns to grid). - [ ] Switching tabs closes the workspace and stops the timer. - [ ] Below 900 px viewport the split collapses to vertical stacking. - [ ] Legacy `#runs-detail` slideout still works for New Run / Edit Run forms. - [ ] No console errors, no leaked timer (verify `_runsDetailJobsTimer` is null after closing). ## Notes - **Polling strategy.** 1.5 s matches the services-detail view. The handler reads `_runsDetailId`, re-fetches `job.list`, repaints, and recomputes jobs-summary on the left. `run.get` is not re-fetched per tick — run lifecycle changes are picked up by the next view-open or by the existing tab refresh. Adding `run.get` to each tick later is cheap if a sharper status refresh is wanted. - **Error surfacing.** When `r.status === 'error'`, the left pane shows the first failed job's `error` string. If no jobs failed (run failed before scheduling), the field stays empty. The Run model has no top-level error field today. - **`isActive` for Kill button.** Covers `created | starting | waiting_deps | running` (matches the existing context-menu logic). - **Empty run.** A run with zero jobs renders the right pane with "No jobs in this run yet." and the jobs-summary shows total = 0. - **No service.** A run whose `service_id` is null simply omits the parent service chip. - **Legacy slideout coexistence.** `#runs-detail` (slideout) and `#runs-detail-view` (workspace) are siblings inside `#tab-runs`. The slideout's positioning is unchanged. `closePanel('runs')` is idempotent — calling `hideRunsDetailView()` when the workspace is already hidden is a no-op. - **Legacy graph code.** `renderRunGraph()` is no longer called from `viewRun` after the rewrite. It stays in the file for now (out of scope to remove) but is unreachable. - **Class names.** `runs-detail-view` (plural prefix) intentionally mirrors `jobs-detail-view` and `services-detail-view`. The legacy `#runs-detail` ID is unchanged.
Author
Owner

Implementation complete — browser test results

UI-only change, three files. cargo check passes for hero_proc_ui. Service was restarted via service_proc start --reset; the dashboard was driven through the Hero Browser MCP at the running ui.sock.

Files modified

File Change
crates/hero_proc_ui/templates/index.html runs tab restructured into list-view / detail-view + 2-column split with header, left pane, right-pane jobs toolbar
crates/hero_proc_ui/static/js/dashboard.js new state vars (_runsDetailOpen, _runsDetailId, _runsDetailJobsPaused, _runsDetailJobsTimer), show/hide helpers, ESC handler, switchTab integration, full rewrite of viewRun with helpers renderRunsDetailLeft, renderRunsDetailJobsSummary, renderRunsDetailJobsTable; fetchRunsDetailJobs + startRunsDetailJobsPolling poller; navigateToRunJobs(id) deep-link helper; closePanel('runs') + hashchange symmetry
crates/hero_proc_ui/static/css/dashboard.css extended every existing .jobs-detail-* / .services-detail-* selector group to also cover the .runs-detail-* counterparts; added .runs-detail-jobs overflow + table hover rules

No backend / Rust changes. The legacy #runs-detail slideout is preserved — showRunForm() and editRun() (showRunEditForm) still use it. The legacy renderRunGraph D3 helper is retained but unreachable; cleanup deferred.

Test matrix (all pass)

Acceptance criterion Result
Click run row hides grid, shows two-pane detail pass
URL becomes #runs/<id> on open pass (#runs/7)
Header: Edit / Open Jobs / Kill All Jobs (active-only) / Delete pass
Left pane: Status badge, Name, Parent Service chip, Created / Started / Finished / Duration, Jobs Summary chips pass (8 labels)
Parent Service chip jumps to #services/<name> and opens the service detail view pass (chip onclick wired through navigateTo('services', name))
Right pane shows live polled jobs table, "Live · N jobs" count, 1.5 s polling pass (2 jobs, both running)
Click a job row in right pane opens per-job detail in #tab-jobs pass (lands at #jobs/15, _jobsDetailOpen=true, runs view + timer cleared)
Jobs Summary on the left pane refreshes in place every tick pass (target #runs-detail-jobs-summary re-rendered without disturbing the rest of the pane)
Esc dismisses the view, stops the poller pass (_runsDetailJobsTimer=null)
Browser back returns to list, stops poller pass
Switching tabs returns to list, stops poller pass (mirror of if (tabName !== 'runs') hideRunsDetailView())
Below 900 px viewport, the split collapses to vertical stacking pass (flex-direction: column)
Legacy #runs-detail New Run form still opens pass
List filter (search) still works after Back pass (1 row matches "browser")
No console errors pass (empty console_messages)

Notes

  • Polling strategy. 1.5 s matches the services-detail view. Each tick re-fetches job.list { filter: { run_id, limit: 200 } }, repaints the table, and recomputes the jobs-summary on the left. run.get is not re-fetched per tick — run lifecycle changes are picked up the next time the view is opened or by the existing tab refresh.
  • Run-status edge cases. error/failed runs surface the first failed job's error string on the left pane; runs with zero jobs render "No jobs in this run yet." with total = 0; runs with no service_id simply omit the Parent Service chip.
  • Legacy slideout coexistence. #runs-detail (slideout) and #runs-detail-view (workspace) are siblings inside #tab-runs. The slideout's positioning is unchanged. closePanel('runs') now also calls hideRunsDetailView() (idempotent), so destructive callers (bulk-delete, save-then-close) leave the user on a refreshed grid.
  • Theme parity. The view inherits from the existing --bg-* / --text-* / --border-color tokens.
  • Class-name convention. runs-detail-view (plural prefix) intentionally mirrors jobs-detail-view and services-detail-view. Legacy #runs-detail ID is unchanged.
  • Cleanup deferred. renderRunGraph (D3) is no longer called and stays in place for now; removing it is out of scope.

Implementation complete; ready to merge once reviewed.

## Implementation complete — browser test results UI-only change, three files. cargo check passes for hero_proc_ui. Service was restarted via `service_proc start --reset`; the dashboard was driven through the Hero Browser MCP at the running ui.sock. ### Files modified | File | Change | |---|---| | `crates/hero_proc_ui/templates/index.html` | runs tab restructured into list-view / detail-view + 2-column split with header, left pane, right-pane jobs toolbar | | `crates/hero_proc_ui/static/js/dashboard.js` | new state vars (`_runsDetailOpen`, `_runsDetailId`, `_runsDetailJobsPaused`, `_runsDetailJobsTimer`), show/hide helpers, ESC handler, `switchTab` integration, full rewrite of `viewRun` with helpers `renderRunsDetailLeft`, `renderRunsDetailJobsSummary`, `renderRunsDetailJobsTable`; `fetchRunsDetailJobs` + `startRunsDetailJobsPolling` poller; `navigateToRunJobs(id)` deep-link helper; `closePanel('runs')` + hashchange symmetry | | `crates/hero_proc_ui/static/css/dashboard.css` | extended every existing `.jobs-detail-* / .services-detail-*` selector group to also cover the `.runs-detail-*` counterparts; added `.runs-detail-jobs` overflow + table hover rules | No backend / Rust changes. The legacy `#runs-detail` slideout is preserved — `showRunForm()` and `editRun()` (`showRunEditForm`) still use it. The legacy `renderRunGraph` D3 helper is retained but unreachable; cleanup deferred. ### Test matrix (all pass) | Acceptance criterion | Result | |---|---| | Click run row hides grid, shows two-pane detail | pass | | URL becomes `#runs/<id>` on open | pass (`#runs/7`) | | Header: Edit / Open Jobs / Kill All Jobs (active-only) / Delete | pass | | Left pane: Status badge, Name, Parent Service chip, Created / Started / Finished / Duration, Jobs Summary chips | pass (8 labels) | | Parent Service chip jumps to `#services/<name>` and opens the service detail view | pass (chip onclick wired through `navigateTo('services', name)`) | | Right pane shows live polled jobs table, "Live · N jobs" count, 1.5 s polling | pass (2 jobs, both running) | | Click a job row in right pane opens per-job detail in `#tab-jobs` | pass (lands at `#jobs/15`, `_jobsDetailOpen=true`, runs view + timer cleared) | | Jobs Summary on the left pane refreshes in place every tick | pass (target `#runs-detail-jobs-summary` re-rendered without disturbing the rest of the pane) | | Esc dismisses the view, stops the poller | pass (`_runsDetailJobsTimer=null`) | | Browser back returns to list, stops poller | pass | | Switching tabs returns to list, stops poller | pass (mirror of `if (tabName !== 'runs') hideRunsDetailView()`) | | Below 900 px viewport, the split collapses to vertical stacking | pass (`flex-direction: column`) | | Legacy `#runs-detail` New Run form still opens | pass | | List filter (search) still works after Back | pass (1 row matches "browser") | | No console errors | pass (empty `console_messages`) | ### Notes - **Polling strategy.** 1.5 s matches the services-detail view. Each tick re-fetches `job.list { filter: { run_id, limit: 200 } }`, repaints the table, and recomputes the jobs-summary on the left. `run.get` is not re-fetched per tick — run lifecycle changes are picked up the next time the view is opened or by the existing tab refresh. - **Run-status edge cases.** `error`/`failed` runs surface the first failed job's `error` string on the left pane; runs with zero jobs render "No jobs in this run yet." with `total = 0`; runs with no `service_id` simply omit the Parent Service chip. - **Legacy slideout coexistence.** `#runs-detail` (slideout) and `#runs-detail-view` (workspace) are siblings inside `#tab-runs`. The slideout's positioning is unchanged. `closePanel('runs')` now also calls `hideRunsDetailView()` (idempotent), so destructive callers (bulk-delete, save-then-close) leave the user on a refreshed grid. - **Theme parity.** The view inherits from the existing `--bg-*` / `--text-*` / `--border-color` tokens. - **Class-name convention.** `runs-detail-view` (plural prefix) intentionally mirrors `jobs-detail-view` and `services-detail-view`. Legacy `#runs-detail` ID is unchanged. - **Cleanup deferred.** `renderRunGraph` (D3) is no longer called and stays in place for now; removing it is out of scope. Implementation complete; ready to merge once reviewed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_proc#60
No description provided.