таски готовы

This commit is contained in:
2026-02-23 10:18:56 +03:00
parent 2946ee9b42
commit 066ef5eab5
48 changed files with 3559 additions and 72 deletions

View File

@@ -0,0 +1,194 @@
<!-- [DEF:UnifiedReportsPage:Component] -->
<script>
/**
* @TIER: CRITICAL
* @SEMANTICS: reports, unified, filters, loading, empty, error
* @PURPOSE: Unified reports page with filtering and resilient UX states for mixed task types.
* @LAYER: UI
* @RELATION: DEPENDS_ON -> frontend/src/lib/api/reports.js
* @RELATION: DEPENDS_ON -> frontend/src/lib/components/reports/ReportsList.svelte
* @INVARIANT: List state remains deterministic for active filter set.
*
* @UX_STATE: Loading -> Skeleton-like block shown; filters visible.
* @UX_STATE: Ready -> Reports list rendered.
* @UX_STATE: NoData -> Friendly empty state for total=0 without filters.
* @UX_STATE: FilteredEmpty -> Filtered empty state with one-click clear.
* @UX_STATE: Error -> Inline error with retry preserving filters.
* @UX_FEEDBACK: Filter change reloads list immediately.
* @UX_RECOVERY: Retry and clear filters actions available.
*/
import { onMount } from 'svelte';
import { t } from '$lib/i18n';
import { PageHeader } from '$lib/ui';
import { getReports, getReportDetail } from '$lib/api/reports.js';
import ReportsList from '$lib/components/reports/ReportsList.svelte';
import ReportDetailPanel from '$lib/components/reports/ReportDetailPanel.svelte';
let loading = true;
let error = '';
let collection = null;
let selectedReport = null;
let selectedReportDetail = null;
let taskType = 'all';
let status = 'all';
let page = 1;
const pageSize = 20;
const TASK_TYPE_OPTIONS = [
{ value: 'all', label: $t.reports?.all_types || 'All types' },
{ value: 'llm_verification', label: 'LLM' },
{ value: 'backup', label: $t.nav?.backups || 'Backups' },
{ value: 'migration', label: $t.nav?.migration || 'Migration' },
{ value: 'documentation', label: 'Documentation' }
];
const STATUS_OPTIONS = [
{ value: 'all', label: $t.reports?.all_statuses || 'All statuses' },
{ value: 'success', label: 'Success' },
{ value: 'failed', label: 'Failed' },
{ value: 'in_progress', label: 'In progress' },
{ value: 'partial', label: 'Partial' }
];
function buildQuery() {
return {
page,
page_size: pageSize,
task_types: taskType === 'all' ? [] : [taskType],
statuses: status === 'all' ? [] : [status],
sort_by: 'updated_at',
sort_order: 'desc'
};
}
async function loadReports({ silent = false } = {}) {
try {
if (!silent) loading = true;
error = '';
collection = await getReports(buildQuery());
if (!selectedReport && collection?.items?.length) {
selectedReport = collection.items[0];
selectedReportDetail = await getReportDetail(selectedReport.report_id);
}
} catch (e) {
error = e?.message || 'Failed to load reports';
collection = null;
} finally {
if (!silent) loading = false;
}
}
function hasActiveFilters() {
return taskType !== 'all' || status !== 'all';
}
function clearFilters() {
taskType = 'all';
status = 'all';
page = 1;
selectedReport = null;
selectedReportDetail = null;
loadReports();
}
function onFilterChange() {
page = 1;
selectedReport = null;
selectedReportDetail = null;
loadReports();
}
async function onSelectReport(event) {
selectedReport = event.detail.report;
selectedReportDetail = await getReportDetail(selectedReport.report_id);
}
onMount(() => {
loadReports();
});
</script>
<div class="container mx-auto max-w-6xl p-4">
<PageHeader
title={$t.reports?.title || 'Reports'}
subtitle={() => null}
actions={() => null}
/>
<div class="mb-4 rounded-lg border border-slate-200 bg-white p-3">
<div class="grid grid-cols-1 gap-2 md:grid-cols-4">
<select
bind:value={taskType}
on:change={onFilterChange}
class="rounded-md border border-slate-300 px-2 py-1.5 text-sm"
>
{#each TASK_TYPE_OPTIONS as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
<select
bind:value={status}
on:change={onFilterChange}
class="rounded-md border border-slate-300 px-2 py-1.5 text-sm"
>
{#each STATUS_OPTIONS as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
<button
class="rounded-md border border-slate-200 px-3 py-1.5 text-sm hover:bg-slate-50"
on:click={() => loadReports()}
>
{$t.common?.refresh || 'Refresh'}
</button>
<button
class="rounded-md border border-slate-200 px-3 py-1.5 text-sm hover:bg-slate-50"
on:click={clearFilters}
>
{$t.reports?.clear_filters || 'Clear filters'}
</button>
</div>
</div>
{#if loading}
<div class="rounded-lg border border-slate-200 bg-white p-6 text-slate-500">
{$t.common?.loading || 'Loading...'}
</div>
{:else if error}
<div class="rounded-lg border border-red-200 bg-red-50 p-4 text-red-700">
<p>{error}</p>
<button class="mt-2 rounded border border-red-300 px-3 py-1 text-sm" on:click={() => loadReports()}>
{$t.common?.retry || 'Retry'}
</button>
</div>
{:else if !collection || collection.total === 0}
<div class="rounded-lg border border-slate-200 bg-white p-6 text-slate-500">
{$t.reports?.empty || 'No reports available.'}
</div>
{:else if collection.items.length === 0 && hasActiveFilters()}
<div class="rounded-lg border border-slate-200 bg-white p-6 text-slate-500">
<p>{$t.reports?.filtered_empty || 'No reports match your filters.'}</p>
<button class="mt-2 rounded border border-slate-200 px-3 py-1 text-sm hover:bg-slate-50" on:click={clearFilters}>
{$t.reports?.clear_filters || 'Clear filters'}
</button>
</div>
{:else}
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
<div class="lg:col-span-2">
<ReportsList
reports={collection?.items || []}
selectedReportId={selectedReport?.report_id}
on:select={onSelectReport}
/>
</div>
<ReportDetailPanel detail={selectedReportDetail} />
</div>
{/if}
</div>
<!-- [/DEF:UnifiedReportsPage:Component] -->