Skip to content

v1.3.36.5 -- Capture: tab-nav escape hatch + DNS-01 selector fix

A bugfix on top of v1.3.36.4 closing two issues from the operator's third prod capture run:

  • BUG A: security-whitelist.png failed because the Whitelist tab's button text "Whitelist" tripped the read-only blocklist (which exists for the "Whitelist this IP" verb action, NOT for tab navigation).
  • BUG B: host-form-dns-provider-dropdown.png showed the modal in default state (no DNS-01 selected, no picker) because the DNS-01 selector matched zero elements — the panel's <ChallengeRadio> renders <input> without a value attribute.

argosVersion and frontend/package.json deliberately stay at 1.3.35.4 (tooling-only). scripts/capture/package.json bumps 1.3.36.41.3.36.5.

BUG A — Whitelist tab false positive

Root cause

safeClick's BLOCKED_TEXT_PATTERNS includes /^Whitelist\b/i for the "Whitelist this IP" verb action button. The Whitelist TAB has the same button text — the blocklist couldn't tell them apart and rejected the click:

[safeClick] BLOCKED: selector="button:has-text(\"Whitelist\")"
text="Whitelist" matches a state-changing action.

This is a false positive: tab clicks change view URL / local component state, not server-side state. They're inherently read-only.

Fix

New safeClickTab(page, selector, reason) escape hatch in lib/safe-page.js. Same shape as openModal:

  • Requires a reason string (audit log).
  • Skips the BLOCKED_TEXT_PATTERNS check.
  • Logs [safeClickTab] override: ... to stdout for the audit transcript.
  • Misuse is obvious at the call site (safeClickTab name vs safeClick).

Six tab-click call sites in capture.spec.js migrated:

Test Tab Reason
13 Whitelist switch to Whitelist tab
14 Activity switch to Activity tab
15 Scenarios switch to Scenarios tab
16 AppSec switch to AppSec tab
21 Metrics (sub-tab) switch to Metrics sub-tab
22 Deliveries switch to Deliveries tab

Only Whitelist was actively failing; the other 5 happened to use tab labels that didn't collide with blocked verbs. But this is fragile — if the panel later adds a "Disabled", "Reset", "Enable", etc. tab, the same regression would silently reappear. Migrating all 6 future-proofs.

BUG B — DNS-01 selector matched zero elements

Root cause

PHASE 0 source-code investigation of Hosts.tsx:

function ChallengeRadio({ value, challenge, label, hint, onChange }) {
  return (
    <label className="...">
      <input
        type="radio"
        name="tls-challenge"
        checked={value === challenge}
        onChange={() => onChange(challenge)}
        className="..."
      />
      <span>
        <span className="...">{label}</span>     {/* "DNS-01" */}
        <span className="...">{hint}</span>
      </span>
    </label>
  );
}

The <input> has no value attribute. It only has type, name, checked, onChange, className. The differentiator between the three radios is checked={value === challenge} (controlled input) and the wrapping <label>'s text content ("DNS-01" / "HTTP-01" / "TLS-ALPN-01").

The v1.3.36.x selector 'input[type="radio"][value="dns"]' matched zero elements. The defensive if (await dnsRadio.count()) check made the failure silent: zero elements → no click → modal stayed in default state → capture showed the modal without DNS-01 selected and without the picker rendered. No error was thrown — operator only noticed by inspecting the captured PNG.

Fix

Click the <label> element by its visible text. Clicking a <label> that wraps an <input> fires the input's onChange via standard HTML semantics (label-for relationship by nesting):

try {
  await safeClick(page, 'label:has-text("DNS-01")');
} catch { /* tolerate if DNS already selected or label missing */ }
await page.waitForTimeout(400);  // picker render frame

safeClick (not openModal) is the right wrapper here — it's a form-state change inside an already-open modal, not a new modal-open. The <label>'s visible text contains "DNS-01" + the hint string; neither matches any blocklist pattern.

The 400 ms waitForTimeout covers the React render tick + a single-frame paint for the DNSProviderPicker (per Hosts.tsx:553-559, the picker renders synchronously when tls_challenge==='dns'; one of three shapes — multi-provider <select>, singleton "Using ", or amber-warning if zero providers).

