refactor(frontend): enforce semantic tokens + extract 3 Screen Models

Color tokens: 3658→0 violations across 186 .svelte/.svelte.ts files.
All raw Tailwind colors (bg-blue-*, text-gray-*, border-red-*, etc.)
replaced with semantic tokens from tailwind.config.js in 5 perl passes.

Model extraction (11→8 oversized pages):
- LLMReportModel.svelte.ts: LLM report page 413→221 lines
- DatasetDetailModel.svelte.ts: dataset detail page 416→218 lines
- DatasetsHubModel.svelte.ts: datasets hub page 468→246 lines

Tests: 14 assertions updated (color classes + model refs).
Build: 1 duplicate class:text-primary fix in ValidationTaskForm.
All 699/699 tests pass.
This commit is contained in:
2026-06-02 17:58:30 +03:00
parent 29acfb4e69
commit cf7b3556f7
156 changed files with 4581 additions and 4569 deletions

View File

@@ -0,0 +1,126 @@
// #region DatasetDetailModel [C:3] [TYPE Model] [SEMANTICS dataset,detail,columns,dashboard,model]
// @BRIEF Screen model for dataset detail page — loads dataset metadata, columns, SQL, and linked dashboards.
// @INVARIANT loadDatasetDetail() must only fire when datasetId and envId are both truthy.
// @STATE idle — No context available (missing datasetId or envId).
// @STATE loading — API call in progress, skeleton visible.
// @STATE loaded — Dataset details fetched, columns and dashboards rendered.
// @STATE error — API call failed, error message displayed with retry.
// @ACTION loadDatasetDetail() — Fetches dataset detail from API.
// @ACTION navigateToDashboard() — Navigates to linked dashboard detail.
// @ACTION goBack() — Navigates back to dataset list.
// @ACTION getColumnTypeClass() — Returns Tailwind class for column type chip.
// @RELATION DEPENDS_ON -> [api]
// @RELATION CALLS -> [log]
import { goto } from '$app/navigation';
import { ROUTES } from '$lib/routes';
import { api } from '$lib/api.js';
import { log } from '$lib/cot-logger';
// ── Types ───────────────────────────────────────────────────────
interface DatasetColumn {
name: string;
type?: string;
is_dttm?: boolean;
is_active?: boolean;
description?: string;
}
interface LinkedDashboard {
id: number | string;
title?: string;
slug?: string;
}
interface DatasetDetail {
table_name: string;
schema?: string;
database: string;
column_count?: number;
linked_dashboard_count?: number;
is_sqllab_view?: boolean;
created_on?: string;
changed_on?: string;
sql?: string;
columns?: DatasetColumn[];
linked_dashboards?: LinkedDashboard[];
}
type ScreenState = 'idle' | 'loading' | 'loaded' | 'error';
export class DatasetDetailModel {
// ── Atoms ─────────────────────────────────────────────────────
dataset: DatasetDetail | null = $state(null);
screenState: ScreenState = $state('idle');
error: string | null = $state(null);
// Set externally from page params
datasetId: string = $state('');
envId: string = $state('');
// ── Derived ───────────────────────────────────────────────────
isLoading = $derived(this.screenState === 'loading');
columnCount = $derived(this.dataset?.column_count ?? 0);
linkedDashCount = $derived(this.dataset?.linked_dashboard_count ?? 0);
// ── Actions ───────────────────────────────────────────────────
async loadDatasetDetail(): Promise<void> {
if (!this.datasetId || !this.envId) {
this.error = 'Missing context — dataset ID or environment ID not available.';
this.screenState = 'error';
return;
}
log('DatasetDetailModel', 'REASON', 'Loading dataset detail', {
datasetId: this.datasetId,
envId: this.envId,
});
this.screenState = 'loading';
this.error = null;
try {
const response = await api.getDatasetDetail(this.envId, this.datasetId);
this.dataset = response as DatasetDetail;
this.screenState = 'loaded';
log('DatasetDetailModel', 'REFLECT', 'Dataset detail loaded', {
table: this.dataset.table_name,
columns: this.dataset.column_count,
});
} catch (err: unknown) {
this.error = err instanceof Error ? err.message : 'Failed to load dataset detail';
this.screenState = 'error';
log('DatasetDetailModel', 'EXPLORE', 'Failed to load dataset', { datasetId: this.datasetId }, String(this.error));
}
}
navigateToDashboard(dashboard: LinkedDashboard): void {
const ref = (dashboard as { slug?: string; id: string | number }).slug
?? (dashboard as { slug?: string; id: string | number }).id;
if (!ref) return;
goto(ROUTES.dashboards.detail(String(ref), this.envId));
}
goBack(): void {
goto(ROUTES.datasets.list(this.envId));
}
// ── Static utilities (pure, no state access) ──────────────────
static getColumnTypeClass(type: string | undefined): string {
if (!type) return 'text-text-muted';
const lt = type.toLowerCase();
if (lt.includes('int') || lt.includes('float') || lt.includes('num')) {
return 'text-primary bg-primary-light';
} else if (lt.includes('date') || lt.includes('time')) {
return 'text-success bg-success-light';
} else if (lt.includes('str') || lt.includes('text') || lt.includes('char')) {
return 'text-info bg-info-light';
} else if (lt.includes('bool')) {
return 'text-warning bg-warning-light';
}
return 'text-text-muted bg-surface-muted';
}
}
// #endregion DatasetDetailModel

