+ {{ render_breakdown_card('Current Breakdown', current_breakdown, 'No section or overlap breakdown is stored for the current-side estimate.') }}
+ {{ render_breakdown_card('Future Breakdown', future_breakdown, 'No section or overlap breakdown is stored for the future-side estimate.') }}
+
+
+ {{ render_package_comparison_card('Package Comparison', future_breakdown, 'No package comparison data is stored for this estimate.') }}
+
{{ render_titled_key_value_table("Package Resolution", package_rows, "detail-table", "detail-table-wrap", "", "") }}
{% if reestimation_rows %}
@@ -198,40 +236,9 @@
Applicability Summary
{% endif %}
{{ render_json_block("Measurement", measurement_json) }}
{{ render_json_block("Confidence", confidence_json) }}
+ {{ render_json_block("Assumptions", assumptions_json) }}
-
-
-
System Comparison
-
-
-
-
- | Item |
- Current System |
- Future System |
-
-
-
- {% for row in system_comparison_rows %}
-
- | {{ row.label }} |
- {{ row.current }} |
- {{ row.future }} |
-
- {% endfor %}
-
-
-
-
-
-
-
- {{ render_json_block("Assumptions", assumptions_json) }}
-
-
-
- {{ render_breakdown_card('Current Breakdown', current_breakdown, 'No section or overlap breakdown is stored for the current-side estimate.') }}
- {{ render_breakdown_card('Future Breakdown', future_breakdown, 'No section or overlap breakdown is stored for the future-side estimate.') }}
+ {{ render_titled_key_value_table("Meta Information", meta_rows, "detail-table", "detail-table-wrap", "detail-card") }}
{% endblock %}
diff --git a/result_server/templates/usage_report.html b/result_server/templates/usage_report.html
index ad473a3..66e1082 100644
--- a/result_server/templates/usage_report.html
+++ b/result_server/templates/usage_report.html
@@ -42,12 +42,20 @@
.usage-node-hours-table {
border-collapse: separate;
border-spacing: 0;
+ table-layout: fixed;
width: max-content;
font-size: 12px;
}
+ .usage-node-hours-table .usage-node-hours-app-col {
+ width: 220px;
+ }
+ .usage-node-hours-table .usage-node-hours-period-col {
+ width: 78px;
+ }
.usage-node-hours-table th,
.usage-node-hours-table td {
border: 1px solid #ddd;
+ box-sizing: border-box;
padding: 3px 5px;
}
.usage-node-hours-table th {
diff --git a/result_server/tests/test_estimated_detail_template.py b/result_server/tests/test_estimated_detail_template.py
index ce1e461..5e697b7 100644
--- a/result_server/tests/test_estimated_detail_template.py
+++ b/result_server/tests/test_estimated_detail_template.py
@@ -275,10 +275,13 @@ def test_estimated_detail_template_renders_sections(app):
assert "Required Actions" in html
assert "section_package_unsupported:half" in html
assert "overlap_package_unsupported:half" in html
- assert "Candidate estimates" in html
+ assert "Current Breakdown" in html
+ assert "Future Breakdown" in html
+ assert "Package Comparison" in html
+ assert "Candidate Package" in html
assert "Time Ratio" in html
- assert "Bench Time" in html
- assert "Estimated Time" in html
+ assert "Before Scaling" in html
+ assert "After Scaling" in html
assert "gpu_kernel_ensemble_average" in html
assert "gpu_kernel_lightgbm_v10" in html
assert "gpu_kernel_mlp_v15" in html
@@ -289,3 +292,6 @@ def test_estimated_detail_template_renders_sections(app):
assert "O-Memory Throughput [%]" in html
assert "Memory Throughput [%]" in html
assert "GB200" in html
+ assert html.index("System Comparison") < html.index("Current Breakdown")
+ assert html.index("Future Breakdown") < html.index("Package Comparison")
+ assert html.index("Package Comparison") < html.index("Meta Information")
diff --git a/result_server/tests/test_portal_list_templates.py b/result_server/tests/test_portal_list_templates.py
index d4ba58e..f796a0f 100644
--- a/result_server/tests/test_portal_list_templates.py
+++ b/result_server/tests/test_portal_list_templates.py
@@ -343,6 +343,60 @@ def test_usage_report_template_renders_search_box():
assert "UNKNOWN_SYSTEM" in html
+def test_usage_report_node_hours_table_uses_explicit_column_widths():
+ app = build_portal_shell_app(
+ templates_dir=os.path.join(os.path.dirname(__file__), "..", "templates"),
+ )
+ with app.test_request_context("/results/usage"):
+ from flask import render_template
+
+ html = render_template(
+ "usage_report.html",
+ result={
+ "apps": ["genesis"],
+ "systems": ["AI4SS", "Fugaku"],
+ "periods": ["2026-04", "2026-05"],
+ "available_fiscal_years": [2026],
+ "table": {
+ "genesis": {
+ "AI4SS": {"2026-04": 0.0, "2026-05": 0.0},
+ "Fugaku": {"2026-04": 1.23, "2026-05": 0.0},
+ }
+ },
+ "row_totals": {"genesis": {"2026-04": 1.23, "2026-05": 0.0}},
+ "col_totals": {
+ "AI4SS": {"2026-04": 0.0, "2026-05": 0.0},
+ "Fugaku": {"2026-04": 1.23, "2026-05": 0.0},
+ },
+ "grand_totals": {"2026-04": 1.23, "2026-05": 0.0},
+ },
+ filtered_periods=["2026-04", "2026-05"],
+ period_type="monthly",
+ fiscal_year=2026,
+ period_filter="",
+ site_diagnostics={
+ "registered_system_count": 0,
+ "unused_systems": [],
+ "missing_system_info": [],
+ "missing_queue_definitions": [],
+ "application_count": 0,
+ "partial_support": [],
+ "application_directory_count": 0,
+ "apps_missing_files": [],
+ "apps_without_estimate": [],
+ "apps_with_estimate_count": 0,
+ "unknown_listed_systems": [],
+ },
+ coverage_systems=[],
+ app_support_rows=[],
+ result_quality_rollup={"rows": []},
+ )
+
+ assert 'class="usage-node-hours-app-col"' in html
+ assert html.count('class="usage-node-hours-period-col"') == 6
+ assert "table-layout: fixed" in html
+
+
def test_result_compare_template_renders_headline():
app = build_portal_shell_app(
templates_dir=os.path.join(os.path.dirname(__file__), "..", "templates"),
diff --git a/result_server/utils/estimated_detail_view.py b/result_server/utils/estimated_detail_view.py
index 52e4084..8c35970 100644
--- a/result_server/utils/estimated_detail_view.py
+++ b/result_server/utils/estimated_detail_view.py
@@ -24,8 +24,8 @@ def build_estimated_detail_context(result):
"measurement_json": result.get("measurement", {}),
"confidence_json": result.get("confidence", {}),
"assumptions_json": result.get("assumptions", {}),
- "current_breakdown": current.get("fom_breakdown", {}),
- "future_breakdown": future.get("fom_breakdown", {}),
+ "current_breakdown": _build_display_breakdown(current.get("fom_breakdown", {})),
+ "future_breakdown": _build_display_breakdown(future.get("fom_breakdown", {})),
}
@@ -177,6 +177,126 @@ def _append_list_row(rows, label, values):
rows.append({"label": label, "list": values})
+def _build_display_breakdown(breakdown):
+ if not isinstance(breakdown, dict):
+ return {}
+
+ return {
+ **breakdown,
+ "sections": [
+ _build_display_breakdown_item(item)
+ for item in breakdown.get("sections", [])
+ if isinstance(item, dict)
+ ],
+ "overlaps": [
+ _build_display_breakdown_item(item)
+ for item in breakdown.get("overlaps", [])
+ if isinstance(item, dict)
+ ],
+ }
+
+
+def _build_display_breakdown_item(item):
+ before = item.get("bench_time", item.get("time", "N/A"))
+ after = item.get("time", "N/A")
+ ratio = _get_nested(item, ("metrics", "time_ratio_predicted_over_source"))
+ if ratio in (None, "", "N/A", "null", "nan"):
+ ratio = _safe_ratio(after, before)
+
+ display_item = {
+ **item,
+ "before_scaling_display": _format_display_numeric(before),
+ "after_scaling_display": _format_display_numeric(after),
+ "time_ratio_display": _format_display_numeric(ratio),
+ }
+ if "candidate_estimates" in item:
+ display_item["candidate_estimates"] = [
+ _build_display_candidate_estimate(candidate)
+ for candidate in item.get("candidate_estimates", [])
+ if isinstance(candidate, dict)
+ ]
+ metrics = item.get("metrics", {})
+ if isinstance(metrics, dict) and metrics.get("kernel_summaries"):
+ display_item["metrics"] = {
+ **metrics,
+ "kernel_summaries": [
+ _build_display_kernel_summary(kernel)
+ for kernel in metrics.get("kernel_summaries", [])
+ if isinstance(kernel, dict)
+ ],
+ }
+ return display_item
+
+
+def _build_display_candidate_estimate(candidate):
+ ratio = _get_nested(candidate, ("metrics", "time_ratio_predicted_over_source"))
+ return {
+ **candidate,
+ "time_display": _format_display_numeric(candidate.get("time", "N/A")),
+ "time_ratio_display": _format_display_numeric(ratio),
+ }
+
+
+def _build_display_kernel_summary(kernel):
+ return {
+ **kernel,
+ "package_summaries": [
+ _build_display_kernel_package(package)
+ for package in kernel.get("package_summaries", [])
+ if isinstance(package, dict)
+ ],
+ }
+
+
+def _build_display_kernel_package(package):
+ return {
+ **package,
+ "source_time_ns_mean_display": _format_display_numeric(package.get("source_time_ns_mean", "N/A")),
+ "predicted_time_ns_mean_display": _format_display_numeric(package.get("predicted_time_ns_mean", "N/A")),
+ "mean_time_ratio_display": _format_display_numeric(package.get("mean_time_ratio_predicted_over_source", "N/A")),
+ "metric_comparisons": [
+ _build_display_metric_comparison(metric)
+ for metric in package.get("metric_comparisons", [])
+ if isinstance(metric, dict)
+ ],
+ }
+
+
+def _build_display_metric_comparison(metric):
+ return {
+ **metric,
+ "source_value_mean_display": _format_display_numeric(metric.get("source_value_mean", "N/A")),
+ "predicted_value_mean_display": _format_display_numeric(metric.get("predicted_value_mean", "N/A")),
+ "ratio_display": _format_display_numeric(metric.get("ratio_predicted_over_source_mean", "N/A")),
+ }
+
+
+def _get_nested(data, keys):
+ current = data
+ for key in keys:
+ if not isinstance(current, dict):
+ return None
+ current = current.get(key)
+ return current
+
+
+def _safe_ratio(numerator, denominator):
+ try:
+ numerator_value = float(numerator)
+ denominator_value = float(denominator)
+ except (TypeError, ValueError):
+ return "N/A"
+ if denominator_value == 0:
+ return "N/A"
+ return numerator_value / denominator_value
+
+
+def _format_display_numeric(value):
+ if value in (None, "", "N/A", "null", "nan"):
+ return "N/A"
+ return format_numeric_value(value)
+
+
def _count_breakdown_fallbacks(system_data):
breakdown = system_data.get("fom_breakdown", {})
section_count = sum(1 for item in breakdown.get("sections", []) if item.get("fallback_used"))