32 Commits

Author SHA1 Message Date
taylanbakircioglu 7d95c737f0 fix(vip): adopt the whole VRRP instance, not one node (v1.10.8)
Four defects found while tracing the adoption flow end to end after v1.10.4
reached a live HA pair.

B3/B4 (one root, one fix). Adoption took only the node whose row was clicked:

  - adopting the BACKUP alone produced a VIP that apply always rejects, because
    apply requires exactly one MASTER member;
  - adopting the MASTER alone left the peer unmanaged, and adopting it
    afterwards hit the VRID-collision guard with 409, so a pair could never be
    completed from the panel;
  - on a UNICAST instance the single-member render dropped the unicast block
    entirely (render_keepalived_conf emits it only when peer_ips is non-empty),
    so keepalived fell back to multicast on the adopted node while its peer
    stayed unicast. They stop seeing each other and BOTH claim the VIP.

Adoption now resolves the whole instance via _collect_instance_participants,
keyed on (virtual_router_id, virtual address) - the same key keepalived uses to
group nodes. Each participant becomes a member with the role, priority and
interface its own file declares, and its own one-shot takeover hash, so the
per-node overwrite guard is unchanged. It refuses, naming the reason, when the
group has other than one MASTER, when advert_int differs across nodes, when a
declared unicast peer is not among the nodes being adopted, or when a node is
already in a live VIP - the one-active-VIP-per-agent rule that create/update
enforce via _validate_members_against_pool and adoption never called.

B1. The Apply Management "View Change" regex matched vip-(create|update|delete)
only, so an adopt version fell through to the generic HAProxy diff and rendered
the cluster's whole haproxy.cfg as removed. Display-only, but alarming. A test
now asserts every action _stage_vip_version can stage is in that alternation.

B2. Rejecting an adoption hid the node from the panel permanently:
vip_discoveries.adopted_vip_id is write-once, a VIP is only ever soft-deleted so
the column's ON DELETE SET NULL never fires, and the agent does not re-report a
file whose hash has not changed. Rather than clearing the column on each path,
adoptability is derived from whether the linked VIP is still active, which
self-heals reject, undo-reject and approved teardown alike.

The panel now lists one row per instance instead of per node, and the adopt
dialog names every node that will be taken over. Blockers are aggregated across
all of them, matching what the endpoint checks.

No schema change, no agent change, no API-shape break: /api/vip/discoveries
gains a derived field and /api/vip/adopt keeps its request body.

Backend suite: 1359 passed, 152 skipped. Frontend build clean (no new lint
warnings in VIPManagement.js).
2026-08-13 21:45:38 +03:00
Mustafa ULUKAYA 47cc79dcf7 fix(acme): address review findings on the challenge-backend hardening
Five confirmed findings from an adversarial review of the branch, four of them
regressions introduced by it.

Fall through to the next source when a stored URL cannot be resolved.
Stopping at the first non-empty candidate emitted a backend section with no
`server` line: the section exists so `haproxy -c` passes and Apply succeeds,
then every challenge request 503s from an empty backend with nothing to show
for it. Scheme-less values are common — the settings field was free text until
this branch — so this was reachable on real installs. Selection moved into
`select_acme_backend_source()` so it is testable and the skipped candidates are
logged rather than silently dropped.

Report a challenge backend with no server line. `extract_acme_backend_target`
returns None for that section, and the loopback filter skipped falsy targets,
so the case above would have been reported as "challenge route present in
applied config" — the new check confirming the very state it exists to catch.

Do not narrow the row set feeding the routing check's `fail` branch. Adding a
mode filter to the WHERE clause turned a tcp-only port-80 cluster from "ok"
into "fail", and the site wizard blocks submit on any failing check, so those
installs would have been locked on upgrade day. Mode is now examined in Python
and only downgrades to `warn`, using an expression that is character-for-
character the renderer's normalisation.

Match the agent's config selector. The applied-config lookup omitted
`is_active = TRUE`, so it could read a superseded row and report on a config
the nodes never received. Extraction now happens in SQL rather than pulling
whole configs — these run to hundreds of KB.

Select `acme_backend_url` when loading the existing cluster. It was absent, so
the entity snapshot recorded old_values as NULL unconditionally and rejecting
the pending version wiped the operator's per-cluster URL back to the global
loopback default — re-creating the exact failure this branch removes.

Also carry `acme_enabled` and `acme_backend_url` through cluster creation. The
create model declared neither and the INSERT wrote neither, so a cluster
created with ACME switched on came back switched off with no error shown.

Refuted and deliberately not changed: settings PUT re-validating a stored
loopback value (it validates only what is submitted), an apply-path connection
leak (the 422 propagates to a handler that closes it), and the modal discarding
backend rejection reasons (the envelope matches).
2026-08-11 19:13:07 +03:00
Mustafa ULUKAYA bb774141d4 fix(acme): make the challenge backend fixable from the panel
Correcting a wrong ACME challenge backend was impossible without a shell, and
even with one the correction did not reach the nodes.

The mint gate only fired when `acme_enabled` flipped. `acme_backend_url` was
written to the DB and minted nothing, so Apply answered "No pending changes to
apply" and the nodes kept the old address forever. It is now decided by
comparing the rendered `server _acme_mgmt` line against the active version —
the one line that answers "would the nodes talk to a different address?".
Comparing whole configs would flag every unrelated pending edit.

The field had no UI at all. Added to the cluster form with validation that
mirrors the backend rules, and keyed on `model_fields_set` so clearing it
reverts to the global setting — with a plain `is not None` test an empty box is
indistinguishable from "not submitted", so a value could never be removed.

Validation is asymmetric on purpose (utils/acme_backend_url):

- at the write boundary, reject what cannot express a reachable target —
  including the two silent traps: a scheme-less value became `localhost`, and
  an out-of-range port raised inside the generator and destroyed the config
- at render time, never reject. The shipped defaults are themselves loopback,
  so refusing to render would make every acme_enabled cluster unappliable,
  including for changes unrelated to ACME. Problems are logged and surfaced.

The port-less default stays 8080 rather than moving to HTTP's 80: the bundled
compose publishes nginx on 8080, so installs relying on it work today and the
first sign of breaking them would be the unattended renewal loop months later.
The omission is warned about instead.

RFC1918 is allowed and is usually the right answer here, and no DNS resolution
is performed — both deliberate departures from utils/ssrf_guard, whose policy
is the opposite of what this address needs. What the management host can
resolve says nothing about what the HAProxy node can reach.

Diagnostics stop reporting success on a dead path:

- check_port80 uses GET instead of HEAD and classifies the body. A proxy that
  has lost its /.well-known/acme-challenge/ location serves its SPA with HTTP
  200, which `status in (200, 404)` accepted as healthy. Warnings also surface
  when other domains pass, which previously hid the most diagnostic outcome.
- check_routing filters `mode`, joins `acme_enabled` and reads the APPLIED
  config instead of counting database rows, and reports a loopback target.
- every new condition is `warn`, never `fail`: the site wizard blocks submit on
  any fail, so a new failing condition would lock every install on upgrade day.

Also: normalise `frontends.mode` once per frontend. It is nullable, and the
raw value was interpolated into `mode {}`, emitting a literal `mode None` that
HAProxy rejects — taking down the whole cluster config. The ACME gate and the
backend-mode check now read the same normalised value.

And stop hardcoding PUBLIC_URL / MANAGEMENT_BASE_URL in docker-compose, which
silently ignored the operator's .env and made the wrong default load-bearing.
2026-08-11 17:15:55 +03:00
Mustafa ULUKAYA acfd32dd63 fix(acme): stop shipping config-generation failures as applied config
`generate_haproxy_config_for_cluster` reports failure by RETURNING a one-line
comment ("# Error generating configuration: ...") rather than raising. Nothing
in the backend checked for it, so the apply path hashed that comment, stored it
as an APPLIED config version and pushed it to every agent — silently replacing
a cluster's entire haproxy.cfg.

Any exception inside the generator triggers this. An out-of-range port in
`acme_backend_url` is enough: `urlparse('http://h:99999').port` raises
ValueError, the outer `except` swallows it, and the cluster loses its config.

- add `is_config_generation_error()` next to the sentinel definitions so call
  sites stop matching the string by hand
- guard the apply path: refuse with 422 and leave the running config in force
- guard the ACME-toggle PENDING mint, and let HTTPException through the local
  `except Exception`, which would otherwise report success while no pending
  version exists

Also make the challenge backend diagnosable without shell access, since this
block previously emitted no log line at all:

- log the rendered `host:port` and WHICH source chose it (cluster override,
  system setting, or the MANAGEMENT_BASE_URL fallback) under the greppable
  `ACME-BACKEND` keyword
- warn when the rendered address is loopback: HAProxy resolves it on the node,
  not on the management host, so it can only work on an all-in-one install —
  and it is exactly what the shipped defaults produce
- log peer, X-Forwarded-For and Host on the challenge endpoint, which answers
  "did the request arrive at all?" — the question that separates a wrong
  backend address from a wrong response body

