refactor(semantics): migrate legacy @TIER to @COMPLEXITY annotations
- Replaced @TIER: TRIVIAL with @COMPLEXITY: 1 - Replaced @TIER: STANDARD with @COMPLEXITY: 3 - Replaced @TIER: CRITICAL with @COMPLEXITY: 5 - Manually elevated specific critical/complex components to levels 2 and 4 - Ignored legacy, specs, and node_modules directories - Updated generated semantic map
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<!-- [DEF:DashboardGrid:Component] -->
|
||||
<!--
|
||||
@TIER: STANDARD
|
||||
@COMPLEXITY: 3
|
||||
@SEMANTICS: dashboard, grid, selection, pagination
|
||||
@PURPOSE: Displays a grid of dashboards with selection and pagination.
|
||||
@LAYER: Component
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- [DEF:Footer:Component] -->
|
||||
<!--
|
||||
@TIER: TRIVIAL
|
||||
@COMPLEXITY: 1
|
||||
@SEMANTICS: footer, layout, copyright
|
||||
@PURPOSE: Displays the application footer with copyright information.
|
||||
@LAYER: UI
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- [DEF:Navbar:Component] -->
|
||||
<!--
|
||||
@TIER: STANDARD
|
||||
@COMPLEXITY: 3
|
||||
@SEMANTICS: navbar, navigation, header, layout
|
||||
@PURPOSE: Main navigation bar for the application.
|
||||
@LAYER: UI
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- [DEF:DashboardGrid:Component] -->
|
||||
<!--
|
||||
@TIER: STANDARD
|
||||
@COMPLEXITY: 3
|
||||
@SEMANTICS: dashboard, grid, selection, pagination
|
||||
@PURPOSE: Displays a grid of dashboards with selection and pagination.
|
||||
@LAYER: Component
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- [DEF:StartupEnvironmentWizard:Component] -->
|
||||
<script>
|
||||
/**
|
||||
* @TIER: STANDARD
|
||||
* @COMPLEXITY: 3
|
||||
* @PURPOSE: Blocking startup wizard for creating the first Superset environment from zero-state screens.
|
||||
* @LAYER: UI
|
||||
* @RELATION: CALLS -> api.addEnvironment
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- [DEF:TaskLogViewer:Component] -->
|
||||
<!--
|
||||
@TIER: CRITICAL
|
||||
@COMPLEXITY: 5
|
||||
@SEMANTICS: task, log, viewer, inline, realtime
|
||||
@PURPOSE: Displays task logs inline (in drawer) or as modal. Merges real-time WebSocket logs with polled historical logs.
|
||||
@LAYER: UI
|
||||
@@ -148,7 +148,7 @@
|
||||
<!-- @PURPOSE: Shows inline logs -->
|
||||
<!-- @LAYER: UI -->
|
||||
<!-- @SEMANTICS: logs, inline -->
|
||||
<!-- @TIER: STANDARD -->
|
||||
<!-- @COMPLEXITY: 3 -->
|
||||
{#if inline}
|
||||
<div class="flex flex-col h-full w-full">
|
||||
{#if loading && logs.length === 0}
|
||||
@@ -186,7 +186,7 @@
|
||||
<!-- @PURPOSE: Shows modal logs -->
|
||||
<!-- @LAYER: UI -->
|
||||
<!-- @SEMANTICS: logs, modal -->
|
||||
<!-- @TIER: STANDARD -->
|
||||
<!-- @COMPLEXITY: 3 -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 overflow-y-auto"
|
||||
aria-labelledby="modal-title"
|
||||
|
||||
@@ -1,384 +1,384 @@
|
||||
<!-- [DEF:TaskRunner:Component] -->
|
||||
<!--
|
||||
@TIER: STANDARD
|
||||
@SEMANTICS: task, runner, logs, websocket
|
||||
@PURPOSE: Connects to a WebSocket to display real-time logs for a running task with filtering support.
|
||||
@LAYER: UI
|
||||
@RELATION: DEPENDS_ON -> frontend/src/lib/stores.js, frontend/src/components/tasks/TaskLogPanel.svelte
|
||||
-->
|
||||
<script>
|
||||
// [SECTION: IMPORTS]
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import { selectedTask, taskLogs } from '../lib/stores.js';
|
||||
import { getWsUrl, api } from '../lib/api.js';
|
||||
import { addToast } from '../lib/toasts.js';
|
||||
import MissingMappingModal from './MissingMappingModal.svelte';
|
||||
import PasswordPrompt from './PasswordPrompt.svelte';
|
||||
<!-- [DEF:TaskRunner:Component] -->
|
||||
<!--
|
||||
@COMPLEXITY: 3
|
||||
@SEMANTICS: task, runner, logs, websocket
|
||||
@PURPOSE: Connects to a WebSocket to display real-time logs for a running task with filtering support.
|
||||
@LAYER: UI
|
||||
@RELATION: DEPENDS_ON -> frontend/src/lib/stores.js, frontend/src/components/tasks/TaskLogPanel.svelte
|
||||
-->
|
||||
<script>
|
||||
// [SECTION: IMPORTS]
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import { selectedTask, taskLogs } from '../lib/stores.js';
|
||||
import { getWsUrl, api } from '../lib/api.js';
|
||||
import { addToast } from '../lib/toasts.js';
|
||||
import MissingMappingModal from './MissingMappingModal.svelte';
|
||||
import PasswordPrompt from './PasswordPrompt.svelte';
|
||||
import TaskLogPanel from './tasks/TaskLogPanel.svelte';
|
||||
import { t } from '../lib/i18n';
|
||||
// [/SECTION]
|
||||
|
||||
let ws;
|
||||
let reconnectAttempts = 0;
|
||||
let maxReconnectAttempts = 10;
|
||||
let initialReconnectDelay = 1000;
|
||||
let maxReconnectDelay = 30000;
|
||||
let reconnectTimeout;
|
||||
let waitingForData = false;
|
||||
let dataTimeout;
|
||||
let connectionStatus = 'disconnected'; // 'connecting', 'connected', 'disconnected', 'waiting', 'completed', 'awaiting_mapping', 'awaiting_input'
|
||||
let showMappingModal = false;
|
||||
let missingDbInfo = { name: '', uuid: '' };
|
||||
let targetDatabases = [];
|
||||
|
||||
let showPasswordPrompt = false;
|
||||
let passwordPromptData = { databases: [], errorMessage: '' };
|
||||
|
||||
let selectedSource = 'all';
|
||||
let selectedLevel = 'all';
|
||||
|
||||
// [DEF:connect:Function]
|
||||
/**
|
||||
* @purpose Establishes WebSocket connection with exponential backoff and filter parameters.
|
||||
* @pre selectedTask must be set in the store.
|
||||
* @post WebSocket instance created and listeners attached.
|
||||
*/
|
||||
function connect() {
|
||||
const task = get(selectedTask);
|
||||
if (!task || connectionStatus === 'completed') return;
|
||||
|
||||
console.log(`[TaskRunner][Entry] Connecting to logs for task: ${task.id} (Attempt ${reconnectAttempts + 1}) filters: source=${selectedSource}, level=${selectedLevel}`);
|
||||
connectionStatus = 'connecting';
|
||||
|
||||
let wsUrl = getWsUrl(task.id);
|
||||
|
||||
// Append filter parameters to WebSocket URL
|
||||
const params = new URLSearchParams();
|
||||
if (selectedSource !== 'all') params.append('source', selectedSource);
|
||||
if (selectedLevel !== 'all') params.append('level', selectedLevel);
|
||||
|
||||
const queryString = params.toString();
|
||||
if (queryString) {
|
||||
wsUrl += (wsUrl.includes('?') ? '&' : '?') + queryString;
|
||||
}
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('[TaskRunner][Coherence:OK] WebSocket connection established');
|
||||
connectionStatus = 'connected';
|
||||
reconnectAttempts = 0;
|
||||
startDataTimeout();
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const logEntry = JSON.parse(event.data);
|
||||
taskLogs.update(logs => [...logs, logEntry]);
|
||||
resetDataTimeout();
|
||||
|
||||
// Check for completion message (if backend sends one)
|
||||
if (logEntry.message && logEntry.message.includes('Task completed successfully')) {
|
||||
connectionStatus = 'completed';
|
||||
ws.close();
|
||||
}
|
||||
|
||||
// Check for missing mapping signal
|
||||
if (logEntry.message && logEntry.message.includes('Missing mapping for database UUID')) {
|
||||
const uuidMatch = logEntry.message.match(/UUID: ([\w-]+)/);
|
||||
if (uuidMatch) {
|
||||
missingDbInfo = { name: 'Unknown', uuid: uuidMatch[1] };
|
||||
connectionStatus = 'awaiting_mapping';
|
||||
fetchTargetDatabases();
|
||||
showMappingModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for password request via log context or message
|
||||
if (logEntry.message && logEntry.message.includes('Task paused for user input') && logEntry.context && logEntry.context.input_request) {
|
||||
const request = logEntry.context.input_request;
|
||||
if (request.type === 'database_password') {
|
||||
connectionStatus = 'awaiting_input';
|
||||
passwordPromptData = {
|
||||
databases: request.databases || [],
|
||||
errorMessage: request.error_message || ''
|
||||
};
|
||||
showPasswordPrompt = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (task && task.status === 'AWAITING_INPUT' && task.input_request && task.input_request.type === 'database_password') {
|
||||
connectionStatus = 'awaiting_input';
|
||||
passwordPromptData = {
|
||||
databases: task.input_request.databases || [],
|
||||
errorMessage: task.input_request.error_message || ''
|
||||
};
|
||||
showPasswordPrompt = true;
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[TaskRunner][Coherence:Failed] WebSocket error:', error);
|
||||
connectionStatus = 'disconnected';
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log(`[TaskRunner][Exit] WebSocket connection closed (Code: ${event.code})`);
|
||||
clearTimeout(dataTimeout);
|
||||
waitingForData = false;
|
||||
|
||||
if (connectionStatus !== 'completed' && reconnectAttempts < maxReconnectAttempts) {
|
||||
const delay = Math.min(initialReconnectDelay * Math.pow(2, reconnectAttempts), maxReconnectDelay);
|
||||
console.log(`[TaskRunner][Action] Reconnecting in ${delay}ms...`);
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
reconnectAttempts++;
|
||||
connect();
|
||||
}, delay);
|
||||
} else if (reconnectAttempts >= maxReconnectAttempts) {
|
||||
console.error('[TaskRunner][Coherence:Failed] Max reconnect attempts reached.');
|
||||
// [/SECTION]
|
||||
|
||||
let ws;
|
||||
let reconnectAttempts = 0;
|
||||
let maxReconnectAttempts = 10;
|
||||
let initialReconnectDelay = 1000;
|
||||
let maxReconnectDelay = 30000;
|
||||
let reconnectTimeout;
|
||||
let waitingForData = false;
|
||||
let dataTimeout;
|
||||
let connectionStatus = 'disconnected'; // 'connecting', 'connected', 'disconnected', 'waiting', 'completed', 'awaiting_mapping', 'awaiting_input'
|
||||
let showMappingModal = false;
|
||||
let missingDbInfo = { name: '', uuid: '' };
|
||||
let targetDatabases = [];
|
||||
|
||||
let showPasswordPrompt = false;
|
||||
let passwordPromptData = { databases: [], errorMessage: '' };
|
||||
|
||||
let selectedSource = 'all';
|
||||
let selectedLevel = 'all';
|
||||
|
||||
// [DEF:connect:Function]
|
||||
/**
|
||||
* @purpose Establishes WebSocket connection with exponential backoff and filter parameters.
|
||||
* @pre selectedTask must be set in the store.
|
||||
* @post WebSocket instance created and listeners attached.
|
||||
*/
|
||||
function connect() {
|
||||
const task = get(selectedTask);
|
||||
if (!task || connectionStatus === 'completed') return;
|
||||
|
||||
console.log(`[TaskRunner][Entry] Connecting to logs for task: ${task.id} (Attempt ${reconnectAttempts + 1}) filters: source=${selectedSource}, level=${selectedLevel}`);
|
||||
connectionStatus = 'connecting';
|
||||
|
||||
let wsUrl = getWsUrl(task.id);
|
||||
|
||||
// Append filter parameters to WebSocket URL
|
||||
const params = new URLSearchParams();
|
||||
if (selectedSource !== 'all') params.append('source', selectedSource);
|
||||
if (selectedLevel !== 'all') params.append('level', selectedLevel);
|
||||
|
||||
const queryString = params.toString();
|
||||
if (queryString) {
|
||||
wsUrl += (wsUrl.includes('?') ? '&' : '?') + queryString;
|
||||
}
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('[TaskRunner][Coherence:OK] WebSocket connection established');
|
||||
connectionStatus = 'connected';
|
||||
reconnectAttempts = 0;
|
||||
startDataTimeout();
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const logEntry = JSON.parse(event.data);
|
||||
taskLogs.update(logs => [...logs, logEntry]);
|
||||
resetDataTimeout();
|
||||
|
||||
// Check for completion message (if backend sends one)
|
||||
if (logEntry.message && logEntry.message.includes('Task completed successfully')) {
|
||||
connectionStatus = 'completed';
|
||||
ws.close();
|
||||
}
|
||||
|
||||
// Check for missing mapping signal
|
||||
if (logEntry.message && logEntry.message.includes('Missing mapping for database UUID')) {
|
||||
const uuidMatch = logEntry.message.match(/UUID: ([\w-]+)/);
|
||||
if (uuidMatch) {
|
||||
missingDbInfo = { name: 'Unknown', uuid: uuidMatch[1] };
|
||||
connectionStatus = 'awaiting_mapping';
|
||||
fetchTargetDatabases();
|
||||
showMappingModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for password request via log context or message
|
||||
if (logEntry.message && logEntry.message.includes('Task paused for user input') && logEntry.context && logEntry.context.input_request) {
|
||||
const request = logEntry.context.input_request;
|
||||
if (request.type === 'database_password') {
|
||||
connectionStatus = 'awaiting_input';
|
||||
passwordPromptData = {
|
||||
databases: request.databases || [],
|
||||
errorMessage: request.error_message || ''
|
||||
};
|
||||
showPasswordPrompt = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (task && task.status === 'AWAITING_INPUT' && task.input_request && task.input_request.type === 'database_password') {
|
||||
connectionStatus = 'awaiting_input';
|
||||
passwordPromptData = {
|
||||
databases: task.input_request.databases || [],
|
||||
errorMessage: task.input_request.error_message || ''
|
||||
};
|
||||
showPasswordPrompt = true;
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[TaskRunner][Coherence:Failed] WebSocket error:', error);
|
||||
connectionStatus = 'disconnected';
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log(`[TaskRunner][Exit] WebSocket connection closed (Code: ${event.code})`);
|
||||
clearTimeout(dataTimeout);
|
||||
waitingForData = false;
|
||||
|
||||
if (connectionStatus !== 'completed' && reconnectAttempts < maxReconnectAttempts) {
|
||||
const delay = Math.min(initialReconnectDelay * Math.pow(2, reconnectAttempts), maxReconnectDelay);
|
||||
console.log(`[TaskRunner][Action] Reconnecting in ${delay}ms...`);
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
reconnectAttempts++;
|
||||
connect();
|
||||
}, delay);
|
||||
} else if (reconnectAttempts >= maxReconnectAttempts) {
|
||||
console.error('[TaskRunner][Coherence:Failed] Max reconnect attempts reached.');
|
||||
addToast($t.tasks?.log_stream_failed, 'error');
|
||||
}
|
||||
};
|
||||
}
|
||||
// [/DEF:connect:Function]
|
||||
|
||||
// [DEF:handleFilterChange:Function]
|
||||
/**
|
||||
* @purpose Handles filter changes and reconnects WebSocket with new parameters.
|
||||
* @pre event.detail contains source and level filter values.
|
||||
* @post WebSocket reconnected with new filter parameters, logs cleared.
|
||||
*/
|
||||
function handleFilterChange(event) {
|
||||
const { source, level } = event.detail;
|
||||
if (selectedSource === source && selectedLevel === level) return;
|
||||
|
||||
selectedSource = source;
|
||||
selectedLevel = level;
|
||||
|
||||
console.log(`[TaskRunner] Filter changed, reconnecting WebSocket: source=${source}, level=${level}`);
|
||||
|
||||
// Clear current logs when filter changes to avoid confusion
|
||||
taskLogs.set([]);
|
||||
|
||||
if (ws) {
|
||||
ws.close(); // This will trigger reconnection via onclose if not completed
|
||||
} else {
|
||||
connect();
|
||||
}
|
||||
}
|
||||
// [/DEF:handleFilterChange:Function]
|
||||
|
||||
// [DEF:fetchTargetDatabases:Function]
|
||||
/**
|
||||
* @purpose Fetches available databases from target environment for mapping.
|
||||
* @pre selectedTask must have to_env parameter set.
|
||||
* @post targetDatabases array populated with available databases.
|
||||
*/
|
||||
async function fetchTargetDatabases() {
|
||||
const task = get(selectedTask);
|
||||
if (!task || !task.params.to_env) return;
|
||||
|
||||
try {
|
||||
const envs = await api.fetchApi('/environments');
|
||||
const targetEnv = envs.find(e => e.name === task.params.to_env);
|
||||
|
||||
if (targetEnv) {
|
||||
targetDatabases = await api.fetchApi(`/environments/${targetEnv.id}/databases`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch target databases', e);
|
||||
}
|
||||
}
|
||||
// [/DEF:fetchTargetDatabases:Function]
|
||||
|
||||
// [DEF:handleMappingResolve:Function]
|
||||
/**
|
||||
* @purpose Resolves missing database mapping and continues migration.
|
||||
* @pre event.detail contains sourceDbUuid, targetDbUuid, targetDbName.
|
||||
* @post Mapping created in backend, task resumed with resolution params.
|
||||
*/
|
||||
async function handleMappingResolve(event) {
|
||||
const task = get(selectedTask);
|
||||
const { sourceDbUuid, targetDbUuid, targetDbName } = event.detail;
|
||||
|
||||
try {
|
||||
const envs = await api.fetchApi('/environments');
|
||||
const srcEnv = envs.find(e => e.name === task.params.from_env);
|
||||
const tgtEnv = envs.find(e => e.name === task.params.to_env);
|
||||
|
||||
await api.postApi('/mappings', {
|
||||
source_env_id: srcEnv.id,
|
||||
target_env_id: tgtEnv.id,
|
||||
source_db_uuid: sourceDbUuid,
|
||||
target_db_uuid: targetDbUuid,
|
||||
source_db_name: missingDbInfo.name,
|
||||
target_db_name: targetDbName
|
||||
});
|
||||
|
||||
await api.postApi(`/tasks/${task.id}/resolve`, {
|
||||
resolution_params: { resolved_mapping: { [sourceDbUuid]: targetDbUuid } }
|
||||
});
|
||||
|
||||
connectionStatus = 'connected';
|
||||
}
|
||||
};
|
||||
}
|
||||
// [/DEF:connect:Function]
|
||||
|
||||
// [DEF:handleFilterChange:Function]
|
||||
/**
|
||||
* @purpose Handles filter changes and reconnects WebSocket with new parameters.
|
||||
* @pre event.detail contains source and level filter values.
|
||||
* @post WebSocket reconnected with new filter parameters, logs cleared.
|
||||
*/
|
||||
function handleFilterChange(event) {
|
||||
const { source, level } = event.detail;
|
||||
if (selectedSource === source && selectedLevel === level) return;
|
||||
|
||||
selectedSource = source;
|
||||
selectedLevel = level;
|
||||
|
||||
console.log(`[TaskRunner] Filter changed, reconnecting WebSocket: source=${source}, level=${level}`);
|
||||
|
||||
// Clear current logs when filter changes to avoid confusion
|
||||
taskLogs.set([]);
|
||||
|
||||
if (ws) {
|
||||
ws.close(); // This will trigger reconnection via onclose if not completed
|
||||
} else {
|
||||
connect();
|
||||
}
|
||||
}
|
||||
// [/DEF:handleFilterChange:Function]
|
||||
|
||||
// [DEF:fetchTargetDatabases:Function]
|
||||
/**
|
||||
* @purpose Fetches available databases from target environment for mapping.
|
||||
* @pre selectedTask must have to_env parameter set.
|
||||
* @post targetDatabases array populated with available databases.
|
||||
*/
|
||||
async function fetchTargetDatabases() {
|
||||
const task = get(selectedTask);
|
||||
if (!task || !task.params.to_env) return;
|
||||
|
||||
try {
|
||||
const envs = await api.fetchApi('/environments');
|
||||
const targetEnv = envs.find(e => e.name === task.params.to_env);
|
||||
|
||||
if (targetEnv) {
|
||||
targetDatabases = await api.fetchApi(`/environments/${targetEnv.id}/databases`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch target databases', e);
|
||||
}
|
||||
}
|
||||
// [/DEF:fetchTargetDatabases:Function]
|
||||
|
||||
// [DEF:handleMappingResolve:Function]
|
||||
/**
|
||||
* @purpose Resolves missing database mapping and continues migration.
|
||||
* @pre event.detail contains sourceDbUuid, targetDbUuid, targetDbName.
|
||||
* @post Mapping created in backend, task resumed with resolution params.
|
||||
*/
|
||||
async function handleMappingResolve(event) {
|
||||
const task = get(selectedTask);
|
||||
const { sourceDbUuid, targetDbUuid, targetDbName } = event.detail;
|
||||
|
||||
try {
|
||||
const envs = await api.fetchApi('/environments');
|
||||
const srcEnv = envs.find(e => e.name === task.params.from_env);
|
||||
const tgtEnv = envs.find(e => e.name === task.params.to_env);
|
||||
|
||||
await api.postApi('/mappings', {
|
||||
source_env_id: srcEnv.id,
|
||||
target_env_id: tgtEnv.id,
|
||||
source_db_uuid: sourceDbUuid,
|
||||
target_db_uuid: targetDbUuid,
|
||||
source_db_name: missingDbInfo.name,
|
||||
target_db_name: targetDbName
|
||||
});
|
||||
|
||||
await api.postApi(`/tasks/${task.id}/resolve`, {
|
||||
resolution_params: { resolved_mapping: { [sourceDbUuid]: targetDbUuid } }
|
||||
});
|
||||
|
||||
connectionStatus = 'connected';
|
||||
addToast($t.tasks?.mapping_resolved, 'success');
|
||||
} catch (e) {
|
||||
addToast($t.tasks?.mapping_resolve_failed?.replace('{error}', e.message), 'error');
|
||||
}
|
||||
}
|
||||
// [/DEF:handleMappingResolve:Function]
|
||||
|
||||
// [DEF:handlePasswordResume:Function]
|
||||
/**
|
||||
* @purpose Submits passwords and resumes paused migration task.
|
||||
* @pre event.detail contains passwords object.
|
||||
* @post Task resumed with passwords, connection status restored to connected.
|
||||
*/
|
||||
async function handlePasswordResume(event) {
|
||||
const task = get(selectedTask);
|
||||
const { passwords } = event.detail;
|
||||
|
||||
try {
|
||||
await api.postApi(`/tasks/${task.id}/resume`, { passwords });
|
||||
|
||||
showPasswordPrompt = false;
|
||||
connectionStatus = 'connected';
|
||||
}
|
||||
// [/DEF:handleMappingResolve:Function]
|
||||
|
||||
// [DEF:handlePasswordResume:Function]
|
||||
/**
|
||||
* @purpose Submits passwords and resumes paused migration task.
|
||||
* @pre event.detail contains passwords object.
|
||||
* @post Task resumed with passwords, connection status restored to connected.
|
||||
*/
|
||||
async function handlePasswordResume(event) {
|
||||
const task = get(selectedTask);
|
||||
const { passwords } = event.detail;
|
||||
|
||||
try {
|
||||
await api.postApi(`/tasks/${task.id}/resume`, { passwords });
|
||||
|
||||
showPasswordPrompt = false;
|
||||
connectionStatus = 'connected';
|
||||
addToast($t.tasks?.passwords_submitted, 'success');
|
||||
} catch (e) {
|
||||
addToast($t.tasks?.resume_failed?.replace('{error}', e.message), 'error');
|
||||
}
|
||||
}
|
||||
// [/DEF:handlePasswordResume:Function]
|
||||
|
||||
// [DEF:startDataTimeout:Function]
|
||||
/**
|
||||
* @purpose Starts timeout timer to detect idle connection.
|
||||
* @pre connectionStatus is 'connected'.
|
||||
* @post waitingForData set to true after 5 seconds if no data received.
|
||||
*/
|
||||
function startDataTimeout() {
|
||||
waitingForData = false;
|
||||
dataTimeout = setTimeout(() => {
|
||||
if (connectionStatus === 'connected') {
|
||||
waitingForData = true;
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
// [/DEF:startDataTimeout:Function]
|
||||
|
||||
// [DEF:resetDataTimeout:Function]
|
||||
/**
|
||||
* @purpose Resets data timeout timer when new data arrives.
|
||||
* @pre dataTimeout must be set.
|
||||
* @post waitingForData reset to false, new timeout started.
|
||||
*/
|
||||
function resetDataTimeout() {
|
||||
clearTimeout(dataTimeout);
|
||||
waitingForData = false;
|
||||
startDataTimeout();
|
||||
}
|
||||
// [/DEF:resetDataTimeout:Function]
|
||||
|
||||
// [DEF:onMount:Function]
|
||||
/**
|
||||
* @purpose Initializes WebSocket connection when component mounts.
|
||||
* @pre Component must be mounted in DOM.
|
||||
* @post WebSocket connection established, subscription to selectedTask active.
|
||||
*/
|
||||
onMount(() => {
|
||||
const unsubscribe = selectedTask.subscribe(task => {
|
||||
if (task) {
|
||||
console.log(`[TaskRunner][Action] Task selected: ${task.id}. Initializing connection.`);
|
||||
if (ws) ws.close();
|
||||
clearTimeout(reconnectTimeout);
|
||||
reconnectAttempts = 0;
|
||||
connectionStatus = 'disconnected';
|
||||
|
||||
if (task.logs && Array.isArray(task.logs)) {
|
||||
console.log(`[TaskRunner] Loaded ${task.logs.length} existing logs.`);
|
||||
taskLogs.set(task.logs);
|
||||
} else {
|
||||
taskLogs.set([]);
|
||||
}
|
||||
|
||||
connect();
|
||||
}
|
||||
});
|
||||
return unsubscribe;
|
||||
});
|
||||
// [/DEF:onMount:Function]
|
||||
|
||||
// [DEF:onDestroy:Function]
|
||||
onDestroy(() => {
|
||||
clearTimeout(reconnectTimeout);
|
||||
clearTimeout(dataTimeout);
|
||||
if (ws) {
|
||||
console.log("[TaskRunner][Action] Closing WebSocket connection.");
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
// [/DEF:onDestroy:Function]
|
||||
</script>
|
||||
|
||||
<!-- [SECTION: TEMPLATE] -->
|
||||
<div class="p-4 border rounded-lg bg-white shadow-md">
|
||||
{#if $selectedTask}
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
}
|
||||
// [/DEF:handlePasswordResume:Function]
|
||||
|
||||
// [DEF:startDataTimeout:Function]
|
||||
/**
|
||||
* @purpose Starts timeout timer to detect idle connection.
|
||||
* @pre connectionStatus is 'connected'.
|
||||
* @post waitingForData set to true after 5 seconds if no data received.
|
||||
*/
|
||||
function startDataTimeout() {
|
||||
waitingForData = false;
|
||||
dataTimeout = setTimeout(() => {
|
||||
if (connectionStatus === 'connected') {
|
||||
waitingForData = true;
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
// [/DEF:startDataTimeout:Function]
|
||||
|
||||
// [DEF:resetDataTimeout:Function]
|
||||
/**
|
||||
* @purpose Resets data timeout timer when new data arrives.
|
||||
* @pre dataTimeout must be set.
|
||||
* @post waitingForData reset to false, new timeout started.
|
||||
*/
|
||||
function resetDataTimeout() {
|
||||
clearTimeout(dataTimeout);
|
||||
waitingForData = false;
|
||||
startDataTimeout();
|
||||
}
|
||||
// [/DEF:resetDataTimeout:Function]
|
||||
|
||||
// [DEF:onMount:Function]
|
||||
/**
|
||||
* @purpose Initializes WebSocket connection when component mounts.
|
||||
* @pre Component must be mounted in DOM.
|
||||
* @post WebSocket connection established, subscription to selectedTask active.
|
||||
*/
|
||||
onMount(() => {
|
||||
const unsubscribe = selectedTask.subscribe(task => {
|
||||
if (task) {
|
||||
console.log(`[TaskRunner][Action] Task selected: ${task.id}. Initializing connection.`);
|
||||
if (ws) ws.close();
|
||||
clearTimeout(reconnectTimeout);
|
||||
reconnectAttempts = 0;
|
||||
connectionStatus = 'disconnected';
|
||||
|
||||
if (task.logs && Array.isArray(task.logs)) {
|
||||
console.log(`[TaskRunner] Loaded ${task.logs.length} existing logs.`);
|
||||
taskLogs.set(task.logs);
|
||||
} else {
|
||||
taskLogs.set([]);
|
||||
}
|
||||
|
||||
connect();
|
||||
}
|
||||
});
|
||||
return unsubscribe;
|
||||
});
|
||||
// [/DEF:onMount:Function]
|
||||
|
||||
// [DEF:onDestroy:Function]
|
||||
onDestroy(() => {
|
||||
clearTimeout(reconnectTimeout);
|
||||
clearTimeout(dataTimeout);
|
||||
if (ws) {
|
||||
console.log("[TaskRunner][Action] Closing WebSocket connection.");
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
// [/DEF:onDestroy:Function]
|
||||
</script>
|
||||
|
||||
<!-- [SECTION: TEMPLATE] -->
|
||||
<div class="p-4 border rounded-lg bg-white shadow-md">
|
||||
{#if $selectedTask}
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h2 class="text-xl font-semibold">{$t.tasks?.task_label}: {$selectedTask.plugin_id}</h2>
|
||||
<div class="flex items-center space-x-2">
|
||||
{#if connectionStatus === 'connecting'}
|
||||
<span class="flex h-3 w-3 relative">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-yellow-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 bg-yellow-500"></span>
|
||||
</span>
|
||||
<div class="flex items-center space-x-2">
|
||||
{#if connectionStatus === 'connecting'}
|
||||
<span class="flex h-3 w-3 relative">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-yellow-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 bg-yellow-500"></span>
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">{$t.tasks?.connecting}</span>
|
||||
{:else if connectionStatus === 'connected'}
|
||||
<span class="h-3 w-3 rounded-full bg-green-500"></span>
|
||||
{:else if connectionStatus === 'connected'}
|
||||
<span class="h-3 w-3 rounded-full bg-green-500"></span>
|
||||
<span class="text-xs text-gray-500">{$t.tasks?.live}</span>
|
||||
{:else if connectionStatus === 'completed'}
|
||||
<span class="h-3 w-3 rounded-full bg-blue-500"></span>
|
||||
{:else if connectionStatus === 'completed'}
|
||||
<span class="h-3 w-3 rounded-full bg-blue-500"></span>
|
||||
<span class="text-xs text-gray-500">{$t.tasks?.completed}</span>
|
||||
{:else if connectionStatus === 'awaiting_mapping'}
|
||||
<span class="h-3 w-3 rounded-full bg-orange-500 animate-pulse"></span>
|
||||
{:else if connectionStatus === 'awaiting_mapping'}
|
||||
<span class="h-3 w-3 rounded-full bg-orange-500 animate-pulse"></span>
|
||||
<span class="text-xs text-gray-500">{$t.tasks?.awaiting_mapping}</span>
|
||||
{:else if connectionStatus === 'awaiting_input'}
|
||||
<span class="h-3 w-3 rounded-full bg-orange-500 animate-pulse"></span>
|
||||
{:else if connectionStatus === 'awaiting_input'}
|
||||
<span class="h-3 w-3 rounded-full bg-orange-500 animate-pulse"></span>
|
||||
<span class="text-xs text-gray-500">{$t.tasks?.awaiting_input}</span>
|
||||
{:else}
|
||||
<span class="h-3 w-3 rounded-full bg-red-500"></span>
|
||||
{:else}
|
||||
<span class="h-3 w-3 rounded-full bg-red-500"></span>
|
||||
<span class="text-xs text-gray-500">{$t.tasks?.disconnected}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Task Info Section -->
|
||||
<div class="mb-4 bg-gray-50 p-3 rounded text-sm border border-gray-200">
|
||||
<details open>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Task Info Section -->
|
||||
<div class="mb-4 bg-gray-50 p-3 rounded text-sm border border-gray-200">
|
||||
<details open>
|
||||
<summary class="cursor-pointer font-medium text-gray-700 focus:outline-none hover:text-indigo-600">{$t.tasks?.details_parameters}</summary>
|
||||
<div class="mt-2 pl-2 border-l-2 border-indigo-200">
|
||||
<div class="grid grid-cols-2 gap-2 mb-2">
|
||||
<div class="mt-2 pl-2 border-l-2 border-indigo-200">
|
||||
<div class="grid grid-cols-2 gap-2 mb-2">
|
||||
<div><span class="font-semibold">{$t.common?.id}:</span> <span class="text-gray-600">{$selectedTask.id}</span></div>
|
||||
<div><span class="font-semibold">{$t.dashboard?.status}:</span> <span class="text-gray-600">{$selectedTask.status}</span></div>
|
||||
<div><span class="font-semibold">{$t.tasks?.started_label}:</span> <span class="text-gray-600">{new Date($selectedTask.started_at || $selectedTask.created_at || Date.now()).toLocaleString()}</span></div>
|
||||
<div><span class="font-semibold">{$t.tasks?.plugin}:</span> <span class="text-gray-600">{$selectedTask.plugin_id}</span></div>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<span class="font-semibold">{$t.tasks?.parameters}:</span>
|
||||
<pre class="text-xs bg-gray-100 p-2 rounded mt-1 overflow-x-auto border border-gray-200">{JSON.stringify($selectedTask.params, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="h-[500px]">
|
||||
<pre class="text-xs bg-gray-100 p-2 rounded mt-1 overflow-x-auto border border-gray-200">{JSON.stringify($selectedTask.params, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="h-[500px]">
|
||||
<TaskLogPanel
|
||||
taskId={$selectedTask.id}
|
||||
logs={$taskLogs}
|
||||
autoScroll={true}
|
||||
onfilterchange={handleFilterChange}
|
||||
/>
|
||||
|
||||
{#if waitingForData && connectionStatus === 'connected'}
|
||||
<div class="text-gray-500 italic mt-2 animate-pulse text-xs">
|
||||
|
||||
{#if waitingForData && connectionStatus === 'connected'}
|
||||
<div class="text-gray-500 italic mt-2 animate-pulse text-xs">
|
||||
{$t.tasks?.waiting_logs}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -387,7 +387,7 @@
|
||||
<p>{$t.tasks?.select_task}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<MissingMappingModal
|
||||
bind:show={showMappingModal}
|
||||
sourceDbName={missingDbInfo.name}
|
||||
@@ -404,6 +404,6 @@
|
||||
onresume={handlePasswordResume}
|
||||
oncancel={() => { showPasswordPrompt = false; }}
|
||||
/>
|
||||
<!-- [/SECTION] -->
|
||||
|
||||
<!-- [/DEF:TaskRunner:Component] -->
|
||||
<!-- [/SECTION] -->
|
||||
|
||||
<!-- [/DEF:TaskRunner:Component] -->
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
<!-- [DEF:Toast:Component] -->
|
||||
<!--
|
||||
@TIER: TRIVIAL
|
||||
@SEMANTICS: toast, notification, feedback, ui
|
||||
@PURPOSE: Displays transient notifications (toasts) in the bottom-right corner.
|
||||
@LAYER: UI
|
||||
@RELATION: DEPENDS_ON -> frontend/src/lib/toasts.js
|
||||
|
||||
@PROPS: None
|
||||
@EVENTS: None
|
||||
-->
|
||||
<script>
|
||||
// [SECTION: IMPORTS]
|
||||
import { toasts } from '../lib/toasts.js';
|
||||
// [/SECTION]
|
||||
</script>
|
||||
|
||||
<!-- [SECTION: TEMPLATE] -->
|
||||
<div class="fixed bottom-0 right-0 p-4 space-y-2">
|
||||
{#each $toasts as toast (toast.id)}
|
||||
<div class="p-4 rounded-md shadow-lg text-white
|
||||
{toast.type === 'info' && 'bg-blue-500'}
|
||||
{toast.type === 'success' && 'bg-green-500'}
|
||||
{toast.type === 'error' && 'bg-red-500'}
|
||||
">
|
||||
{toast.message}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<!-- [/SECTION] -->
|
||||
|
||||
<!-- [/DEF:Toast:Component] -->
|
||||
<!-- [DEF:Toast:Component] -->
|
||||
<!--
|
||||
@COMPLEXITY: 1
|
||||
@SEMANTICS: toast, notification, feedback, ui
|
||||
@PURPOSE: Displays transient notifications (toasts) in the bottom-right corner.
|
||||
@LAYER: UI
|
||||
@RELATION: DEPENDS_ON -> frontend/src/lib/toasts.js
|
||||
|
||||
@PROPS: None
|
||||
@EVENTS: None
|
||||
-->
|
||||
<script>
|
||||
// [SECTION: IMPORTS]
|
||||
import { toasts } from '../lib/toasts.js';
|
||||
// [/SECTION]
|
||||
</script>
|
||||
|
||||
<!-- [SECTION: TEMPLATE] -->
|
||||
<div class="fixed bottom-0 right-0 p-4 space-y-2">
|
||||
{#each $toasts as toast (toast.id)}
|
||||
<div class="p-4 rounded-md shadow-lg text-white
|
||||
{toast.type === 'info' && 'bg-blue-500'}
|
||||
{toast.type === 'success' && 'bg-green-500'}
|
||||
{toast.type === 'error' && 'bg-red-500'}
|
||||
">
|
||||
{toast.message}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<!-- [/SECTION] -->
|
||||
|
||||
<!-- [/DEF:Toast:Component] -->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// [DEF:frontend.src.components.__tests__.task_log_viewer:Module]
|
||||
// @TIER: STANDARD
|
||||
// @COMPLEXITY: 3
|
||||
// @SEMANTICS: tests, task-log, viewer, mount, components
|
||||
// @PURPOSE: Unit tests for TaskLogViewer component by mounting it and observing the DOM.
|
||||
// @LAYER: UI (Tests)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!--[DEF:ProtectedRoute.svelte:Module] -->
|
||||
<!--
|
||||
@TIER: CRITICAL
|
||||
@COMPLEXITY: 5
|
||||
@SEMANTICS: auth, route-guard, permission, redirect, session-validation
|
||||
@PURPOSE: Enforces authenticated and authorized access before protected route content is rendered.
|
||||
@LAYER: UI
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- [DEF:CommitModal:Component] -->
|
||||
<!--
|
||||
@TIER: STANDARD
|
||||
@COMPLEXITY: 3
|
||||
@SEMANTICS: git, commit, modal, version_control, diff
|
||||
@PURPOSE: Модальное окно для создания коммита с просмотром изменений (diff).
|
||||
@LAYER: Component
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// [DEF:frontend.src.components.git.__tests__.git_manager_unfinished_merge_integration:Module]
|
||||
// @TIER: STANDARD
|
||||
// @COMPLEXITY: 3
|
||||
// @SEMANTICS: git-manager, unfinished-merge, dialog, integration-test
|
||||
// @PURPOSE: Protect unresolved-merge dialog contract in GitManager pull flow.
|
||||
// @LAYER: UI Tests
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- [DEF:DocPreview:Component] -->
|
||||
<!--
|
||||
@TIER: STANDARD
|
||||
@COMPLEXITY: 3
|
||||
@PURPOSE: UI component for previewing generated dataset documentation before saving.
|
||||
@LAYER: UI
|
||||
@RELATION: DEPENDS_ON -> backend/src/plugins/llm_analysis/plugin.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- [DEF:ProviderConfig:Component] -->
|
||||
<!--
|
||||
@TIER: STANDARD
|
||||
@COMPLEXITY: 3
|
||||
@PURPOSE: UI form for managing LLM provider configurations.
|
||||
@LAYER: UI
|
||||
@RELATION: DEPENDS_ON -> backend/src/api/routes/llm.py
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- [DEF:frontend/src/components/llm/ValidationReport.svelte:Component] -->
|
||||
<!-- @TIER: STANDARD -->
|
||||
<!-- @COMPLEXITY: 3 -->
|
||||
<!-- @PURPOSE: Displays the results of an LLM-based dashboard validation task. -->
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// [DEF:frontend.src.components.llm.__tests__.provider_config_integration:Module]
|
||||
// @TIER: STANDARD
|
||||
// @COMPLEXITY: 3
|
||||
// @SEMANTICS: llm, provider-config, integration-test, edit-flow, delete-flow
|
||||
// @PURPOSE: Protect edit and delete interaction contracts in LLM provider settings UI.
|
||||
// @LAYER: UI Tests
|
||||
@@ -16,7 +16,7 @@ const COMPONENT_PATH = path.resolve(
|
||||
);
|
||||
|
||||
// [DEF:provider_config_edit_contract_tests:Function]
|
||||
// @TIER: STANDARD
|
||||
// @COMPLEXITY: 3
|
||||
// @PURPOSE: Validate edit and delete handler wiring plus normalized edit form state mapping.
|
||||
// @PRE: ProviderConfig component source exists in expected path.
|
||||
// @POST: Contract checks ensure edit click cannot degrade into no-op flow.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- [DEF:FileList:Component] -->
|
||||
<!--
|
||||
@TIER: STANDARD
|
||||
@COMPLEXITY: 3
|
||||
@SEMANTICS: storage, files, list, table
|
||||
@PURPOSE: Displays a table of files with metadata and actions.
|
||||
@LAYER: UI
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- [DEF:FileUpload:Component] -->
|
||||
<!--
|
||||
@TIER: STANDARD
|
||||
@COMPLEXITY: 3
|
||||
@SEMANTICS: storage, upload, files
|
||||
@PURPOSE: Provides a form for uploading files to a specific category.
|
||||
@LAYER: UI
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- [DEF:LogEntryRow:Component] -->
|
||||
<!--
|
||||
@TIER: STANDARD
|
||||
@COMPLEXITY: 3
|
||||
@SEMANTICS: log, entry, row, ui
|
||||
@PURPOSE: Renders a single log entry with stacked layout optimized for narrow drawer panels.
|
||||
@LAYER: UI
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- [DEF:LogFilterBar:Component] -->
|
||||
<!--
|
||||
@TIER: STANDARD
|
||||
@COMPLEXITY: 3
|
||||
@SEMANTICS: log, filter, ui
|
||||
@PURPOSE: Compact filter toolbar for logs — level, source, and text search in a single dense row.
|
||||
@LAYER: UI
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- [DEF:TaskLogPanel:Component] -->
|
||||
<!--
|
||||
@TIER: STANDARD
|
||||
@COMPLEXITY: 3
|
||||
@SEMANTICS: task, log, panel, filter, list
|
||||
@PURPOSE: Combines log filtering and display into a single cohesive dark-themed panel.
|
||||
@LAYER: UI
|
||||
|
||||
Reference in New Issue
Block a user