96fd910603
- Add concurrency limiter (max 4 GET requests) to API layer, leaving slots for SSE and health checks. Write ops bypass the limiter. - Add AbortController to ContainerStats, project detail page, and dashboard to cancel in-flight requests on navigation/unmount. - Move global SSE connection from layout to events page (only consumer). - Add 30s heartbeat to SSE endpoint to detect zombie connections. - Serialize dashboard project fetches to avoid parallel burst. - Rebuild frontend in dev-server.sh so go:embed stays in sync.
105 lines
3.1 KiB
Svelte
105 lines
3.1 KiB
Svelte
<!--
|
|
Compact CPU/memory stats bars for embedding in instance cards.
|
|
-->
|
|
<script lang="ts">
|
|
import type { ContainerStats } from '$lib/types';
|
|
import * as api from '$lib/api';
|
|
import { t } from '$lib/i18n';
|
|
|
|
interface Props {
|
|
projectId: string;
|
|
stageId: string;
|
|
instanceId: string;
|
|
}
|
|
|
|
const { projectId, stageId, instanceId }: Props = $props();
|
|
|
|
let stats = $state<ContainerStats | null>(null);
|
|
let error = $state(false);
|
|
|
|
$effect(() => {
|
|
let controller = new AbortController();
|
|
|
|
async function load() {
|
|
// Abort any previous in-flight request before starting a new one.
|
|
controller.abort();
|
|
controller = new AbortController();
|
|
try {
|
|
const result = await api.fetchContainerStats(projectId, stageId, instanceId, controller.signal);
|
|
stats = result;
|
|
error = false;
|
|
} catch (e) {
|
|
if (e instanceof DOMException && e.name === 'AbortError') return;
|
|
error = true;
|
|
}
|
|
}
|
|
|
|
load();
|
|
|
|
// Poll every 30 seconds (reduced from 10s to limit concurrent connections).
|
|
const interval = setInterval(load, 30_000);
|
|
|
|
return () => {
|
|
controller.abort();
|
|
clearInterval(interval);
|
|
};
|
|
});
|
|
|
|
function formatBytes(bytes: number): string {
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
const kb = bytes / 1024;
|
|
if (kb < 1024) return `${kb.toFixed(0)} KB`;
|
|
const mb = kb / 1024;
|
|
if (mb < 1024) return `${mb.toFixed(1)} MB`;
|
|
const gb = mb / 1024;
|
|
return `${gb.toFixed(2)} GB`;
|
|
}
|
|
|
|
const cpuColor = $derived(() => {
|
|
if (!stats) return 'bg-gray-300';
|
|
if (stats.cpu_percent > 80) return 'bg-red-500';
|
|
if (stats.cpu_percent > 50) return 'bg-amber-500';
|
|
return 'bg-emerald-500';
|
|
});
|
|
|
|
const memColor = $derived(() => {
|
|
if (!stats) return 'bg-gray-300';
|
|
if (stats.memory_percent > 80) return 'bg-red-500';
|
|
if (stats.memory_percent > 50) return 'bg-amber-500';
|
|
return 'bg-blue-500';
|
|
});
|
|
</script>
|
|
|
|
{#if stats}
|
|
<div class="mt-2 space-y-1">
|
|
<!-- CPU bar -->
|
|
<div class="flex items-center gap-2">
|
|
<span class="w-8 text-[10px] font-medium text-[var(--text-tertiary)]">{$t('stats.cpu')}</span>
|
|
<div class="relative h-1.5 flex-1 overflow-hidden rounded-full bg-[var(--surface-card-hover)]">
|
|
<div
|
|
class="absolute inset-y-0 left-0 rounded-full transition-all duration-500 {cpuColor()}"
|
|
style="width: {Math.min(stats.cpu_percent, 100)}%"
|
|
></div>
|
|
</div>
|
|
<span class="w-10 text-right text-[10px] tabular-nums text-[var(--text-tertiary)]">
|
|
{stats.cpu_percent.toFixed(1)}%
|
|
</span>
|
|
</div>
|
|
<!-- Memory bar -->
|
|
<div class="flex items-center gap-2">
|
|
<span class="w-8 text-[10px] font-medium text-[var(--text-tertiary)]">{$t('stats.mem')}</span>
|
|
<div class="relative h-1.5 flex-1 overflow-hidden rounded-full bg-[var(--surface-card-hover)]">
|
|
<div
|
|
class="absolute inset-y-0 left-0 rounded-full transition-all duration-500 {memColor()}"
|
|
style="width: {Math.min(stats.memory_percent, 100)}%"
|
|
></div>
|
|
</div>
|
|
<span class="w-24 text-right text-[10px] tabular-nums text-[var(--text-tertiary)]">
|
|
{formatBytes(stats.memory_usage)} / {formatBytes(stats.memory_limit)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{:else if error}
|
|
<p class="mt-2 text-[10px] text-[var(--text-tertiary)]">{$t('stats.unavailable')}</p>
|
|
{/if}
|