таски готовы

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,63 @@
<!-- [DEF:ReportCard:Component] -->
<script>
/**
* @TIER: CRITICAL
* @SEMANTICS: reports, card, type-profile, accessibility, fallback
* @PURPOSE: Render one report with explicit textual type label and profile-driven visual variant.
* @LAYER: UI
* @RELATION: DEPENDS_ON -> frontend/src/lib/components/reports/reportTypeProfiles.js
* @RELATION: DEPENDS_ON -> frontend/src/lib/i18n/index.ts
* @INVARIANT: Unknown task type always uses fallback profile.
*
* @UX_STATE: Ready -> Card displays summary/status/type.
* @UX_RECOVERY: Missing fields are rendered with explicit placeholder text.
*/
import { createEventDispatcher } from 'svelte';
import { t } from '$lib/i18n';
import { getReportTypeProfile } from './reportTypeProfiles.js';
let { report, selected = false } = $props();
const dispatch = createEventDispatcher();
const profile = $derived(getReportTypeProfile(report?.task_type));
const profileLabel = $derived(typeof profile?.label === 'function' ? profile.label() : profile?.label);
function getStatusClass(status) {
if (status === 'success') return 'bg-green-100 text-green-700';
if (status === 'failed') return 'bg-red-100 text-red-700';
if (status === 'in_progress') return 'bg-blue-100 text-blue-700';
if (status === 'partial') return 'bg-amber-100 text-amber-700';
return 'bg-slate-100 text-slate-700';
}
function formatDate(value) {
if (!value) return $t.reports?.not_provided || 'Not provided';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return $t.reports?.not_provided || 'Not provided';
return date.toLocaleString();
}
function onSelect() {
dispatch('select', { report });
}
</script>
<button
class="w-full rounded-lg border p-3 text-left transition hover:bg-slate-50 {selected ? 'border-blue-400 bg-blue-50' : 'border-slate-200 bg-white'}"
on:click={onSelect}
aria-label={`Report ${report?.report_id || ''} type ${profileLabel || ($t.reports?.unknown_type || 'Other / Unknown Type')}`}
>
<div class="mb-2 flex items-center justify-between gap-2">
<span class="rounded px-2 py-0.5 text-xs font-semibold {profile?.variant || 'bg-slate-100 text-slate-700'}">
{profileLabel || ($t.reports?.unknown_type || 'Other / Unknown Type')}
</span>
<span class="rounded px-2 py-0.5 text-xs font-semibold {getStatusClass(report?.status)}">
{report?.status || ($t.reports?.not_provided || 'Not provided')}
</span>
</div>
<p class="text-sm font-medium text-slate-800">{report?.summary || ($t.reports?.not_provided || 'Not provided')}</p>
<p class="mt-1 text-xs text-slate-500">{formatDate(report?.updated_at)}</p>
</button>
<!-- [/DEF:ReportCard:Component] -->

View File

