mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-19 23:06:49 +00:00
feat: add service-scoped Compose update and restore (#1648)
* feat: add service-scoped Compose update and restore Allow updating or rebuilding one declared Compose service on multi-service stacks without recreating siblings, with recovery snapshots, health-gate observation, and prune holds for rollback images. Full-stack update paths and single-service UX stay unchanged. * fix: sanitize service-scoped update log messages for CodeQL * fix: address service-scoped update audit findings B-01 through B-07 * fix: complete service-scoped update audit metadata and surfaces * test: wrap Updates readiness tests for deploy-feedback context * fix: keep service recovery reachable without Deploy Progress Make failed service-gate recovery discoverable when Deploy Progress is disabled or dismissed, suppress stale image-scan notification side effects, normalize ComposeService line endings, and add focused regression coverage. * fix: resurface ContainersHealth density and expand on multi-service stacks Service grouping hid the summary strip and Compact/Detailed/Expand controls that still applied to multi-container stacks.
This commit is contained in:
@@ -11,6 +11,11 @@
|
||||
|
||||
export type StackOpAction = 'deploy' | 'down' | 'restart' | 'stop' | 'start' | 'update';
|
||||
|
||||
export interface StackOpRecordMeta {
|
||||
targetScope?: 'stack' | 'service';
|
||||
serviceName?: string | null;
|
||||
}
|
||||
|
||||
interface StackOpStats {
|
||||
count: number;
|
||||
successCount: number;
|
||||
@@ -22,6 +27,10 @@ interface StackOpStats {
|
||||
* computed from this window on demand.
|
||||
*/
|
||||
recentSamples: number[];
|
||||
/** Most recent target scope recorded for this bucket (additive; stack default). */
|
||||
lastTargetScope: 'stack' | 'service';
|
||||
/** Most recent service name when lastTargetScope is service; otherwise null. */
|
||||
lastServiceName: string | null;
|
||||
}
|
||||
|
||||
export interface StackOpSnapshotEntry {
|
||||
@@ -33,6 +42,8 @@ export interface StackOpSnapshotEntry {
|
||||
avgMs: number;
|
||||
p50Ms: number;
|
||||
p95Ms: number;
|
||||
targetScope: 'stack' | 'service';
|
||||
serviceName: string | null;
|
||||
}
|
||||
|
||||
const MAX_SAMPLES = 1000;
|
||||
@@ -66,12 +77,26 @@ export class StackOpMetricsService {
|
||||
* Record one completed op. Call from the route layer after the lifecycle
|
||||
* call resolves or rejects; `ok=false` for the rejection path.
|
||||
*/
|
||||
public record(nodeId: number, action: StackOpAction, durationMs: number, ok: boolean): void {
|
||||
public record(
|
||||
nodeId: number,
|
||||
action: StackOpAction,
|
||||
durationMs: number,
|
||||
ok: boolean,
|
||||
meta?: StackOpRecordMeta,
|
||||
): void {
|
||||
if (!Number.isFinite(durationMs) || durationMs < 0) return;
|
||||
const k = this.key(nodeId, action);
|
||||
let s = this.stats.get(k);
|
||||
if (!s) {
|
||||
s = { count: 0, successCount: 0, errorCount: 0, totalMs: 0, recentSamples: [] };
|
||||
s = {
|
||||
count: 0,
|
||||
successCount: 0,
|
||||
errorCount: 0,
|
||||
totalMs: 0,
|
||||
recentSamples: [],
|
||||
lastTargetScope: 'stack',
|
||||
lastServiceName: null,
|
||||
};
|
||||
this.stats.set(k, s);
|
||||
}
|
||||
s.count += 1;
|
||||
@@ -84,6 +109,12 @@ export class StackOpMetricsService {
|
||||
// and this path is once-per-stack-op (low cadence).
|
||||
s.recentSamples.shift();
|
||||
}
|
||||
if (meta?.targetScope) s.lastTargetScope = meta.targetScope;
|
||||
if (meta?.targetScope === 'service') {
|
||||
s.lastServiceName = meta.serviceName ?? null;
|
||||
} else if (meta?.targetScope === 'stack') {
|
||||
s.lastServiceName = null;
|
||||
}
|
||||
}
|
||||
|
||||
public snapshot(): StackOpSnapshotEntry[] {
|
||||
@@ -102,6 +133,8 @@ export class StackOpMetricsService {
|
||||
avgMs: s.count === 0 ? 0 : Math.round(s.totalMs / s.count),
|
||||
p50Ms: percentile(sorted, 0.5),
|
||||
p95Ms: percentile(sorted, 0.95),
|
||||
targetScope: s.lastTargetScope,
|
||||
serviceName: s.lastServiceName,
|
||||
});
|
||||
}
|
||||
// Stable ordering: nodeId asc, then action asc.
|
||||
|
||||
Reference in New Issue
Block a user