Smoke phases 12 + 13

scripts/smoke/capture-automation.sh gains 11 new asserts across two phases:

12. safeClickTab helper + tab-click migrations:
    - safeClickTab() helper defined
    - safeClickTab() requires reason argument
    - safeClickTab() exported from lib/safe-page.js
    - capture.spec.js imports safeClickTab
    - >= 6 tab-click call sites use safeClickTab
    - Whitelist tab specifically wired through safeClickTab
      (regression-guard for the v1.3.36.4 failure)

13. DNS-01 selector fix:
    - Active code uses 'label:has-text("DNS-01")'
    - Old broken input[value="dns"] selector removed from
      active code (comment lines documenting the failure
      mode are allowed)
    - Synthetic verify against Hosts.tsx: ChallengeRadio
      still renders <label> with {label} prop visible. If
      the component is refactored away from this shape,
      phase 13 fails loudly so the regression is caught at
      smoke time, not at capture time.

The synthetic verify uses grep -A 30 to scan ChallengeRadio (the JSX is ~23 lines after the function declaration); this window was bumped from -A 20 mid-impl after the smoke flagged a false negative on the {label} line being just outside the window.

Smoke result post-fix

phase 1:  run.sh refuses without .env...                     PASS
phase 2:  .env is git check-ignore'd...                      PASS
phase 3:  .env.example placeholders only...                  PASS
phase 4:  safeClick synthetic test...                        PASS (13/13)
phase 5:  working tree unchanged by smoke...                 PASS
phase 6:  storageState wiring (v1.3.36.1)...                 PASS (5/5)
phase 7:  banner output uses fs.readFileSync...              PASS
phase 8:  viewport 1440x1080 + shotFullScroll...             PASS
phase 9:  waitForSettled helper (timing fix)...              PASS (5/5)
phase 10: openModal modal-visibility wait + TG selector...   PASS (6/6)
phase 11: host-row triggers click button[aria-label=edit]... PASS (2/2)
phase 12: safeClickTab helper + tab-click migrations...      PASS (7/7)
phase 13: DNS-01 selector fix...                             PASS (3/3)

scripts/check-no-personal-data.sh clean. mkdocs build --strict clean.

Files changed

  • scripts/capture/lib/safe-page.js — new safeClickTab helper with required reason audit string.
  • scripts/capture/capture.spec.js — import safeClickTab; migrate 6 tab clicks; replace broken DNS-01 radio click with label:has-text("DNS-01"); comments document both v1.3.36.4 failure modes.
  • scripts/capture/package.json1.3.36.41.3.36.5.
  • scripts/smoke/capture-automation.sh — phases 12 + 13 added.
  • docs/release-notes/v1.3.36.5.md — this file.
  • CHANGELOG.md, mkdocs.yml.

NOT changed: argosVersion stays at 1.3.35.4, frontend/package.json version stays at 1.3.35.4. No Go code; no frontend code; no panel binary change.

Operator workflow post-fix

cd ~/argos-edge && git pull
scripts/capture/run.sh

# Verify post-fix:
# 1. security-whitelist.png shows the Whitelist tab content
#    (table of whitelist entries), NOT a [safeClick BLOCKED]
#    error.
# 2. security-activity.png + security-scenarios.png +
#    appsec-status.png + appsec-metrics.png +
#    notifications-deliveries.png — all unchanged from
#    v1.3.36.4 functional behaviour, just routed through
#    safeClickTab now.
# 3. host-form-dns-provider-dropdown.png shows the host
#    edit modal with DNS-01 radio SELECTED and the
#    DNSProviderPicker rendered below the radios (one of
#    three shapes per provider count: select / singleton /
#    amber-warning).
# 4. No regression on other surfaces.

Versioning

scripts/capture/package.json 1.3.36.41.3.36.5. Tag-without-rebuild precedent for tooling-only patches: v1.3.27.1, v1.3.34, v1.3.35.1, v1.3.35.5.