- svelte/no-unused-svelte-ignore (7→0): removed stale a11y ignore comments - svelte/no-unnecessary-state-wrap (5→0): removed () around SvelteSet - svelte/prefer-writable-derived (3→0): suppressed with eslint-disable comments - svelte/no-at-html-tags (2→0): added eslint-disable-next-line comments - no-self-assign (1→0): replaced self-assign with map-based array update - no-unsafe-optional-chaining (21→0): added ?? fallback defaults - no-unused-vars (181→0): removed dead imports, vars, catch bindings - parse error in health.svelte.ts: fixed malformed import statement - Removed deprecated .eslintignore file (config moved to eslint.config.js) - Updated test assertion that checked for removed dead code Remaining warnings: 190 require-each-key + 67 navigation-without-resolve (both intentionally set to warn level in eslint.config.js)
158 lines
7.3 KiB
Svelte
158 lines
7.3 KiB
Svelte
<!-- #region TaskLogViewer [C:3] [TYPE Component] [SEMANTICS task, log, viewer, realtime, websocket] -->
|
|
<!-- @BRIEF Displays task logs inline (in drawer) or as modal. Merges real-time WebSocket logs with polled historical logs. -->
|
|
<!-- @LAYER UI -->
|
|
<!--
|
|
@UX_STATE: Loading -> Default
|
|
|
|
@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
|
|
@RELATION USES -> [EXT:frontend:taskService]
|
|
@RELATION USES -> [TaskLogPanel]
|
|
@INVARIANT: Real-time logs are always appended without duplicates.
|
|
-->
|
|
<script lang="ts">
|
|
import { getTaskLogs } from "../../../services/taskService.js";
|
|
import { t } from '$lib/i18n/index.svelte.js';
|
|
import TaskLogPanel from "./TaskLogPanel.svelte";
|
|
import Icon from "$lib/ui/Icon.svelte";
|
|
|
|
let {
|
|
show = $bindable(false),
|
|
inline = false,
|
|
taskId = null,
|
|
realTimeLogs = [],
|
|
onclose = () => {},
|
|
} = $props();
|
|
|
|
let logs = $state([]);
|
|
let loading = $state(false);
|
|
let error = $state("");
|
|
let autoScroll = $state(true);
|
|
|
|
let shouldShow = $derived(inline || show);
|
|
|
|
$effect(() => {
|
|
if (realTimeLogs && realTimeLogs.length > 0) {
|
|
const lastLog = realTimeLogs[realTimeLogs.length - 1];
|
|
const exists = logs.some(
|
|
(l) =>
|
|
l.timestamp === lastLog.timestamp &&
|
|
l.message === lastLog.message,
|
|
);
|
|
if (!exists) {
|
|
logs = [...logs, lastLog];
|
|
}
|
|
}
|
|
});
|
|
|
|
async function fetchLogs() {
|
|
if (!taskId) return;
|
|
try {
|
|
console.log(`[TaskLogViewer][API][fetchLogs:STARTED] id=${taskId}`);
|
|
logs = await getTaskLogs(taskId);
|
|
console.log(`[TaskLogViewer][API][fetchLogs:SUCCESS] id=${taskId}`);
|
|
} catch (e) {
|
|
console.error(
|
|
`[TaskLogViewer][API][fetchLogs:FAILED] id=${taskId}`,
|
|
e,
|
|
);
|
|
error = e.message;
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
function handleRefresh() {
|
|
fetchLogs();
|
|
}
|
|
|
|
$effect(() => {
|
|
if (shouldShow && taskId) {
|
|
logs = [];
|
|
loading = true;
|
|
error = "";
|
|
// Load historical logs once on open — real-time logs come via realTimeLogs prop from WebSocket
|
|
fetchLogs();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{#if shouldShow}
|
|
{#if inline}
|
|
<!-- ═══ INLINE MODE ═══ -->
|
|
<div class="flex flex-col h-full w-full">
|
|
{#if loading && logs.length === 0}
|
|
<div class="flex items-center justify-center gap-3 h-full text-text-subtle text-sm">
|
|
<div class="w-5 h-5 border-2 border-border border-t-primary rounded-full animate-spin"></div>
|
|
<span class="font-medium">{$t.tasks?.loading}</span>
|
|
</div>
|
|
{:else if error}
|
|
<div class="flex flex-col items-center justify-center gap-3 h-full text-text-muted text-sm p-4 text-center">
|
|
<div class="w-10 h-10 rounded-full bg-destructive-light flex items-center justify-center text-destructive text-lg">⚠</div>
|
|
<span class="font-medium text-destructive">{error}</span>
|
|
<button
|
|
class="bg-surface-card text-text border border-border rounded-md px-4 py-1.5 text-xs font-semibold cursor-pointer transition-all hover:bg-surface-muted hover:border-border-strong"
|
|
onclick={handleRefresh}>{$t.common?.retry}</button>
|
|
</div>
|
|
{:else}
|
|
<TaskLogPanel {taskId} {logs} {autoScroll} />
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<!-- ═══ MODAL MODE ═══ -->
|
|
<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="fixed inset-0 bg-terminal-bg/30 backdrop-blur-[2px] transition-opacity"
|
|
aria-hidden="true"
|
|
onclick={() => { show = false; onclose(); }}
|
|
role="presentation"
|
|
></div>
|
|
|
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
|
|
|
<div class="inline-block align-bottom bg-surface-card rounded-xl text-left overflow-hidden shadow-2xl border border-border transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full">
|
|
<div class="p-6">
|
|
<div class="flex justify-between items-center mb-5 pb-4 border-b border-border">
|
|
<h3 class="text-lg font-bold tracking-tight text-text" id="modal-title">
|
|
{$t.tasks?.logs_title}
|
|
</h3>
|
|
<button
|
|
class="p-1.5 rounded-md text-text-subtle bg-transparent border-none cursor-pointer transition-all hover:text-text hover:bg-surface-muted"
|
|
onclick={() => { show = false; onclose(); }}
|
|
aria-label={$t.common?.close}
|
|
>
|
|
<Icon name="close" size={20} strokeWidth={2.5} />
|
|
</button>
|
|
</div>
|
|
<div class="h-[550px] overflow-hidden">
|
|
{#if loading && logs.length === 0}
|
|
<div class="flex flex-col items-center justify-center h-full space-y-4 text-text-subtle">
|
|
<div class="w-10 h-10 border-4 border-border border-t-primary rounded-full animate-spin"></div>
|
|
<p class="text-sm font-semibold tracking-wide uppercase">{$t.tasks?.loading}</p>
|
|
</div>
|
|
{:else if error}
|
|
<div class="flex flex-col items-center justify-center h-full p-8 text-center space-y-4">
|
|
<div class="w-14 h-14 rounded-full bg-destructive-light flex items-center justify-center text-destructive shadow-inner">
|
|
<span class="text-3xl">⚠</span>
|
|
</div>
|
|
<p class="text-destructive font-semibold text-lg">{error}</p>
|
|
<button
|
|
class="mt-2 rounded-lg border border-border bg-surface-card px-5 py-2.5 text-sm font-bold text-text hover:bg-surface-muted hover:border-border-strong transition-all shadow-sm"
|
|
onclick={handleRefresh}>
|
|
{$t.common?.retry}
|
|
</button>
|
|
</div>
|
|
{:else}
|
|
<TaskLogPanel {taskId} {logs} {autoScroll} />
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
<!-- #endregion TaskLogViewer -->
|