diff --git a/src/dashboard/routes/scorecards.py b/src/dashboard/routes/scorecards.py
index c94f17d5..5ed97363 100644
--- a/src/dashboard/routes/scorecards.py
+++ b/src/dashboard/routes/scorecards.py
@@ -10,6 +10,7 @@ from fastapi.responses import HTMLResponse, JSONResponse
from dashboard.services.scorecard_service import (
PeriodType,
+ ScorecardSummary,
generate_all_scorecards,
generate_scorecard,
get_tracked_agents,
@@ -26,6 +27,216 @@ def _format_period_label(period_type: PeriodType) -> str:
return "Daily" if period_type == PeriodType.daily else "Weekly"
+def _parse_period(period: str) -> PeriodType:
+ """Parse period string into PeriodType, defaulting to daily on invalid input.
+
+ Args:
+ period: The period string ('daily' or 'weekly')
+
+ Returns:
+ PeriodType.daily or PeriodType.weekly
+ """
+ try:
+ return PeriodType(period.lower())
+ except ValueError:
+ return PeriodType.daily
+
+
+def _format_token_display(token_net: int) -> str:
+ """Format token net value with +/- prefix for display.
+
+ Args:
+ token_net: The net token value
+
+ Returns:
+ Formatted string with + prefix for positive values
+ """
+ return f"{'+' if token_net > 0 else ''}{token_net}"
+
+
+def _format_token_class(token_net: int) -> str:
+ """Get CSS class for token net value based on sign.
+
+ Args:
+ token_net: The net token value
+
+ Returns:
+ 'text-success' for positive/zero, 'text-danger' for negative
+ """
+ return "text-success" if token_net >= 0 else "text-danger"
+
+
+def _build_patterns_html(patterns: list[str]) -> str:
+ """Build HTML for patterns section if patterns exist.
+
+ Args:
+ patterns: List of pattern strings
+
+ Returns:
+ HTML string for patterns section or empty string
+ """
+ if not patterns:
+ return ""
+
+ patterns_list = "".join([f"
{p}" for p in patterns])
+ return f"""
+
+ """
+
+
+def _build_narrative_html(bullets: list[str]) -> str:
+ """Build HTML for narrative bullets.
+
+ Args:
+ bullets: List of narrative bullet strings
+
+ Returns:
+ HTML string with list items
+ """
+ return "".join([f"{b}" for b in bullets])
+
+
+def _build_metrics_row_html(metrics: dict) -> str:
+ """Build HTML for the metrics summary row.
+
+ Args:
+ metrics: Dictionary with PRs, issues, tests, and token metrics
+
+ Returns:
+ HTML string for the metrics row
+ """
+ prs_opened = metrics["prs_opened"]
+ prs_merged = metrics["prs_merged"]
+ pr_merge_rate = int(metrics["pr_merge_rate"] * 100)
+ issues_touched = metrics["issues_touched"]
+ tests_affected = metrics["tests_affected"]
+ token_net = metrics["token_net"]
+
+ token_class = _format_token_class(token_net)
+ token_display = _format_token_display(token_net)
+
+ return f"""
+
+
+
PRs
+
{prs_opened}/{prs_merged}
+
+ {pr_merge_rate}% merged
+
+
+
+
Issues
+
{issues_touched}
+
+
+
Tests
+
{tests_affected}
+
+
+
Tokens
+
{token_display}
+
+
+ """
+
+
+def _render_scorecard_panel(
+ agent_id: str,
+ period_type: PeriodType,
+ data: dict,
+) -> str:
+ """Render HTML for a single scorecard panel.
+
+ Args:
+ agent_id: The agent ID
+ period_type: Daily or weekly period
+ data: Scorecard data dictionary with metrics, patterns, narrative_bullets
+
+ Returns:
+ HTML string for the scorecard panel
+ """
+ patterns_html = _build_patterns_html(data.get("patterns", []))
+ bullets_html = _build_narrative_html(data.get("narrative_bullets", []))
+ metrics_row = _build_metrics_row_html(data["metrics"])
+
+ return f"""
+
+
+
+
+ {metrics_row}
+ {patterns_html}
+
+
+ """
+
+
+def _render_empty_scorecard(agent_id: str) -> str:
+ """Render HTML for an empty scorecard (no activity).
+
+ Args:
+ agent_id: The agent ID
+
+ Returns:
+ HTML string for the empty scorecard panel
+ """
+ return f"""
+
+
{agent_id.title()}
+
No activity recorded for this period.
+
+ """
+
+
+def _render_error_scorecard(agent_id: str, error: str) -> str:
+ """Render HTML for a scorecard that failed to load.
+
+ Args:
+ agent_id: The agent ID
+ error: Error message string
+
+ Returns:
+ HTML string for the error scorecard panel
+ """
+ return f"""
+
+
{agent_id.title()}
+
Error loading scorecard: {error}
+
+ """
+
+
+def _render_single_panel_wrapper(
+ agent_id: str,
+ period_type: PeriodType,
+ scorecard: ScorecardSummary | None,
+) -> str:
+ """Render a complete scorecard panel with wrapper div for single panel view.
+
+ Args:
+ agent_id: The agent ID
+ period_type: Daily or weekly period
+ scorecard: ScorecardSummary object or None
+
+ Returns:
+ HTML string for the complete panel
+ """
+ if scorecard is None:
+ return _render_empty_scorecard(agent_id)
+
+ return _render_scorecard_panel(agent_id, period_type, scorecard.to_dict())
+
+
@router.get("/api/agents")
async def list_tracked_agents() -> dict[str, list[str]]:
"""Return the list of tracked agent IDs.
@@ -149,99 +360,50 @@ async def agent_scorecard_panel(
Returns:
HTML panel with scorecard content
"""
- try:
- period_type = PeriodType(period.lower())
- except ValueError:
- period_type = PeriodType.daily
+ period_type = _parse_period(period)
try:
scorecard = generate_scorecard(agent_id, period_type)
-
- if scorecard is None:
- return HTMLResponse(
- content=f"""
-
-
{agent_id.title()}
-
No activity recorded for this period.
-
- """,
- status_code=200,
- )
-
- data = scorecard.to_dict()
-
- # Build patterns HTML
- patterns_html = ""
- if data["patterns"]:
- patterns_list = "".join([f"{p}" for p in data["patterns"]])
- patterns_html = f"""
-
- """
-
- # Build bullets HTML
- bullets_html = "".join([f"{b}" for b in data["narrative_bullets"]])
-
- # Build metrics summary
- metrics = data["metrics"]
-
- html_content = f"""
-
-
-
-
-
-
-
-
PRs
-
{metrics["prs_opened"]}/{metrics["prs_merged"]}
-
- {int(metrics["pr_merge_rate"] * 100)}% merged
-
-
-
-
Issues
-
{metrics["issues_touched"]}
-
-
-
Tests
-
{metrics["tests_affected"]}
-
-
-
Tokens
-
= 0 else "text-danger"}">
- {"+" if metrics["token_net"] > 0 else ""}{metrics["token_net"]}
-
-
-
-
- {patterns_html}
-
-
- """
-
+ html_content = _render_single_panel_wrapper(agent_id, period_type, scorecard)
return HTMLResponse(content=html_content)
except Exception as exc:
logger.error("Failed to render scorecard panel for %s: %s", agent_id, exc)
- return HTMLResponse(
- content=f"""
-
-
{agent_id.title()}
-
Error loading scorecard: {str(exc)}
-
- """,
- status_code=200,
+ return HTMLResponse(content=_render_error_scorecard(agent_id, str(exc)))
+
+
+def _render_all_panels_grid(
+ scorecards: list[ScorecardSummary],
+ period_type: PeriodType,
+) -> str:
+ """Render all scorecard panels in a grid layout.
+
+ Args:
+ scorecards: List of scorecard summaries
+ period_type: Daily or weekly period
+
+ Returns:
+ HTML string with all panels in a grid
+ """
+ panels: list[str] = []
+ for scorecard in scorecards:
+ panel_html = _render_scorecard_panel(
+ scorecard.agent_id,
+ period_type,
+ scorecard.to_dict(),
)
+ # Wrap each panel in a grid column
+ wrapped = f'{panel_html}
'
+ panels.append(wrapped)
+
+ return f"""
+
+ {"".join(panels)}
+
+
+ Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")}
+
+ """
@router.get("/all/panels", response_class=HTMLResponse)
@@ -258,96 +420,15 @@ async def all_scorecard_panels(
Returns:
HTML with all scorecard panels
"""
- try:
- period_type = PeriodType(period.lower())
- except ValueError:
- period_type = PeriodType.daily
+ period_type = _parse_period(period)
try:
scorecards = generate_all_scorecards(period_type)
-
- panels: list[str] = []
- for scorecard in scorecards:
- data = scorecard.to_dict()
-
- # Build patterns HTML
- patterns_html = ""
- if data["patterns"]:
- patterns_list = "".join([f"{p}" for p in data["patterns"]])
- patterns_html = f"""
-
- """
-
- # Build bullets HTML
- bullets_html = "".join([f"{b}" for b in data["narrative_bullets"]])
- metrics = data["metrics"]
-
- panel_html = f"""
-
-
-
-
-
-
-
-
-
PRs
-
{metrics["prs_opened"]}/{metrics["prs_merged"]}
-
- {int(metrics["pr_merge_rate"] * 100)}% merged
-
-
-
-
Issues
-
{metrics["issues_touched"]}
-
-
-
Tests
-
{metrics["tests_affected"]}
-
-
-
Tokens
-
= 0 else "text-danger"}">
- {"+" if metrics["token_net"] > 0 else ""}{metrics["token_net"]}
-
-
-
-
- {patterns_html}
-
-
-
- """
- panels.append(panel_html)
-
- html_content = f"""
-
- {"".join(panels)}
-
-
- Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")}
-
- """
-
+ html_content = _render_all_panels_grid(scorecards, period_type)
return HTMLResponse(content=html_content)
except Exception as exc:
logger.error("Failed to render all scorecard panels: %s", exc)
return HTMLResponse(
- content=f"""
-
- Error loading scorecards: {str(exc)}
-
- """,
- status_code=200,
+ content=f'Error loading scorecards: {exc}
'
)