feat: block self-stack lifecycle ops with UI and preflight guardrails (#1569)

* feat: block self-stack lifecycle ops with UI and preflight guardrails

Refuse update, deploy, down, stop, and delete when the stack matches Sencho's compose project.

Return 409 self_stack_protected. Expose isSelf on /statuses and disable guarded UI actions.

Add SelfStackProtectedDialog and self-managed-stack preflight warning.

Closes #1564

* fix: add missing stackSelfFlags mock to useSidebarContextMenu test

The production hook now reads stackListState.stackSelfFlags[file], but the
test mock did not include it, causing 6 tests to fail with TypeError:
Cannot read properties of undefined (reading 'web.yml').

* fix: harden self-stack protection during startup

Add a global environment preflight warning when Sencho is managed inside COMPOSE_DIR.

Align status decoration and route guards on Docker label fallback detection.

Block rollback and service-level stop on the protected self stack.

* fix: add self_stack_location to diagnostics-route expected check IDs
This commit is contained in:
Anso
2026-07-06 02:08:16 -04:00
committed by GitHub
parent f30a65ee08
commit 0f9925e04f
30 changed files with 905 additions and 31 deletions
@@ -53,6 +53,7 @@ interface StackStatusInfo {
mainPort?: number;
running?: number;
total?: number;
isSelf?: boolean;
}
export interface RemoteResult {
@@ -92,6 +93,7 @@ export function useStackListState() {
const [searchQuery, setSearchQuery] = useState('');
const [stackStatuses, setStackStatuses] = useState<StackStatus>({});
const [stackPorts, setStackPorts] = useState<Record<string, number | undefined>>({});
const [stackSelfFlags, setStackSelfFlags] = useState<Record<string, boolean>>({});
const [stackCounts, setStackCounts] = useState<StackCounts>({});
const [labels, setLabels] = useState<StackLabel[]>([]);
const [stackLabelMap, setStackLabelMap] = useState<Record<string, StackLabel[]>>({});
@@ -206,6 +208,7 @@ export function useStackListState() {
if (stale()) return fileList;
let bulkStatuses: Record<string, StackRowStatus> = {};
const bulkPorts: Record<string, number | undefined> = {};
const bulkSelf: Record<string, boolean> = {};
const bulkCounts: StackCounts = {};
const raw: unknown = statusRes.ok ? await statusRes.json() : null;
@@ -213,6 +216,7 @@ export function useStackListState() {
for (const [key, val] of Object.entries(raw as Record<string, StackStatusInfo>)) {
bulkStatuses[key] = val.status;
if (val.mainPort) bulkPorts[key] = val.mainPort;
if (val.isSelf) bulkSelf[key] = true;
if (val.running !== undefined && val.total !== undefined) {
bulkCounts[key] = { running: val.running, total: val.total };
}
@@ -233,6 +237,7 @@ export function useStackListState() {
if (keys.length === Object.keys(prev).length && keys.every(k => prev[k] === bulkPorts[k])) return prev;
return bulkPorts;
});
setStackSelfFlags(bulkSelf);
setStackCounts(bulkCounts);
refreshLabels();
return fileList;
@@ -403,6 +408,7 @@ export function useStackListState() {
searchQuery, setSearchQuery,
stackStatuses, setStackStatuses,
stackPorts, setStackPorts,
stackSelfFlags,
stackCounts,
labels,
stackLabelMap,