From 431330231f4754d0e2b15c36e8e14140535c2629 Mon Sep 17 00:00:00 2001 From: busya Date: Sun, 31 May 2026 22:32:14 +0300 Subject: [PATCH] cleanup(validation): remove v1 validation routes and frontend pages Delete deprecated code that has been fully replaced by v2: - backend: validation.py routes, validation_run_service.py - frontend: validation.js API module, validation/ pages --- backend/src/api/routes/validation.py | 279 ------- .../src/services/validation_run_service.py | 141 ---- frontend/src/lib/api/validation.js | 102 --- frontend/src/routes/validation/+page.svelte | 399 ---------- .../src/routes/validation/[id]/+page.svelte | 731 ------------------ .../routes/validation/history/+page.svelte | 405 ---------- 6 files changed, 2057 deletions(-) delete mode 100644 backend/src/api/routes/validation.py delete mode 100644 backend/src/services/validation_run_service.py delete mode 100644 frontend/src/lib/api/validation.js delete mode 100644 frontend/src/routes/validation/+page.svelte delete mode 100644 frontend/src/routes/validation/[id]/+page.svelte delete mode 100644 frontend/src/routes/validation/history/+page.svelte diff --git a/backend/src/api/routes/validation.py b/backend/src/api/routes/validation.py deleted file mode 100644 index 848e703f..00000000 --- a/backend/src/api/routes/validation.py +++ /dev/null @@ -1,279 +0,0 @@ -# #region ValidationRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, validation, api, task, run] -# @BRIEF API routes for validation task management and run history. -# @LAYER API -# @RELATION DEPENDS_ON -> [ValidationTaskService] -# @RELATION DEPENDS_ON -> [ValidationRunService] -# @RELATION DEPENDS_ON -> [TaskManager] - -from datetime import date - -from fastapi import APIRouter, Depends, HTTPException, Query, status -from sqlalchemy.orm import Session - -from ...core.config_manager import ConfigManager -from ...core.database import get_db -from ...core.logger import logger -from ...core.scheduler import SchedulerService -from ...core.task_manager import TaskManager -from ...dependencies import get_config_manager, get_current_user, get_scheduler_service, get_task_manager, has_permission -from ...schemas.auth import User -from ...schemas.validation import ( - TriggerRunResponse, - ValidationRunDetailResponse, - ValidationRunListResponse, - ValidationTaskCreate, - ValidationTaskListResponse, - ValidationTaskResponse, - ValidationTaskUpdate, -) -from ...services.validation_run_service import ValidationRunService -from ...services.validation_service import ValidationTaskService - -# #region router [C:1] [TYPE Global] -router = APIRouter(tags=["Validation"]) -# #endregion router - - -# #region _get_task_service [C:1] [TYPE Function] -def _get_task_service( - db: Session = Depends(get_db), - config_manager: ConfigManager = Depends(get_config_manager), - current_user: User = Depends(get_current_user), -) -> ValidationTaskService: - return ValidationTaskService(db=db, config_manager=config_manager, username=current_user.username) -# #endregion _get_task_service - - -# #region _get_run_service [C:1] [TYPE Function] -def _get_run_service( - db: Session = Depends(get_db), -) -> ValidationRunService: - return ValidationRunService(db=db) -# #endregion _get_run_service - - -# ============================================================ -# Tasks CRUD -# ============================================================ - - -# #region list_tasks [C:4] [TYPE Function] -# @RATIONALE Route handler delegates to service with permission check — keeps API layer thin and testable; auth gate happens before any business logic. -# @REJECTED Business logic in route handler rejected — would duplicate validation logic and make permission checks harder to audit. -@router.get("/tasks", response_model=ValidationTaskListResponse) -async def list_tasks( - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), - is_active: bool | None = Query(None), - environment_id: str | None = Query(None), - current_user: User = Depends(get_current_user), - _=Depends(has_permission("validation.task", "VIEW")), - service: ValidationTaskService = Depends(_get_task_service), -): - """List all validation tasks with pagination and optional filters.""" - logger.reason(f"list_tasks — User: {current_user.username}", extra={"src": "validation_routes"}) - try: - total, items = service.list_tasks(is_active=is_active, environment_id=environment_id, page=page, page_size=page_size) - return ValidationTaskListResponse(items=items, total=total, page=page, page_size=page_size) - except ValueError as e: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) -# #endregion list_tasks - - -# #region create_task [C:4] [TYPE Function] -# @RATIONALE Returns 201 with created resource — follows REST conventions for POST resource creation; signals to clients that a new entity was created. -# @REJECTED Returning 200 with wrapped response rejected — non-standard for POST create; 201 Created signals the correct semantics to HTTP clients and code generators. -@router.post("/tasks", response_model=ValidationTaskResponse, status_code=status.HTTP_201_CREATED) -async def create_task( - payload: ValidationTaskCreate, - current_user: User = Depends(get_current_user), - _=Depends(has_permission("validation.task", "CREATE")), - service: ValidationTaskService = Depends(_get_task_service), - scheduler: SchedulerService = Depends(get_scheduler_service), -): - """Create a new validation task with provider and environment validation.""" - logger.reason(f"create_task — User: {current_user.username}, name: {payload.name}", extra={"src": "validation_routes"}) - try: - result = service.create_task(payload) - scheduler.reload_validation_policy(result.id) - return result - except ValueError as e: - raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e)) -# #endregion create_task - - -# #region get_task [C:4] [TYPE Function] -# @RATIONALE Joins task detail with recent runs in a single endpoint — reduces frontend API calls from 2 to 1, improving page load performance. -# @REJECTED Separate /tasks/{id}/runs endpoint for recent runs rejected — would require frontend to make 2 sequential API calls, doubling load time on the config page. -@router.get("/tasks/{task_id}", response_model=dict) -async def get_task( - task_id: str, - current_user: User = Depends(get_current_user), - _=Depends(has_permission("validation.task", "VIEW")), - task_service: ValidationTaskService = Depends(_get_task_service), - run_service: ValidationRunService = Depends(_get_run_service), -): - """Get a validation task with its recent runs.""" - logger.reason(f"get_task — Task: {task_id}, User: {current_user.username}", extra={"src": "validation_routes"}) - try: - task = task_service.get_task(task_id) - # Find recent runs for this specific task via task_id - recent = run_service.list_runs( - task_id=task_id, - dashboard_id=task.dashboard_ids[0] if task.dashboard_ids else None, - environment_id=task.environment_id, - page=1, - page_size=10, - ) - return { - "task": task.model_dump(), - "recent_runs": [r.model_dump() for r in recent[1]], - } - except ValueError as e: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) -# #endregion get_task - - -# #region update_task [C:4] [TYPE Function] -# @RATIONALE PUT semantics for full resource update — consistent with existing API patterns; Pydantic's exclude_unset handles partial updates while keeping PUT as the endpoint verb. -# @REJECTED PATCH semantics rejected — using PUT with exclude_unset provides the same partial-update flexibility without introducing a second HTTP method for the same resource. -@router.put("/tasks/{task_id}", response_model=ValidationTaskResponse) -async def update_task( - task_id: str, - payload: ValidationTaskUpdate, - current_user: User = Depends(get_current_user), - _=Depends(has_permission("validation.task", "EDIT")), - service: ValidationTaskService = Depends(_get_task_service), - scheduler: SchedulerService = Depends(get_scheduler_service), -): - """Update a validation task.""" - logger.reason(f"update_task — Task: {task_id}, User: {current_user.username}", extra={"src": "validation_routes"}) - try: - result = service.update_task(task_id, payload) - scheduler.reload_validation_policy(result.id) - return result - except ValueError as e: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) -# #endregion update_task - - -# #region delete_task [C:4] [TYPE Function] -# @RATIONALE DELETE returns 204 No Content per REST convention — minimizes response payload for delete operations; supports optional delete_runs query param for cascade control. -# @REJECTED Returning deleted resource in response body rejected — unnecessary data transfer; 204 with no body is the REST standard for delete operations. -@router.delete("/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_task( - task_id: str, - delete_runs: bool = Query(False, description="Also delete associated validation runs"), - current_user: User = Depends(get_current_user), - _=Depends(has_permission("validation.task", "DELETE")), - service: ValidationTaskService = Depends(_get_task_service), - scheduler: SchedulerService = Depends(get_scheduler_service), -): - """Delete a validation task.""" - logger.reason(f"delete_task — Task: {task_id}, User: {current_user.username}", extra={"src": "validation_routes"}) - try: - service.delete_task(task_id, delete_runs=delete_runs) - scheduler.remove_validation_job(task_id) - except ValueError as e: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) -# #endregion delete_task - - -# ============================================================ -# Run — trigger immediate validation -# ============================================================ - - -# #region trigger_run [C:4] [TYPE Function] -# @RATIONALE Returns spawned task ID so frontend can poll for completion — enables progress tracking without WebSocket. Async spawn is essential since LLM validation can take 30+ seconds. -# @REJECTED Blocking until run completes rejected — LLM-based validation is slow (30-120s); synchronous wait would timeout HTTP requests and create terrible UX. -@router.post("/tasks/{task_id}/run", response_model=TriggerRunResponse) -async def trigger_run( - task_id: str, - current_user: User = Depends(get_current_user), - _=Depends(has_permission("validation.task", "EXECUTE")), - service: ValidationTaskService = Depends(_get_task_service), - task_manager: TaskManager = Depends(get_task_manager), -): - """Trigger immediate validation for a task.""" - logger.reason(f"trigger_run — Task: {task_id}, User: {current_user.username}", extra={"src": "validation_routes"}) - try: - spawned_task_id = await service.trigger_run(task_id, task_manager) - return TriggerRunResponse(task_id=task_id, spawned_task_id=spawned_task_id, status="PENDING", message="Validation task spawned") - except ValueError as e: - raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e)) -# #endregion trigger_run - - -# ============================================================ -# Runs — history listing and detail -# ============================================================ - - -# #region list_runs [C:4] [TYPE Function] -# @RATIONALE Exposes 6 query parameters for cross-task run filtering — matches the run service's filter capabilities exactly, enabling flexible history exploration. -# @REJECTED Fixed filter set rejected — would limit the history page's debugging use cases; users need to filter by task, status, dashboard, environment, and date range. -@router.get("/runs", response_model=ValidationRunListResponse) -async def list_runs( - task_id: str | None = Query(None, description="Filter by spawned task ID"), - dashboard_id: str | None = Query(None, description="Filter by dashboard ID"), - environment_id: str | None = Query(None, description="Filter by environment ID"), - status: str | None = Query(None, description="Filter by status: PASS, WARN, FAIL, UNKNOWN"), - date_from: date | None = Query(None, description="Start date (ISO format)"), - date_to: date | None = Query(None, description="End date (ISO format)"), - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), - current_user: User = Depends(get_current_user), - _=Depends(has_permission("validation.run", "VIEW")), - service: ValidationRunService = Depends(_get_run_service), -): - """List validation runs with cross-task filtering and pagination.""" - logger.reason(f"list_runs — User: {current_user.username}", extra={"src": "validation_routes"}) - try: - total, items = service.list_runs( - task_id=task_id, dashboard_id=dashboard_id, environment_id=environment_id, - status=status, date_from=date_from, date_to=date_to, page=page, page_size=page_size, - ) - return ValidationRunListResponse(items=items, total=total, page=page, page_size=page_size) - except ValueError as e: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) -# #endregion list_runs - - -# #region get_run_detail [C:4] [TYPE Function] -# @RATIONALE Returns full run detail including issues list and raw response for the report view — single endpoint serves the complete detail page without multiple fetches. -# @REJECTED Paginated issues endpoint rejected — issues per run are typically < 20 items; pagination overhead is unnecessary for such small collections. -@router.get("/runs/{run_id}", response_model=ValidationRunDetailResponse) -async def get_run_detail( - run_id: str, - current_user: User = Depends(get_current_user), - _=Depends(has_permission("validation.run", "VIEW")), - service: ValidationRunService = Depends(_get_run_service), -): - """Get detailed validation run info including issues and raw response.""" - logger.reason(f"get_run_detail — Run: {run_id}, User: {current_user.username}", extra={"src": "validation_routes"}) - try: - return service.get_run_detail(run_id) - except ValueError as e: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) -# #endregion get_run_detail - - -# #region delete_run [C:3] [TYPE Function] -# @RATIONALE Returns 404 when run not found — explicit error handling rather than silent success; helps frontend distinguish between successful delete and missing resource. -# @REJECTED Silent success on missing run rejected — would mask bugs in the UI that reference already-deleted runs; explicit 404 helps debugging. -@router.delete("/runs/{run_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_run( - run_id: str, - current_user: User = Depends(get_current_user), - _=Depends(has_permission("validation.run", "DELETE")), - service: ValidationRunService = Depends(_get_run_service), -): - """Delete a validation run record.""" - logger.reason(f"delete_run — Run: {run_id}, User: {current_user.username}", extra={"src": "validation_routes"}) - if not service.delete_run(run_id): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Run not found") -# #endregion delete_run - - -# #endregion ValidationRoutes diff --git a/backend/src/services/validation_run_service.py b/backend/src/services/validation_run_service.py deleted file mode 100644 index aa58f06b..00000000 --- a/backend/src/services/validation_run_service.py +++ /dev/null @@ -1,141 +0,0 @@ -# #region ValidationRunService [C:3] [TYPE Module] [SEMANTICS validation, run, service, sqlalchemy] -# @BRIEF Business logic for validation run queries: list, detail, delete. -# @LAYER Service - -from datetime import date - -from sqlalchemy.orm import Session - -from ..core.logger import logger -from ..models.llm import ValidationPolicy, ValidationRecord -from ..schemas.validation import ValidationRunDetailResponse, ValidationRunResponse - - -# #region ValidationRunService [C:4] [TYPE Class] -# @BRIEF Queries and manages validation run history records. -# @RATIONALE Separates run history queries from task CRUD — run data has different access patterns (filter-heavy, paginated) than task configuration. -# @REJECTED Merging run service into task service rejected — would create a large class with mixed responsibilities and different query patterns. -class ValidationRunService: - - def __init__(self, db: Session): - self.db = db - - # #region _resolve_task_name [C:2] [TYPE Function] - def _resolve_task_name(self, record: ValidationRecord) -> str | None: - """Find matching ValidationPolicy name by dashboard + environment.""" - if not record.dashboard_id or not record.environment_id: - return None - candidates = ( - self.db.query(ValidationPolicy) - .filter(ValidationPolicy.environment_id == record.environment_id) - .all() - ) - for policy in candidates: - dids = list(policy.dashboard_ids) if policy.dashboard_ids else [] - if record.dashboard_id in dids: - return policy.name - return None - - # #endregion _resolve_task_name - - # #region list_runs [C:3] [TYPE Function] - # @RATIONALE Supports 6 optional filters + pagination at the query level, enabling flexible cross-task run exploration without application-level filtering. - # @REJECTED Application-level filtering after loading all runs rejected — would be unscalable with large run histories; database-level WHERE clauses are more efficient. - def list_runs( - self, - task_id: str | None = None, - dashboard_id: str | None = None, - environment_id: str | None = None, - status: str | None = None, - date_from: date | None = None, - date_to: date | None = None, - page: int = 1, - page_size: int = 20, - ) -> tuple[int, list[ValidationRunResponse]]: - query = self.db.query(ValidationRecord) - - if task_id: - # Match both spawned task_id (TaskRecord) and policy_id (ValidationPolicy.id) - # because the frontend sends the validation policy UUID, but ValidationRecord.task_id - # stores the spawned task UUID. Fall back to policy_id for the common case. - query = query.filter( - (ValidationRecord.task_id == task_id) | (ValidationRecord.policy_id == task_id) - ) - if dashboard_id: - query = query.filter(ValidationRecord.dashboard_id == dashboard_id) - if environment_id: - query = query.filter(ValidationRecord.environment_id == environment_id) - if status: - query = query.filter(ValidationRecord.status == status.upper()) - if date_from: - query = query.filter(ValidationRecord.timestamp >= date_from) - if date_to: - query = query.filter(ValidationRecord.timestamp <= date_to) - - total = query.count() - records = ( - query.order_by(ValidationRecord.timestamp.desc()) - .offset((page - 1) * page_size) - .limit(page_size) - .all() - ) - - items = [ - ValidationRunResponse( - id=r.id, - task_id=r.task_id, - dashboard_id=r.dashboard_id, - environment_id=r.environment_id, - status=r.status, - summary=r.summary or "", - timestamp=r.timestamp, - screenshot_path=r.screenshot_path, - task_name=self._resolve_task_name(r), - issues_count=len(r.issues) if r.issues else 0, - ) - for r in records - ] - return total, items - - # #endregion list_runs - - # #region get_run_detail [C:3] [TYPE Function] - # @RATIONALE Returns full join including issues list and raw_response for the report UI — single query avoids N+1 when displaying the run detail page. - # @REJECTED Lazy-loading issues and raw_response separately rejected — would cause N+1 queries on the detail page; both fields are always needed for the report view. - def get_run_detail(self, run_id: str) -> ValidationRunDetailResponse: - record = self.db.query(ValidationRecord).filter(ValidationRecord.id == run_id).first() - if not record: - raise ValueError(f"Validation run '{run_id}' not found") - - return ValidationRunDetailResponse( - id=record.id, - task_id=record.task_id, - dashboard_id=record.dashboard_id, - environment_id=record.environment_id, - status=record.status, - summary=record.summary or "", - timestamp=record.timestamp, - screenshot_path=record.screenshot_path, - issues=list(record.issues) if record.issues else [], - raw_response=record.raw_response, - task_name=self._resolve_task_name(record), - ) - - # #endregion get_run_detail - - # #region delete_run [C:2] [TYPE Function] - # @RATIONALE Cleans up the database record only — screenshot cleanup is caller responsibility to maintain separation of concerns between DB and filesystem. - # @REJECTED Automatic screenshot file deletion in service layer rejected — would couple DB cleanup to filesystem operations, complicating testing and error handling. - def delete_run(self, run_id: str) -> bool: - record = self.db.query(ValidationRecord).filter(ValidationRecord.id == run_id).first() - if not record: - return False - self.db.delete(record) - self.db.commit() - logger.info("[ValidationRunService] Deleted run '%s'", run_id) - return True - - # #endregion delete_run - -# #endregion ValidationRunService -# #endregion ValidationRunService diff --git a/frontend/src/lib/api/validation.js b/frontend/src/lib/api/validation.js deleted file mode 100644 index 7a1db74d..00000000 --- a/frontend/src/lib/api/validation.js +++ /dev/null @@ -1,102 +0,0 @@ -// #region ValidationApi [C:2] [TYPE Module] [SEMANTICS validation, api] -// @BRIEF API client functions for the Validation section — tasks and runs CRUD. -// @RATIONALE Centralized API module for all validation endpoints — single import point for pages, consistent error handling through shared fetchApi/postApi/deleteApi wrappers, and easy mocking in tests. -// @REJECTED Inline fetch calls in each page rejected — would duplicate base URL construction and auth header logic; centralized module enables consistent error handling and simplifies test mocking. -// @RELATION CALLED_BY -> [ValidationTaskList] -// @RELATION CALLED_BY -> [ValidationTaskConfig] -// @RELATION CALLED_BY -> [ValidationHistory] - -import { fetchApi, postApi, requestApi, deleteApi } from '$lib/api.js'; - -// #region buildParams:Function [C:1] [TYPE Function] -// @BRIEF Build URLSearchParams from a params object, skipping null/undefined/empty values. -/** @param {Record} params */ -function buildParams(params) { - const qs = new URLSearchParams(); - for (const [key, value] of Object.entries(params)) { - if (value !== undefined && value !== null && value !== '') { - qs.append(key, String(value)); - } - } - return qs.toString(); -} -// #endregion buildParams:Function - -// ── Tasks ────────────────────────────────────────────────────── - -// #region fetchTasks:Function [C:1] [TYPE Function] -/** @param {{ is_active?: boolean, environment_id?: string, page?: number, page_size?: number }} [params] */ -export async function fetchTasks(params = {}) { - const qs = buildParams(params); - return fetchApi(`/validation/tasks${qs ? `?${qs}` : ''}`); -} -// #endregion fetchTasks:Function - -// #region createTask:Function [C:1] [TYPE Function] -/** @param {Record} data */ -export async function createTask(data) { - return postApi('/validation/tasks', data); -} -// #endregion createTask:Function - -// #region fetchTask:Function [C:1] [TYPE Function] -/** @param {string} id */ -export async function fetchTask(id) { - return fetchApi(`/validation/tasks/${id}`); -} -// #endregion fetchTask:Function - -// #region updateTask:Function [C:1] [TYPE Function] -/** @param {string} id @param {Record} data */ -export async function updateTask(id, data) { - return requestApi(`/validation/tasks/${id}`, 'PUT', data); -} -// #endregion updateTask:Function - -// #region deleteTask:Function [C:1] [TYPE Function] -/** @param {string} id @param {boolean} [deleteRuns] */ -export async function deleteTask(id, deleteRuns = false) { - const qs = deleteRuns ? '?delete_runs=true' : ''; - return deleteApi(`/validation/tasks/${id}${qs}`); -} -// #endregion deleteTask:Function - -// #region triggerRun:Function [C:1] [TYPE Function] -/** @param {string} id */ -export async function triggerRun(id) { - return postApi(`/validation/tasks/${id}/run`, {}); -} -// #endregion triggerRun:Function - -// #region duplicateTask:Function [C:1] [TYPE Function] -/** @param {string} id */ -export async function duplicateTask(id) { - return postApi(`/validation/tasks/${id}/duplicate`, {}); -} -// #endregion duplicateTask:Function - -// ── Runs ──────────────────────────────────────────────────────── - -// #region fetchRuns:Function [C:1] [TYPE Function] -/** @param {{ task_id?: string, dashboard_id?: string, environment_id?: string, status?: string, date_from?: string, date_to?: string, page?: number, page_size?: number }} [params] */ -export async function fetchRuns(params = {}) { - const qs = buildParams(params); - return fetchApi(`/validation/runs${qs ? `?${qs}` : ''}`); -} -// #endregion fetchRuns:Function - -// #region fetchRunDetail:Function [C:1] [TYPE Function] -/** @param {string} id */ -export async function fetchRunDetail(id) { - return fetchApi(`/validation/runs/${id}`); -} -// #endregion fetchRunDetail:Function - -// #region deleteRun:Function [C:1] [TYPE Function] -/** @param {string} id */ -export async function deleteRun(id) { - return deleteApi(`/validation/runs/${id}`); -} -// #endregion deleteRun:Function - -// #endregion ValidationApi diff --git a/frontend/src/routes/validation/+page.svelte b/frontend/src/routes/validation/+page.svelte deleted file mode 100644 index f6596ec0..00000000 --- a/frontend/src/routes/validation/+page.svelte +++ /dev/null @@ -1,399 +0,0 @@ - - - - - - - - - - - -
-
-
-

