mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 18:32:52 +00:00
fix: bind deploy progress, request, and health gate to the captured node (#1357)
* fix: bind deploy progress, request, and health gate to the captured node A deploy/update/install/git-apply re-read the active node from localStorage independently at three points: the progress WebSocket at mount, the POST at call time, and the health-gate poll. If the active node changed between the click and any of those, the operation, its live output, and its health verdict could target different nodes, and the socket and POST splitting across nodes broke output streaming. Capture the operation's node once when it starts and thread it through every leg. A new nodeId option on apiFetch overrides the active-node read, the progress terminal takes a nodeId prop for its socket URL, the health gate polls on the captured node, and a failed gate records its recovery entry only on the node it ran on. The surface, the request, and the gate now always agree. * fix: scope failed-gate recovery to the file list's node and harden node targeting Addresses review findings on the captured-node binding: - Track the node the stack file list was fetched for (filesNodeId) and record a failed gate's recovery entry only when it matches the gate's node. This closes a race where switching back to the gate's node could match a same-named stack from the previous node's still-loaded list before the new list lands, keying the record to the wrong file and blocking the correct one. refreshStacks now carries a sequence token so an out-of-order resolution cannot leave files and filesNodeId inconsistent. - Make an explicit apiFetch nodeId authoritative over a caller-supplied x-node-id header. - Add the missing stack-logs nodeId cases (null, and active-node fallback) to the terminal tests.
This commit is contained in:
@@ -33,10 +33,20 @@ export function useStackListState() {
|
||||
const { nodes, activeNode } = useNodes();
|
||||
|
||||
const [files, setFiles] = useState<string[]>([]);
|
||||
// Node the current `files` list belongs to (null = local). Stamped together
|
||||
// with `files` from the node active when the fetch started, so a consumer can
|
||||
// tell whether the list it is reading is the one it expects, even during the
|
||||
// async gap right after a node switch when `files` still holds the old node's
|
||||
// entries. Filenames repeat across nodes, so a name lookup against the wrong
|
||||
// list would resolve to the wrong file.
|
||||
const [filesNodeId, setFilesNodeId] = useState<number | null>(null);
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [stackActions, setStackActions] = useState<Record<string, StackAction>>({});
|
||||
const stackActionsRef = useRef<Record<string, StackAction>>({});
|
||||
// Monotonic token per refreshStacks call; lets a superseded fetch skip its
|
||||
// state writes so a rapid node switch cannot leave a stale files/filesNodeId.
|
||||
const fetchSeqRef = useRef(0);
|
||||
|
||||
// Per-stack terminal failure records driving the in-detail recovery panel.
|
||||
// In-memory only. Node scoping is enforced by the caller, which clears these
|
||||
@@ -133,18 +143,28 @@ export function useStackListState() {
|
||||
|
||||
const refreshStacks = async (background = false): Promise<string[]> => {
|
||||
if (!background) setIsLoading(true);
|
||||
// Snapshot the node this fetch targets and a sequence token so a superseded
|
||||
// or out-of-order resolution (from a rapid node switch) cannot overwrite a
|
||||
// newer node's list, keeping `files` and `filesNodeId` consistent.
|
||||
const fetchNodeId = activeNode?.id ?? null;
|
||||
const mySeq = ++fetchSeqRef.current;
|
||||
const stale = () => fetchSeqRef.current !== mySeq;
|
||||
try {
|
||||
const res = await apiFetch('/stacks');
|
||||
if (stale()) return [];
|
||||
if (!res.ok) {
|
||||
setFiles([]);
|
||||
setFilesNodeId(fetchNodeId);
|
||||
return [];
|
||||
}
|
||||
const data = await res.json();
|
||||
const fileList: string[] = Array.isArray(data) ? data : [];
|
||||
setFiles(fileList);
|
||||
setFilesNodeId(fetchNodeId);
|
||||
|
||||
// Fetch all stack statuses in a single bulk call (falls back to per-stack queries for older remote nodes)
|
||||
const statusRes = await apiFetch('/stacks/statuses');
|
||||
if (stale()) return fileList;
|
||||
let bulkStatuses: Record<string, 'running' | 'exited' | 'unknown'> | null = null;
|
||||
const bulkPorts: Record<string, number | undefined> = {};
|
||||
if (statusRes.ok) {
|
||||
@@ -194,8 +214,10 @@ export function useStackListState() {
|
||||
refreshLabels();
|
||||
return fileList;
|
||||
} catch (error) {
|
||||
if (stale()) return [];
|
||||
console.error('Failed to refresh stacks:', error);
|
||||
setFiles([]);
|
||||
setFilesNodeId(fetchNodeId);
|
||||
return [];
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -341,7 +363,7 @@ export function useStackListState() {
|
||||
}, [remoteStackResults, nodes]);
|
||||
|
||||
return {
|
||||
files, setFiles,
|
||||
files, setFiles, filesNodeId,
|
||||
selectedFile, setSelectedFile,
|
||||
isLoading, setIsLoading,
|
||||
stackActions, stackActionsRef,
|
||||
|
||||
Reference in New Issue
Block a user