View File

@@ -0,0 +1,293 @@
// #region DatasetsHubModel [C:4] [TYPE Model] [SEMANTICS datasets,list,filter,pagination,bulk-action,model]
// @BRIEF Screen model for the datasets hub — manages list state, selection, detail preview, modals, and WebSocket updates.
// @INVARIANT Changing filter or search resets pagination to page 1.
// @INVARIANT selectedIds is always a subset of loaded datasets — orphan IDs cleaned after each load.
// @INVARIANT Detail panel opens only when selectedDatasetId is truthy; closing clears detailData.
// @STATE idle — No environment selected.
// @STATE loading — Dataset list is loading.
// @STATE loaded — Datasets loaded, list rendered.
// @STATE empty — No datasets match current filters.
// @STATE error — Load failed, error shown.
// @ACTION loadDatasets(filter?, page?) — Fetches paginated list with current search/filter.
// @ACTION loadDetail(datasetId) — Fetches detail preview for selected dataset.
// @ACTION setFilter(filterKey) — Sets active filter, resets pagination, reloads.
// @ACTION setSearch(query) — Debounced search with pagination reset.
// @ACTION setPage(n) — Navigates to page.
// @ACTION toggleSelect(dataset, checked) — Toggles single dataset in selection set.
// @ACTION selectAll() / clearAll() — Bulk selection operations.
// @ACTION handleAction(dataset, action) — Opens map-columns or generate-docs modal.
// @ACTION handleBulkMapColumns() — Runs bulk map-columns task.
// @ACTION handleBulkGenerateDocs() — Runs bulk generate-docs task.
// @ACTION selectDataset(id) — Opens detail preview panel.
// @ACTION closeDetail() — Closes detail preview panel.
// @RELATION DEPENDS_ON -> [api]
// @RELATION CALLS -> [log]
// @INVARIANT DECOMPOSITION GATE: 20+ atoms. If model exceeds 400 lines or 40 methods, split into submodels.
import { SvelteSet } from 'svelte/reactivity';
import { api } from '$lib/api.js';
import { log } from '$lib/cot-logger';
import { debounce } from '$lib/utils/debounce.js';
// ── Types ───────────────────────────────────────────────────────
type ScreenState = 'idle' | 'loading' | 'loaded' | 'empty' | 'error';
interface DatasetItem {
id: string;
table_name: string;
schema: string;
database: string;
column_count: number;
metric_count: number;
mappedFields: { total: number; mapped: number } | null;
lastTask: { status: string; id: string } | null;
}
interface DatasetStats {
total?: number;
mapped?: number;
[key: string]: unknown;
}
interface MapColumnsResponse {
task_id: string;
[key: string]: unknown;
}
export class DatasetsHubModel {
// ── Environment ───────────────────────────────────────────────
selectedEnv: string | null = $state(null);
// ── List state ────────────────────────────────────────────────
datasets: DatasetItem[] = $state([]);
stats: DatasetStats | null = $state(null);
screenState: ScreenState = $state('idle');
dsError: string | null = $state(null);
page: number = $state(1);
total: number = $state(0);
totalPages: number = $state(1);
pageSize: number = $state(10);
searchQuery: string = $state('');
activeFilter: string | null = $state(null);
// ── Selection ─────────────────────────────────────────────────
selectedIds: Set<string> = $state(new SvelteSet());
// ── Detail preview ────────────────────────────────────────────
selectedDatasetId: string | null = $state(null);
detailData: Record<string, unknown> | null = $state(null);
detailLoading: boolean = $state(false);
detailError: string | null = $state(null);
// ── Modals ────────────────────────────────────────────────────
showMapColumnsModal: boolean = $state(false);
showGenerateDocsModal: boolean = $state(false);
mapSourceType: string = $state('sqllab');
mapDatabases: unknown[] = $state([]);
mapDatabaseId: string | null = $state(null);
mapSqlQuery: string = $state('');
mapFileData: unknown = $state(null);
llmProvider: string = $state('');
llmOptions: Record<string, unknown> = $state({});
// ── Mobile ────────────────────────────────────────────────────
isMobile: boolean = $state(false);
mobileSlideOpen: boolean = $state(false);
// ── Derived ───────────────────────────────────────────────────
isLoading = $derived(this.screenState === 'loading');
hasSelection = $derived(this.selectedIds.size > 0);
selectionCount = $derived(this.selectedIds.size);
// ── Private ───────────────────────────────────────────────────
private _ws: unknown = null;
private _debouncedSearch: ReturnType<typeof debounce>;
constructor() {
this._debouncedSearch = debounce((q: string) => {
this.searchQuery = q;
this.page = 1;
this.loadDatasets();
}, 300);
}
// ── Actions ───────────────────────────────────────────────────
async loadDatasets(filter?: string | null, p?: number): Promise<void> {
if (!this.selectedEnv) return;
log('DatasetsHubModel', 'REASON', 'Loading datasets', {
env: this.selectedEnv, filter: filter ?? this.activeFilter, page: p ?? this.page,
});
this.screenState = 'loading';
this.dsError = null;
try {
const resp = await api.getDatasets(this.selectedEnv, {
search: this.searchQuery || undefined,
filter: (filter ?? this.activeFilter) ?? undefined,
page: p ?? this.page,
page_size: this.pageSize,
});
this.datasets = (resp.datasets as DatasetItem[]).map((d: { id: string; table_name: string; schema: string; database: string; column_count?: number; metric_count?: number; mapped_fields?: { total: number; mapped: number }; last_task?: { status: string; task_id: string } }) => ({
id: d.id,
table_name: d.table_name,
schema: d.schema,
database: d.database,
column_count: d.mapped_fields?.total ?? 0,
metric_count: d.metric_count ?? 0,
mappedFields: d.mapped_fields ? { total: d.mapped_fields.total, mapped: d.mapped_fields.mapped } : null,
lastTask: d.last_task ? { status: d.last_task.status, id: d.last_task.task_id } : null,
}));
this.stats = resp.stats ?? null;
this.total = resp.total ?? 0;
this.totalPages = resp.total_pages ?? 1;
this.page = resp.page ?? 1;
// Clean orphan selection IDs
const keepIds = new Set<string>();
this.datasets.forEach(d => { if (this.selectedIds.has(d.id)) keepIds.add(d.id); });
this.selectedIds = keepIds;
this.screenState = this.datasets.length === 0 ? 'empty' : 'loaded';
log('DatasetsHubModel', 'REFLECT', 'Datasets loaded', {
count: this.datasets.length, total: this.total, page: this.page,
});
} catch (e: unknown) {
this.dsError = e instanceof Error ? e.message : 'Failed to load datasets';
this.screenState = 'error';
log('DatasetsHubModel', 'EXPLORE', 'Failed to load datasets', { env: this.selectedEnv }, String(this.dsError));
}
}
setFilter(filterKey: string): void {
this.activeFilter = filterKey;
this.page = 1;
this.loadDatasets(filterKey, 1);
}
setSearch(q: string): void {
this._debouncedSearch(q);
}
setPage(p: number): void {
this.page = p;
this.loadDatasets(undefined, p);
}
toggleSelect(dataset: DatasetItem, checked: boolean | 'all' | 'clear-all'): void {
if (checked === 'all') {
this.datasets.forEach(d => this.selectedIds.add(d.id));
this.selectedIds = new Set(this.selectedIds);
} else if (checked === 'clear-all') {
this.selectedIds = new SvelteSet();
} else if (checked) {
this.selectedIds.add(dataset.id);
this.selectedIds = new Set(this.selectedIds);
} else {
this.selectedIds.delete(dataset.id);
this.selectedIds = new Set(this.selectedIds);
}
}
selectAll(): void { this.toggleSelect({ id: '' } as DatasetItem, 'all'); }
clearAll(): void { this.toggleSelect({ id: '' } as DatasetItem, 'clear-all'); }
async loadDetail(datasetId: string): Promise<void> {
if (!datasetId) return;
this.detailLoading = true;
this.detailError = null;
try {
const d = await api.getDatasetDetail(this.selectedEnv!, datasetId);
this.detailData = { ...d, schema: (d as { schema_name?: string; schema?: string }).schema_name ?? (d as { schema_name?: string; schema?: string }).schema };
} catch (e: unknown) {
this.detailError = e instanceof Error ? e.message : 'Failed to load detail';
} finally {
this.detailLoading = false;
}
}
selectDataset(id: string): void {
this.selectedDatasetId = id;
this.detailData = null;
this.detailError = null;
if (this.isMobile) this.mobileSlideOpen = true;
this.loadDetail(id);
}
closeDetail(): void {
this.selectedDatasetId = null;
this.detailData = null;
this.detailError = null;
this.mobileSlideOpen = false;
}
handleAction(dataset: DatasetItem, action: string): void {
if (action === 'map_columns') {
this.selectedIds = new SvelteSet([dataset.id]);
this.showMapColumnsModal = true;
this.mapSourceType = 'sqllab';
this.mapDatabaseId = null;
this.mapSqlQuery = '';
this.mapFileData = null;
if (this.selectedEnv) {
api.getEnvironmentDatabases(this.selectedEnv).then((dbs: unknown) => { this.mapDatabases = (dbs as unknown[]) ?? []; }).catch(() => {});
}
} else if (action === 'generate_docs') {
this.selectedIds = new SvelteSet([dataset.id]);
this.showGenerateDocsModal = true;
this.llmProvider = '';
this.llmOptions = {};
}
}
async handleBulkMapColumns(): Promise<void> {
if (this.selectedIds.size === 0) return;
if (this.mapSourceType === 'sqllab' && !this.mapDatabaseId) return;
if (this.mapSourceType === 'xlsx' && (!this.mapFileData || (this.mapFileData as unknown[]).length === 0)) return;
try {
let fileData = null;
if (this.mapSourceType === 'xlsx' && (this.mapFileData as unknown[])?.length)
fileData = (this.mapFileData as unknown[])[0] as { name: string };
const resp = await api.postApi('/datasets/map-columns', {
env_id: this.selectedEnv,
dataset_ids: [...this.selectedIds],
source_type: this.mapSourceType,
database_id: this.mapDatabaseId,
sql_query: this.mapSqlQuery,
file_data: fileData?.name,
}) as MapColumnsResponse;
log('DatasetsHubModel', 'REFLECT', 'Bulk map columns started', { taskId: resp.task_id });
this.showMapColumnsModal = false;
this.selectedIds = new SvelteSet();
} catch (e: unknown) {
log('DatasetsHubModel', 'EXPLORE', 'Bulk map columns failed', {}, String(e));
}
}
async handleBulkGenerateDocs(): Promise<void> {
if (this.selectedIds.size === 0) return;
try {
const resp = await api.postApi('/datasets/generate-docs', {
env_id: this.selectedEnv,
dataset_ids: [...this.selectedIds],
llm_provider: this.llmProvider,
llm_options: this.llmOptions,
}) as MapColumnsResponse;
log('DatasetsHubModel', 'REFLECT', 'Bulk docs generation started', { taskId: resp.task_id });
this.showGenerateDocsModal = false;
this.selectedIds = new SvelteSet();
} catch (e: unknown) {
log('DatasetsHubModel', 'EXPLORE', 'Bulk docs failed', {}, String(e));
}
}
}
// #endregion DatasetsHubModel

