feat: restore legacy data and add typed task result views

This commit is contained in:
2026-02-21 23:17:56 +03:00
parent 0cf0ef25f1
commit 6ffdf5f8a4
10 changed files with 411 additions and 195 deletions

View File

@@ -8,20 +8,26 @@
-->
<script>
import { onMount, onDestroy } from 'svelte';
import { getTasks, createTask, getEnvironmentsList } from '../../lib/api';
import { addToast } from '../../lib/toasts';
import { getTasks } 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 { Button, Card, PageHeader, Select } from '$lib/ui';
import { PageHeader } from '$lib/ui';
let tasks = [];
let environments = [];
let loading = true;
let selectedTaskId = null;
let selectedTask = null;
let pollInterval;
let showBackupModal = false;
let selectedEnvId = '';
let taskTypeFilter = 'all';
const TASK_TYPE_OPTIONS = [
{ value: 'all', label: 'Все типы' },
{ value: 'llm_validation', label: 'LLM проверки' },
{ value: 'backup', label: 'Бэкапы' },
{ value: 'migration', label: 'Миграции' }
];
// [DEF:loadInitialData:Function]
/**
@@ -30,16 +36,19 @@
* @post tasks and environments variables are populated.
*/
async function loadInitialData() {
console.log("[loadInitialData][Action] Loading initial tasks and environments");
console.log("[loadInitialData][Action] Loading completed tasks");
try {
loading = true;
const [tasksData, envsData] = await Promise.all([
getTasks(),
getEnvironmentsList()
]);
const tasksData = await getTasks({
limit: 100,
completed_only: true,
task_type: taskTypeFilter === 'all' ? undefined : taskTypeFilter
});
tasks = tasksData;
environments = envsData;
console.log(`[loadInitialData][Coherence:OK] Data loaded context={{'tasks': ${tasks.length}, 'envs': ${environments.length}}}`);
console.log(`[loadInitialData][Coherence:OK] Data loaded context={{'tasks': ${tasks.length}}}`);
if (selectedTaskId && !tasks.some((task) => task.id === selectedTaskId)) {
selectedTaskId = null;
}
} catch (error) {
console.error(`[loadInitialData][Coherence:Failed] Failed to load tasks data context={{'error': '${error.message}'}}`);
} finally {
@@ -56,7 +65,11 @@
*/
async function refreshTasks() {
try {
const data = await getTasks();
const data = await getTasks({
limit: 100,
completed_only: true,
task_type: taskTypeFilter === 'all' ? undefined : taskTypeFilter
});
// Ensure we don't try to parse HTML as JSON if the route returns 404
if (Array.isArray(data)) {
tasks = data;
@@ -79,32 +92,6 @@
}
// [/DEF:handleSelectTask:Function]
// [DEF:handleRunBackup:Function]
/**
* @purpose Triggers a manual backup task for the selected environment.
* @pre selectedEnvId must not be empty.
* @post Backup task is created and task list is refreshed.
*/
async function handleRunBackup() {
if (!selectedEnvId) {
addToast('Please select an environment', 'error');
return;
}
console.log(`[handleRunBackup][Action] Starting backup for env context={{'envId': '${selectedEnvId}'}}`);
try {
const task = await createTask('superset-backup', { environment_id: selectedEnvId });
addToast('Backup task started', 'success');
showBackupModal = false;
selectedTaskId = task.id;
await refreshTasks();
console.log(`[handleRunBackup][Coherence:OK] Backup task created context={{'taskId': '${task.id}'}}`);
} catch (error) {
console.error(`[handleRunBackup][Coherence:Failed] Failed to start backup context={{'error': '${error.message}'}}`);
}
}
// [/DEF:handleRunBackup:Function]
onMount(() => {
loadInitialData();
pollInterval = setInterval(refreshTasks, 3000);
@@ -113,6 +100,13 @@
onDestroy(() => {
if (pollInterval) clearInterval(pollInterval);
});
function handleTaskTypeChange() {
selectedTaskId = null;
loadInitialData();
}
$: selectedTask = tasks.find((task) => task.id === selectedTaskId) || null;
</script>
<div class="container mx-auto p-4 max-w-6xl">
@@ -120,57 +114,46 @@
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-1">
<h2 class="text-lg font-semibold mb-3 text-gray-700">{$t.tasks.recent}</h2>
<div class="mb-3 flex items-center justify-between gap-2">
<h2 class="text-lg font-semibold text-gray-700">Результаты задач</h2>
<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>
</div>
<TaskList {tasks} {loading} on:select={handleSelectTask} />
</div>
<div class="lg:col-span-2">
<h2 class="text-lg font-semibold mb-3 text-gray-700">{$t.tasks.details_logs}</h2>
<h2 class="text-lg font-semibold mb-3 text-gray-700">Результат и логи</h2>
{#if selectedTaskId}
<Card padding="none">
<div class="h-[600px] flex flex-col overflow-hidden rounded-lg">
<TaskLogViewer
taskId={selectedTaskId}
taskStatus={tasks.find(t => t.id === selectedTaskId)?.status}
inline={true}
/>
<div class="space-y-4">
<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-[420px] flex flex-col overflow-hidden rounded-b-lg">
<TaskLogViewer
taskId={selectedTaskId}
taskStatus={selectedTask?.status}
inline={true}
/>
</div>
</div>
</Card>
</div>
{:else}
<div class="bg-gray-50 border-2 border-dashed border-gray-100 rounded-lg h-[600px] flex items-center justify-center text-gray-400">
<p>{$t.tasks.select_task}</p>
<p>Выберите задачу из списка слева</p>
</div>
{/if}
</div>
</div>
</div>
{#if showBackupModal}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/50 backdrop-blur-sm p-4">
<div class="w-full max-w-md">
<Card title={$t.tasks.manual_backup}>
<div class="space-y-6">
<Select
label={$t.tasks.target_env}
bind:value={selectedEnvId}
options={[
{ value: '', label: $t.tasks.select_env },
...environments.map(e => ({ value: e.id, label: e.name }))
]}
/>
<div class="flex justify-end gap-3 pt-2">
<Button variant="secondary" on:click={() => showBackupModal = false}>
{$t.common.cancel}
</Button>
<Button variant="primary" on:click={handleRunBackup}>
Start Backup
</Button>
</div>
</div>
</Card>
</div>
</div>
{/if}
<!-- [/DEF:TaskManagementPage:Component] -->
<!-- [/DEF:TaskManagementPage:Component] -->