@@ -195,6 +195,7 @@ def _prepare_dashboard_data(
195195 runs : List [Dict [str , Any ]],
196196 task_results : List [Dict [str , Any ]],
197197 default_runs : int ,
198+ selected_workflow : Optional [str ] = None ,
198199) -> Dict [str , Any ]:
199200 """Prepare all data for the dashboard template.
200201
@@ -330,16 +331,38 @@ def _prepare_dashboard_data(
330331 }
331332 )
332333
333- # Taskfile metadata from the most recent run
334- latest = runs [0 ] if runs else {}
334+ # Workflow metadata from the most recent run per workflow.
335+ workflow_meta : Dict [str , Dict [str , Any ]] = {}
336+ for run in enriched_runs :
337+ wf = run .get ("workflow" ) or ""
338+ if wf not in workflow_meta :
339+ workflow_meta [wf ] = {
340+ "taskfile_name" : run .get ("taskfile_name" , "" ),
341+ "taskfile_description" : run .get ("taskfile_description" , "" ),
342+ "taskfile_author" : run .get ("taskfile_author" , "" ),
343+ "run_count" : 0 ,
344+ }
345+ workflow_meta [wf ]["run_count" ] += 1
346+
347+ workflows = list (workflow_meta .keys ())
348+ effective_workflow = selected_workflow if selected_workflow in workflow_meta else ""
349+ if not effective_workflow and workflows :
350+ effective_workflow = workflows [0 ]
351+
352+ workflow_run_count = workflow_meta .get (effective_workflow , {}).get ("run_count" , 0 )
353+ effective_default_runs = min (default_runs , workflow_run_count )
335354
336355 return {
337- "workflow" : latest .get ("workflow" , "" ),
338- "taskfile_name" : latest .get ("taskfile_name" , "" ),
339- "taskfile_description" : latest .get ("taskfile_description" , "" ),
340- "taskfile_author" : latest .get ("taskfile_author" , "" ),
356+ "workflow" : effective_workflow ,
357+ "workflows" : workflows ,
358+ "workflow_meta" : workflow_meta ,
359+ "taskfile_name" : workflow_meta .get (effective_workflow , {}).get ("taskfile_name" , "" ),
360+ "taskfile_description" : workflow_meta .get (effective_workflow , {}).get (
361+ "taskfile_description" , ""
362+ ),
363+ "taskfile_author" : workflow_meta .get (effective_workflow , {}).get ("taskfile_author" , "" ),
341364 "generated_at" : datetime .now ().strftime ("%Y-%m-%d %H:%M:%S" ),
342- "default_runs" : min ( default_runs , len ( runs )) ,
365+ "default_runs" : effective_default_runs ,
343366 "total_runs" : len (runs ),
344367 "runs" : enriched_runs ,
345368 "task_results" : slim_task_results ,
@@ -367,7 +390,12 @@ def generate_dashboard(
367390 :param dag_url: Optional relative URL to the DAG visualization HTML
368391 :return: Path to the generated HTML file
369392 """
370- data = _prepare_dashboard_data (runs , task_results , default_runs )
393+ data = _prepare_dashboard_data (
394+ runs ,
395+ task_results ,
396+ default_runs ,
397+ selected_workflow = workflow ,
398+ )
371399 data_json = json .dumps (data , default = str )
372400
373401 # Build conditional DAG link HTML
@@ -623,6 +651,10 @@ def generate_dashboard(
623651 </div>
624652 <div class="header-right">
625653 { dag_link_html }
654+ <div class="run-selector">
655+ <label for="workflowSelect">Workflow</label>
656+ <select id="workflowSelect" onchange="onWorkflowChange()"></select>
657+ </div>
626658 <div class="run-selector">
627659 <label for="runCount">Show last</label>
628660 <select id="runCount" onchange="updateDashboard()"></select>
@@ -758,33 +790,78 @@ def generate_dashboard(
758790 ];
759791
760792 function init() {{
761- // Build run count selector options
793+ // Build workflow selector first, then run selector for the selected workflow.
794+ const workflowSel = document.getElementById('workflowSelect');
795+ DATA.workflows.forEach(wf => {{
796+ const opt = document.createElement('option');
797+ const wfRunCount = (DATA.workflow_meta[wf] && DATA.workflow_meta[wf].run_count) || 0;
798+ opt.value = wf;
799+ opt.textContent = `${{wf}} (${{wfRunCount}})`;
800+ workflowSel.appendChild(opt);
801+ }});
802+ if (DATA.workflow && DATA.workflows.includes(DATA.workflow)) {{
803+ workflowSel.value = DATA.workflow;
804+ }}
805+
806+ rebuildRunCountSelector(DATA.default_runs);
807+
808+ document.getElementById('generatedAt').textContent = 'Generated: ' + DATA.generated_at;
809+ updateDashboard();
810+ }}
811+
812+ function getSelectedWorkflow() {{
813+ return document.getElementById('workflowSelect').value;
814+ }}
815+
816+ function getRunsForWorkflow(workflow) {{
817+ return DATA.runs.filter(r => (r.workflow || '') === workflow);
818+ }}
819+
820+ function rebuildRunCountSelector(preferredCount) {{
762821 const sel = document.getElementById('runCount');
763- const total = DATA.total_runs;
822+ const total = getRunsForWorkflow(getSelectedWorkflow()).length;
823+ sel.innerHTML = '';
824+
825+ if (total === 0) {{
826+ const opt = document.createElement('option');
827+ opt.value = 0;
828+ opt.textContent = '0';
829+ sel.appendChild(opt);
830+ sel.value = 0;
831+ return;
832+ }}
833+
764834 const options = [];
765835 for (let i = 1; i <= Math.min(total, 5); i++) options.push(i);
766836 if (total > 5) options.push(5);
767837 if (total > 10) options.push(10);
768838 if (total > 20) options.push(20);
769839 if (total > 5) options.push(total);
770- // Deduplicate and sort
840+
771841 const unique = [...new Set(options)].sort((a, b) => a - b);
772842 unique.forEach(n => {{
773843 const opt = document.createElement('option');
774844 opt.value = n;
775845 opt.textContent = n === total ? `All (${{total}})` : n;
776846 sel.appendChild(opt);
777847 }});
778- sel.value = DATA.default_runs;
779848
780- document.getElementById('generatedAt').textContent = 'Generated: ' + DATA.generated_at;
849+ const defaultCount = preferredCount || DATA.default_runs || unique[0];
850+ const boundedCount = Math.min(defaultCount, total);
851+ sel.value = String(boundedCount);
852+ }}
853+
854+ function onWorkflowChange() {{
855+ rebuildRunCountSelector();
781856 updateDashboard();
782857 }}
783858
784859 function getSelectedRuns() {{
785860 const n = parseInt(document.getElementById('runCount').value);
861+ const workflowRuns = getRunsForWorkflow(getSelectedWorkflow());
862+ if (!n) return [];
786863 // Runs are ordered DESC (newest first); take last N and reverse for chronological display
787- return DATA.runs .slice(0, n).reverse();
864+ return workflowRuns .slice(0, n).reverse();
788865 }}
789866
790867 function statusBadge(status) {{
@@ -815,25 +892,29 @@ def generate_dashboard(
815892
816893 function updateDashboard() {{
817894 destroyCharts();
895+ const selectedWorkflow = getSelectedWorkflow();
896+ const selectedWorkflowMeta = DATA.workflow_meta[selectedWorkflow] || {{}};
897+ const workflowTotalRuns = getRunsForWorkflow(selectedWorkflow).length;
818898 const runs = getSelectedRuns();
819899
820900 // Header subtitle
821901 document.getElementById('headerSubtitle').textContent =
822- `${{DATA.workflow }}${{DATA .taskfile_name ? ' — ' + DATA .taskfile_name : ''}}`;
902+ `${{selectedWorkflow }}${{selectedWorkflowMeta .taskfile_name ? ' - ' + selectedWorkflowMeta .taskfile_name : ''}}`;
823903
824904 // Metadata
825905 const meta = document.getElementById('metadata');
826906 let metaHtml = '';
827- if (DATA.taskfile_description) metaHtml += `<span>${{DATA.taskfile_description}}</span>`;
828- if (DATA.taskfile_author) metaHtml += `<span>Author: <strong>${{DATA.taskfile_author}}</strong></span>`;
907+ if (selectedWorkflowMeta.taskfile_description) metaHtml += `<span>${{selectedWorkflowMeta.taskfile_description}}</span>`;
908+ if (selectedWorkflowMeta.taskfile_author) metaHtml += `<span>Author: <strong>${{selectedWorkflowMeta.taskfile_author}}</strong></span>`;
909+ metaHtml += `<span>Workflow runs: <strong>${{workflowTotalRuns}}</strong></span>`;
829910 metaHtml += `<span>Total runs in DB: <strong>${{DATA.total_runs}}</strong></span>`;
830911 if (runs.length > 0) {{
831912 metaHtml += `<span>Date range: <strong>${{formatDate(runs[0].start_time)}} — ${{formatDate(runs[runs.length-1].start_time)}}</strong></span>`;
832913 }}
833914 meta.innerHTML = metaHtml;
834915
835916 // Summary cards
836- updateSummaryCards(runs);
917+ updateSummaryCards(runs, workflowTotalRuns );
837918
838919 // Charts
839920 renderRunDurationChart(runs);
@@ -850,7 +931,7 @@ def generate_dashboard(
850931 updateConfigDetails(runs);
851932 }}
852933
853- function updateSummaryCards(runs) {{
934+ function updateSummaryCards(runs, workflowTotalRuns ) {{
854935 const cards = document.getElementById('summaryCards');
855936 if (runs.length === 0) {{ cards.innerHTML = '<div class="no-data">No runs available</div>'; return; }}
856937
@@ -866,7 +947,7 @@ def generate_dashboard(
866947 <div class="card">
867948 <div class="card-label">Runs Shown</div>
868949 <div class="card-value">${{runs.length}}</div>
869- <div class="card-sub">of ${{DATA.total_runs }} total </div>
950+ <div class="card-sub">of ${{workflowTotalRuns }} in workflow </div>
870951 </div>
871952 <div class="card">
872953 <div class="card-label">Tasks per Run</div>
0 commit comments