From d6ee9517d03528dce2f31227f5fa06ffad2d53b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Taylan=20Bak=C4=B1rc=C4=B1o=C4=9Flu?= Date: Mon, 17 Nov 2025 11:56:15 +0300 Subject: [PATCH] fix(backend): Inactive APPLIED entities incorrectly showing as pending MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL FIX: has_pending_config calculation for both backends and frontends Problem Found (Console Log): Backend/Frontend: { last_config_status: 'APPLIED', <- Already applied! has_pending_config: true, <- But flag is TRUE! is_active: false <- Inactive (soft-deleted) } Apply response: 'No pending changes to apply' Reject response: 'No pending changes found to reject' Result: Entity stuck in Apply Management forever! Root Cause: OLD LOGIC (backend.py line 438, frontend.py line 363): has_pending_config = (config_version OR status=PENDING OR is_inactive) AND NOT rejected For inactive + APPLIED entity: - has_config_version: FALSE - has_pending_status: FALSE (status=APPLIED) - is_inactive: TRUE - Result: TRUE (incorrectly marked as pending!) Why It Matters: - Design: Inactive entities should show as pending for soft-delete workflow - Problem: Entity with is_active=FALSE + last_config_status='APPLIED' = soft-delete already applied! - Apply/Reject: Both look for PENDING status, find none, do nothing - UI: Entity remains in Apply Management (has_pending_config=true forever) Solution (Both backend.py and frontend.py): Inactive entity is only pending if last_config_status is PENDING, not APPLIED: OLD: is_inactive → always pending NEW: (is_inactive AND status=PENDING) → only pending if not yet applied Formula: has_pending_config = ( config_version OR status=PENDING OR (is_inactive AND status=PENDING) ) AND NOT rejected AND NOT (is_inactive AND is_applied) Test Cases: ✅ Active entity, PENDING → pending=TRUE ✅ Active entity, APPLIED → pending=FALSE ✅ Inactive entity, PENDING → pending=TRUE (soft-delete needs apply) ✅ Inactive entity, APPLIED → pending=FALSE (soft-delete already applied) <- FIXED! ✅ Inactive entity, REJECTED → pending=FALSE Backend.py Changes (lines 426-445): - Added is_applied flag - Added is_inactive_and_pending logic - Updated has_pending_config formula - Comprehensive comments Frontend.py Changes (lines 359-370): - Same logic as backend for consistency - Inline expression (no loop variables) - Comprehensive comments Impact: - 'deneme-sil' backend will show has_pending_config=FALSE - Backend/Frontend will disappear from Apply Management - No more stuck entities after soft-delete apply - Agent sync race condition protected (inactive+APPLIED=not pending) - Consistent behavior across all entity types Related: c120445 (apply endpoint fix), f80c026 (debug logs) Refs: #has-pending-config #inactive-entity #apply-management #consistency --- backend/routers/backend.py | 13 ++++++++++--- backend/routers/frontend.py | 12 ++++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/backend/routers/backend.py b/backend/routers/backend.py index 554a733..bea2519 100644 --- a/backend/routers/backend.py +++ b/backend/routers/backend.py @@ -428,14 +428,21 @@ async def get_backends(cluster_id: Optional[int] = None, include_inactive: bool # Check if backend has pending changes via: # 1. Config versions (pending_backend_ids) - version name parsing # 2. Entity's own last_config_status (for bulk import and other operations) - # 3. Backend is inactive (soft delete) - BUT NOT if REJECTED + # 3. Backend is inactive (soft delete) AND pending - NOT if already APPLIED has_config_version = backend["id"] in pending_backend_ids has_pending_status = backend.get("last_config_status") == "PENDING" is_inactive = not backend.get("is_active", True) is_rejected = backend.get("last_config_status") == "REJECTED" + is_applied = backend.get("last_config_status") == "APPLIED" - # CRITICAL: Exclude REJECTED entities from pending (already rejected, no action needed) - backend["has_pending_config"] = (has_config_version or has_pending_status or is_inactive) and not is_rejected + # CRITICAL FIX: Inactive backend should only be pending if not already APPLIED/REJECTED + # Problem: Backend with is_active=FALSE and last_config_status='APPLIED' was showing as pending + # Result: Apply/Reject couldn't process it (no PENDING status), but UI kept showing it + # Solution: Inactive backend is only pending if last_config_status is PENDING + is_inactive_and_pending = is_inactive and has_pending_status + + # CRITICAL: Exclude REJECTED and APPLIED inactive entities from pending + backend["has_pending_config"] = (has_config_version or has_pending_status or is_inactive_and_pending) and not is_rejected and not (is_inactive and is_applied) # Enhanced debug logging if backend['name'] == 'backend7' or has_config_version or has_pending_status: diff --git a/backend/routers/frontend.py b/backend/routers/frontend.py index be69e0d..e001908 100644 --- a/backend/routers/frontend.py +++ b/backend/routers/frontend.py @@ -357,12 +357,16 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool "updated_at": f["updated_at"].isoformat().replace('+00:00', 'Z') if f["updated_at"] else None, "cluster_id": f.get("cluster_id"), "has_pending_config": ( + # CRITICAL FIX: Same as backend.py logic for consistency + # Problem: Frontend with is_active=FALSE and last_config_status='APPLIED' was showing as pending + # Solution: Inactive frontend is only pending if last_config_status is PENDING, not APPLIED ( - f["id"] in pending_frontend_ids or - f.get("last_config_status") == "PENDING" or - not f.get("is_active", True) + (f["id"] in pending_frontend_ids) or + (f.get("last_config_status") == "PENDING") or + (not f.get("is_active", True) and f.get("last_config_status") == "PENDING") ) and - f.get("last_config_status") != "REJECTED" # Exclude REJECTED entities + (f.get("last_config_status") != "REJECTED") and + not (not f.get("is_active", True) and f.get("last_config_status") == "APPLIED") ) } for f in frontends ]