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
+20
View File
@@ -46,6 +46,8 @@ type settingsRequest struct {
BackupEnabled *bool `json:"backup_enabled,omitempty"`
BackupIntervalHours *int `json:"backup_interval_hours,omitempty"`
BackupRetentionCount *int `json:"backup_retention_count,omitempty"`
StatsIntervalSeconds *int `json:"stats_interval_seconds,omitempty"`
StatsRetentionHours *int `json:"stats_retention_hours,omitempty"`
}
// getSettings handles GET /api/settings.
@@ -86,6 +88,8 @@ func (s *Server) getSettings(w http.ResponseWriter, r *http.Request) {
"backup_enabled": settings.BackupEnabled,
"backup_interval_hours": settings.BackupIntervalHours,
"backup_retention_count": settings.BackupRetentionCount,
"stats_interval_seconds": settings.StatsIntervalSeconds,
"stats_retention_hours": settings.StatsRetentionHours,
"updated_at": settings.UpdatedAt,
})
}
@@ -238,6 +242,22 @@ func (s *Server) updateSettings(w http.ResponseWriter, r *http.Request) {
}
updated.BackupRetentionCount = *req.BackupRetentionCount
}
if req.StatsIntervalSeconds != nil {
v := *req.StatsIntervalSeconds
if v != 0 && (v < 5 || v > 300) {
respondError(w, http.StatusBadRequest, "stats_interval_seconds must be 0 (disabled) or between 5 and 300")
return
}
updated.StatsIntervalSeconds = v
}
if req.StatsRetentionHours != nil {
v := *req.StatsRetentionHours
if v < 0 || v > 24 {
respondError(w, http.StatusBadRequest, "stats_retention_hours must be between 0 and 24")
return
}
updated.StatsRetentionHours = v
}
if err := s.store.UpdateSettings(updated); err != nil {
respondError(w, http.StatusInternalServerError, "failed to update settings: "+err.Error())