// #region ValidationTasks.ListModel [C:4] [TYPE Model] [SEMANTICS validation,tasks,list,pagination,crud,model] // @ingroup ValidationTasks // @BRIEF Screen model for validation tasks list — manages paginated list, search, inline actions (run/delete/toggle), and navigation. // @RATIONALE Svelte 5 $state/$derived model-first pattern. Uses map-based array update instead of self-assignment hack for reactivity. // @INVARIANT Search resets pagination to page 1. // @INVARIANT Delete removes task from list and decrements total atomically. // @INVARIANT isMounted prevents state mutation after component unmount. // @STATE idle — Initial state, no data loaded. // @STATE loading — API call in progress. // @STATE loaded — Data fetched, list populated. // @STATE empty — Query returned zero results. // @STATE error — API call failed. // @ACTION loadTasks() — Fetches paginated task list. // @ACTION handleSearchInput() — Debounced search with pagination reset. // @ACTION handleRunNow(id) — Triggers validation run. // @ACTION handleDelete(id) — Deletes task with atomic list removal. // @ACTION handleToggleStatus(task) — Toggles is_active. // @ACTION goToPage(p) — Navigates to page. // @RELATION DEPENDS_ON -> [ApiModule] // @RELATION CALLS -> [CotLogger] import { goto } from '$app/navigation'; import { ROUTES } from '$lib/routes'; import { api } from '$lib/api.js'; import { log } from '$lib/cot-logger'; import { addToast } from '$lib/toasts.svelte.js'; import { getT } from '$lib/i18n/index.svelte.js'; // ── Types ─────────────────────────────────────────────────────── interface ValidationTask { id: string; name?: string; plugin_id?: string; is_active?: boolean; schedule?: string; [key: string]: unknown; } interface TaskListData { tasks?: ValidationTask[]; total?: number; page?: number; page_size?: number; error?: string | null; } export class ValidationTasksListModel { // ── Data from load function ────────────────────────────────── tasks: ValidationTask[] = $state([]); total: number = $state(0); currentPage: number = $state(1); pageSize: number = $state(20); error: string | null = $state(null); isLoading: boolean = $state(false); searchQuery: string = $state(''); // ── UI state ────────────────────────────────────────────────── runningTasks: Record = $state({}); deleteConfirmId: string | null = $state(null); isMounted: boolean = $state(true); // ── Internal ────────────────────────────────────────────────── private _searchTimeout: ReturnType | undefined; // ── Derived ─────────────────────────────────────────────────── totalPages = $derived(Math.max(1, Math.ceil(this.total / this.pageSize))); // ── Init from load data ────────────────────────────────────── initFromLoad(data: TaskListData): void { this.tasks = data.tasks ?? []; this.total = data.total ?? 0; this.currentPage = data.page ?? 1; this.pageSize = data.page_size ?? 20; this.error = data.error ?? null; } // ── Actions ─────────────────────────────────────────────────── async loadTasks(): Promise { this.isLoading = true; this.error = null; log('ValidationTasksListModel', 'REASON', 'Loading validation tasks', { page: this.currentPage, pageSize: this.pageSize, search: this.searchQuery || undefined }); try { const result = await api.getValidationTasks({ page: this.currentPage, page_size: this.pageSize, search: this.searchQuery || undefined }); if (!this.isMounted) return; const items = (result as { tasks?: ValidationTask[]; results?: ValidationTask[] })?.tasks ?? (result as { tasks?: ValidationTask[]; results?: ValidationTask[] })?.results ?? []; this.tasks = items; this.total = (result as { total?: number })?.total ?? 0; log('ValidationTasksListModel', 'REFLECT', 'Tasks loaded', { count: items.length, total: this.total }); } catch (e: unknown) { if (!this.isMounted) return; this.error = e instanceof Error ? e.message : 'Failed to load tasks'; log('ValidationTasksListModel', 'EXPLORE', 'Failed to load tasks', {}, String(this.error)); } finally { if (this.isMounted) this.isLoading = false; } } handleSearchInput(value: string): void { this.searchQuery = value; if (this._searchTimeout) clearTimeout(this._searchTimeout); this._searchTimeout = setTimeout(() => { this.currentPage = 1; this.loadTasks(); }, 300); } goToPage(p: number): void { if (p < 1 || p > this.totalPages) return; this.currentPage = p; this.loadTasks(); } // ── Navigation ──────────────────────────────────────────────── navToNew(): void { goto(ROUTES.validationTasks.new(), { invalidateAll: false }); } navToDetail(id: string): void { goto(ROUTES.validationTasks.detail(id), { invalidateAll: false }); } navToEdit(id: string): void { goto(ROUTES.validationTasks.edit(id), { invalidateAll: false }); } // ── CRUD Actions ───────────────────────────────────────────── async handleRunNow(id: string): Promise { this.runningTasks = { ...this.runningTasks, [id]: true }; log('ValidationTasksListModel', 'REASON', 'Triggering run', { taskId: id }); try { await api.triggerValidationRun(id); if (!this.isMounted) return; addToast(getT()?.validation?.run_started || 'Validation task started', 'success'); log('ValidationTasksListModel', 'REFLECT', 'Run triggered', { taskId: id }); } catch (e: unknown) { if (!this.isMounted) return; addToast(e instanceof Error ? e.message : 'Failed to trigger run', 'error'); log('ValidationTasksListModel', 'EXPLORE', 'Failed to trigger run', { taskId: id }, String(e)); } finally { if (this.isMounted) this.runningTasks = { ...this.runningTasks, [id]: false }; } } async handleDelete(id: string): Promise { log('ValidationTasksListModel', 'REASON', 'Deleting task', { taskId: id }); try { await api.deleteValidationTask(id); if (!this.isMounted) return; addToast(getT()?.validation?.delete_success || 'Task deleted', 'success'); this.deleteConfirmId = null; this.tasks = this.tasks.filter(t => t.id !== id); this.total = Math.max(0, this.total - 1); log('ValidationTasksListModel', 'REFLECT', 'Task deleted', { taskId: id }); } catch (e: unknown) { if (!this.isMounted) return; addToast(e instanceof Error ? e.message : 'Failed to delete', 'error'); this.deleteConfirmId = null; log('ValidationTasksListModel', 'EXPLORE', 'Failed to delete task', { taskId: id }, String(e)); } } async handleToggleStatus(task: ValidationTask): Promise { const newActive = !(task.is_active !== false); log('ValidationTasksListModel', 'REASON', 'Toggling status', { taskId: task.id, to: newActive }); try { await api.toggleValidationTaskStatus(task.id, newActive); if (!this.isMounted) return; task.is_active = newActive; this.tasks = this.tasks.map(t => t.id === task.id ? { ...t, is_active: newActive } : t); log('ValidationTasksListModel', 'REFLECT', 'Status toggled', { taskId: task.id }); } catch (e: unknown) { if (!this.isMounted) return; addToast(e instanceof Error ? e.message : 'Failed to update status', 'error'); log('ValidationTasksListModel', 'EXPLORE', 'Failed to toggle status', { taskId: task.id }, String(e)); } } destroy(): void { this.isMounted = false; } } // #endregion ValidationTasks.ListModel