feat(apps): per-workload deploy history, rollback, and resource metrics

Two additions to the app detail page, each backed by a per-workload
endpoint.

Deploy history + rollback:
- New deploy_history table — a structured, version-pinned ledger of every
  dispatch (success AND failure), distinct from the free-text event_log.
  Recorded at the single DispatchPlugin choke point so every source kind
  is covered. The raw deploy error is never persisted (it can carry
  registry-auth / compose-stdout secrets) — only a generic marker, with
  detail going to slog. Pruned to the newest N per workload; cascade-
  deleted with the workload.
- GET /api/workloads/{id}/deploys lists the ledger; POST .../rollback
  (admin) replays a prior successful deploy's pinned reference as a
  rollback-reason dispatch. Phase 1 is image-source only (RollbackCapable);
  git-built sources need checkout-by-commit, a later phase.
- DeployHistoryPanel.svelte renders the ledger with confirm-gated rollback.

Per-workload metrics:
- ListContainerStatsSamplesByWorkload joins the existing container stats
  samples through the containers index; GET /api/workloads/{id}/stats/history
  aggregates CPU/memory per timestamp across the workload's containers.
- WorkloadMetricsPanel.svelte reuses ResourceChart (CPU% + memory MiB,
  windowed, 15s poll).

en/ru i18n added with parity. Tests: store CRUD + cascade + workload-scoped
join, deployer recording (incl. secret-non-leak on failure), API rollback
guards, and per-timestamp aggregation. Plans under docs/plans/.
This commit is contained in:
2026-06-19 16:22:12 +03:00
parent c8e71a0c34
commit 0c4c338bfe
23 changed files with 1828 additions and 0 deletions
+60
View File
@@ -938,6 +938,66 @@ export function deployPluginWorkload(
return post(`/api/workloads/${id}/deploy`, body ?? {});
}
// ── Deploy history + rollback ───────────────────────────────────────
// Structured, version-pinned ledger of every deploy dispatch (success and
// failure). `rollbackable` is computed server-side: a successful deploy of a
// source kind that supports reference-pinned redeploy (image today).
export interface DeployHistoryEntry {
id: number;
workload_id: string;
source_kind: string;
reference: string;
reason: string;
triggered_by: string;
note: string;
outcome: 'success' | 'failure';
error: string;
started_at: string;
finished_at: string;
rollbackable: boolean;
}
export function fetchWorkloadDeploys(
id: string,
params?: { limit?: number; offset?: number },
signal?: AbortSignal
): Promise<DeployHistoryEntry[]> {
const query = new URLSearchParams();
if (params?.limit) query.set('limit', String(params.limit));
if (params?.offset) query.set('offset', String(params.offset));
const qs = query.toString();
return get<DeployHistoryEntry[]>(`/api/workloads/${id}/deploys${qs ? `?${qs}` : ''}`, signal);
}
export function rollbackWorkload(
id: string,
deployId: number
): Promise<{ workload_id: string; reference: string; rollback_of: number; triggered_by: string }> {
return post(`/api/workloads/${id}/rollback`, { deploy_id: deployId });
}
// ── Per-workload metrics history ────────────────────────────────────
// CPU% and memory (bytes) summed across the workload's containers, one
// point per sampled timestamp. Empty when stats collection is off / Docker
// was down / the workload is new.
export interface WorkloadStatsPoint {
ts: number;
cpu_percent: number;
memory_usage: number;
memory_limit: number;
}
export function fetchWorkloadStatsHistory(
id: string,
window = '2h',
signal?: AbortSignal
): Promise<WorkloadStatsPoint[]> {
return get<WorkloadStatsPoint[]>(
`/api/workloads/${id}/stats/history?window=${encodeURIComponent(window)}`,
signal
);
}
export function listHookKinds(signal?: AbortSignal): Promise<import('./types').HookKinds> {
return get<import('./types').HookKinds>('/api/hooks/kinds', signal);
}