Files
ss-tools/frontend/src/lib/stores/taskDrawer.svelte.ts
busya 33ee976c48 feat(reports): Task Status Center — unified /reports dashboard
Страница /reports трансформирована в Центр статусов задач:

Backend:
- GET /api/reports/summary — агрегированные счётчики тип×статус (5 корзин)
- GET/PUT /api/settings/reports — глобальные настройки отчётов
- _filter_tasks_by_rbac() — row-level фильтрация по роли
- normalize_task_report: LLM-валидация с ошибками → FAILED/PARTIAL
- get_summary(): 5 корзин pending/running/awaiting_input/success/failed

Frontend:
- TaskCenterModel.svelte.ts (400 строк) — Screen Model
- SummaryPanel — сводная панель с цветовым кодированием и active filter
- ReportCard — humanized labels, duration, task_id, failed border
- FilterBar — search + sort + time range с label'ами
- Pagination — showingText, уникальные id для select
- Quick views: «Упавшие», «В работе», «Успешные»
- TaskDrawer: scroll-to-error, footer скрыт для terminal, «Н/Д» fix

Тесты: 48 backend + 38 frontend (2521 всего)
Build:   Console errors: 0
2026-07-02 18:53:58 +03:00

135 lines
3.8 KiB
TypeScript

// #region TaskDrawerStore [C:3] [TYPE Store] [SEMANTICS task, drawer, store, resource, mapping]
// @ingroup Stores
// @BRIEF Manage Task Drawer visibility, active task binding, and resource-to-task mapping.
// @LAYER UI
// @RELATION BINDS_TO -> [ActivityStore]
// @UX_STATE Closed -> Drawer hidden, no active task
// @UX_STATE Open -> Drawer visible with task logs or task list
// @RATIONALE Migrated from Svelte 4 writable to Svelte 5 $state runes.
// Backward-compatible .subscribe() retained for $taskDrawerStore syntax.
interface TaskInfo {
taskId: string;
status: string;
}
interface ResourceTaskMap {
[k: string]: TaskInfo;
}
interface TaskDrawerState {
isOpen: boolean;
activeTaskId: string | null;
scrollToHint: string | null;
resourceTaskMap: ResourceTaskMap;
}
const TASK_DRAWER_AUTO_OPEN_STORAGE_KEY = 'ss_tools.profile.auto_open_task_drawer';
function readAutoOpenTaskDrawerPreference(): boolean {
if (typeof window === 'undefined') return true;
const rawValue = window.localStorage.getItem(TASK_DRAWER_AUTO_OPEN_STORAGE_KEY);
if (rawValue === 'false') return false;
return true;
}
let autoOpenTaskDrawerPreference: boolean = readAutoOpenTaskDrawerPreference();
const initialState: TaskDrawerState = {
isOpen: false,
activeTaskId: null,
scrollToHint: null,
resourceTaskMap: {},
};
// Reactive state (Svelte 5 rune)
let _state: TaskDrawerState = $state(initialState);
// Subscriber list for backward compat with $taskDrawerStore syntax
let _subscribers: Array<(_value: TaskDrawerState) => void> = [];
function _notify(): void {
const current = _state;
_subscribers.forEach((fn) => fn(current));
}
export const taskDrawerStore = {
subscribe(run: (_value: TaskDrawerState) => void): () => void {
_subscribers.push(run);
run(_state);
return () => {
_subscribers = _subscribers.filter((fn) => fn !== run);
};
},
get value(): TaskDrawerState {
return _state;
},
set(newState: TaskDrawerState) {
_state = newState;
_notify();
},
update(fn: (_state: TaskDrawerState) => TaskDrawerState) {
_state = fn(_state);
_notify();
},
};
export function openDrawerForTask(taskId: string, scrollToHint?: string): boolean {
if (!taskId) {
return false;
}
if (_state.isOpen && _state.activeTaskId === taskId) return true; // Already open for this task
_state = { ..._state, isOpen: true, activeTaskId: taskId, scrollToHint: scrollToHint ?? null };
_notify();
return true;
}
export function setTaskDrawerAutoOpenPreference(enabled: boolean): void {
autoOpenTaskDrawerPreference = enabled !== false;
if (typeof window !== 'undefined') {
window.localStorage.setItem(
TASK_DRAWER_AUTO_OPEN_STORAGE_KEY,
autoOpenTaskDrawerPreference ? 'true' : 'false',
);
}
}
export function getTaskDrawerAutoOpenPreference(): boolean {
return autoOpenTaskDrawerPreference;
}
export function openDrawerForTaskIfPreferred(taskId: string): boolean {
if (autoOpenTaskDrawerPreference !== true) {
return false;
}
return openDrawerForTask(taskId);
}
export function openDrawer(): void {
if (_state.isOpen && _state.activeTaskId === null) return;
_state = { ..._state, isOpen: true, activeTaskId: null };
_notify();
}
export function closeDrawer(): void {
if (!_state.isOpen) return;
_state = { ..._state, isOpen: false, activeTaskId: null };
_notify();
}
export function updateResourceTask(resourceId: string, taskId: string, status: string): void {
const newMap: ResourceTaskMap = { ..._state.resourceTaskMap };
if (status === 'IDLE' || status === 'SUCCESS' || status === 'ERROR') {
delete newMap[resourceId];
} else {
newMap[resourceId] = { taskId, status };
}
_state = { ..._state, resourceTaskMap: newMap };
_notify();
}
export function getTaskForResource(resourceId: string): TaskInfo | null {
return _state.resourceTaskMap[resourceId] || null;
}
// #endregion TaskDrawerStore