TaskLog fix

This commit is contained in:
2026-01-19 17:10:43 +03:00
parent 2d2435642d
commit 3bbe320949
2 changed files with 184 additions and 98 deletions

View File

@@ -1,15 +1,16 @@
<!-- [DEF:TaskLogViewer:Component] --> <!-- [DEF:TaskLogViewer:Component] -->
<!-- <!--
@SEMANTICS: task, log, viewer, modal @SEMANTICS: task, log, viewer, modal, inline
@PURPOSE: Displays detailed logs for a specific task in a modal. @PURPOSE: Displays detailed logs for a specific task in a modal or inline.
@LAYER: UI @LAYER: UI
@RELATION: USES -> frontend/src/lib/api.js (inferred) @RELATION: USES -> frontend/src/services/taskService.js
--> -->
<script> <script>
import { createEventDispatcher, onMount, onDestroy } from 'svelte'; import { createEventDispatcher, onMount, onDestroy } from 'svelte';
import { getTaskLogs } from '../services/taskService.js'; import { getTaskLogs } from '../services/taskService.js';
export let show = false; export let show = false;
export let inline = false;
export let taskId = null; export let taskId = null;
export let taskStatus = null; // To know if we should poll export let taskStatus = null; // To know if we should poll
@@ -22,19 +23,27 @@
let autoScroll = true; let autoScroll = true;
let logContainer; let logContainer;
$: shouldShow = inline || show;
// [DEF:fetchLogs:Function] // [DEF:fetchLogs:Function]
// @PURPOSE: Fetches logs for the current task. /**
// @PRE: taskId must be set. * @purpose Fetches logs for the current task.
// @POST: logs array is updated with data from taskService. * @pre taskId must be set.
* @post logs array is updated with data from taskService.
* @side_effect Updates logs, loading, and error state.
*/
async function fetchLogs() { async function fetchLogs() {
if (!taskId) return; if (!taskId) return;
console.log(`[fetchLogs][Action] Fetching logs for task context={{'taskId': '${taskId}'}}`);
try { try {
logs = await getTaskLogs(taskId); logs = await getTaskLogs(taskId);
if (autoScroll) { if (autoScroll) {
scrollToBottom(); scrollToBottom();
} }
console.log(`[fetchLogs][Coherence:OK] Logs fetched context={{'count': ${logs.length}}}`);
} catch (e) { } catch (e) {
error = e.message; error = e.message;
console.error(`[fetchLogs][Coherence:Failed] Error fetching logs context={{'error': '${e.message}'}}`);
} finally { } finally {
loading = false; loading = false;
} }
@@ -42,9 +51,11 @@
// [/DEF:fetchLogs:Function] // [/DEF:fetchLogs:Function]
// [DEF:scrollToBottom:Function] // [DEF:scrollToBottom:Function]
// @PURPOSE: Scrolls the log container to the bottom. /**
// @PRE: logContainer element must be bound. * @purpose Scrolls the log container to the bottom.
// @POST: logContainer scrollTop is set to scrollHeight. * @pre logContainer element must be bound.
* @post logContainer scrollTop is set to scrollHeight.
*/
function scrollToBottom() { function scrollToBottom() {
if (logContainer) { if (logContainer) {
setTimeout(() => { setTimeout(() => {
@@ -55,9 +66,11 @@
// [/DEF:scrollToBottom:Function] // [/DEF:scrollToBottom:Function]
// [DEF:handleScroll:Function] // [DEF:handleScroll:Function]
// @PURPOSE: Updates auto-scroll preference based on scroll position. /**
// @PRE: logContainer scroll event fired. * @purpose Updates auto-scroll preference based on scroll position.
// @POST: autoScroll boolean is updated. * @pre logContainer scroll event fired.
* @post autoScroll boolean is updated.
*/
function handleScroll() { function handleScroll() {
if (!logContainer) return; if (!logContainer) return;
// If user scrolls up, disable auto-scroll // If user scrolls up, disable auto-scroll
@@ -68,9 +81,11 @@
// [/DEF:handleScroll:Function] // [/DEF:handleScroll:Function]
// [DEF:close:Function] // [DEF:close:Function]
// @PURPOSE: Closes the log viewer modal. /**
// @PRE: Modal is open. * @purpose Closes the log viewer modal.
// @POST: Modal is closed and close event is dispatched. * @pre Modal is open.
* @post Modal is closed and close event is dispatched.
*/
function close() { function close() {
dispatch('close'); dispatch('close');
show = false; show = false;
@@ -78,9 +93,11 @@
// [/DEF:close:Function] // [/DEF:close:Function]
// [DEF:getLogLevelColor:Function] // [DEF:getLogLevelColor:Function]
// @PURPOSE: Returns the CSS color class for a given log level. /**
// @PRE: level string is provided. * @purpose Returns the CSS color class for a given log level.
// @POST: Returns tailwind color class string. * @pre level string is provided.
* @post Returns tailwind color class string.
*/
function getLogLevelColor(level) { function getLogLevelColor(level) {
switch (level) { switch (level) {
case 'INFO': return 'text-blue-600'; case 'INFO': return 'text-blue-600';
@@ -92,8 +109,10 @@
} }
// [/DEF:getLogLevelColor:Function] // [/DEF:getLogLevelColor:Function]
// React to changes in show/taskId // React to changes in show/taskId/taskStatus
$: if (show && taskId) { $: if (shouldShow && taskId) {
if (interval) clearInterval(interval);
logs = []; logs = [];
loading = true; loading = true;
error = ""; error = "";
@@ -108,16 +127,59 @@
} }
// [DEF:onDestroy:Function] // [DEF:onDestroy:Function]
// @PURPOSE: Cleans up the polling interval. /**
// @PRE: Component is being destroyed. * @purpose Cleans up the polling interval.
// @POST: Polling interval is cleared. * @pre Component is being destroyed.
* @post Polling interval is cleared.
*/
onDestroy(() => { onDestroy(() => {
if (interval) clearInterval(interval); if (interval) clearInterval(interval);
}); });
// [/DEF:onDestroy:Function] // [/DEF:onDestroy:Function]
</script> </script>
{#if show} {#if shouldShow}
{#if inline}
<div class="flex flex-col h-full w-full p-4">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-gray-900">
Task Logs <span class="text-sm text-gray-500 font-normal">({taskId})</span>
</h3>
<button on:click={fetchLogs} class="text-sm text-indigo-600 hover:text-indigo-900">Refresh</button>
</div>
<div class="flex-1 border rounded-md bg-gray-50 p-4 overflow-y-auto font-mono text-sm"
bind:this={logContainer}
on:scroll={handleScroll}>
{#if loading && logs.length === 0}
<p class="text-gray-500 text-center">Loading logs...</p>
{:else if error}
<p class="text-red-500 text-center">{error}</p>
{:else if logs.length === 0}
<p class="text-gray-500 text-center">No logs available.</p>
{:else}
{#each logs as log}
<div class="mb-1 hover:bg-gray-100 p-1 rounded">
<span class="text-gray-400 text-xs mr-2">
{new Date(log.timestamp).toLocaleTimeString()}
</span>
<span class="font-bold text-xs mr-2 w-16 inline-block {getLogLevelColor(log.level)}">
[{log.level}]
</span>
<span class="text-gray-800 break-words">
{log.message}
</span>
{#if log.context}
<div class="ml-24 text-xs text-gray-500 mt-1 bg-gray-100 p-1 rounded overflow-x-auto">
<pre>{JSON.stringify(log.context, null, 2)}</pre>
</div>
{/if}
</div>
{/each}
{/if}
</div>
</div>
{:else}
<div class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true"> <div class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!-- Background overlay --> <!-- Background overlay -->
@@ -179,5 +241,6 @@
</div> </div>
</div> </div>
</div> </div>
{/if}
{/if} {/if}
<!-- [/DEF:TaskLogViewer:Component] --> <!-- [/DEF:TaskLogViewer:Component] -->

View File

@@ -1,3 +1,11 @@
<!-- [DEF:TaskManagementPage:Component] -->
<!--
@SEMANTICS: tasks, management, history, logs
@PURPOSE: Page for managing and monitoring tasks.
@LAYER: Page
@RELATION: USES -> TaskList
@RELATION: USES -> TaskLogViewer
-->
<script> <script>
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { getTasks, createTask, getEnvironmentsList } from '../../lib/api'; import { getTasks, createTask, getEnvironmentsList } from '../../lib/api';
@@ -14,11 +22,13 @@
let selectedEnvId = ''; let selectedEnvId = '';
// [DEF:loadInitialData:Function] // [DEF:loadInitialData:Function]
/* @PURPOSE: Loads tasks and environments on page initialization. /**
@PRE: API must be reachable. * @purpose Loads tasks and environments on page initialization.
@POST: tasks and environments variables are populated. * @pre API must be reachable.
* @post tasks and environments variables are populated.
*/ */
async function loadInitialData() { async function loadInitialData() {
console.log("[loadInitialData][Action] Loading initial tasks and environments");
try { try {
loading = true; loading = true;
const [tasksData, envsData] = await Promise.all([ const [tasksData, envsData] = await Promise.all([
@@ -27,8 +37,9 @@
]); ]);
tasks = tasksData; tasks = tasksData;
environments = envsData; environments = envsData;
console.log(`[loadInitialData][Coherence:OK] Data loaded context={{'tasks': ${tasks.length}, 'envs': ${environments.length}}}`);
} catch (error) { } catch (error) {
console.error('Failed to load tasks data:', error); console.error(`[loadInitialData][Coherence:Failed] Failed to load tasks data context={{'error': '${error.message}'}}`);
} finally { } finally {
loading = false; loading = false;
} }
@@ -36,9 +47,10 @@
// [/DEF:loadInitialData:Function] // [/DEF:loadInitialData:Function]
// [DEF:refreshTasks:Function] // [DEF:refreshTasks:Function]
/* @PURPOSE: Periodically refreshes the task list. /**
@PRE: API must be reachable. * @purpose Periodically refreshes the task list.
@POST: tasks variable is updated if data is valid. * @pre API must be reachable.
* @post tasks variable is updated if data is valid.
*/ */
async function refreshTasks() { async function refreshTasks() {
try { try {
@@ -48,25 +60,28 @@
tasks = data; tasks = data;
} }
} catch (error) { } catch (error) {
console.error('Failed to refresh tasks:', error); console.error(`[refreshTasks][Coherence:Failed] Failed to refresh tasks context={{'error': '${error.message}'}}`);
} }
} }
// [/DEF:refreshTasks:Function] // [/DEF:refreshTasks:Function]
// [DEF:handleSelectTask:Function] // [DEF:handleSelectTask:Function]
/* @PURPOSE: Updates the selected task ID when a task is clicked. /**
@PRE: event.detail.id must be provided. * @purpose Updates the selected task ID when a task is clicked.
@POST: selectedTaskId is updated. * @pre event.detail.id must be provided.
* @post selectedTaskId is updated.
*/ */
function handleSelectTask(event) { function handleSelectTask(event) {
selectedTaskId = event.detail.id; selectedTaskId = event.detail.id;
console.log(`[handleSelectTask][Action] Task selected context={{'taskId': '${selectedTaskId}'}}`);
} }
// [/DEF:handleSelectTask:Function] // [/DEF:handleSelectTask:Function]
// [DEF:handleRunBackup:Function] // [DEF:handleRunBackup:Function]
/* @PURPOSE: Triggers a manual backup task for the selected environment. /**
@PRE: selectedEnvId must not be empty. * @purpose Triggers a manual backup task for the selected environment.
@POST: Backup task is created and task list is refreshed. * @pre selectedEnvId must not be empty.
* @post Backup task is created and task list is refreshed.
*/ */
async function handleRunBackup() { async function handleRunBackup() {
if (!selectedEnvId) { if (!selectedEnvId) {
@@ -74,14 +89,16 @@
return; return;
} }
console.log(`[handleRunBackup][Action] Starting backup for env context={{'envId': '${selectedEnvId}'}}`);
try { try {
const task = await createTask('superset-backup', { environment_id: selectedEnvId }); const task = await createTask('superset-backup', { environment_id: selectedEnvId });
addToast('Backup task started', 'success'); addToast('Backup task started', 'success');
showBackupModal = false; showBackupModal = false;
selectedTaskId = task.id; selectedTaskId = task.id;
await refreshTasks(); await refreshTasks();
console.log(`[handleRunBackup][Coherence:OK] Backup task created context={{'taskId': '${task.id}'}}`);
} catch (error) { } catch (error) {
console.error('Failed to start backup:', error); console.error(`[handleRunBackup][Coherence:Failed] Failed to start backup context={{'error': '${error.message}'}}`);
} }
} }
// [/DEF:handleRunBackup:Function] // [/DEF:handleRunBackup:Function]
@@ -117,7 +134,11 @@
<h2 class="text-lg font-semibold mb-3 text-gray-700">Task Details & Logs</h2> <h2 class="text-lg font-semibold mb-3 text-gray-700">Task Details & Logs</h2>
{#if selectedTaskId} {#if selectedTaskId}
<div class="bg-white rounded-lg shadow-lg h-[600px] flex flex-col"> <div class="bg-white rounded-lg shadow-lg h-[600px] flex flex-col">
<TaskLogViewer taskId={selectedTaskId} /> <TaskLogViewer
taskId={selectedTaskId}
taskStatus={tasks.find(t => t.id === selectedTaskId)?.status}
inline={true}
/>
</div> </div>
{:else} {:else}
<div class="bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg h-[600px] flex items-center justify-center text-gray-500"> <div class="bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg h-[600px] flex items-center justify-center text-gray-500">
@@ -162,3 +183,5 @@
</div> </div>
</div> </div>
{/if} {/if}
<!-- [/DEF:TaskManagementPage:Component] -->