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""" +
    +
    Patterns
    + +
    + """ + + +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""" +
    +
    +
    {agent_id.title()}
    + {_format_period_label(period_type)} +
    +
    + + {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""" -
    -
    Patterns
    - -
    - """ - - # Build bullets HTML - bullets_html = "".join([f"
  • {b}
  • " for b in data["narrative_bullets"]]) - - # Build metrics summary - metrics = data["metrics"] - - html_content = f""" -
    -
    -
    {agent_id.title()}
    - {_format_period_label(period_type)} -
    -
    - - -
    -
    -
    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""" -
    -
    Patterns
    - -
    - """ - - # Build bullets HTML - bullets_html = "".join([f"
  • {b}
  • " for b in data["narrative_bullets"]]) - metrics = data["metrics"] - - panel_html = f""" -
    -
    -
    -
    {scorecard.agent_id.title()}
    - {_format_period_label(period_type)} -
    -
    -
      - {bullets_html} -
    - -
    -
    -
    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}
    ' )