feat(stats): resource metrics dashboard + sites logs/stats
Build / build (push) Successful in 10m50s

Background collector samples CPU/memory/network/block I/O for every
instance and site on a configurable interval (default 15s, range
5-300s), persists samples to SQLite with a configurable retention
window (default 2h, range 0-24h), and skips ticks gracefully when
the Docker daemon is unreachable. Settings are reloadable without
a restart — each tick re-reads them.

New API endpoints:
- GET /api/system/stats (host snapshot: info + df)
- GET /api/system/stats/history
- GET /api/system/stats/top?by=cpu|memory
- GET /api/projects/{id}/stages/{s}/instances/{iid}/stats/history
- GET /api/sites/{id}/stats[/history]
- GET /api/sites/{id}/logs (SSE + JSON, reuses instance log streamer)

Frontend:
- ECharts added with tree-shaken imports (~180KB gzip) for
  future-proof time-series/gantt/graph visualizations
- CollapsibleSection wraps all dashboard sections (system health,
  daemons, system resources, static sites, projects) with
  localStorage-persisted open state
- SystemResourcesCard shows capacity tiles, workload utilization
  chart with 30m/2h/6h/24h window picker, disk breakdown with
  reclaimable callouts, and top 5 consumers
- ContainerStats and ContainerLogs take a source discriminated union
  so sites reuse the same components as instances; sites detail page
  embeds both for Deno backend debugging
- Settings › Maintenance exposes collection interval + retention
- Docker-unavailable state returns 503 and renders an amber banner
  instead of a generic 500

Full i18n coverage (en + ru) for all new strings.
This commit is contained in:
2026-04-24 15:02:43 +03:00
parent 0632f512e6
commit 05440a5f92
27 changed files with 1897 additions and 112 deletions
+55
View File
@@ -1,6 +1,9 @@
import type {
ApiEnvelope,
ContainerStats,
ContainerStatsSample,
SystemStats,
SystemStatsSample,
Deploy,
DeployLog,
DockerHealth,
@@ -677,6 +680,58 @@ export function fetchContainerStats(
);
}
export function fetchInstanceStatsHistory(
projectId: string,
stageId: string,
instanceId: string,
window = '2h',
signal?: AbortSignal
): Promise<ContainerStatsSample[]> {
return get<ContainerStatsSample[]>(
`/api/projects/${projectId}/stages/${stageId}/instances/${instanceId}/stats/history?window=${encodeURIComponent(window)}`,
signal
);
}
export function fetchSystemStats(signal?: AbortSignal): Promise<SystemStats> {
return get<SystemStats>('/api/system/stats', signal);
}
export function fetchSystemStatsHistory(
window = '2h',
signal?: AbortSignal
): Promise<SystemStatsSample[]> {
return get<SystemStatsSample[]>(`/api/system/stats/history?window=${encodeURIComponent(window)}`, signal);
}
export function fetchTopContainers(
by: 'cpu' | 'memory' = 'cpu',
limit = 5,
signal?: AbortSignal
): Promise<ContainerStatsSample[]> {
return get<ContainerStatsSample[]>(`/api/system/stats/top?by=${by}&limit=${limit}`, signal);
}
export function fetchStaticSiteStats(id: string, signal?: AbortSignal): Promise<ContainerStats> {
return get<ContainerStats>(`/api/sites/${id}/stats`, signal);
}
export function fetchStaticSiteStatsHistory(
id: string,
window = '2h',
signal?: AbortSignal
): Promise<ContainerStatsSample[]> {
return get<ContainerStatsSample[]>(
`/api/sites/${id}/stats/history?window=${encodeURIComponent(window)}`,
signal
);
}
export async function fetchStaticSiteLogs(id: string, tail = 200): Promise<string[]> {
const result = await get<string[] | null>(`/api/sites/${id}/logs?tail=${tail}`);
return result ?? [];
}
// ── Static Sites ──────────────────────────────────────────────────────
import type { StaticSite, StaticSiteSecret, FolderEntry, GitProvider, RepoInfo } from './types';