fix(frontend): stabilize CrowdSec first-enable UX and guard empty-value regression

When CrowdSec is first enabled, the 10-60 second startup window caused
the toggle to immediately flicker back to unchecked, the card badge to
show 'Disabled' throughout startup, CrowdSecKeyWarning to flash before
bouncer registration completed, and CrowdSecConfig to show alarming
LAPI-not-ready banners to the user.

Root cause: the toggle, badge, and warning conditions all read from
stale sources (crowdsecStatus local state and status.crowdsec.enabled
server data) which neither reflects user intent during a pending mutation.

- Derive crowdsecChecked from crowdsecPowerMutation.variables during
  the pending window so the UI reflects intent immediately on click,
  not the lagging server state
- Show a 'Starting...' badge in warning variant throughout the startup
  window so the user knows the operation is in progress
- Suppress CrowdSecKeyWarning unconditionally while the mutation is
  pending, preventing the bouncer key alert from flashing before
  registration completes on the backend
- Broadcast the mutation's running state to the QueryClient cache via
  a synthetic crowdsec-starting key so CrowdSecConfig.tsx can read it
  without prop drilling
- In CrowdSecConfig, suppress the LAPI 'not running' (red) and
  'initializing' (yellow) banners while the startup broadcast is active,
  with a 90-second safety cap to prevent stale state from persisting
  if the tab is closed mid-mutation
- Add security.crowdsec.starting translation key to all five locales
- Add two backend regression tests confirming that empty-string setting
  values are accepted (not rejected by binding validation), preventing
  silent re-introduction of the Issue 4 bug
- Add nine RTL tests covering toggle stabilization, badge text, warning
  suppression, and LAPI banner suppression/expiry
- Add four Playwright E2E tests using route interception to simulate
  the startup delay in a real browser context

Fixes Issues 3 and 4 from the fresh-install bug report.
This commit is contained in:
GitHub Actions
2026-03-18 16:57:23 +00:00
parent ac2026159e
commit 1de29fe6fc
13 changed files with 1331 additions and 10 deletions

View File

@@ -40,6 +40,22 @@ export default function CrowdSecConfig() {
const [validationError, setValidationError] = useState<string | null>(null)
const [applyInfo, setApplyInfo] = useState<{ status?: string; backup?: string; reloadHint?: boolean; usedCscli?: boolean; cacheKey?: string } | null>(null)
const queryClient = useQueryClient()
// Read the "CrowdSec is starting" signal broadcast by Security.tsx via the
// QueryClient cache. No HTTP call is made; this is pure in-memory coordination.
const { data: crowdsecStartingCache } = useQuery<{ isStarting: boolean; startedAt?: number }>({
queryKey: ['crowdsec-starting'],
queryFn: () => ({ isStarting: false, startedAt: 0 }),
staleTime: Infinity,
gcTime: Infinity,
})
// isStartingUp is true only while the mutation is genuinely running.
// The 90-second cap guards against stale cache if Security.tsx onSuccess/onError
// never fired (e.g., browser tab was closed mid-mutation).
const isStartingUp =
(crowdsecStartingCache?.isStarting === true) &&
Date.now() - (crowdsecStartingCache.startedAt ?? 0) < 90_000
const isLocalMode = !!status && status.crowdsec?.mode !== 'disabled'
// Note: CrowdSec mode is now controlled via Security Dashboard toggle
const { data: featureFlags } = useQuery({ queryKey: ['feature-flags'], queryFn: getFeatureFlags })
@@ -579,7 +595,7 @@ export default function CrowdSecConfig() {
)}
{/* Yellow warning: Process running but LAPI initializing */}
{lapiStatusQuery.data && lapiStatusQuery.data.running && !lapiStatusQuery.data.lapi_ready && initialCheckComplete && (
{lapiStatusQuery.data && lapiStatusQuery.data.running && !lapiStatusQuery.data.lapi_ready && initialCheckComplete && !isStartingUp && (
<div className="flex items-start gap-3 p-4 bg-yellow-900/20 border border-yellow-700/50 rounded-lg" data-testid="lapi-warning">
<AlertTriangle className="w-5 h-5 text-yellow-400 shrink-0 mt-0.5" />
<div className="flex-1">
@@ -605,7 +621,7 @@ export default function CrowdSecConfig() {
)}
{/* Red warning: Process not running at all */}
{lapiStatusQuery.data && !lapiStatusQuery.data.running && initialCheckComplete && (
{lapiStatusQuery.data && !lapiStatusQuery.data.running && initialCheckComplete && !isStartingUp && (
<div className="flex items-start gap-3 p-4 bg-red-900/20 border border-red-700/50 rounded-lg" data-testid="lapi-not-running-warning">
<AlertTriangle className="w-5 h-5 text-red-400 shrink-0 mt-0.5" />
<div className="flex-1">