{$t.validation?.tasks_title || 'Validation Tasks'}

-

{$t.validation?.last_run_status || 'Monitor and validate dashboard quality'}

-
- -
- - -
- {#each statusPills as pill (pill.value)} - - {/each} -
- - - {#if isLoading} -
- {#each Array(3) as _, i (i)} -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/each} -
- - - {:else if uxState === 'error'} -
-

{error}

- -
- - - {:else if uxState === 'empty'} -
- - - -

{$t.validation?.no_tasks || 'No validation tasks yet. Create your first task to start monitoring dashboards.'}

- -
- - - {:else if uxState === 'filtered_empty'} -
- - - -

{$t.validation?.no_tasks_filtered || 'No tasks match your filters.'}

- -
- - - {:else if uxState === 'populated'} -
- {#each tasks as task (task.id)} - -
handleNavToConfig(task.id)} - onkeydown={(e) => { if (e.key === 'Enter') handleNavToConfig(task.id); }} - role="button" - tabindex="0" - class="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-md hover:border-gray-300 transition-all cursor-pointer" - > -
-
-
-

{task.name}

- - {task.is_active !== false ? ($t.validation?.active || 'Active') : ($t.validation?.inactive || 'Inactive')} - -
-
- {#if task.dashboard_id} - - {$t.validation?.dashboard || 'Dashboard'}: {task.dashboard_id?.substring(0, 16)}... - - {/if} - {#if task.environment_id} - {$t.validation?.environment || 'Env'}: {task.environment_id?.substring(0, 12)}... - {/if} - {#if task.last_run_status} - - {$t.validation?.last_run || 'Last run'}: - - {task.last_run_status} - - - {/if} - {#if task.last_run_at} - {new Date(task.last_run_at).toLocaleString()} - {/if} - {#if task.provider_id} - {$t.validation?.provider || 'LLM'}: {task.provider_id?.substring(0, 12)}... - {/if} - {#if task.schedule_days?.length} - - {$t.validation?.scheduled || 'Scheduled'} - - {:else} - - {$t.validation?.manual_only || 'Manual only'} - - {/if} -
-
- -
e.stopPropagation()}> - - - {#if showDeleteConfirm === task.id} -
- - -
- {:else} - - {/if} -
-
- {#if showDeleteConfirm === task.id} -
- -
- {/if} -
- {/each} -
- - - {#if total > pageSize} -
-

- {($t.validation?.showing || 'Showing {count} of {total}') - .replace('{count}', String(tasks.length)) - .replace('{total}', String(total))} -

-
- - -
-
- {/if} - {/if} -
- diff --git a/frontend/src/routes/validation/[id]/+page.svelte b/frontend/src/routes/validation/[id]/+page.svelte deleted file mode 100644 index 248c9fb1..00000000 --- a/frontend/src/routes/validation/[id]/+page.svelte +++ /dev/null @@ -1,731 +0,0 @@ - - - - - - - - - - - - - - - {pageTitle} - - -
- -
-
-

- {isNewTask ? ($t.validation?.new_task || 'New Validation Task') : ($t.validation?.edit_task || 'Edit Validation Task')} -

- {#if !isNewTask && task} -

{$t.validation?.created_at || 'Created'}: {task?.created_at ? new Date(task.created_at).toLocaleString() : '-'}

- {/if} -
-
- {#if !isNewTask} - - {/if} - - -
-
- - - {#if uxState === 'loading'} -
-
-
-
-
-
-
-
-
- - - {:else if uxState === 'error'} -
-

{error || $t.validation?.load_task_failed || 'Failed to load task.'}

-
- - -
-
- - - {:else if uxState === 'not_found'} -
-

{$t.validation?.task_not_found || 'Task not found'}

-

{$t.validation?.task_not_found_desc || 'The task you are looking for does not exist or has been deleted.'}

- -
- - - {:else if uxState === 'configured' || uxState === 'saving'} - -
- -
- - - {#if !isNewTask && providerId && !isProviderMultimodal} -
-

- - - - {$t.validation?.provider_multimodal_required || 'Dashboard validation requires a multimodal LLM provider (supports images).'} -

-
- {/if} - - - {#if activeTab === 'config'} -
- -
-

{$t.validation?.task_name || 'Basic Info'}

-
-
- - - {#if validationErrors.name} -

{validationErrors.name}

- {/if} -
-
-
- - -
-

{$t.validation?.dashboard || 'Dashboard & Environment'}

-
-
- - - {#if validationErrors.environment_id} -

{validationErrors.environment_id}

- {/if} -
-
- - ({ id: String(d.id), name: d.title || d.dashboard_title || d.name || String(d.id), subtitle: d.schema || '' }))} - bind:selected={dashboardIds} - placeholder={$t.validation?.dashboard || 'Select dashboard...'} - selectAllLabel={$t.dashboard?.select_all || 'Select All'} - clearLabel="Clear" - emptyMessage="No dashboards found" - disabled={!environmentId} - /> - {#if validationErrors.dashboard_id} -

{validationErrors.dashboard_id}

- {/if} - {#if dashboardsLoading} -

Loading...

- {/if} -
-
-
- - -
-

{$t.validation?.llm_provider || 'LLM Provider'}

-
- - - {#if validationErrors.provider_id} -

{validationErrors.provider_id}

- {/if} - {#if providerId && !isProviderMultimodal} -

- - - - {$t.validation?.provider_multimodal_required || 'Dashboard validation requires a multimodal LLM provider.'} -

- {/if} -
-
- - -
-
-

{$t.validation?.schedule || 'Schedule'}

- -
- - {#if !showSchedule} -

{$t.validation?.manual_only || 'Manual only — runs triggered by user.'}

- {:else} -
-
- - -
- {#each DAY_LABELS as day, idx (day)} - - {/each} -
-
- -
-
- - -
-
- - -
-
- - -

- - - - {$t.validation?.schedule_timezone_hint || 'All times in'} {appTimezone} -

-
- {/if} -
- - -
- -
-
- - - {:else if activeTab === 'history'} -
- -
- - - -
-

{$t.validation?.history_info_title || 'About run history'}

-

{$t.validation?.history_info_desc || 'This tab shows validation runs for this specific task. Runs appear after you click "Run Now" or when the scheduled task executes. Each run captures a dashboard screenshot and sends it to the LLM provider for analysis.'}

-
-
- - {#if runsLoading} -
- {#each Array(3) as _, i (i)} -
- {/each} -
- {:else if runs.length === 0} -
-

{$t.validation?.run_history_empty || 'No runs for this task yet.'}

-

{$t.validation?.run_history_empty_hint || 'Click "Run Now" above to start a validation run. The result will appear here once analysis completes.'}

-
- {:else} -
- - - - - - - - - - - - {#each runs as run (run.id)} - - - - - - - - {/each} - -
{$t.validation?.status || 'Status'}{$t.validation?.summary || 'Summary'}{$t.validation?.issues || 'Issues'}{$t.validation?.timestamp || 'Timestamp'}{$t.validation?.actions || 'Actions'}
- - {run.status || 'UNKNOWN'} - - - {run.summary || '-'} - - {run.issues_count ?? run.issues ?? '-'} - - {run.timestamp ? new Date(run.timestamp).toLocaleString() : '-'} - - -
-
- - - {#if runsTotal > runsPageSize} -
-

- {($t.validation?.showing || 'Showing {count} of {total}') - .replace('{count}', String(runs.length)) - .replace('{total}', String(runsTotal))} -

-
- - -
-
- {/if} - {/if} -
- {/if} - {/if} -
- diff --git a/frontend/src/routes/validation/history/+page.svelte b/frontend/src/routes/validation/history/+page.svelte deleted file mode 100644 index 4a98d828..00000000 --- a/frontend/src/routes/validation/history/+page.svelte +++ /dev/null @@ -1,405 +0,0 @@ - - - - - - - - - - - -
-
-
-

{$t.validation?.history_title || 'Validation History'}

-

{$t.validation?.last_run || 'Review past validation runs'}

-
- -
- - -
-
- - - - - - -
- - -
-
-
- - - {#if isLoading} -
-
- {#each Array(3) as _, i (i)} -
- {/each} -
- - - {:else if uxState === 'error'} -
-

{error}

- -
- - - {:else if uxState === 'empty'} -
- - - -

{$t.validation?.no_runs || 'No validation runs yet. Create a task and run it.'}

-
- - - {:else if uxState === 'filtered_empty'} -
- - - -

{$t.validation?.no_runs_filtered || 'No runs match your filters.'}

- -
- - - {:else if uxState === 'populated'} - -
-
-

{$t.validation?.total_runs || 'Total runs'}

-

{totalRuns}

-
-
-

{$t.validation?.passed || 'Passed'}

-

{passedCount}

-
-
-

{$t.validation?.failed || 'Failed'}

-

{failedCount}

-
-
-

{$t.validation?.warn_rate || 'Warn rate'}

-

{warnRate}%

-
-
- - -
- {#each runs as run (run.id)} -
-
-
-
- {getTaskName(run.task_id)} - - {run.status || 'UNKNOWN'} - - {#if run.issues_count > 0} - - {$t.validation?.issues || 'Issues'}: {run.issues_count} - - {/if} -
- {#if run.summary} -

{run.summary}

- {/if} -
- {run.timestamp ? new Date(run.timestamp).toLocaleString() : '-'} - {$t.validation?.dashboard || 'Dashboard'}: {run.dashboard_id || getTaskName(run.task_id)} -
-
-
e.stopPropagation()}> - - {#if showDeleteConfirm === run.id} -
- - -
- {:else} - - {/if} -
-
-
- {/each} -
- - -
-

- {($t.validation?.showing || 'Showing {count} of {total}') - .replace('{count}', String(runs.length)) - .replace('{total}', String(total))} -

-
- - -
-
- {/if} -
-