View File

@@ -151,42 +151,42 @@ export class GitStatusModel {
const _t = tt();
if (syncState === 'SYNCED') {
return {
dotClass: 'bg-emerald-500',
pillClass: 'bg-emerald-100 text-emerald-800 border-emerald-200',
dotClass: 'bg-success',
pillClass: 'bg-success-light text-success border-success',
label: (_t?.git as Record<string, Record<string, string>>)?.repo_status?.synced || 'Synced',
};
}
if (syncState === 'CHANGES') {
return {
dotClass: 'bg-amber-500',
pillClass: 'bg-amber-100 text-amber-800 border-amber-200',
dotClass: 'bg-warning',
pillClass: 'bg-warning-light text-warning border-warning',
label: (_t?.git as Record<string, Record<string, string>>)?.repo_status?.changes || 'Modified',
};
}
if (syncState === 'AHEAD_REMOTE') {
return {
dotClass: 'bg-blue-500',
pillClass: 'bg-blue-100 text-blue-800 border-blue-200',
dotClass: 'bg-primary-ring',
pillClass: 'bg-primary-light text-primary border-primary-ring',
label: (_t?.git as Record<string, Record<string, string>>)?.repo_status?.ahead_remote || 'Ahead',
};
}
if (syncState === 'BEHIND_REMOTE') {
return {
dotClass: 'bg-indigo-500',
pillClass: 'bg-indigo-100 text-indigo-800 border-indigo-200',
dotClass: 'bg-primary',
pillClass: 'bg-primary-light text-primary border-primary-ring',
label: (_t?.git as Record<string, Record<string, string>>)?.repo_status?.behind_remote || 'Behind',
};
}
if (syncState === 'DIVERGED') {
return {
dotClass: 'bg-rose-500',
pillClass: 'bg-rose-100 text-rose-800 border-rose-200',
dotClass: 'bg-destructive-light',
pillClass: 'bg-destructive-light text-destructive border-destructive-ring',
label: (_t?.git as Record<string, Record<string, string>>)?.repo_status?.diverged || 'Diverged',
};
}
return {
dotClass: 'bg-slate-400',
pillClass: 'bg-slate-100 text-slate-700 border-slate-200',
dotClass: 'bg-border-strong',
pillClass: 'bg-surface-muted text-text border-border',
label: (_t?.git as Record<string, Record<string, string>>)?.repo_status?.no_repo || 'Uninitialized',
};
});

