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:
2026-06-02 09:54:42 +03:00
parent 4fc3356312
commit 8219540ade
7 changed files with 307 additions and 135 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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,
)
)

View File

@@ -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 = (