semantic update

This commit is contained in:
2026-02-23 13:15:48 +03:00
parent 008b6d72c9
commit 26880d2e09
29 changed files with 5134 additions and 958 deletions

View File

@@ -14,6 +14,14 @@
-->
<!-- [DEF:layout:Module] -->
<!--
@TIER: STANDARD
@SEMANTICS: app-layout, auth-gating, navigation-shell
@PURPOSE: Bind global layout shell and conditional login/full-app rendering.
@LAYER: UI
@RELATION: BINDS_TO -> frontend.src.lib.components.layout.Sidebar
@INVARIANT: Login route bypasses shell; all other routes are wrapped by ProtectedRoute.
-->
<script>
import '../app.css';
import Navbar from '../components/Navbar.svelte';

View File

@@ -1,232 +0,0 @@
<!-- [DEF:TaskManagementPage:Component] -->
<!--
@SEMANTICS: tasks, management, history, logs
@PURPOSE: Page for managing and monitoring tasks.
@LAYER: Page
@RELATION: USES -> TaskList
@RELATION: USES -> TaskLogViewer
-->
<script>
import { onMount, onDestroy } from 'svelte';
import { getTasks, getTask } from '../../lib/api';
import TaskList from '../../components/TaskList.svelte';
import TaskLogViewer from '../../components/TaskLogViewer.svelte';
import TaskResultPanel from '../../components/tasks/TaskResultPanel.svelte';
import { t } from '$lib/i18n';
import { PageHeader } from '$lib/ui';
let tasks = [];
let loading = true;
let error = '';
let selectedTaskId = null;
let selectedTask = null;
let pollInterval;
let taskTypeFilter = 'all';
let pageSize = 20;
let currentPage = 1;
let hasNextPage = false;
const TASK_TYPE_OPTIONS = [
{ value: 'all', label: 'Все типы' },
{ value: 'llm_validation', label: 'LLM проверки' },
{ value: 'backup', label: 'Бэкапы' },
{ value: 'migration', label: 'Миграции' }
];
const PAGE_SIZE_OPTIONS = [10, 20, 50];
// [DEF:loadInitialData:Function]
/**
* @purpose Loads tasks and environments on page initialization.
* @pre API must be reachable.
* @post tasks and environments variables are populated.
*/
async function loadTasks({ silent = false } = {}) {
try {
if (!silent) loading = true;
error = '';
const tasksData = await getTasks({
limit: pageSize + 1,
offset: (currentPage - 1) * pageSize,
completed_only: true,
task_type: taskTypeFilter === 'all' ? undefined : taskTypeFilter
});
if (Array.isArray(tasksData)) {
hasNextPage = tasksData.length > pageSize;
tasks = hasNextPage ? tasksData.slice(0, pageSize) : tasksData;
} else {
tasks = [];
hasNextPage = false;
}
if (selectedTaskId) {
const taskFromPage = tasks.find((task) => task.id === selectedTaskId);
if (taskFromPage) {
selectedTask = taskFromPage;
} else {
selectedTask = await getTask(selectedTaskId);
}
}
} catch (err) {
console.error(`[loadTasks][Coherence:Failed] Failed to load tasks data context={{'error': '${err.message}'}}`);
error = err.message || 'Не удалось загрузить задачи';
} finally {
if (!silent) loading = false;
}
}
// [/DEF:loadTasks:Function]
// [DEF:refreshTasks:Function]
/**
* @purpose Periodically refreshes the task list.
* @pre API must be reachable.
* @post tasks variable is updated if data is valid.
*/
async function refreshTasks() {
await loadTasks({ silent: true });
}
// [/DEF:refreshTasks:Function]
// [DEF:handleSelectTask:Function]
/**
* @purpose Updates the selected task ID when a task is clicked.
* @pre event.detail.id must be provided.
* @post selectedTaskId is updated.
*/
function handleSelectTask(event) {
selectedTaskId = event.detail.id;
selectedTask = event.detail.task || null;
console.log(`[handleSelectTask][Action] Task selected context={{'taskId': '${selectedTaskId}'}}`);
}
// [/DEF:handleSelectTask:Function]
onMount(() => {
loadTasks();
pollInterval = setInterval(refreshTasks, 3000);
});
onDestroy(() => {
if (pollInterval) clearInterval(pollInterval);
});
function handleTaskTypeChange() {
currentPage = 1;
selectedTaskId = null;
selectedTask = null;
loadTasks();
}
function handlePageSizeChange() {
currentPage = 1;
loadTasks();
}
function goToPrevPage() {
if (currentPage === 1) return;
currentPage -= 1;
loadTasks();
}
function goToNextPage() {
if (!hasNextPage) return;
currentPage += 1;
loadTasks();
}
</script>
<div class="container mx-auto p-4 max-w-6xl">
<PageHeader title={$t.tasks.management} />
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
<div class="lg:col-span-1">
<div class="rounded-lg border border-slate-200 bg-white p-3 lg:sticky lg:top-20">
<div class="mb-3 flex items-center justify-between gap-2">
<h2 class="text-lg font-semibold text-gray-700">Результаты задач</h2>
<button
on:click={() => loadTasks()}
class="rounded-md border border-slate-200 px-2 py-1 text-xs font-medium text-slate-700 hover:bg-slate-50"
>
{$t.tasks?.refresh || 'Обновить'}
</button>
</div>
<div class="mb-3 grid grid-cols-2 gap-2">
<select
bind:value={taskTypeFilter}
on:change={handleTaskTypeChange}
class="rounded-md border border-gray-300 bg-white px-2 py-1 text-sm text-gray-700 focus:border-blue-500 focus:outline-none"
>
{#each TASK_TYPE_OPTIONS as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
<select
bind:value={pageSize}
on:change={handlePageSizeChange}
class="rounded-md border border-gray-300 bg-white px-2 py-1 text-sm text-gray-700 focus:border-blue-500 focus:outline-none"
>
{#each PAGE_SIZE_OPTIONS as size}
<option value={size}>{size} / стр</option>
{/each}
</select>
</div>
{#if error}
<div class="mb-3 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700">
{error}
</div>
{/if}
<div class="max-h-[58vh] overflow-y-auto rounded-md border border-slate-100">
<TaskList tasks={tasks} {loading} {selectedTaskId} on:select={handleSelectTask} />
</div>
<div class="mt-3 flex items-center justify-between gap-2 text-sm">
<button
on:click={goToPrevPage}
disabled={currentPage === 1}
class="rounded-md border border-slate-200 px-3 py-1.5 text-slate-700 disabled:cursor-not-allowed disabled:opacity-50 hover:bg-slate-50"
>
Назад
</button>
<span class="text-slate-500">Страница {currentPage}</span>
<button
on:click={goToNextPage}
disabled={!hasNextPage}
class="rounded-md border border-slate-200 px-3 py-1.5 text-slate-700 disabled:cursor-not-allowed disabled:opacity-50 hover:bg-slate-50"
>
Вперед
</button>
</div>
</div>
</div>
<div class="lg:col-span-2">
<h2 class="text-lg font-semibold mb-3 text-gray-700">Результат и логи</h2>
{#if selectedTaskId}
<div class="space-y-4 min-h-[60vh]">
<TaskResultPanel task={selectedTask} />
<div class="rounded-lg border border-slate-200 bg-white">
<div class="border-b border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700">
Логи задачи
</div>
<div class="h-[56vh] min-h-[360px] flex flex-col overflow-hidden rounded-b-lg">
<TaskLogViewer
taskId={selectedTaskId}
taskStatus={selectedTask?.status}
inline={true}
/>
</div>
</div>
</div>
{:else}
<div class="bg-gray-50 border-2 border-dashed border-gray-100 rounded-lg h-[60vh] min-h-[380px] flex items-center justify-center text-gray-400">
<p>Выберите задачу из списка слева</p>
</div>
{/if}
</div>
</div>
</div>
<!-- [/DEF:TaskManagementPage:Component] -->