fix(backend): Inactive APPLIED entities incorrectly showing as pending

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
This commit is contained in:
Taylan Bakırcıoğlu
2025-11-17 11:56:15 +03:00
committed by taylanbakircioglu
parent 60b77734a5
commit d6ee9517d0
2 changed files with 18 additions and 7 deletions
+10 -3
View File
@@ -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:
+8 -4
View File
@@ -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
]