feat: container logs viewer with SSE streaming and line limiter

- Add GET /api/projects/{id}/stages/{stage}/instances/{iid}/logs endpoint
- Supports JSON mode (returns array of lines) and SSE mode (streams in real-time)
- Docker log stream header (8-byte prefix) stripped automatically
- ContainerLogs component with:
  - Tail line selector (50/200/500/1000)
  - Follow button for real-time streaming via SSE
  - Auto-scroll to bottom
  - Dark terminal-style display
  - Close button
- Logs button (events icon) on each instance card
- i18n keys in EN and RU
This commit is contained in:
2026-04-05 14:04:45 +03:00
parent ac3132d172
commit d03cc3c811
8 changed files with 322 additions and 1 deletions
+22 -1
View File
@@ -5,8 +5,9 @@
import type { Instance } from '$lib/types';
import StatusBadge from './StatusBadge.svelte';
import ContainerStats from './ContainerStats.svelte';
import ContainerLogs from './ContainerLogs.svelte';
import ConfirmDialog from './ConfirmDialog.svelte';
import { IconPlay, IconStop, IconRestart, IconTrash, IconExternalLink } from '$lib/components/icons';
import { IconPlay, IconStop, IconRestart, IconTrash, IconExternalLink, IconEvents } from '$lib/components/icons';
import { t } from '$lib/i18n';
import * as api from '$lib/api';
@@ -22,6 +23,7 @@
let loading = $state(false);
let error = $state('');
let confirmAction = $state<'stop' | 'restart' | 'remove' | null>(null);
let showLogs = $state(false);
const subdomainUrl = $derived(
instance.subdomain && domain
@@ -133,6 +135,14 @@
<IconPlay size={16} />
</button>
{/if}
<button
type="button"
class="rounded-lg p-2 text-[var(--text-tertiary)] hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-800 dark:hover:text-gray-300 transition-all duration-150"
title={$t('logs.title')}
onclick={() => { showLogs = !showLogs; }}
>
<IconEvents size={16} />
</button>
<button
type="button"
class="rounded-lg p-2 text-[var(--text-tertiary)] hover:bg-red-50 hover:text-red-600 disabled:opacity-50 transition-all duration-150 active:animate-press"
@@ -149,6 +159,17 @@
<ContainerStats projectId={projectId} stageId={instance.stage_id} instanceId={instance.id} />
{/if}
{#if showLogs}
<div class="mt-2">
<ContainerLogs
{projectId}
stageId={instance.stage_id}
instanceId={instance.id}
onclose={() => { showLogs = false; }}
/>
</div>
{/if}
{#if error}
<p class="mt-2 text-xs text-[var(--color-danger)]">{error}</p>
{/if}