mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 02:12:59 +00:00
d113004359
* feat: add confirmed Take down stack action with optional volume removal Expose Take down in the stack header and sidebar with a confirmation dialog that runs compose down while keeping the stack definition on disk. Optional volume removal is gated by node capability and stack:deploy permission, with remote gateway preflight before proxying removeVolumes requests. Closes #1582 * fix: reset take-down volume checkbox when dialog closes * test: align getStackMenuVisibility assertions with showTakeDown key getStackMenuVisibility now returns a fifth lifecycle flag, showTakeDown, but three exhaustive toEqual assertions still listed only the prior four keys and failed. Add the expected showTakeDown value to each: true for the partial and exited running-stack cases, false for the self stack. * test: cover Take down visibility for running non-self stacks The getStackMenuVisibility assertions exercised the partial and exited branches and the self-stack guard, but not the raw === 'running' literal that drives showTakeDown for a normal running stack. Add a case so a regression dropping 'running' from that check is caught. * fix: drop Take down from header overflow and wire activity shortcut Remove duplicate Take down from More actions. Keep inline button when running, sidebar menu, and Cmd+ArrowDown. Record stack_taken_down in activity on successful POST /down.
53 lines
1.8 KiB
TypeScript
53 lines
1.8 KiB
TypeScript
import { NodeRegistry } from '../services/NodeRegistry';
|
|
import { CROSS_NODE_RBAC_CAPABILITY } from '../services/CapabilityRegistry';
|
|
import { getErrorMessage } from '../utils/errors';
|
|
|
|
// In-flight probes deduped per node+capability so concurrent checks for
|
|
// different capabilities on the same node cannot share the wrong boolean.
|
|
const inFlight = new Map<string, Promise<boolean>>();
|
|
|
|
function probeKey(nodeId: number, capability: string): string {
|
|
return `${nodeId}:${capability}`;
|
|
}
|
|
|
|
/**
|
|
* Whether a remote node advertises a given capability. Probes the remote's live
|
|
* /api/meta on every call (concurrent calls for the same node+capability share
|
|
* one probe).
|
|
*
|
|
* Fails closed: unsupported, offline, or unreachable remotes return false.
|
|
*/
|
|
export async function remoteAdvertisesCapability(nodeId: number, capability: string): Promise<boolean> {
|
|
const key = probeKey(nodeId, capability);
|
|
const existing = inFlight.get(key);
|
|
if (existing) return existing;
|
|
|
|
const probe = (async (): Promise<boolean> => {
|
|
try {
|
|
const meta = await NodeRegistry.getInstance().fetchMetaForNode(nodeId);
|
|
return meta.capabilities.includes(capability);
|
|
} catch (err) {
|
|
console.warn(
|
|
`[RemoteCapability] Could not verify "${capability}" for node ${nodeId}; treating as unsupported:`,
|
|
getErrorMessage(err, 'unknown'),
|
|
);
|
|
return false;
|
|
}
|
|
})();
|
|
|
|
inFlight.set(key, probe);
|
|
try {
|
|
return await probe;
|
|
} finally {
|
|
inFlight.delete(key);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether a remote node advertises cross-node RBAC enforcement for proxied
|
|
* requests. Thin wrapper over {@link remoteAdvertisesCapability}.
|
|
*/
|
|
export async function remoteSupportsCrossNodeRbac(nodeId: number): Promise<boolean> {
|
|
return remoteAdvertisesCapability(nodeId, CROSS_NODE_RBAC_CAPABILITY);
|
|
}
|