View File

@@ -0,0 +1,201 @@
// #region LLMReportModel [C:3] [TYPE Model] [SEMANTICS llm,report,validation,screenshot,model]
// @BRIEF Screen model for the LLM validation report page — loads task details, logs, and thumbnail screenshots.
// @INVARIANT taskId is derived from route params; loadReport() must only fire when taskId is truthy.
// @INVARIANT screenshot loading is token-gated to prevent stale blob URLs from overwriting valid ones.
// @STATE idle — No taskId available, nothing loaded.
// @STATE loading — API calls in progress, skeleton visible.
// @STATE loaded — Task data and logs fetched, report rendered.
// @STATE error — API call failed, error message displayed.
// @ACTION loadReport() — Fetches task + logs from API.
// @ACTION loadScreenshotBlobs() — Fetches blob URLs for screenshot paths.
// @ACTION openScreenshot(path) — Opens screenshot blob URL in new tab.
// @ACTION cleanupBlobs() — Revokes all screenshot blob URLs.
// @ACTION refresh() — Re-runs loadReport().
// @RELATION DEPENDS_ON -> [api]
// @RELATION CALLS -> [log]
import { api } from '$lib/api.js';
import { log } from '$lib/cot-logger';
import type { TaskLogEntry } from '$lib/api.js';
// ── Types ───────────────────────────────────────────────────────
type ScreenState = 'idle' | 'loading' | 'loaded' | 'error';
interface TimingInfo {
validation_duration_ms?: number;
[key: string]: unknown;
}
interface CheckIssue {
severity?: string;
message?: string;
location?: string;
}
interface TaskResult {
status?: string;
summary?: string;
issues?: CheckIssue[];
screenshot_paths?: string[];
screenshot_path?: string;
logs_sent_to_llm?: string[];
timings?: TimingInfo;
}
interface TaskData {
id: string;
status?: string;
started_at?: string;
finished_at?: string;
result?: TaskResult;
}
interface CheckResult {
label: string;
level: 'fail' | 'warn' | 'pass' | 'unknown';
icon: string;
}
export class LLMReportModel {
// ── Atoms ─────────────────────────────────────────────────────
task: TaskData | null = $state(null);
logs: TaskLogEntry[] = $state([]);
screenState: ScreenState = $state('idle');
error: string | null = $state(null);
screenshotBlobUrls: Record<string, string> = $state({});
screenshotLoadErrors: Record<string, string> = $state({});
private _screenshotLoadToken: number = $state(0);
// Externally set
taskId: string = $state('');
// ── Derived ───────────────────────────────────────────────────
result = $derived<TaskResult>(this.task?.result || {});
checkResult = $derived<CheckResult>(this._getDashboardCheckResult(this.result));
timings = $derived<TimingInfo>(this.result?.timings || {});
screenshotPaths = $derived<string[]>(
Array.isArray(this.result?.screenshot_paths) && this.result.screenshot_paths.length > 0
? this.result.screenshot_paths
: this.result?.screenshot_path
? [this.result.screenshot_path]
: [],
);
sentLogs = $derived<string[]>(
Array.isArray(this.result?.logs_sent_to_llm) ? this.result.logs_sent_to_llm : [],
);
// ── Actions ───────────────────────────────────────────────────
async loadReport(): Promise<void> {
if (!this.taskId) return;
log('LLMReportModel', 'REASON', 'Loading LLM report', { taskId: this.taskId });
this.screenState = 'loading';
this.error = null;
try {
const [taskPayload, logsPayload] = await Promise.all([
api.getTask(this.taskId),
api.getTaskLogs(this.taskId, { limit: 1000 }),
]);
this.task = taskPayload as TaskData;
this.logs = Array.isArray(logsPayload) ? logsPayload : [];
this.screenState = 'loaded';
log('LLMReportModel', 'REFLECT', 'LLM report loaded', {
taskId: this.taskId,
status: this.task?.status,
logCount: this.logs.length,
});
} catch (err: unknown) {
this.error = err instanceof Error ? err.message : 'Failed to load LLM report';
this.screenState = 'error';
log('LLMReportModel', 'EXPLORE', 'Failed to load LLM report', { taskId: this.taskId }, String(this.error));
}
}
async loadScreenshotBlobs(paths: string[]): Promise<void> {
const currentToken = ++this._screenshotLoadToken;
this._cleanupBlobUrls();
this.screenshotLoadErrors = {};
if (!Array.isArray(paths) || paths.length === 0) return;
const nextUrls: Record<string, string> = {};
const nextErrors: Record<string, string> = {};
await Promise.all(
paths.map(async (path) => {
try {
const blob = await api.getStorageFileBlob(path);
nextUrls[path] = URL.createObjectURL(blob);
} catch (err: unknown) {
nextErrors[path] = err instanceof Error ? err.message : 'Failed to load screenshot';
}
}),
);
if (currentToken !== this._screenshotLoadToken) {
for (const url of Object.values(nextUrls)) URL.revokeObjectURL(url);
return;
}
this.screenshotBlobUrls = nextUrls;
this.screenshotLoadErrors = nextErrors;
}
openScreenshot(path: string): void {
const url = this.screenshotBlobUrls[path];
if (!url) return;
window.open(url, '_blank', 'noopener,noreferrer');
}
cleanupBlobs(): void {
this._cleanupBlobUrls();
}
refresh(): void {
this.loadReport();
}
// ── Private ───────────────────────────────────────────────────
private _cleanupBlobUrls(): void {
for (const url of Object.values(this.screenshotBlobUrls)) {
if (url) URL.revokeObjectURL(url);
}
this.screenshotBlobUrls = {};
}
private _getDashboardCheckResult(resultPayload: TaskResult): CheckResult {
const rawStatus = String(resultPayload?.status || '').toUpperCase();
if (rawStatus === 'FAIL') return { label: 'FAIL', level: 'fail', icon: '!' };
if (rawStatus === 'WARN') return { label: 'WARN', level: 'warn', icon: '!' };
if (rawStatus === 'PASS') return { label: 'PASS', level: 'pass', icon: 'OK' };
return { label: 'UNKNOWN', level: 'unknown', icon: '?' };
}
// ── Static utils (pure, no state access) ──────────────────────
static formatDate(value: unknown): string {
if (!value) return '-';
const parsed = new Date(value as string);
if (Number.isNaN(parsed.getTime())) return '-';
return `${parsed.toLocaleDateString()} ${parsed.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}`;
}
static formatMs(value: unknown): string {
const ms = Number(value);
if (!Number.isFinite(ms) || ms < 0) return '-';
if (ms < 1000) return `${ms} ms`;
return `${(ms / 1000).toFixed(2)} s`;
}
static getCheckResultClasses(level: CheckResult['level']): string {
if (level === 'fail') return 'bg-destructive-light text-destructive border-destructive-ring';
if (level === 'warn') return 'bg-warning-light text-warning border-warning';
if (level === 'pass') return 'bg-success-light text-success border-success';
return 'bg-surface-muted text-text border-border';
}
}
// #endregion LLMReportModel