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:
@@ -48,13 +48,14 @@ See `semantics-core` §VI for the canonical tool reference. For Svelte frontend
|
||||
## ss-tools Frontend Scope
|
||||
You own:
|
||||
- SvelteKit routes (`frontend/src/routes/`)
|
||||
- Svelte 5 components (`frontend/src/lib/components/`)
|
||||
- Svelte 5 components (`frontend/src/lib/components/` — **only directory for NEW domain components**)
|
||||
- **UI atoms** (`frontend/src/lib/ui/` — Button, Card, Input, Select, PageHeader, Icon, HelpTooltip, LanguageSwitcher)
|
||||
- **Screen Models** (`frontend/src/lib/models/` — `[TYPE Model]` contracts for screen-level state)
|
||||
- Svelte stores (`frontend/src/lib/stores/`)
|
||||
- API client layer (`frontend/src/lib/api/`)
|
||||
- i18n localization (`frontend/src/i18n/`)
|
||||
- Pages, layouts, and services
|
||||
- Tailwind-first UI implementation
|
||||
- Tailwind-first UI implementation (semantic tokens ONLY — no raw blue-600, gray-*, indigo-*)
|
||||
- UX state repair and route-level behavior
|
||||
- Browser-driven acceptance for frontend scenarios
|
||||
- Screenshot and console-driven debugging
|
||||
@@ -64,6 +65,9 @@ You do not own:
|
||||
- Backend-only implementation unless explicitly scoped
|
||||
- Semantic repair outside the frontend boundary unless required by the UI change
|
||||
|
||||
### Frozen zones (LEGACY — migrate away, do NOT add)
|
||||
- `frontend/src/components/` — legacy component directory. **Do not create new files here.** All new domain components go in `lib/components/<domain>/`.
|
||||
|
||||
## Required Workflow
|
||||
1. **Discover or create the Model first.** For any screen with cross-widget state:
|
||||
- grep `@semantics.*<keyword>` across `frontend/src/` to find existing models
|
||||
@@ -107,13 +111,25 @@ For frontend design and implementation tasks, default to these rules unless the
|
||||
- Prefer whitespace, alignment, scale, and contrast before adding chrome.
|
||||
- Default to cardless layouts; use cards only when a card is the actual interaction container for a specific resource (Dashboard, Dataset, Task).
|
||||
|
||||
### Visual system (ss-tools design tokens)
|
||||
- Primary: `blue-600` / `blue-700` (buttons, links, accents)
|
||||
- Success: `green-500` / `green-600`
|
||||
- Error: `red-500` / `red-600`
|
||||
- Warning: `amber-400` / `amber-500`
|
||||
- Background: `gray-50` (page), `white` (cards/surfaces)
|
||||
- Text: `gray-900` (primary), `gray-500` (muted)
|
||||
### Visual system (ss-tools design tokens — source: `tailwind.config.js`)
|
||||
**Raw Tailwind colors (`blue-600`, `green-500`, `red-600`, `gray-*`, `indigo-*`) are DEPRECATED in page and component code.** Use ONLY these semantic tokens:
|
||||
|
||||
- Primary action: `bg-primary text-white hover:bg-primary-hover` (maps to blue-600/700)
|
||||
- Destructive action / error: `bg-destructive text-white`, `bg-destructive-light text-destructive border-destructive-ring`
|
||||
- Page background: `bg-surface-page`
|
||||
- Card surface: `bg-surface-card`
|
||||
- Muted surface: `bg-surface-muted`
|
||||
- Default border: `border-border`; strong border (inputs): `border-border-strong`
|
||||
- Primary text: `text-text`; muted text: `text-text-muted`; subtle text (placeholders): `text-text-subtle`
|
||||
- Success: `text-success bg-success-light border-success-*`
|
||||
- Warning: `text-warning bg-warning-light border-warning-*`
|
||||
- Info: `text-info bg-info-light border-info-*`
|
||||
|
||||
### UI component reuse (MANDATORY)
|
||||
- **Page-level UI MUST use `$lib/ui` atoms:** `<Button>`, `<Card>`, `<Input>`, `<Select>`, `<PageHeader>`. Raw `<button>` and manual `<div class="bg-white rounded...">` in page files is a violation unless there is a documented exception.
|
||||
- **`src/components/` is LEGACY FROZEN.** Do not create new files there. Do not extend it. New domain components go in `src/lib/components/<domain>/`.
|
||||
- **`migration/+page.svelte`** is the state architecture reference (model-first with thin component) but NOT the visual reference — it still has legacy raw colors and manual buttons. Use the canonical template in `semantics-svelte` §VI for visual patterns.
|
||||
- **Button variant naming:** Use `"destructive"` (canonical). `"danger"` is a deprecated alias — prefer `"destructive"`.
|
||||
|
||||
### ss-tools specific pages
|
||||
- **Dashboard Hub** — Git-tracked dashboards with status badges
|
||||
|
||||
@@ -11,7 +11,10 @@ description: Svelte 5 (Runes) protocol for ss-tools: UX State Machines, Tailwind
|
||||
@RATIONALE Svelte 5 runes ($state, $derived, $effect, $props) chosen for reactive precision and native compiler optimisations over Svelte 4 legacy reactivity ($:). Tailwind CSS selected for zero-runtime utility-first styling and rapid visual validation via chrome-devtools MCP. FSM-based UX contracts (@UX_STATE, @UX_FEEDBACK, @UX_RECOVERY) chosen to create verifiable state-transition tests that the browser Judge Agent can execute deterministically. ss-tools internal API wrappers (fetchApi/requestApi) chosen over native fetch to enforce auth, error normalisation, and trace_id propagation.
|
||||
@REJECTED React (JSX) rejected — Svelte's compiler-first approach yields smaller bundles and native reactivity without virtual DOM overhead. Vue rejected — Svelte 5 runes provide simpler mental model. Legacy Svelte 4 syntax (export let, $:, on:event) rejected — incompatible with Svelte 5 runes mode. CSS Modules / styled-components rejected in favour of Tailwind's utility-first approach, which avoids style leakage and simplifies chrome-devtools visual diffing. Native fetch() rejected — bypasses ss-tools middleware chain (auth, trace_id, error normalisation). Plain-text logging rejected per MolecularCoTLogging §VII — JSON lines are mandatory for agent-parsable traces.
|
||||
@INVARIANT Frontend components MUST be verifiable by the browser toolset via `chrome-devtools` MCP.
|
||||
@INVARIANT Use Tailwind CSS exclusively. Native `fetch` is forbidden — use `requestApi`/`fetchApi` wrappers.
|
||||
@INVARIANT Use Tailwind CSS exclusively. Raw Tailwind color classes (`blue-600`, `green-500`, `red-600`, `gray-*`, `indigo-*`) are DEPRECATED in page and component code — use semantic tokens from `tailwind.config.js` only (`primary`, `destructive`, `success`, `warning`, `surface-*`, `border-*`, `text-*`).
|
||||
@INVARIANT Page-level UI MUST use `$lib/ui` atoms: `<Button>`, `<Card>`, `<Input>`, `<Select>`, `<PageHeader>`. Raw `<button>` elements and manual card `<div>` containers in page files are a violation.
|
||||
@INVARIANT `src/components/` is LEGACY FROZEN. New domain components go in `src/lib/components/<domain>/`. Do not create new files under `src/components/`.
|
||||
@INVARIANT Native `fetch` is forbidden — use `requestApi`/`fetchApi` wrappers.
|
||||
|
||||
## 0. SVELTE 5 PARADIGM & UX PHILOSOPHY (SS-TOOLS)
|
||||
|
||||
@@ -319,17 +322,19 @@ Region format for HTML/Svelte comments:
|
||||
<!-- @RELATION BINDS_TO -> [notificationStore] -->
|
||||
<!-- @UX_STATE Idle -> Default card view with task summary. -->
|
||||
<!-- @UX_STATE Loading -> Action button disabled, spinner active, progress bar animated. -->
|
||||
<!-- @UX_STATE Error -> Red border, error icon, retry button visible. -->
|
||||
<!-- @UX_STATE Success -> Green border, checkmark, duration displayed. -->
|
||||
<!-- @UX_STATE Error -> Card border + bg use destructive tokens, retry button visible. -->
|
||||
<!-- @UX_STATE Success -> Card border + bg use success tokens, checkmark, duration displayed. -->
|
||||
<!-- @UX_FEEDBACK Toast notification on start/fail/complete. -->
|
||||
<!-- @UX_FEEDBACK Drawer opens on "View Logs" click. -->
|
||||
<!-- @UX_RECOVERY Retry button on error. Clear/Cancel on running task. -->
|
||||
<!-- @UX_REACTIVITY Props -> $props(), LocalState -> $state(isLoading, error). -->
|
||||
<!-- @UX_TEST: Idle -> {click: "Run Migration", expected: Loading -> Success toast}. -->
|
||||
<script>
|
||||
<!-- @RATIONALE Uses semantic tokens (destructive-light, success-light, border, surface-card) instead of raw Tailwind (red-*, green-*, gray-*). Uses $lib/ui Button instead of raw <button>. This is the canonical visual reference for all components. -->
|
||||
<script lang="ts">
|
||||
import { fetchApi } from "$lib/api";
|
||||
import { log } from "$lib/cot-logger";
|
||||
import { t } from "$lib/i18n";
|
||||
import { Button } from "$lib/ui";
|
||||
import { taskDrawerStore } from "$lib/stores";
|
||||
import { notificationStore } from "$lib/stores";
|
||||
import StatusBadge from "./StatusBadge.svelte";
|
||||
@@ -338,8 +343,8 @@ Region format for HTML/Svelte comments:
|
||||
let { taskId, dashboardName, sourceEnv, targetEnv } = $props();
|
||||
|
||||
let isLoading = $state(false);
|
||||
let error = $state(null);
|
||||
let status = $state("idle");
|
||||
let error: string | null = $state(null);
|
||||
let status: "idle" | "loading" | "success" | "error" = $state("idle");
|
||||
|
||||
async function handleRunMigration() {
|
||||
isLoading = true;
|
||||
@@ -355,8 +360,8 @@ Region format for HTML/Svelte comments:
|
||||
notificationStore.add({ type: "success", message: $t("migration.completed", { name: dashboardName }) });
|
||||
} catch (e) {
|
||||
status = "error";
|
||||
error = e.message;
|
||||
log("MigrationTaskCard", "EXPLORE", "Migration failed", { taskId }, error = e.message);
|
||||
error = e instanceof Error ? e.message : "Migration failed";
|
||||
log("MigrationTaskCard", "EXPLORE", "Migration failed", { taskId }, error);
|
||||
notificationStore.add({ type: "error", message: $t("migration.failed", { name: dashboardName }) });
|
||||
} finally {
|
||||
isLoading = false;
|
||||
@@ -369,17 +374,21 @@ Region format for HTML/Svelte comments:
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- @RATIONALE Card uses semantic surface/border/text tokens. Dynamic state uses destructive/success token families. Button component from $lib/ui instead of raw <button> tags. -->
|
||||
<div
|
||||
class="rounded-lg border p-4 transition-colors {status === 'error' ? 'border-red-500 bg-red-50' : ''} {status === 'success' ? 'border-green-500 bg-green-50' : 'border-gray-200 bg-white'}"
|
||||
class="rounded-lg p-4 transition-colors
|
||||
{status === 'error' ? 'border border-destructive-ring bg-destructive-light' : ''}
|
||||
{status === 'success' ? 'border border-success-DEFAULT bg-success-light' : ''}
|
||||
{status !== 'error' && status !== 'success' ? 'border border-border bg-surface-card' : ''}"
|
||||
role="region"
|
||||
aria-label={$t("migration.task_card", { name: dashboardName })}
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="font-semibold text-gray-900">{dashboardName}</h3>
|
||||
<h3 class="font-semibold text-text">{dashboardName}</h3>
|
||||
<StatusBadge status={status} />
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-gray-500 mb-3">
|
||||
<div class="text-sm text-text-muted mb-3">
|
||||
{$t("migration.from")}: {sourceEnv} → {$t("migration.to")}: {targetEnv}
|
||||
</div>
|
||||
|
||||
@@ -388,45 +397,80 @@ Region format for HTML/Svelte comments:
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<p class="text-sm text-red-600 mb-2" aria-live="polite">{error}</p>
|
||||
<p class="text-sm text-destructive mb-2" aria-live="polite">{error}</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2 mt-3">
|
||||
<button
|
||||
class="btn-primary text-sm"
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onclick={handleRunMigration}
|
||||
disabled={isLoading}
|
||||
aria-busy={isLoading}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{#if isLoading}
|
||||
<span class="spinner mr-1" aria-hidden="true"></span>
|
||||
{/if}
|
||||
{status === "error" ? $t("actions.retry") : $t("actions.run")}
|
||||
</button>
|
||||
<button class="btn-secondary text-sm" onclick={handleViewLogs}>
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onclick={handleViewLogs}>
|
||||
{$t("actions.view_logs")}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion MigrationTaskCard -->
|
||||
```
|
||||
|
||||
## VII. SS-TOOLS TAILWIND CONVENTIONS
|
||||
## VII. SS-TOOLS DESIGN TOKEN CANON & COMPONENT REUSE
|
||||
|
||||
### Design tokens (from project config)
|
||||
- Primary: `blue-600` / `blue-700` (buttons, links, accents)
|
||||
- Success: `green-500` / `green-600`
|
||||
- Error: `red-500` / `red-600`
|
||||
- Warning: `amber-400` / `amber-500`
|
||||
- Background: `gray-50` (page), `white` (cards)
|
||||
- Text: `gray-900` (primary), `gray-500` (muted)
|
||||
### Design tokens (source of truth: `frontend/tailwind.config.js`)
|
||||
|
||||
### Component class conventions
|
||||
- Page layout: `max-w-7xl mx-auto px-4 py-6`
|
||||
- Card: `bg-white rounded-lg shadow-sm border border-gray-200 p-4`
|
||||
- Button primary: `bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50`
|
||||
- Button secondary: `border border-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-50`
|
||||
- Table: `min-w-full divide-y divide-gray-200`
|
||||
**Raw Tailwind colors (`blue-600`, `green-500`, `red-600`, `gray-*`, `indigo-*`) are DEPRECATED in page-level and component code.** Use ONLY semantic tokens below.
|
||||
|
||||
| Purpose | Token | Maps to |
|
||||
|---------|-------|---------|
|
||||
| Primary action | `text-primary bg-primary hover:bg-primary-hover` | blue-600/700 |
|
||||
| Destructive action / error surface | `text-destructive bg-destructive-light border-destructive-ring` | red family |
|
||||
| Page background | `bg-surface-page` | near-white |
|
||||
| Card surface | `bg-surface-card` | white |
|
||||
| Muted surface (hover/filter bars) | `bg-surface-muted` | gray-100 |
|
||||
| Default border | `border-border` | gray-200 |
|
||||
| Strong border (inputs, focus) | `border-border-strong` | gray-300 |
|
||||
| Primary text | `text-text` | near-black |
|
||||
| Muted text (secondary, captions) | `text-text-muted` | gray-500 |
|
||||
| Subtle text (placeholders) | `text-text-subtle` | gray-400 |
|
||||
| Success | `text-success bg-success-light border-success-*` | green family |
|
||||
| Warning | `text-warning bg-warning-light border-warning-*` | amber family |
|
||||
| Info | `text-info bg-info-light border-info-*` | sky family |
|
||||
|
||||
### Component reuse rules (MANDATORY for page-level code)
|
||||
|
||||
| Rule | Requirement |
|
||||
|------|------------|
|
||||
| **$lib/ui mandatory** | All page files (`src/routes/**/+page.svelte`) MUST import from `$lib/ui` for buttons, cards, inputs, selects, page headers. Raw `<button>` and `<div class="bg-white rounded...">` in page files are a violation unless covered by a documented exception. |
|
||||
| **Component directory** | New domain components go in `src/lib/components/<domain>/`. `src/components/` is **LEGACY FROZEN** — do not add new files, do not extend, migrate out only. |
|
||||
| **Button variants** | Use `<Button variant="primary">` (default), `<Button variant="secondary">`, `<Button variant="destructive">`, `<Button variant="ghost">`. The string `"danger"` is kept as a deprecated alias for `"destructive"` — prefer `"destructive"`. |
|
||||
| **Page layout** | `<div class="max-w-7xl mx-auto px-4 py-6">` or `<div class="mx-auto w-full px-4 lg:px-8 space-y-6">`. |
|
||||
| **Table pattern** | `min-w-full divide-y divide-border` — border via token. |
|
||||
|
||||
### Canonical semantic token reference (copy-paste for agents)
|
||||
|
||||
```
|
||||
// ✅ CORRECT — semantic tokens
|
||||
bg-surface-page // page background
|
||||
bg-surface-card // card container
|
||||
bg-surface-muted // filter bar, secondary button hover
|
||||
border-border // card border, table divider
|
||||
border-border-strong // input border, select border
|
||||
text-text // heading, body
|
||||
text-text-muted // secondary label, caption
|
||||
text-text-subtle // placeholder
|
||||
bg-primary text-white // primary action button
|
||||
bg-destructive-light // error surface
|
||||
text-destructive // error text
|
||||
border-destructive-ring // error border
|
||||
|
||||
// ❌ WRONG — raw Tailwind (deprecated in page/component code)
|
||||
bg-blue-600 text-blue-700 bg-gray-50 bg-white border-gray-200
|
||||
text-gray-900 text-gray-500 bg-red-50 border-red-400 bg-green-50
|
||||
bg-indigo-50 text-indigo-700 bg-gradient-to-br from-slate-50 via-white to-sky-50
|
||||
```
|
||||
|
||||
## VIII. VITEST CONVENTIONS
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -1,73 +1,109 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{svelte,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Semantic UI colors (light surfaces)
|
||||
primary: {
|
||||
DEFAULT: '#2563eb', // blue-600
|
||||
hover: '#1d4ed8', // blue-700
|
||||
ring: '#3b82f6', // blue-500
|
||||
light: '#eff6ff', // blue-50
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: '#f3f4f6', // gray-100
|
||||
hover: '#e5e7eb', // gray-200
|
||||
text: '#111827', // gray-900
|
||||
ring: '#6b7280', // gray-500
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: '#dc2626', // red-600
|
||||
hover: '#b91c1c', // red-700
|
||||
ring: '#ef4444', // red-500
|
||||
light: '#fef2f2', // red-50
|
||||
},
|
||||
ghost: {
|
||||
hover: '#f3f4f6', // gray-100
|
||||
text: '#374151', // gray-700
|
||||
ring: '#6b7280', // gray-500
|
||||
},
|
||||
// Dark terminal palette (log viewer, task drawer)
|
||||
terminal: {
|
||||
bg: '#0f172a', // slate-900
|
||||
surface: '#1e293b', // slate-800
|
||||
border: '#334155', // slate-700
|
||||
text: {
|
||||
DEFAULT: '#cbd5e1', // slate-300
|
||||
muted: '#475569', // slate-600
|
||||
subtle: '#64748b', // slate-500
|
||||
bright: '#e2e8f0', // slate-200
|
||||
heading: '#f1f5f9', // slate-100
|
||||
},
|
||||
accent: '#22d3ee', // cyan-400
|
||||
},
|
||||
// Log level palette
|
||||
log: {
|
||||
debug: '#64748b',
|
||||
info: '#38bdf8',
|
||||
warning: '#fbbf24',
|
||||
error: '#f87171',
|
||||
},
|
||||
// Log source palette
|
||||
source: {
|
||||
plugin: '#4ade80',
|
||||
api: '#c084fc',
|
||||
git: '#fb923c',
|
||||
system: '#38bdf8',
|
||||
},
|
||||
},
|
||||
width: {
|
||||
sidebar: '240px',
|
||||
'sidebar-collapsed': '64px',
|
||||
},
|
||||
fontFamily: {
|
||||
mono: ['"JetBrains Mono"', '"Fira Code"', 'monospace'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{svelte,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// ── Semantic action palette (light surfaces) ──────────────
|
||||
primary: {
|
||||
DEFAULT: '#2563eb', // blue-600
|
||||
hover: '#1d4ed8', // blue-700
|
||||
ring: '#3b82f6', // blue-500
|
||||
light: '#eff6ff', // blue-50
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: '#f3f4f6', // gray-100
|
||||
hover: '#e5e7eb', // gray-200
|
||||
text: '#111827', // gray-900
|
||||
ring: '#6b7280', // gray-500
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: '#dc2626', // red-600
|
||||
hover: '#b91c1c', // red-700
|
||||
ring: '#ef4444', // red-500
|
||||
light: '#fef2f2', // red-50
|
||||
},
|
||||
success: {
|
||||
DEFAULT: '#22c55e', // green-500
|
||||
hover: '#16a34a', // green-600
|
||||
ring: '#4ade80', // green-400
|
||||
light: '#f0fdf4', // green-50
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: '#f59e0b', // amber-500
|
||||
hover: '#d97706', // amber-600
|
||||
ring: '#fbbf24', // amber-400
|
||||
light: '#fffbeb', // amber-50
|
||||
},
|
||||
info: {
|
||||
DEFAULT: '#0ea5e9', // sky-500
|
||||
hover: '#0284c7', // sky-600
|
||||
ring: '#38bdf8', // sky-400
|
||||
light: '#f0f9ff', // sky-50
|
||||
},
|
||||
ghost: {
|
||||
hover: '#f3f4f6', // gray-100
|
||||
text: '#374151', // gray-700
|
||||
ring: '#6b7280', // gray-500
|
||||
},
|
||||
// ── Surface hierarchy ─────────────────────────────────────
|
||||
surface: {
|
||||
page: '#f8fafc', // slate-50
|
||||
card: '#ffffff', // white
|
||||
muted: '#f1f5f9', // slate-100
|
||||
},
|
||||
// ── Border hierarchy ──────────────────────────────────────
|
||||
border: {
|
||||
DEFAULT: '#e2e8f0', // slate-200
|
||||
strong: '#cbd5e1', // slate-300
|
||||
},
|
||||
// ── Text hierarchy ────────────────────────────────────────
|
||||
text: {
|
||||
DEFAULT: '#0f172a', // slate-900
|
||||
muted: '#64748b', // slate-500
|
||||
subtle: '#94a3b8', // slate-400
|
||||
inverse: '#ffffff', // white
|
||||
},
|
||||
// ── Dark terminal palette (log viewer, task drawer) ───────
|
||||
terminal: {
|
||||
bg: '#0f172a', // slate-900
|
||||
surface: '#1e293b', // slate-800
|
||||
border: '#334155', // slate-700
|
||||
text: {
|
||||
DEFAULT: '#cbd5e1', // slate-300
|
||||
muted: '#475569', // slate-600
|
||||
subtle: '#64748b', // slate-500
|
||||
bright: '#e2e8f0', // slate-200
|
||||
heading: '#f1f5f9', // slate-100
|
||||
},
|
||||
accent: '#22d3ee', // cyan-400
|
||||
},
|
||||
// ── Log level palette ─────────────────────────────────────
|
||||
log: {
|
||||
debug: '#64748b',
|
||||
info: '#38bdf8',
|
||||
warning: '#fbbf24',
|
||||
error: '#f87171',
|
||||
},
|
||||
// ── Log source palette ────────────────────────────────────
|
||||
source: {
|
||||
plugin: '#4ade80',
|
||||
api: '#c084fc',
|
||||
git: '#fb923c',
|
||||
system: '#38bdf8',
|
||||
},
|
||||
},
|
||||
width: {
|
||||
sidebar: '240px',
|
||||
'sidebar-collapsed': '64px',
|
||||
},
|
||||
fontFamily: {
|
||||
mono: ['"JetBrains Mono"', '"Fira Code"', 'monospace'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user