@@ -0,0 +1,66 @@
<!-- [DEF:ReportDetailPanel:Component] -->
<script>
/**
* @TIER: CRITICAL
* @SEMANTICS: reports, detail, diagnostics, next-actions, placeholders
* @PURPOSE: Display detailed report context with diagnostics and actionable recovery guidance.
* @LAYER: UI
* @RELATION: DEPENDS_ON -> frontend/src/lib/i18n/index.ts
* @INVARIANT: Failed/partial reports surface actionable hints when available.
*
* @UX_STATE: Ready -> Report detail content visible.
* @UX_RECOVERY: Failed/partial report shows next actions and placeholder-safe diagnostics.
*/
import { t } from '$lib/i18n';
let { detail = null } = $props();
function notProvided(value) {
if (value === null || value === undefined || value === '') {
return $t.reports?.not_provided || 'Not provided';
}
return value;
}
function formatDate(value) {
if (!value) return $t.reports?.not_provided || 'Not provided';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return $t.reports?.not_provided || 'Not provided';
return date.toLocaleString();
}
</script>
<div class="rounded-lg border border-slate-200 bg-white p-4">
<h3 class="mb-3 text-sm font-semibold text-slate-700">{$t.reports?.view_details || 'View details'}</h3>
{#if !detail || !detail.report}
<p class="text-sm text-slate-500">{$t.reports?.not_provided || 'Not provided'}</p>
{:else}
<div class="space-y-2 text-sm">
<p><span class="text-slate-500">ID:</span> {notProvided(detail.report.report_id)}</p>
<p><span class="text-slate-500">Type:</span> {notProvided(detail.report.task_type)}</p>
<p><span class="text-slate-500">Status:</span> {notProvided(detail.report.status)}</p>
<p><span class="text-slate-500">Summary:</span> {notProvided(detail.report.summary)}</p>
<p><span class="text-slate-500">Updated:</span> {formatDate(detail.report.updated_at)}</p>
</div>
<div class="mt-4">
<p class="mb-1 text-xs font-semibold uppercase text-slate-500">Diagnostics</p>
<pre class="max-h-48 overflow-auto rounded bg-slate-50 p-2 text-xs text-slate-700">{JSON.stringify(detail.diagnostics || { note: $t.reports?.not_provided || 'Not provided' }, null, 2)}</pre>
</div>
{#if (detail.next_actions && detail.next_actions.length > 0) || (detail.report.error_context && detail.report.error_context.next_actions && detail.report.error_context.next_actions.length > 0)}
<div class="mt-4">
<p class="mb-1 text-xs font-semibold uppercase text-slate-500">Next actions</p>
<ul class="list-disc space-y-1 pl-5 text-sm text-slate-700">
{#each (detail.next_actions && detail.next_actions.length > 0 ? detail.next_actions : detail.report.error_context.next_actions) as action}
<li>{action}</li>
{/each}
</ul>
</div>
{/if}
{/if}
</div>
<!-- [/DEF:ReportDetailPanel:Component] -->

View File

@@ -0,0 +1,37 @@
<!-- [DEF:ReportsList:Component] -->
<script>
/**
* @TIER: CRITICAL
* @SEMANTICS: reports, list, card, unified, mixed-types
* @PURPOSE: Render unified list of normalized reports with canonical minimum fields.
* @LAYER: UI
* @RELATION: DEPENDS_ON -> frontend/src/lib/i18n/index.ts
* @INVARIANT: Every rendered row shows task_type label, status, summary, and updated_at.
*
* @UX_STATE: Ready -> Mixed-type list visible and scannable.
* @UX_FEEDBACK: Click on report emits select event.
* @UX_RECOVERY: Unknown/missing values rendered with explicit placeholders.
*/
import { createEventDispatcher } from 'svelte';
import ReportCard from './ReportCard.svelte';
let { reports = [], selectedReportId = null } = $props();
const dispatch = createEventDispatcher();
function handleSelect(event) {
dispatch('select', { report: event.detail.report });
}
</script>
<div class="space-y-2">
{#each reports as report (report.report_id)}
<ReportCard
{report}
selected={selectedReportId === report.report_id}
on:select={handleSelect}
/>
{/each}
</div>
<!-- [/DEF:ReportsList:Component] -->

View File

@@ -0,0 +1,90 @@
// [DEF:reports.fixtures:Module]
// @TIER: STANDARD
// @SEMANTICS: reports, fixtures, test-data
// @PURPOSE: Shared frontend fixtures for unified reports states.
// @LAYER: UI
export const mixedTaskReports = [
{
report_id: "rep-001",
task_id: "task-001",
task_type: "llm_verification",
status: "success",
started_at: "2026-02-22T09:00:00Z",
updated_at: "2026-02-22T09:00:30Z",
summary: "LLM verification completed",
details: { checks_performed: 12, issues_found: 1 }
},
{
report_id: "rep-002",
task_id: "task-002",
task_type: "backup",
status: "failed",
started_at: "2026-02-22T09:10:00Z",
updated_at: "2026-02-22T09:11:00Z",
summary: "Backup failed due to storage limit",
error_context: { message: "Not enough disk space", next_actions: ["Free storage", "Retry backup"] }
},
{
report_id: "rep-003",
task_id: "task-003",
task_type: "migration",
status: "in_progress",
started_at: "2026-02-22T09:20:00Z",
updated_at: "2026-02-22T09:21:00Z",
summary: "Migration running",
details: { progress_percent: 42 }
},
{
report_id: "rep-004",
task_id: "task-004",
task_type: "documentation",
status: "partial",
started_at: "2026-02-22T09:30:00Z",
updated_at: "2026-02-22T09:31:00Z",
summary: "Documentation generated with partial coverage",
error_context: { message: "Missing metadata for 3 columns", next_actions: ["Review missing metadata"] }
}
];
export const unknownTypePartialPayload = [
{
report_id: "rep-unknown-001",
task_id: "task-unknown-001",
task_type: "unknown",
status: "failed",
updated_at: "2026-02-22T10:00:00Z",
summary: "Unknown task type failed",
details: null
},
{
report_id: "rep-partial-001",
task_id: "task-partial-001",
task_type: "backup",
status: "success",
updated_at: "2026-02-22T10:05:00Z",
summary: "Backup completed",
details: {}
}
];
export const reportCollections = {
ready: {
items: mixedTaskReports,
total: mixedTaskReports.length,
page: 1,
page_size: 20,
has_next: false,
applied_filters: { page: 1, page_size: 20, sort_by: "updated_at", sort_order: "desc" }
},
empty: {
items: [],
total: 0,
page: 1,
page_size: 20,
has_next: false,
applied_filters: { page: 1, page_size: 20, sort_by: "updated_at", sort_order: "desc" }
}
};
// [/DEF:reports.fixtures:Module]

View File

@@ -0,0 +1,45 @@
// [DEF:frontend.src.lib.components.reports.__tests__.report_detail.integration:Module]
// @TIER: CRITICAL
// @SEMANTICS: tests, reports, detail, recovery-guidance, integration
// @PURPOSE: Validate detail-panel behavior for failed reports and recovery guidance visibility.
// @LAYER: UI (Tests)
// @RELATION: TESTS -> frontend/src/lib/components/reports/ReportDetailPanel.svelte
// @RELATION: TESTS -> frontend/src/routes/reports/+page.svelte
// @INVARIANT: Failed report detail exposes actionable next actions when available.
import { describe, it, expect } from 'vitest';
import { mixedTaskReports } from './fixtures/reports.fixtures.js';
function buildFailedDetailFixture() {
const failed = mixedTaskReports.find((item) => item.status === 'failed');
return {
report: failed,
diagnostics: {
error_context: failed?.error_context || { message: 'Not provided', next_actions: [] }
},
next_actions: failed?.error_context?.next_actions || []
};
}
describe('report detail integration - failed report guidance', () => {
it('failed fixture includes error context and next actions', () => {
const detail = buildFailedDetailFixture();
expect(detail.report).toBeTruthy();
expect(detail.report.status).toBe('failed');
expect(detail.diagnostics).toBeTruthy();
expect(Array.isArray(detail.next_actions)).toBe(true);
expect(detail.next_actions.length).toBeGreaterThan(0);
});
it('next actions are human-readable strings for operator recovery', () => {
const detail = buildFailedDetailFixture();
for (const action of detail.next_actions) {
expect(typeof action).toBe('string');
expect(action.trim().length).toBeGreaterThan(0);
}
});
});
// [/DEF:frontend.src.lib.components.reports.__tests__.report_detail.integration:Module]

View File

@@ -0,0 +1,32 @@
// [DEF:frontend.src.lib.components.reports.__tests__.report_type_profiles:Module]
// @TIER: CRITICAL
// @SEMANTICS: tests, reports, type-profiles, fallback
// @PURPOSE: Validate report type profile mapping and unknown fallback behavior.
// @LAYER: UI (Tests)
// @RELATION: TESTS -> frontend/src/lib/components/reports/reportTypeProfiles.js
// @INVARIANT: Unknown task_type always resolves to the fallback profile.
import { describe, it, expect } from 'vitest';
import { getReportTypeProfile, REPORT_TYPE_PROFILES } from '../reportTypeProfiles.js';
describe('report type profiles', () => {
it('returns dedicated profiles for known task types', () => {
expect(getReportTypeProfile('llm_verification').key).toBe('llm_verification');
expect(getReportTypeProfile('backup').key).toBe('backup');
expect(getReportTypeProfile('migration').key).toBe('migration');
expect(getReportTypeProfile('documentation').key).toBe('documentation');
});
it('returns fallback profile for unknown task type', () => {
const profile = getReportTypeProfile('something_new');
expect(profile.key).toBe('unknown');
expect(profile.fallback).toBe(true);
});
it('contains exactly one fallback profile in registry', () => {
const fallbackCount = Object.values(REPORT_TYPE_PROFILES).filter((p) => p.fallback === true).length;
expect(fallbackCount).toBe(1);
});
});
// [/DEF:frontend.src.lib.components.reports.__tests__.report_type_profiles:Module]

View File

@@ -0,0 +1,48 @@
// [DEF:frontend.src.lib.components.reports.__tests__.reports_filter_performance:Module]
// @TIER: STANDARD
// @SEMANTICS: tests, reports, performance, filtering
// @PURPOSE: Guard test for report filter responsiveness on moderate in-memory dataset.
// @LAYER: UI (Tests)
// @RELATION: TESTS -> frontend/src/routes/reports/+page.svelte
import { describe, it, expect } from 'vitest';
function applyFilters(items, { taskType = 'all', status = 'all' } = {}) {
return items.filter((item) => {
const typeMatch = taskType === 'all' || item.task_type === taskType;
const statusMatch = status === 'all' || item.status === status;
return typeMatch && statusMatch;
});
}
function makeDataset(size = 2000) {
const taskTypes = ['llm_verification', 'backup', 'migration', 'documentation'];
const statuses = ['success', 'failed', 'in_progress', 'partial'];
const out = [];
for (let i = 0; i < size; i += 1) {
out.push({
report_id: `r-${i}`,
task_id: `t-${i}`,
task_type: taskTypes[i % taskTypes.length],
status: statuses[i % statuses.length],
summary: `Report ${i}`,
updated_at: '2026-02-22T10:00:00Z'
});
}
return out;
}
describe('reports filter performance guard', () => {
it('applies task_type+status filter quickly on 2000 records', () => {
const dataset = makeDataset(2000);
const start = Date.now();
const result = applyFilters(dataset, { taskType: 'migration', status: 'failed' });
const duration = Date.now() - start;
expect(Array.isArray(result)).toBe(true);
expect(duration).toBeLessThan(100);
});
});
// [/DEF:frontend.src.lib.components.reports.__tests__.reports_filter_performance:Module]

View File

@@ -0,0 +1,40 @@
// [DEF:frontend.src.lib.components.reports.__tests__.reports_page.integration:Module]
// @TIER: CRITICAL
// @SEMANTICS: tests, reports, integration, mixed-types, rendering
// @PURPOSE: Integration-style checks for unified mixed-type reports rendering expectations.
// @LAYER: UI (Tests)
// @RELATION: TESTS -> frontend/src/routes/reports/+page.svelte
// @RELATION: TESTS -> frontend/src/lib/components/reports/ReportsList.svelte
// @INVARIANT: Mixed fixture includes all supported report types in one list.
import { describe, it, expect } from 'vitest';
import { mixedTaskReports } from './fixtures/reports.fixtures.js';
function collectVisibleTypeLabels(items) {
return items.map((item) => item.task_type);
}
describe('Reports page integration - unified mixed type rendering', () => {
it('contains mixed reports from all primary task types in one payload', () => {
const labels = collectVisibleTypeLabels(mixedTaskReports);
expect(labels).toContain('llm_verification');
expect(labels).toContain('backup');
expect(labels).toContain('migration');
expect(labels).toContain('documentation');
expect(mixedTaskReports.length).toBeGreaterThanOrEqual(4);
});
it('ensures canonical minimum fields are present for each report item', () => {
for (const report of mixedTaskReports) {
expect(typeof report.report_id).toBe('string');
expect(typeof report.task_id).toBe('string');
expect(typeof report.task_type).toBe('string');
expect(typeof report.status).toBe('string');
expect(typeof report.summary).toBe('string');
expect(report.updated_at).toBeTruthy();
}
});
});
// [/DEF:frontend.src.lib.components.reports.__tests__.reports_page.integration:Module]

View File

@@ -0,0 +1,59 @@
// [DEF:frontend.src.lib.components.reports.reportTypeProfiles:Module]
// @TIER: CRITICAL
// @SEMANTICS: reports, ui, profiles, fallback, mapping
// @PURPOSE: Deterministic mapping from report task_type to visual profile with one fallback.
// @LAYER: UI
// @RELATION: DEPENDS_ON -> frontend/src/lib/i18n/index.ts
// @INVARIANT: Unknown type always resolves to fallback profile.
import { _ } from '$lib/i18n';
export const REPORT_TYPE_PROFILES = {
llm_verification: {
key: 'llm_verification',
label: 'LLM',
variant: 'bg-violet-100 text-violet-700',
icon: 'sparkles',
fallback: false
},
backup: {
key: 'backup',
label: () => _('nav.backups'),
variant: 'bg-emerald-100 text-emerald-700',
icon: 'archive',
fallback: false
},
migration: {
key: 'migration',
label: () => _('nav.migration'),
variant: 'bg-amber-100 text-amber-700',
icon: 'shuffle',
fallback: false
},
documentation: {
key: 'documentation',
label: 'Documentation',
variant: 'bg-sky-100 text-sky-700',
icon: 'file-text',
fallback: false
},
unknown: {
key: 'unknown',
label: () => _('reports.unknown_type'),
variant: 'bg-slate-100 text-slate-700',
icon: 'help-circle',
fallback: true
}
};
// [DEF:getReportTypeProfile:Function]
// @PURPOSE: Resolve visual profile by task type with guaranteed fallback.
// @PRE: taskType may be known/unknown/empty.
// @POST: Returns one profile object.
export function getReportTypeProfile(taskType) {
const key = typeof taskType === 'string' ? taskType : 'unknown';
return REPORT_TYPE_PROFILES[key] || REPORT_TYPE_PROFILES.unknown;
}
// [/DEF:getReportTypeProfile:Function]
// [/DEF:frontend.src.lib.components.reports.reportTypeProfiles:Module]