Refs the HTTP-01 investigation: a split deployment rendered
`server _acme_mgmt <mgmt>:8080` against a port with no listener, and every
existing check reported success.
2026-08-11 16:58:00 +03:00
taylanbakircioglu 9e2ea04777 feat(haproxy): preserve SPOE filter + frontend log-format on import/edit (v1.8.8, Issue #38)
Bulk import / manual edit silently dropped `filter spoe engine ...` (Coraza WAF)
and frontend `log-format` because the parser recognised only a fixed directive
set. The regenerated config then missed the SPOE engine, so HAProxy failed with
"unable to find SPOE engine 'coraza' used by the send-spoe-group".

- parser: capture `filter` + `log-format`/`log-format-sd` into new ParsedFrontend fields
- db: additive nullable `log_format` + `filters` TEXT columns on frontends (SCHEMA_VERSION 8->9)
- generator: new `filter` bucket flushed before http-request rules so `filter` precedes
  `send-spoe-group`; `log-format` kept in prelude
- bulk import: preview dict, change-detection, persist (create + merge-update); cluster-aware
  SPOE pre-flight advisories (missing-filter + host-prerequisite) surfaced in the UI
- manual CRUD: full round-trip (get/create/update) incl. React form fields (no null-wipe)
- reject/rollback: restore the new columns; restore path + wizard helper kept in parity
- backend `option spop-check` recognised (suppresses spurious warning for coraza-spoa)
- tests: test_spoe_filter_import.py; full suite green (1079 passed)
2026-07-10 18:34:36 +03:00
taylanbakircioglu 70ebc02e09 fix(acme): Cloudflare token, ZeroSSL EAB, Apply Management (v1.8.1)
Follow-up fixes for the DNS-01 feature reported on #35:

- Cloudflare: sanitize the API token (strip surrounding quotes + any non
  token68 chars) so a pasted token with quotes/spaces no longer fails with
  "Invalid request headers"; verify-on-save shows a precise hint when it
  cleaned the input. Covers the automated orchestrator path too.
- ZeroSSL/Google EAB: enter the EAB Key ID and HMAC Key per-account in the
  Register Account dialog (falls back to the global Settings value when blank);
  base64-validate the HMAC key; humanize the externalAccountRequired failure;
  and preserve the deliberate 409/422 instead of downgrading them to 400.
- Apply Management: cluster ACME enable/disable changes now show under a
  dedicated "ACME Challenge Routing" section, are counted in the Apply/Reject
  dialogs, and Apply/Reject All process them (previously "Rejected 0 HA/VIP
  change(s)") - consistent with every other entity. Reject rolls acme_enabled
  back to the original via ORDER BY created_at ASC over the snapshot chain.
- getErrorMsg surfaces field-level validation messages.

Backward compatible (additive / strict superset; HTTP-01 unchanged).

Addresses #35.
2026-06-24 20:37:05 +03:00
taylanbakircioglu a1192e602d feat: HA / VIP (Keepalived) management from the UI (#27)
Manage highly-available virtual IPs backed by Keepalived (VRRP) directly from the OpenManager
UI — no more SSHing into nodes to install/configure Keepalived by hand. Builds on the agent
pull-architecture: define the VIP centrally, click Apply, and the agents converge.

Highlights:
- New "HA / VIP" tab: create a virtual IP, pick a per-node interface, select which pool nodes
  participate (MASTER/BACKUP roles + priorities); live MASTER/BACKUP/FAULT per node.
- On Apply, agents install & configure Keepalived (unicast VRRP, cloud-safe default) across the
  major distros (Debian/Ubuntu, RHEL/CentOS/Alma/Rocky, Fedora, SUSE/openSUSE, Alpine) with a
  HAProxy health-check, so the VIP fails over automatically when HAProxy drops.
- Single-node (a managed floating IP without failover) and multi-node VRRP failover both work.
- VIP changes ride the standard Apply Management flow with the standard "View Change" diff.
- Approval-gated deletion (safety): deleting a running VIP is staged for approval and the VIP
  keeps running, untouched, until you approve it — an agent never tears a VIP down without an
  explicit human approval. Per-VIP Diagnostics view; opt-in package uninstall (only on nodes
  where OpenManager installed it). A node already running a hand-managed Keepalived is detected
  and never overwritten ("externally managed").
- Fully opt-in and backward compatible: nodes/clusters without a VIP are unaffected. Adds
  vip_instances + vip_members tables (idempotent SCHEMA_VERSION bump; existing data and
  passwords unaffected) and a `vip` RBAC permission group.
- Also includes a HAProxy config-generator robustness fix: auto-inject a stick-table when a
  frontend uses a stick counter (track-sc / sc_*_rate) but declares none.

On-prem / L2 (VRRP) scope; the UI notes the cloud caveat.
2026-06-07 01:52:20 +03:00
taylanbakircioglu f3d4fb11bb fix(agent): accept agent token (X-API-Key) on cluster read endpoints (Issue #22)
Assigning a HAProxy agent failed with '401: Authorization header missing'
on GET /api/clusters. A cluster-read hardening had made GET /api/clusters and
GET /api/clusters/{id} accept only a user JWT in the Authorization header;
agents authenticate with their agent token in the X-API-Key header, so the
token was never read.

Both endpoints now accept either a user JWT (Authorization) or an agent token
(X-API-Key via validate_agent_api_key), mirroring the existing dual-auth on
POST /api/agents/generate-install-script. Anonymous access is still rejected,
so the original hardening is preserved. The auth guard is placed before the
try block so the failure surfaces as a clean 401 (not the 500-wrapped-401 in
the report). Agent install scripts now consistently send the token via
X-API-Key (pre-flight cluster check on linux/macos, and macOS get_cluster_paths
which previously used the wrong Authorization: Bearer header).

Also normalizes the platform in the uninstall-script generator so macOS agents
(which report platform 'darwin') no longer get a 400 from
GET /api/agents/generate-uninstall-script/darwin.

version 1.6.0 -> 1.6.2.
2026-05-30 19:09:26 +03:00
taylanbakircioglu 02b1cb2bca feat: v1.5.0 — Site Wizard (Issue #14) + ACME Diagnostic Panel (Issue #13)
Closes #13, Closes #14.

This release squashes the v1.4.0 → v1.5.0 development line. v1.4.0
shipped the ACME stability & enterprise audit (Issues #10/#11/#12).
v1.5.0 builds on that foundation with two co-equal headline features
plus a 22-round audit campaign hardening the prior configuration
surface. License remains MIT for v1.5.0 (relicense to AGPL-3.0
lands in v1.5.2).

------------------------------------------------------------------
HEADLINE FEATURE A — ACME Diagnostic Panel (Issue #13)
------------------------------------------------------------------
A live pre-flight + post-failure diagnostic surface for every ACME
order, reachable from the ACME Automation page. The panel exists
to make ACME failures legible to operators who do NOT have shell
access to the API host.

Endpoints (`backend/routers/acme_diagnostics.py`):
  POST /api/letsencrypt/orders/{order_id}/diagnostics
       Run the full 5-check suite (DNS / port-80 / routing /
       account / agents) and humanize the order's `error_detail`
       (>=11 RFC-8555 problem types, backwards compatible with
       legacy plain-string failures).
  POST /api/letsencrypt/orders/{order_id}/diagnostics/
                                {check_id}/rerun
       Re-run a single check in place — used by the "Re-run"
       button on every row of the modal's pre-flight table.
  GET  /api/letsencrypt/orders/{order_id}/events
       Merged event timeline combining the typed
       `acme_order_events` rows with correlated
       `user_activity_logs` entries (resource_type =
       'letsencrypt_order' AND resource_id = order_id). The
       diagnostic modal auto-tails this timeline every 5 seconds
       while open.

Service-level checks (`backend/services/acme_diagnostics.py`):
  * DNS resolution via stdlib socket.gethostbyname_ex through
    run_in_executor (intentionally avoiding an aiodns runtime
    dep for v1.5.0).
  * Port-80 HEAD probe, target locked to the order's domains,
    success on HTTP 200 OR 404, warns on egress timeout
    (corp egress policies routinely blackhole outbound 80 —
    fail-hard would be too noisy).
  * SSRF guard: probe refuses non-public IPs and surfaces the
    skip in the diagnostic result; IPv4-mapped IPv6 normalisation
    closes the `::ffff:169.254.169.254` cloud-metadata vector.
  * HAProxy routing presence check: matches the order's
    cluster_ids to a port-80 HTTP frontend.
  * ACME account validity check against `letsencrypt_accounts`.
  * Agent presence check (>=1 active agent in target cluster).
  * Every sub-check wrapped in a wall-clock timeout to bound
    impact on the API event loop.

RBAC: ssl.read for run, ssl.read for events. Per-user 5/min rate
limit on both run and rerun, backed by the (user_id, action,
created_at DESC) composite index.

Frontend (`frontend/src/components/ACMEAutomation.js`):
  * "Diagnose" button on every order row + the existing
    "stuck order" warning row.
  * Modal with two tabs:
    - Pre-flight Checks (Antd Table with status pills + Re-run
      buttons + humanized error banner)
    - Event Log (Antd Timeline with auto-tail polling, scroll-
      to-bottom, pause-on-hover)
  * Correlation IDs surfaced in error banners and individual
    check fail details for backend-log lookup.

------------------------------------------------------------------
HEADLINE FEATURE B — Site Setup Wizard (Issue #14)
------------------------------------------------------------------
A single guided flow that creates a Backend + Servers + HTTP
Frontend (and optional HTTPS Frontend) in one atomic transaction.

Endpoints (`backend/routers/site_wizard.py`):
  POST /api/site-wizard/preview     — diff-preview the changeset
  POST /api/site-wizard/create      — atomic execute
  POST /api/site-wizard/reject      — clean rollback (including
                                       any wizard_staged ACME
                                       orders)
  GET  /api/site-wizard/drafts      — draft persistence
  PUT  /api/site-wizard/drafts/{id} — save/update
  DELETE /api/site-wizard/drafts/{id}

Feature surface:
  * One screen captures both backend (mode + servers) AND
    frontend (http + optional https + SSL mode) inputs.
  * SSL modes: ACME (new order, HTTP-01 only for v1.5.0),
    Upload (existing PEM), Existing (link to a stored cert),
    or None.
  * ACME-staged path: wizard_staged_until watermark on the
    `letsencrypt_orders` row defers finalisation until agent
    confirmation; per-mode reject cleanly cancels and rolls
    back the staged order.
  * Live diff preview against the cluster's current generated
    config (renderer-evolution noise stripped — track-sc<N>
    dedup, per-server cookie strip, defaults-cookie
    inheritance, listen-block flattening).
  * Draft persistence with PEM stripped at save time (private
    keys never round-trip through the drafts table).
  * Per-cluster multi-tenancy: drafts and wizard_staged orders
    are isolated to the creating user's cluster scope.

Frontend (`frontend/src/components/SiteWizard.js`):
  * 4-step Antd Steps flow: Backend → Frontend → SSL → Review.
  * Render the live diff preview inline before commit.
  * Antd Form-level validation mirrors backend Pydantic
    validators (numeric bounds, HAProxy reserved keywords, ALPN
    consistency, IPv6 scope-id, domain regex, server name
    dedup).

------------------------------------------------------------------
AUDIT CAMPAIGN — Rounds 1 → 22 (Bulgu #1 → #82)
------------------------------------------------------------------
v1.5.0 includes 22 adversarial review passes. Each round produced
its own commit set in the corporate development line; this squash
collapses those into the v1.5.0 release artefact. Highlights:

  Round 1-4   Site Wizard core: dry-run parity, single-line
              value injection guard, ACL -f pattern-file block,
              SSL parity, timeout regex, form-state pin.
  Round 5-7   defaults-cookie inheritance, server-named-cookie
              guard, fe/be mode mismatch, duplicate server
              names, health_check_uri + server_address
              validators.
  Round 8-10  cookie_name / cookie_options newline-injection
              guard, dry-run parity (round 9), TCP-mode HTTP-only
              feature blockers.
  Round 11    SSL name path traversal + health-check >= 1.
  Round 12-13 SSL & ACME deep dive (Bulgu #23-#32).
  Round 14    single-line value injection (Bulgu #33).
  Round 15-17 ACME multi-tenant UX, numeric bounds, HAProxy
              reserved keywords, ALPN/TLS consistency,
              all-backup, multi-domain & multi-user enterprise
              edges, drain/HSTS/post-completion (Bulgu
              #34-#53).
  Round 18-21 concurrency, agent state, TCP-mode HTTP-only,
              list size caps, IPv6 scope-id, preview account
              validation, TCP backend + balance uri reject
              (Bulgu #54-#61).
  Round 22    FE error visibility + 3x stale-data lockouts,
              referential integrity + cascade safety,
              authentication & authorization, multi-cluster
              isolation, apply_pending_changes concurrency,
              script injection + bulk import multi-tenancy,
              prefix-stripped signature comparison
              (Bulgu #62-#82).

------------------------------------------------------------------
NO CORPORATE-SPECIFIC ARTIFACTS
------------------------------------------------------------------
This squash deliberately sanitises corporate hostnames, container
registry references, and TLS secret names into generic
placeholders (`your-registry.example.com/your-org`,
`haproxy-openmanager*.example.com`, `wildcard-tls`,
`taylanbakircioglu/haproxy-openmanager-*`) so the public artefact
contains no internal infrastructure detail. Pilot / development
history that retained those values stays in the corporate fork
and is NOT part of this commit.
2026-05-14 00:04:19 +03:00
taylanbakircioglu 07942a82e8 feat: ACME stability & enterprise audit (v1.4.0) — fixes #10 #11 #12
Issue #10 — Silent timezone failure on ACME certificate save
- Normalize tz-aware expiry_date to UTC tz-naive before INSERT/UPDATE in
  ssl_certificates (TIMESTAMP WITHOUT TIME ZONE) — restores ACME download path

Issue #11 — Duplicate _acme_challenge_backend in generated config
- Generator guard: skip auto-append when backend already rendered
- Agent-sync filter: _should_sync_backend() drops system-managed backends and
  detaches their server rows to prevent orphans
- Restore filter: IGNORED_BACKENDS skips reserved names during cluster restore
- Parser warning: reserved_backend_names blocks accidental manual import
- Cleanup migration: removes orphan rows + cascading server entries (idempotent)

Issue #12 — Validated orders required manual completion
- New 60s background task complete_pending_acme_orders, flag-independent,
  multi-replica safe via FOR UPDATE SKIP LOCKED + 30s updated_at watermark
- Per-order pg_advisory_lock(0x41434D45, order_id) serializes UI-Complete and
  auto-task races; idempotency guard returns existing certificate cleanly
- retry_order endpoint reports in_progress: true within 30s window so the UI
  surfaces an info toast instead of duplicating CA requests
- ACMEAutomation surfaces stuck orders (status=valid && !ssl_certificate_id)
  with a one-click Complete action and Cancel fallback; conditional 30s polling

Other hardening
- ACME state machine error_detail persisted as structured JSON across challenge,
  finalize, download stages for actionable post-mortems
- CertificateRequest Pydantic model: domain regex + min_length/max_length and
  cluster_ids defaulting to all ACME-enabled clusters when "global" is selected
- Renewal cluster fallback now requires acme_enabled=TRUE in addition to active
- _complete_certificate preserves manual cluster assignments on renewal,
  surfaces cluster_errors, raises explicit error on missing private key
- Audit logging covers acme_certificate_requested/revoked, ca_chain_imported,
  account created/deactivated/purged, order retried/cancelled
- Settings UI exposes acme.staging_url_override for private test CAs (Pebble)
- Schema additions: acme_challenges.attempts (default 0) and last_attempt_at,
  index idx_letsencrypt_orders_status_updated; all migrations idempotent

Tests (41/41 passing)
- test_acme_expiry_normalize, test_acme_duplicate_backend,
  test_acme_state_machine, test_acme_pydantic_validation,
  test_acme_audit_logging, test_acme_concurrency

CI / packaging
- docker-build.yml reads version.json and pushes additional product-version
  tag (e.g. 1.4.0) alongside latest and timestamp build id

Closes #10
Closes #11
Closes #12
2026-05-06 23:53:43 +03:00
taylanbakircioglu 36fdba52bc fix: ACME setup guide accuracy, reject rollback, and UX improvements
- Step 3 (Enable ACME on Cluster) now shows a process icon instead of
  a misleading green checkmark when ACME is enabled but not yet applied.
  Per-cluster "(pending apply)" annotation for multi-cluster setups.
- Step 4 button and all /apply-management navigation buttons now say
  "Apply Changes" instead of "Configure" for clearer guidance.
- Setup Guide auto-selects the correct cluster before navigating to
  Apply Management, showing pending cluster names in alerts.
- Pending ACME disable changes are now correctly detected in Step 4
  even when acme_enabled is already FALSE in the database.
- Entity snapshot rollback for cluster ACME settings: reject correctly
  restores acme_enabled/acme_backend_url to pre-change values.
- Deduplication logic prevents "last wins" bug when multiple ACME
  toggles are rejected in sequence.
- Connection leak prevention with try/finally around conn2 in ACME
  config version creation.
- Step 4 branching uses boolean has_enabled instead of fragile string
  truthiness check.

Made-with: Cursor
2026-04-04 16:57:57 +03:00
taylanbakircioglu 93d7ad8fdb feat: add ACME Auto SSL with Let's Encrypt integration (v1.1.0)
Add automated SSL certificate management via ACME protocol (RFC 8555):
- Full ACME client implementation (account registration, HTTP-01 challenges, certificate issuance/renewal)
- Configurable ACME providers (Let's Encrypt, ZeroSSL, custom CA) via Settings UI
- Auto-renewal scheduler with PENDING -> Apply -> APPLIED flow alignment
- ACME account management (register, deactivate) from UI
- Certificate request wizard with domain validation and cluster targeting
- Zero changes to HAProxy agent scripts - challenges routed through existing architecture
- Comprehensive security hardening (no private key exposure in API responses)
- Full backward compatibility with existing SSL, Apply, Rollback, and Restore workflows
- Updated README, API documentation, and Kubernetes deployment notes
- Version management embedded in code (v1.1.0)
- UI messaging improvements for agent-pull architecture accuracy

Made-with: Cursor
2026-04-02 00:25:50 +03:00
taylanbakircioglu 1f100d01e7 fix: SSL reject rollback, expiry calculation, and UI improvements
- SSL Reject: Remove conditional has_same_update_applied check, always
  rollback since Auto-Reject handles cross-cluster consistency
- SSL Rollback: Restore expiry_date by parsing ISO string back to datetime
- SSL UI: Add In Use filter toggle, fix Expiring Soon count using actual
  date calculation, smart Private Key content validation messages
- Remove emojis from log messages

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 16:36:27 +03:00
taylanbakircioglu 7dedd0f573 feat: SSL sync status tracking and smart reload optimization
- Add per-cluster SSL sync status tracking with APPLYING/SYNCED/PARTIAL states
- Add timeout detection (10min) for stalled SSL deployments showing PARTIAL status
- Add SSL-specific info in Apply Management dialog (cluster count, auto-apply notice)
- Add Deployment Status tab in SSL details showing per-cluster agent sync progress
- Implement SSL auto-reject (reject propagates to all clusters) and auto-undo
- Add conditional entity rollback for SSL reject (time-window based safety check)
- Filter REJECTED status from SSL last_config_status display
- Add pending_cluster_names to SSL list API response
- Optimize agent SSL reload: only trigger HAProxy reload when changed cert is
  actually referenced in the cluster's HAProxy config (grep -qF check), preventing
  unnecessary reloads on clusters that don't use the updated certificate
- Applied to all 4 SSL deploy paths: Linux/macOS standalone and daemon embedded modes

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 16:36:27 +03:00
taylanbakircioglu 4c7a6d79f3 fix: apply sync for empty config, UI responsiveness improvements
- Fix backend cleanup order: delete backend_servers before backends
  to prevent orphan records and foreign key constraint violations
- Fix Apply progress getting stuck at "0/55" when all entities deleted
  by using verifyRealAgentSync result for accurate syncedCount
- Add completion condition for "all entities deleted" scenario
- Fix cluster selector truncation with dynamic width calculation
- Fix sidebar menu label truncation: increase sider width to 240px,
  use concise menu labels, add CSS overflow handling
- Add responsive breakpoints for header title and content padding
- Auto-collapse/expand sidebar on breakpoint change

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 16:45:29 +03:00
taylanbakircioglu 6349f1ad7c fix: exclude disabled (OFF) agents from sync calculations
Disabled agents were blocking apply sync progress indefinitely because
they were counted in total_agents but could never report as synced.

Backend:
- agent-sync endpoint: disabled agents excluded from total/synced/unsynced
  counts, added disabled_agents and total_agents_including_disabled fields
- SSL cert agent-sync endpoint: same disabled agent exclusion
- Both endpoints still return disabled agents in the list with
  sync_excluded: true for UI display

Frontend:
- ApplyManagement: cluster sync complete logic handles 0 enabled agents,
  all updateEntityCounts calls pass disabled count, agent table shows
  OFF/Excluded tags for disabled agents
- GlobalProgress: shows "(X off)" indicator, visible even when all
  agents are disabled
- ProgressContext: agentCounts state supports disabled field
- agentSync utility: verifyRealAgentSync treats null/0-agent sync_status
  as synced (nothing to wait for)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 16:45:28 +03:00
taylanbakircioglu 83204c86a8 fix: critical security and UX improvements for config management
SECURITY FIX (agent.py):
- Ensure ONLY APPLIED versions are sent to agents
- Fixed fallback query that could return PENDING versions
- Agents will never receive unapproved configurations

UX FIX (cluster.py):
- Clear validation_error when creating new version via Apply
- Prevents stale validation errors from showing after re-apply

UX FIX (agent.py):
- Clear validation_error when agent successfully applies config
- Clear last_validation_error on agent record after success
2026-01-26 15:25:15 +03:00
taylanbakircioglu 97e32114e0 fix: translate Turkish error messages to English
- Updated SUGGESTION_TEMPLATES in haproxy_error_parser.py
- Fixed fallback error messages in cluster.py
- Product language should be English throughout
2026-01-26 15:25:15 +03:00
taylanbakircioglu 8076f3fcfd feat: add intelligent HAProxy validation error display in UI
- Add haproxy_error_parser.py: Parses HAProxy validation errors with
  confidence scoring, extracts entity type/name, line number, error type
- Add ValidationErrorModal.js: Rich modal with parsed error summary,
  quick fix suggestions, and manual troubleshooting guide
- Update cluster.py: Integrate error parser into agent-sync and
  config-versions endpoints with graceful fallback
- Update ApplyManagement.js: Add validation error banner with quick
  navigation buttons and error detail modal
- Update FrontendManagement.js & BackendServers.js: Handle URL params
  for deep-linking to entity edit forms with field highlighting

Enables users to see actionable validation failure details directly
in the UI without needing server access for debugging.
2026-01-26 15:25:15 +03:00
Taylan Bakırcıoğlu 1d17dd8fd5 fix(reject): prevent deletion of existing entities on bulk import reject
CRITICAL DATA LOSS BUG FIX

Problem:
- User updates existing entity via bulk import
- User clicks Reject
- EXPECTED: Entity reverts to old values
- ACTUAL: Entity is COMPLETELY DELETED!

Root Cause:
Reject logic tracked ALL bulk import entities for force deletion.
Did not distinguish between CREATE and UPDATE operations.

Fix:
Added operation check - only track CREATE operations for force deletion.
UPDATE operations are rolled back, not deleted.

Impact:
- CREATE scenarios: No change in behavior
- UPDATE scenarios: Data loss prevented
- Mixed CREATE+UPDATE: Only CREATE entities deleted
- Rollback failures: Safety mechanism preserved

Files Changed:
- backend/routers/cluster.py: Line 4494 added operation check
2025-11-18 21:58:05 +03:00
Taylan Bakırcıoğlu 001b467e42 FIX: Rejected bulk import data corruption
CRITICAL BUG FIX: Rejected bulk import entities were marked APPLIED

ROOT CAUSE:
- Bulk import versions (bulk-import-*) don't match entity-ID regex
- Final cleanup incorrectly marked all PENDING entities as APPLIED
- 594 entities (56 FE + 55 BE + 483 SRV) corrupted in production

SOLUTION:
1. Detect bulk import versions (bulk-import-*, restore-*)
2. Extract entity IDs from metadata bulk_snapshots
3. Verify rollback deleted entities (should be 0 remaining)
4. If rollback failed, force DELETE entities
5. Skip final cleanup for bulk import (entities already handled)

IMPACT:
- Prevents data corruption on rejected bulk imports
- Detects and auto-fixes failed rollbacks
- Maintains data integrity for multi-entity operations
- Preserves existing orphan cleanup for single-entity changes

TESTING:
- Bulk import + reject: Entities deleted (not APPLIED)
- Normal entity + reject: Status updated to APPLIED (rollback)
- Orphan entities: Cleaned only if safe (no bulk versions)

Ref: demo-cluster bulk-import-1763450049 issue
2025-11-18 21:58:05 +03:00
Taylan Bakırcıoğlu f1a3826334 fix(backend): Allow backends without servers to be deployed
MINIMAL FIX: Enable backend-first workflow (add servers later)

Problem:
1. User creates backend 'deneme-sil' without servers
2. Config generator SKIPs backend (no servers = skip)
3. User adds server 'server1-sil'
4. Config shows: 'server server1-sil 1.1.1.1:1233' WITHOUT backend block
5. HAProxy validation FAILS (server without backend = syntax error)

User Requirement:
- Create backend first (without servers)
- Assign backend to frontend (default_backend)
- Add servers later
- Standard HAProxy workflow

Solution (MINIMAL - 2 small changes):

1. haproxy_config.py (line 453-454):
   OLD: Skip backend if no servers (continue)
   NEW: Write backend block anyway (remove continue)

   Result:
   backend deneme-sil
       balance roundrobin
       mode http
       # (no servers yet - backend will show as DOWN)

   Valid HAProxy syntax - check

2. cluster.py (line 1600-1607):
   OLD: Only mark backends with servers as APPLIED
   NEW: Mark ALL backends as APPLIED (servers optional)

   Reason: ALL backends are now in config (even without servers)

Why This Is Better Than Previous Approach:
- Only 2 lines changed (vs 50+ lines)
- No complex logic added
- No risk to existing functionality
- HAProxy naturally handles backends without servers (shows as DOWN)
- Aligns with standard HAProxy usage patterns

Test Scenarios:
- Create backend without servers -> Backend block written to config
- Apply -> HAProxy accepts config (backend DOWN)
- Frontend can use backend (use_backend, default_backend)
- Add server later -> Server added to existing backend block
- Apply -> HAProxy accepts, backend goes UP

HAProxy Behavior:
- Backend without servers: DOWN (no available servers)
- Backend with disabled servers: DOWN (all servers disabled)
- Backend with active servers: UP (servers available)

Related: caa21f0 (has_pending_config fix)
Refs: #backend-workflow #server-optional #haproxy-syntax
2025-11-17 14:15:44 +03:00
Taylan Bakırcıoğlu d2bf64af8b fix(backend): Prevent duplicate key errors from inactive backends + race condition safety
CRITICAL FIX: Handle soft-deleted backends that block unique constraint

Problem Scenario:
1. User creates backend 'deneme-sil' without servers
2. Backend gets soft-deleted (is_active=FALSE) somehow
3. Backend remains in DB but invisible in UI (API filters is_active=TRUE)
4. User tries to create same backend again
5. ERROR: duplicate key value violates unique constraint

Root Causes:
A) Soft-deleted backends remain in DB and block unique constraint
B) Apply endpoint marks ALL pending backends as APPLIED, even those skipped by config generator
C) Backend without servers shows as APPLIED but isn't in haproxy.cfg (inconsistent)
D) Race condition: Agent sync temporarily marks backends as inactive

Solutions:
1️⃣ Backend CREATE (backend.py lines 473-506):
   - Check for inactive backends with same name before creating
   - SAFETY: Only cleanup if inactive for >30 seconds (avoid agent sync race)
   - If found: Hard delete inactive backend + related data
   - Then allow new backend creation
   - Prevents: duplicate key constraint errors + race conditions

2️⃣ Backend DELETE (backend.py lines 1044, 1056-1090):
   - Detect if backend is already inactive (is_active=FALSE)
   - If inactive: Hard delete (permanent removal from DB)
   - If active: Soft delete (mark as inactive for Apply workflow)
   - Prevents: Orphan inactive backends accumulating in DB

3️⃣ Apply Endpoint (cluster.py lines 1600-1638):
   - Only mark backends as APPLIED if they have active servers
   - Check: EXISTS(backend_servers WHERE is_active=TRUE)
   - Backends without servers remain PENDING (correct state)
   - Log warning: 'Backend X remains PENDING (no active servers)'
   - Prevents: Inconsistent state (APPLIED in DB, missing in haproxy.cfg)

Race Condition Protection:
⚠️  Agent config-sync temporarily marks backends as is_active=FALSE
⚠️  If we hard delete during sync, backend could be lost!
✅ Solution: Only cleanup backends inactive for >30 seconds
✅ Agent sync takes <5 seconds, so safe window
✅ Protects against: sync running while user creates backend

Impact Analysis (All Scenarios Tested):
✅ Normal backend create (with servers) - No impact
✅ Backend create (without servers) - FIXED: Stays PENDING until servers added
✅ Backend delete → recreate - FIXED: Old backend cleaned up automatically
✅ Agent sync race condition - PROTECTED: 30-second safety window
✅ Multi-cluster (same name) - No impact: cluster_id already checked
✅ Bulk import reactivation - No impact: Has own logic
✅ Config restore/rollback - No impact: Has own conflict handling
✅ Frontend-backend relations - No impact: Cleanup preserved
✅ Dashboard statistics - No impact: Only counts active
✅ Maintenance status - IMPROVED: Stale inactive backends auto-cleaned

Benefits:
✅ No more duplicate key errors
✅ Users can recreate backends with same name
✅ Inactive backends are automatically cleaned up (after 30s)
✅ Consistent state: APPLIED = actually in haproxy.cfg
✅ Clear warning when backend needs servers to deploy
✅ Race condition protection during agent sync
✅ No risk of data loss during concurrent operations

How to Fix Current 'deneme-sil' Backend:
Option 1: Reject in Apply Management (easiest)
Option 2: Add servers + Apply
Option 3: Delete backend + Apply (auto-cleanup after 30s)
Option 4: Manual DB cleanup (fastest right now)

Related: Previous commits (frontend null check, config skip, UX messages)
Refs: #backend-creation #duplicate-key #soft-delete #apply-consistency #race-condition
2025-11-17 14:15:43 +03:00
taylanbakircioglu a13daabf90 feat: SSL scope-aware apply - restore original behavior with improvements
PROBLEM:
- SSL content was updated (Global or Cluster-specific)
- Required separate Apply action for each cluster (poor UX)
- Original system: Single Apply propagated to all clusters in scope

SOLUTION:
- Added apply_ssl_related_configs() helper function (line 48-163)
- Detects SSL-related PENDING configs
- Auto-applies based on SSL scope:
  * Global SSL → APPLIED in all clusters with PENDING configs
  * Cluster-specific SSL → APPLIED in associated clusters only

KEY IMPROVEMENTS:
✅ Transaction-safe (old code was NOT)
✅ Direct UPDATE (old code used recursive calls)
✅ Cluster-specific SSL support (old code did NOT handle this)
✅ Version name based (more reliable than metadata parsing)
✅ Single transaction (old code had partial success risk)

WORKFLOW:
1. SSL 'demo-global' updated (Global scope)
2. PENDING configs created for Cluster-1, 2, 3
3. User clicks Apply in any cluster
4. Backend detects SSL scope
5. Auto-APPLIED in ALL affected clusters ✅
6. Agents pull SSL and deploy ✅

TECHNICAL DETAILS:
- Helper function: apply_ssl_related_configs() (line 48-163)
  * SSL ID extraction from version name (ssl-{id}-update-{ts})
  * Scope detection from ssl_certificates table
  * Target cluster discovery based on scope
  * Direct UPDATE (no recursion)
  * is_active=FALSE (consolidated version will be TRUE)

- Integration: apply_pending_changes() (line 1329-1332)
  * Called INSIDE transaction for atomicity
  * Before consolidated version creation
  * Replaces old recursive logic

- Old logic disabled: (line 1253-1255)
  * Empty lists prevent old recursive apply
  * Old code only handled global SSL
  * New code handles both global AND cluster-specific SSL

PERFORMANCE:
- Old: N recursive calls (1 per cluster)
- New: 1 transaction with direct UPDATEs
- Result: Faster and safer

SAFETY:
✅ Transaction rollback tested
✅ All edge cases handled (SSL deleted, cluster deleted, etc)
✅ No UI breaking changes (global_ssl_applied field not used)
✅ Backward compatible

FILES MODIFIED:
- backend/routers/cluster.py
  + apply_ssl_related_configs() helper function
  + Integration in apply_pending_changes()
  + Old recursive logic disabled
2025-11-14 01:06:38 +03:00
taylanbakircioglu 285698ccfd fix(snapshot): Include metadata column in pending_versions SELECT query
CRITICAL BUG FOUND:
- Snapshot created successfully (metadata exists in database)
- But reject_all_pending_changes() was not fetching metadata column
- Line 4151: SELECT id, version_name FROM config_versions (missing metadata!)
- Result: KeyError: 'metadata' during reject rollback

Fix:
- Added 'metadata' to SELECT query
- Line 4151: SELECT id, version_name, metadata FROM config_versions

Impact:
- Rollback will now work (metadata accessible)
- entity_snapshot will be parsed correctly
- Entities will be restored to old values on reject

Log evidence:
- SNAPSHOT: Created successfully ✅
- REJECT ROLLBACK ERROR: KeyError 'metadata' ❌
- Root cause: Missing column in SELECT query
2025-11-14 01:06:38 +03:00
taylanbakircioglu f9ca700352 debug(snapshot): Add comprehensive logging for rollback troubleshooting
Problem 1: Apply affects all entities (should only affect changed ones)
Problem 2: Reject rollback not working (entity stays at new value)

Added detailed logging:
- Snapshot creation: JSON test result, field count
- Frontend update: metadata keys, entity_snapshot presence
- Reject: metadata parsing, entity_snapshot detection
- Rollback: entity data, operation type, old_values
- _rollback_update: Before/after values, UPDATE query result
- Verify: Post-rollback database state

This will help identify:
- Is snapshot being created?
- Is metadata being saved to database?
- Is metadata being parsed during reject?
- Is rollback function being called?
- Is UPDATE query executing?
- What are the actual values being restored?

Log locations to check:
kubectl logs deployment/haproxy-openmanager-backend -n haproxy-openmanager | grep 'SNAPSHOT\|ROLLBACK\|REJECT'
2025-11-14 01:06:38 +03:00
taylanbakircioglu b427db4c78 feat(snapshot): PHASE 4 & 5 - Bulk Import & Restore snapshot integration
PHASE 4: Bulk Import Integration
- config.py - bulk_create_entities() enhanced with snapshot support
- Backend UPDATE: Snapshot before update (bulk_snapshots array)
- Backend CREATE: Snapshot for rollback (DELETE on reject)
- Frontend UPDATE: Snapshot before update
- Frontend CREATE: Snapshot for rollback (DELETE on reject)
- Server CREATE: Snapshot for rollback (DELETE on reject)
- Bulk metadata: bulk_snapshots array, operation=BULK_IMPORT
- Total entity count tracking in metadata

PHASE 5: Restore Integration
- cluster.py - confirm_restore_config_version() enhanced
- Frontend UPDATE: Snapshot before restore (operation=UPDATE_RESTORE)
- Backend UPDATE: Snapshot before restore (operation=UPDATE_RESTORE)
- SELECT * for full field capture (not just parsed fields)
- Restore metadata: bulk_snapshots + pre_apply_snapshot
- operation=RESTORE tracking
- Rollback support for restore + reject scenario

Key Features:
- Bulk import now creates single config version with multiple entity snapshots
- Restore creates snapshots for all updated entities
- Reject after bulk import: Rollback all entities (UPDATE to old values, CREATE deleted)
- Reject after restore: Rollback to pre-restore state
- All emojis removed from code (clean logging)
- Diff viewer compatibility maintained (pre_apply_snapshot)

Implementation Complete:
- PHASE 1: Infrastructure (entity_snapshot.py) - DONE
- PHASE 2: Entity updates (5 entities) - DONE
- PHASE 3: Reject rollback logic - DONE
- PHASE 4: Bulk import snapshot - DONE
- PHASE 5: Restore snapshot - DONE

Next: Production testing with feature flag (ENTITY_SNAPSHOT_ENABLED=false by default)
2025-11-14 01:06:38 +03:00
taylanbakircioglu b9d618b4ac feat(snapshot): PHASE 2 & 3 - Entity snapshot integration + Reject rollback
PHASE 2: Entity Update Integration (ALL entities)
- Frontend update: Full snapshot with 27 fields
- Backend update: Full snapshot with 23 fields
- WAF rule update: Full snapshot with 11 fields
- SSL certificate update: Full snapshot with 16 fields
- Server update: Full snapshot with 23 fields
- ALL database fields included (no missing fields)

PHASE 3: Reject with Rollback Logic
- cluster.py - reject_all_pending_changes() enhanced
- Entity rollback before marking REJECTED
- Support for single entity snapshot
- Support for bulk snapshots (bulk import/restore)
- Entity status: REJECTED -> APPLIED (entities rolled back)
- Rollback statistics in response (success/failed/skipped)

Key Changes:
- entity_snapshot.py: All field schemas validated against migrations
- Backend rollback: 23 fields (including options, cookie_*, default_server_*)
- Server rollback: 23 fields (including ssl_certificate_id, haproxy_status)
- SSL rollback: 16 fields (including issuer, fingerprint, all_domains)
- WAF rollback: 11 fields (including enabled, cluster_id)
- No emoji in code (clean logging)
- Feature flag: ENTITY_SNAPSHOT_ENABLED (default: false)

Next: PHASE 4 (Bulk Import) + PHASE 5 (Restore) integration
2025-11-14 01:06:38 +03:00
taylanbakircioglu b29c9e044e feat: Multi-cluster isolation and comprehensive bug fixes
This update consolidates bug fixes and improvements from internal development:

## Multi-Cluster Isolation (CRITICAL)
- Backend delete now isolated per cluster (added cluster_id to WHERE clauses)
- Orphan config version auto-detection and cleanup
- Prevents cross-cluster contamination when deleting entities
- REJECTED entities excluded from pending list

## Apply Management Fixes
- Fixed phantom pending entities (REJECTED entities no longer shown)
- Fixed cluster switch 404 errors (state cleared before fetch)
- Fixed page reload not refreshing data (added mount useEffect)
- Orphan entity status auto-cleanup on reject

## Backend Delete Improvements
- Added cluster_id to server/frontend updates (multi-cluster safe)
- Automatic ACL/use_backend cleanup from frontends
- NULL cluster_id support for legacy data
- Prevents HAProxy validation errors

## Orphan Version Detection
- Backend GET: Validates entity belongs to version's cluster
- Frontend GET: Validates entity belongs to version's cluster
- Apply: Auto-detects and removes orphan versions before apply
- Reject: Auto-detects and removes orphan versions before reject

## UI/UX Improvements
- Apply Management state management improved
- Debug logging for troubleshooting
- Better cluster switch handling
- README: Orphan version troubleshooting section

Technical Changes:
- backend/routers/backend.py: Multi-cluster isolation, orphan detection, NULL handling
- backend/routers/frontend.py: Orphan detection, REJECTED filter
- backend/routers/cluster.py: Orphan auto-cleanup, entity status cleanup
- frontend/ApplyManagement.js: State management, mount useEffect
- README.md: Troubleshooting documentation

Impact:
- Multi-cluster environments now fully isolated ✅
- Orphan config versions automatically cleaned ✅
- REJECTED entities properly filtered ✅
- Apply Management works correctly across cluster switches ✅
- No manual database intervention needed ✅
2025-11-12 13:34:07 +03:00
taylanbakircioglu 7d18341319 Fix: Backend Server config status not updating on Reject/Undo operations
🐛 Critical Bug Fix - Server Config Status:
- Server edit → Reject → Config Status stayed PENDING (should be REJECTED)
- Server edit → Reject → Undo → Config Status stayed REJECTED (should be PENDING)

✅ Fixed Operations:

1. Reject All Pending Changes (Line 4088-4099):
   - Extract server IDs from version names: 'server-{id}-update-{timestamp}'
   - Mark servers as REJECTED: UPDATE backend_servers SET last_config_status = 'REJECTED'
   - Added to existing logic (frontends, backends, WAF already working)

2. Undo Reject (Line 3552-3578):
   - Extract server IDs from version names
   - Mark servers as PENDING: UPDATE backend_servers SET last_config_status = 'PENDING'
   - Added to existing undo logic

🎯 Version Name Patterns:
  - Frontend: frontend-{id}-{action}-{timestamp}
  - Backend: backend-{id}-{action}-{timestamp}
  - WAF: waf-{id}-{action}-{timestamp}
  - Server: server-{id}-{action}-{timestamp} ← Now supported!

✅ Complete Workflow Now:
  User edits server → PENDING ✓
  User rejects → REJECTED ✓ (FIXED)
  User undos reject → PENDING ✓ (FIXED)
  User applies → APPLIED ✓ (Already working)

🔍 Regex Pattern:
  r'^server-(\d+)-' matches:
  - server-220-update-1762457460 ✓
  - server-15-create-1762457500 ✓

Impact: All entity types (Frontend, Backend, WAF, Server) now have consistent config status behavior
2025-11-07 11:51:15 +03:00
taylanbakircioglu 704a0c0022 Fix: Bulk import parsing and entity status management improvements
🐛 Bug Fixes:
- Fixed http-response capture directive parsing with improved regex pattern
- Fixed ACL rules display in Frontend UI (array to multi-line string conversion)
- Added SSL certificate dropdown to Backend Server edit when ssl_enabled=true
- Fixed rejected entity config status remaining after apply operation

🔧 Improvements:
- Enhanced SSL ca-file detection with user-friendly warnings
- Apply operation now correctly updates both PENDING and REJECTED entities to APPLIED
- Added dynamic SSL certificate selection for backend servers with validation
- Improved bulk import warnings for SSL management workflow

📝 Technical Details:
- Parser: Enhanced capture pattern matching for flexible http-response directives
- UI: Added conditional SSL certificate select field in BackendServers component
- Backend: Updated apply cleanup to handle REJECTED status in addition to PENDING
- Frontend: Fixed ACL/redirect rules formatting for proper textarea display

✅ All changes tested and verified with scenario analysis
2025-11-07 11:51:14 +03:00
taylanbakircioglu 6aae0f4309 Initial commit 2025-10-27 12:14:03 +03:00