Stream update progress into a bar on the stack's row (0.42.0)
CI / build-and-push (push) Successful in 1m55s
CI / build-and-push (push) Successful in 1m55s
Update ran as a blocking POST with nothing to show but a spinner, so the
status added in 86c67df could only sit above the table as a banner.
Adds /ws/update/{stack_id}, streaming `compose pull` then `up -d` with
--progress json, and feeds it through the existing DeployTracker — the
same weighting the deploy console uses. The result renders as a progress
bar inside the stack's own row: percentage, phase label, and layer/byte
detail. Non-streaming actions (start/stop/restart/pull/down) reuse the
bar in its indeterminate form, so every row action looks consistent.
compose_service gains _stream_phase, shared by stream_up and the new
stream_update; a failed pull short-circuits before `up`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pMmFFkdfxkoYjcEcpZTa5
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Drives a streaming compose WebSocket (`/ws/update/{id}`, `/ws/deploy/{id}`)
|
||||
* and folds its output into a progress snapshot via {@link DeployTracker}.
|
||||
*
|
||||
* The deploy console renders the same stream with its full log view; this is
|
||||
* the headless counterpart, for places that only want the progress bar — the
|
||||
* stacks list, where an update needs to be legible at a glance in one row.
|
||||
*/
|
||||
import { DeployTracker, parseDeployLine } from "./deployProgress";
|
||||
|
||||
/** Compose emits status changes far faster than the UI needs to repaint. */
|
||||
const FLUSH_MS = 150;
|
||||
|
||||
export type ProgressSnapshot = {
|
||||
pct: number;
|
||||
indeterminate: boolean;
|
||||
label: string;
|
||||
detail: string;
|
||||
};
|
||||
|
||||
function snapshot(tracker: DeployTracker): ProgressSnapshot {
|
||||
const s = tracker.snapshot();
|
||||
return { pct: s.pct, indeterminate: s.indeterminate, label: s.label, detail: s.detail };
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens `path`, reporting progress until compose finishes. Resolves on exit
|
||||
* code 0, rejects with the failure reason otherwise. The compose subprocess
|
||||
* keeps running server-side even if this socket is closed early.
|
||||
*/
|
||||
export function streamCompose(
|
||||
path: string,
|
||||
token: string,
|
||||
onProgress: (p: ProgressSnapshot) => void
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tracker = new DeployTracker();
|
||||
const proto = window.location.protocol === "https:" ? "wss" : "ws";
|
||||
const ws = new WebSocket(
|
||||
`${proto}://${window.location.host}${path}?token=${encodeURIComponent(token)}`
|
||||
);
|
||||
|
||||
let dirty = false;
|
||||
let settled = false;
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
if (!dirty) return;
|
||||
dirty = false;
|
||||
onProgress(snapshot(tracker));
|
||||
}, FLUSH_MS);
|
||||
|
||||
const settle = (err?: Error) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
window.clearInterval(timer);
|
||||
tracker.finish(!err);
|
||||
onProgress(snapshot(tracker));
|
||||
ws.close();
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
};
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
try {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (msg.type === "log") {
|
||||
const parsed = parseDeployLine(msg.line as string);
|
||||
if (parsed?.kind === "event") {
|
||||
tracker.apply(parsed.event);
|
||||
dirty = true;
|
||||
}
|
||||
} else if (msg.type === "done") {
|
||||
if (msg.returncode === 0) settle();
|
||||
else settle(new Error(`compose exited with code ${msg.returncode}`));
|
||||
} else if (msg.type === "error") {
|
||||
settle(new Error(msg.detail || "update failed"));
|
||||
}
|
||||
} catch {
|
||||
/* ignore malformed frames */
|
||||
}
|
||||
};
|
||||
ws.onerror = () => settle(new Error("connection to the server failed"));
|
||||
// Reached normally after settle() closes the socket; the guard makes this
|
||||
// a no-op then, so it only fires for a stream that dropped mid-update.
|
||||
ws.onclose = () => settle(new Error("connection closed before the update finished"));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user