From fb7f393675a79e3952c46f00e72b7fd9d27124a6 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Wed, 11 Nov 2026 11:10:54 +0300 Subject: [PATCH 1/3] feat: add per stack resources usage. --- frontend/app/pages/stacks/[node]/[stack].vue | 94 +++++++- frontend/app/store/realtime.ts | 1 - frontend/app/types/index.d.ts | 30 ++- frontend/app/utils/stacks.ts | 63 +++++ frontend/tests/stack-ui.test.ts | 122 ++++++++++ frontend/tests/stacks.test.ts | 105 ++++++++- frontend/tests/workspace-stream.test.ts | 39 ++++ internal/agent/channel.go | 8 +- internal/agent/events.go | 23 ++ internal/agent/events_test.go | 98 ++++++++ internal/agent/run.go | 2 +- internal/agent/stats.go | 94 ++++++++ internal/agent/stats_test.go | 233 +++++++++++++++++++ internal/engine/docker/info.go | 15 +- internal/engine/docker/stats.go | 42 ++++ internal/engine/engine.go | 1 + internal/engine/podman/info.go | 15 +- internal/engine/podman/stats.go | 42 ++++ internal/engine/stats_test.go | 187 +++++++++++++++ internal/server/emitter_logging_test.go | 3 +- internal/server/events.go | 2 +- internal/server/stats_test.go | 230 ++++++++++++++++++ internal/server/stream.go | 26 ++- 23 files changed, 1437 insertions(+), 38 deletions(-) create mode 100644 internal/agent/stats.go create mode 100644 internal/agent/stats_test.go create mode 100644 internal/engine/docker/stats.go create mode 100644 internal/engine/podman/stats.go create mode 100644 internal/engine/stats_test.go create mode 100644 internal/server/stats_test.go diff --git a/frontend/app/pages/stacks/[node]/[stack].vue b/frontend/app/pages/stacks/[node]/[stack].vue index db40e22..485adfe 100644 --- a/frontend/app/pages/stacks/[node]/[stack].vue +++ b/frontend/app/pages/stacks/[node]/[stack].vue @@ -19,9 +19,17 @@ import StatCard from '~/components/StatCard.vue' import ContainerShell from '~/components/stacks/ContainerShell.vue' import StackLogs from '~/components/stacks/StackLogs.vue' import { useDialog } from '~/composables/useDialog' +import { useWorkspaceStream } from '~/composables/useWorkspaceStream' import { useWorkspaceStore } from '~/store/workspace' import { useAuthStore } from '~/store/auth' -import type { Service, Stack, StackAction } from '~/types' +import type { + ContainerStats, + Service, + Stack, + StackAction, + StackStatsTotals, + StatCardProps, +} from '~/types' import { notification, parse_api_response, request } from '~/utils' import { workspaceErrorMessage, @@ -30,6 +38,8 @@ import { stackStatusLabel, containerCreatedAt, containerUptime, + parseStackStats, + aggregateStackStats, } from '~/utils/stacks' definePageMeta({ layout: 'workspace' }) @@ -141,6 +151,51 @@ const busy = computed( const active = computed(() => runtime.value?.services.some((service) => 'running' === service.state), ) +const stats = ref | null>(null) +const statsStream = useWorkspaceStream( + (message) => { + if ('stack.stats' === message.event) { + stats.value = parseStackStats(message.data).containers + } + }, + () => { + stats.value = null + }, +) +const statsError = statsStream.error +const startStats = () => { + statsStream.stop() + stats.value = null + if (connected.value && active.value && !isNew.value) { + statsStream.start({ type: 'stats', node: node.value, resource: name.value }) + } +} +const servicesWithStats = computed(() => + (runtime.value?.services ?? []).map((service) => ({ + ...service, + stats: stats.value?.find((item) => service.name === item.name), + })), +) +const stackTotals = computed(() => { + if (connected.value && !active.value) { + return aggregateStackStats([]) + } + return stats.value ? aggregateStackStats(stats.value) : undefined +}) +const resourceCards = ( + stats?: StackStatsTotals & { memory_percent?: string }, +): Array => [ + { label: 'CPU', value: stats?.cpu || '-', icon: 'i-lucide-cpu' }, + { + label: 'Memory', + value: stats?.memory || '-', + hint: stats?.memory_percent, + icon: 'i-lucide-memory-stick', + }, + { label: 'Network I/O', value: stats?.network_io || '-', icon: 'i-lucide-network' }, + { label: 'Block I/O', value: stats?.block_io || '-', icon: 'i-lucide-hard-drive' }, + { label: 'PIDs', value: stats?.pids || '-', icon: 'i-lucide-list-tree' }, +] const checkingImages = computed(() => workspace.isCheckingImages(node.value, name.value)) const unavailable = computed( () => @@ -521,6 +576,7 @@ watch( }, ) watch(() => [node.value, name.value], load, { immediate: true }) +watch([node, name, connected, active, isNew], startStats, { immediate: true }) const actionCompleted = (action: StackAction | 'delete', exitCode?: number) => { if ('delete' === action && 0 === exitCode) { void navigateTo('/') @@ -655,6 +711,21 @@ onBeforeUnmount(() => { />