From 713a534946168e70e0bbf4bbf15909c17e8e5f82 Mon Sep 17 00:00:00 2001 From: Hermes Date: Tue, 18 Aug 2026 23:46:48 -0700 Subject: [PATCH] feat(caddy-builder): DC-106 visual reverse proxy builder (frontend) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /api/v1/caddycode/{generate,validate,templates} endpoints already existed (commit 7f83151, GLM grade B); the frontend visual builder page is the missing piece. This commit ships: - status/js/caddy-builder.js — IIFE module that injects a modal with a form-driven visual builder. Form state → JSON payload → live POST /generate → preview pane. 5 presets loaded from /api/v1/caddycode/templates (simple, websocket, auth-gated, cors-api, subdirectory). Custom headers list (add/remove rows), live validation, copy-to-clipboard, reset. State machine is a single plain-object `state` snapshot — debounced regeneration (250 ms). Exposes window.__caddyBuilder for testing. - status/css/caddy-builder.css — page-specific styles, themed via existing --bg / --border / --accent / --ok-fg / --warn-fg / --err-fg CSS variables. Mobile-friendly single-column layout below 880 px. - status/index.html — adds /css/caddy-builder.css link + the "🔧 Reverse Proxy Builder" button in the Tools menu (id caddy-builder-btn). No other UI changes. - status/build.js — registers caddy-builder.js in features.js bundle. Same load-order conventions as the surrounding modules. - dashcaddy-api/__tests__/unit/caddy-builder.unit.test.js — 19 pure-function tests covering state defaults, buildPayload (whitespace trim, blank-key drop, authService/stripPrefix nullification, all booleans explicit), applyTemplate (5 presets + unknown-id no-op), and generate() against mocked fetch (POST shape, 400 error capture, network-failure graceful handling, short-circuit on missing fields). Includes XSS regression: verifies the global escapeHtml contract used by renderHeadersList is safe against "&<>\'onerror=x'; + const escaped = global.escapeHtml(malicious); + expect(escaped).not.toContain('