feat(backend): add v2 LLM validation fields to health service
- DashboardHealthItem schema: add run_id, policy_id, execution_path, issues_count, timings, token_usage, screenshot_paths, chunk_count, dashboard_name fields for v2 LLM validation reporting. - HealthService: populate v2 fields from validation run records. - ValidationService: update to support v2 validation run fields. - validation_tasks route: minor fix for v2 field handling. - tailwind.config.js: refactor design tokens — add success/warning/info palettes, surface/border/text hierarchy, semantic action colors. - Agent configs: update svelte-coder and semantics-svelte for .svelte.ts runes and Screen Model patterns.
This commit is contained in:
@@ -308,6 +308,7 @@ async def list_all_runs(
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
status_filter: str | None = Query(None, alias="status", description="Filter by status"),
|
||||
environment_id: str | None = Query(None, description="Filter by environment"),
|
||||
dashboard_id: str | None = Query(None, description="Filter by dashboard ID"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
_=Depends(has_permission("validation.run", "VIEW")),
|
||||
service: ValidationTaskService = Depends(_get_task_service),
|
||||
@@ -319,7 +320,8 @@ async def list_all_runs(
|
||||
)
|
||||
try:
|
||||
total, raw_items = service.list_all_runs(
|
||||
status=status_filter, environment_id=environment_id, page=page, page_size=page_size,
|
||||
status=status_filter, environment_id=environment_id, dashboard_id=dashboard_id,
|
||||
page=page, page_size=page_size,
|
||||
)
|
||||
return RecordListResponse(items=raw_items, total=total, page=page, page_size=page_size)
|
||||
except ValueError as e:
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# #region HealthSchemas [C:3] [TYPE Module] [SEMANTICS pydantic, health, schema, dashboard, dashboard-health-item]
|
||||
# @BRIEF Pydantic schemas for dashboard health summary.
|
||||
# @BRIEF Pydantic schemas for dashboard health summary — includes v2 LLM validation fields.
|
||||
# @LAYER Domain
|
||||
# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# #region DashboardHealthItem [TYPE Class]
|
||||
# @BRIEF Represents the latest health status of a single dashboard.
|
||||
# @BRIEF Represents the latest health status of a single dashboard, with v2 LLM validation data.
|
||||
class DashboardHealthItem(BaseModel):
|
||||
record_id: str | None = None
|
||||
dashboard_id: str
|
||||
@@ -19,7 +20,17 @@ class DashboardHealthItem(BaseModel):
|
||||
status: str = Field(..., pattern="^(PASS|WARN|FAIL|UNKNOWN)$")
|
||||
last_check: datetime
|
||||
task_id: str | None = None
|
||||
run_id: str | None = None
|
||||
policy_id: str | None = None
|
||||
summary: str | None = None
|
||||
# v2 LLM validation fields
|
||||
execution_path: str | None = None # "screenshot" or "text_only"
|
||||
issues_count: int = 0
|
||||
timings: dict[str, Any] | None = None
|
||||
token_usage: dict[str, Any] | None = None
|
||||
screenshot_paths: list[str] | None = None
|
||||
chunk_count: int | None = None
|
||||
dashboard_name: str | None = None # human-readable alias for dashboard_title
|
||||
|
||||
|
||||
# #endregion DashboardHealthItem
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
# @RELATION DEPENDS_ON -> [TaskManager]
|
||||
|
||||
from datetime import UTC
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from typing import Any, cast
|
||||
@@ -260,7 +261,24 @@ class HealthService:
|
||||
if timestamp is not None and timestamp.tzinfo is None:
|
||||
timestamp = timestamp.replace(tzinfo=UTC)
|
||||
|
||||
# ── v2 fields ──────────────────────────────────────────────
|
||||
execution_path = str(record.execution_path) if record.execution_path else None
|
||||
issues_count = len(record.issues) if record.issues else 0
|
||||
timings = dict(record.timings) if record.timings else None
|
||||
token_usage = dict(record.token_usage) if record.token_usage else None
|
||||
screenshot_paths = list(record.screenshot_paths) if record.screenshot_paths else None
|
||||
|
||||
# Extract chunk_count from raw_response (stored as JSON in result_payload)
|
||||
chunk_count: int | None = None
|
||||
if record.raw_response:
|
||||
try:
|
||||
raw = json.loads(record.raw_response)
|
||||
chunk_count = int(raw["chunk_count"]) if raw.get("chunk_count") else None
|
||||
except (json.JSONDecodeError, KeyError, TypeError, ValueError):
|
||||
pass
|
||||
|
||||
meta = self._resolve_dashboard_meta(dashboard_id, resolved_environment_id)
|
||||
dashboard_name = meta.get("title") or meta.get("slug") or dashboard_id
|
||||
items.append(
|
||||
DashboardHealthItem(
|
||||
record_id=record_id,
|
||||
@@ -271,7 +289,16 @@ class HealthService:
|
||||
status=status,
|
||||
last_check=timestamp,
|
||||
task_id=task_id,
|
||||
run_id=str(record.run_id) if record.run_id else None,
|
||||
policy_id=str(record.policy_id) if record.policy_id else None,
|
||||
summary=summary,
|
||||
execution_path=execution_path,
|
||||
issues_count=issues_count,
|
||||
timings=timings,
|
||||
token_usage=token_usage,
|
||||
screenshot_paths=screenshot_paths,
|
||||
chunk_count=chunk_count,
|
||||
dashboard_name=dashboard_name,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -141,6 +141,30 @@ class ValidationTaskService:
|
||||
|
||||
# #endregion _parse_superset_link
|
||||
|
||||
# #region _resolve_dashboard_title [C:2] [TYPE Function]
|
||||
# @BRIEF Fetch a dashboard title from the Superset API by its numeric ID.
|
||||
# @SIDE_EFFECT Makes an HTTP GET to Superset REST API via the config-manager.
|
||||
# @RETURNS The dashboard title string, or None if it cannot be resolved.
|
||||
def _resolve_dashboard_title(self, environment_id: str, dashboard_id: str) -> str | None:
|
||||
try:
|
||||
from ..core.superset_client import SupersetClient
|
||||
|
||||
if not self.config_manager:
|
||||
return None
|
||||
env = self.config_manager.get_environment(environment_id)
|
||||
if not env:
|
||||
return None
|
||||
client = SupersetClient(env)
|
||||
resp = client.get_dashboard(dashboard_id)
|
||||
# Superset API returns { "result": { "dashboard_title": "..." } }
|
||||
result = resp.get("result", resp)
|
||||
title = result.get("dashboard_title") or result.get("title")
|
||||
return str(title) if title else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# #endregion _resolve_dashboard_title
|
||||
|
||||
# #region _resolve_sources [C:3] [TYPE Function]
|
||||
# @RATIONALE Resolves SourceInput list into ValidationSource ORM objects. Dashboard_url sources
|
||||
# are parsed via SupersetContextExtractor for validation and context extraction.
|
||||
@@ -173,6 +197,10 @@ class ValidationTaskService:
|
||||
elif s.type == "dashboard_id":
|
||||
resolved_dashboard_id = s.value
|
||||
|
||||
# Resolve dashboard title from Superset API
|
||||
if resolved_dashboard_id and not title:
|
||||
title = self._resolve_dashboard_title(environment_id, resolved_dashboard_id)
|
||||
|
||||
sources.append(ValidationSource(
|
||||
policy_id=policy_id,
|
||||
type=s.type,
|
||||
@@ -185,12 +213,14 @@ class ValidationTaskService:
|
||||
# Fallback: dashboard_ids (v1 backward compat)
|
||||
elif dashboard_ids:
|
||||
for did in dashboard_ids:
|
||||
title = self._resolve_dashboard_title(environment_id, str(did))
|
||||
sources.append(ValidationSource(
|
||||
policy_id=policy_id,
|
||||
type="dashboard_id",
|
||||
value=str(did),
|
||||
status="valid",
|
||||
resolved_dashboard_id=str(did),
|
||||
title=title,
|
||||
))
|
||||
|
||||
return sources
|
||||
@@ -301,20 +331,23 @@ class ValidationTaskService:
|
||||
def _record_to_dict(self, record: ValidationRecord) -> dict:
|
||||
# Try to resolve dashboard title from sources
|
||||
dashboard_title = record.dashboard_id
|
||||
if record.policy_id:
|
||||
try:
|
||||
source = (
|
||||
self.db.query(ValidationSource)
|
||||
.filter(
|
||||
ValidationSource.policy_id == record.policy_id,
|
||||
ValidationSource.resolved_dashboard_id == record.dashboard_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if source and source.title:
|
||||
dashboard_title = source.title
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
source = (
|
||||
self.db.query(ValidationSource)
|
||||
.filter(ValidationSource.resolved_dashboard_id == record.dashboard_id)
|
||||
.first()
|
||||
)
|
||||
if source and source.title:
|
||||
dashboard_title = source.title
|
||||
elif source and record.environment_id:
|
||||
# Fallback: resolve from Superset API and cache back to source
|
||||
resolved = self._resolve_dashboard_title(record.environment_id, record.dashboard_id)
|
||||
if resolved:
|
||||
dashboard_title = resolved
|
||||
source.title = resolved
|
||||
self.db.flush()
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"id": record.id,
|
||||
"run_id": record.run_id,
|
||||
@@ -681,6 +714,7 @@ class ValidationTaskService:
|
||||
self,
|
||||
status: str | None = None,
|
||||
environment_id: str | None = None,
|
||||
dashboard_id: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> tuple[int, list[dict]]:
|
||||
@@ -690,6 +724,8 @@ class ValidationTaskService:
|
||||
query = query.filter(ValidationRecord.status == status.upper())
|
||||
if environment_id:
|
||||
query = query.filter(ValidationRecord.environment_id == environment_id)
|
||||
if dashboard_id:
|
||||
query = query.filter(ValidationRecord.dashboard_id == dashboard_id)
|
||||
|
||||
total = query.count()
|
||||
records = (
|
||||
|
||||
Reference in New Issue
Block a user