# Dashboard Toggle UI Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add per-detector and per-category toggle checkboxes to the AEP dashboard so workshop presenters can enable/disable safety detectors and Trust Engine categories with clicks instead of curl commands.

**Architecture:** Pure frontend — a new "Policy Controls" section in the developer view renders checkboxes from the policy state returned by `GET /aep/api/safety`. Toggling a checkbox sends `POST /aep/api/safety {"policy": {...}}` with the updated detector config. No backend changes needed.

**Tech Stack:** Vanilla HTML/CSS/JS (no build step), Starlette (existing), existing dashboard dark theme

**Issue:** https://github.com/aceteam-ai/aceteam-aep/issues/65

---

## File Structure

| File | Action | Responsibility |
|------|--------|---------------|
| `src/aceteam_aep/dashboard/templates/index.html` | Modify | Add policy controls section + JS logic |
| `tests/test_dashboard_views.py` | Modify | Test that controls section renders |
| `tests/integration/test_safety_toggle.py` | Modify | E2E test for checkbox-driven policy swap |

---

## Task 1: Add Policy Controls Section to Dashboard HTML

**Files:**
- Modify: `src/aceteam_aep/dashboard/templates/index.html`

This is the main task. Add a "Policy Controls" card section between the budget section and the safety signals section in the developer view.

- [ ] **Step 1: Add CSS for the toggle controls**

In the `<style>` block (before `</style>` on line 102), add:

```css
.policy-controls { display: flex; flex-wrap: wrap; gap: 8px; }
.detector-toggle { display: flex; align-items: center; gap: 6px; background: #0d1117; padding: 6px 12px; border-radius: 6px; cursor: pointer; user-select: none; border: 1px solid #30363d; transition: border-color 0.2s; }
.detector-toggle:hover { border-color: #58a6ff; }
.detector-toggle.disabled { opacity: 0.4; }
.detector-toggle input[type="checkbox"] { accent-color: #58a6ff; cursor: pointer; width: 14px; height: 14px; }
.detector-toggle label { font-size: 12px; color: #c9d1d9; cursor: pointer; text-transform: uppercase; letter-spacing: 0.5px; }
.detector-toggle.disabled label { text-decoration: line-through; }
.category-divider { width: 100%; font-size: 10px; color: #484f58; text-transform: uppercase; letter-spacing: 1px; margin: 4px 0; }
```

- [ ] **Step 2: Add the Policy Controls HTML section**

After the budget section (`</div>` on line ~149) and before the Safety Signals section (`<div class="section">` on line ~151), add:

```html
<div class="section" id="policy-section">
  <h2>Policy Controls</h2>
  <div class="policy-controls" id="policy-controls">
    <span class="empty">Loading policy...</span>
  </div>
</div>
```

- [ ] **Step 3: Add JavaScript to fetch and render policy controls**

In the `<script>` block, add the following functions after the `toggleSafety()` function (after line ~295). **IMPORTANT: Use only safe DOM methods (createElement, textContent, appendChild). Never use innerHTML with dynamic content.**

```javascript
var currentPolicy = null;

function fetchPolicy() {
  fetch('api/safety')
    .then(function(r) { return r.json(); })
    .then(function(data) {
      safetyEnabled = data.safety_enabled;
      var toggle = document.getElementById('safety-toggle');
      var label = document.getElementById('safety-label');
      toggle.className = 'toggle-switch' + (safetyEnabled ? ' on' : '');
      label.textContent = safetyEnabled ? 'Safety ON' : 'Safety OFF';
      currentPolicy = data.policy;
      renderPolicyControls(data.policy);
    })
    .catch(function() {});
}

function renderPolicyControls(policy) {
  var container = document.getElementById('policy-controls');
  if (!container || !policy) return;
  container.textContent = '';

  var detectors = policy.detectors || {};
  var detectorNames = Object.keys(detectors);

  var displayNames = {
    agent_threat: 'Agent Threat',
    pii: 'PII Detection',
    cost_anomaly: 'Cost Anomaly',
    content_safety: 'Content Safety',
    trust_engine: 'Trust Engine',
    ferpa: 'FERPA'
  };

  var domainCategories = ['finance', 'iot', 'software', 'web', 'program'];
  var categoryNames = {
    finance: 'Finance', iot: 'IoT', software: 'Software', web: 'Web', program: 'Program'
  };

  if (detectorNames.length === 0) {
    var empty = document.createElement('span');
    empty.className = 'empty';
    empty.textContent = 'No detectors configured';
    container.appendChild(empty);
    return;
  }

  detectorNames.forEach(function(name) {
    var det = detectors[name];
    var isEnabled = det.enabled !== false;

    var wrap = document.createElement('div');
    wrap.className = 'detector-toggle' + (isEnabled ? '' : ' disabled');

    var cb = document.createElement('input');
    cb.type = 'checkbox';
    cb.checked = isEnabled;
    cb.id = 'det-' + name;
    cb.onchange = function() { toggleDetector(name, cb.checked); };

    var lbl = document.createElement('label');
    lbl.htmlFor = 'det-' + name;
    lbl.textContent = displayNames[name] || name;

    wrap.appendChild(cb);
    wrap.appendChild(lbl);
    container.appendChild(wrap);
  });

  // If trust_engine is present and enabled, show category sub-toggles
  var teConfig = detectors.trust_engine;
  if (teConfig && teConfig.enabled !== false) {
    var divider = document.createElement('div');
    divider.className = 'category-divider';
    divider.textContent = 'Trust Engine Categories';
    container.appendChild(divider);

    var activeDims = (teConfig.extra && teConfig.extra.dimensions) || domainCategories;

    domainCategories.forEach(function(cat) {
      var isActive = activeDims.indexOf(cat) >= 0;

      var wrap = document.createElement('div');
      wrap.className = 'detector-toggle' + (isActive ? '' : ' disabled');

      var cb = document.createElement('input');
      cb.type = 'checkbox';
      cb.checked = isActive;
      cb.id = 'cat-' + cat;
      cb.onchange = function() { toggleCategory(cat, cb.checked); };

      var lbl = document.createElement('label');
      lbl.htmlFor = 'cat-' + cat;
      lbl.textContent = categoryNames[cat] || cat;

      wrap.appendChild(cb);
      wrap.appendChild(lbl);
      container.appendChild(wrap);
    });
  }
}

function toggleDetector(name, enabled) {
  if (!currentPolicy) return;
  var detectors = JSON.parse(JSON.stringify(currentPolicy.detectors || {}));
  if (!detectors[name]) detectors[name] = {};
  detectors[name].enabled = enabled;

  var newPolicy = {
    default_action: currentPolicy.default_action,
    block_on: currentPolicy.block_on,
    flag_on: currentPolicy.flag_on,
    detectors: detectors
  };

  fetch('api/safety', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({policy: newPolicy})
  }).then(function(r) { return r.json(); })
    .then(function(data) {
      currentPolicy = data.policy;
      renderPolicyControls(data.policy);
    });
}

function toggleCategory(cat, enabled) {
  if (!currentPolicy) return;
  var detectors = JSON.parse(JSON.stringify(currentPolicy.detectors || {}));
  var te = detectors.trust_engine || {};
  var extra = te.extra || {};
  var dims = extra.dimensions || ['finance', 'iot', 'software', 'web', 'program'];

  if (enabled && dims.indexOf(cat) < 0) {
    dims.push(cat);
  } else if (!enabled) {
    dims = dims.filter(function(d) { return d !== cat; });
  }

  extra.dimensions = dims;
  te.extra = extra;
  detectors.trust_engine = te;

  var newPolicy = {
    default_action: currentPolicy.default_action,
    block_on: currentPolicy.block_on,
    flag_on: currentPolicy.flag_on,
    detectors: detectors
  };

  fetch('api/safety', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({policy: newPolicy})
  }).then(function(r) { return r.json(); })
    .then(function(data) {
      currentPolicy = data.policy;
      renderPolicyControls(data.policy);
    });
}
```

- [ ] **Step 4: Wire fetchPolicy into the polling loop**

Find the existing `setInterval` polling call (search for `setInterval` in the script block — it polls `/api/state` every 2 seconds). Add a `fetchPolicy()` call alongside it, but at a slower interval (every 5 seconds is fine for policy state):

```javascript
// Add after the existing setInterval for state polling:
fetchPolicy(); // Initial fetch
setInterval(fetchPolicy, 5000);
```

Also call `fetchPolicy()` immediately on page load (add it next to the existing initial fetch call).

- [ ] **Step 5: Verify rendering manually**

```bash
cd /home/jason/workspace/aceteam-ai/aceteam-aep
uv run aceteam-aep proxy --port 8899
# Open http://localhost:8899/aep/ in browser
# Verify: Policy Controls section appears between Budget and Safety Signals
# Verify: Checkboxes for agent_threat, pii, cost_anomaly are shown
# Verify: Clicking a checkbox visually toggles it and calls the API
# Verify: Safety master toggle still works
```

- [ ] **Step 6: Commit**

```bash
git add src/aceteam_aep/dashboard/templates/index.html
git commit -m "feat(dashboard): add per-detector and per-category toggle checkboxes"
```

---

## Task 2: Add Dashboard Unit Test for Policy Controls

**Files:**
- Modify: `tests/test_dashboard_views.py`

- [ ] **Step 1: Write the tests**

Add to `tests/test_dashboard_views.py`:

```python
def test_dashboard_has_policy_controls_section():
    """Dashboard HTML contains the policy controls section."""
    from pathlib import Path
    html = (Path(__file__).parent.parent / "src" / "aceteam_aep" / "dashboard" / "templates" / "index.html").read_text()
    assert 'id="policy-controls"' in html
    assert 'id="policy-section"' in html
    assert "renderPolicyControls" in html
    assert "toggleDetector" in html
    assert "toggleCategory" in html


def test_dashboard_policy_controls_detector_names():
    """Dashboard JS contains display names for all known detectors."""
    from pathlib import Path
    html = (Path(__file__).parent.parent / "src" / "aceteam_aep" / "dashboard" / "templates" / "index.html").read_text()
    for name in ["Agent Threat", "PII Detection", "Cost Anomaly", "Trust Engine"]:
        assert name in html, f"Missing detector display name: {name}"


def test_dashboard_policy_controls_category_names():
    """Dashboard JS contains the 5 R-Judge domain categories."""
    from pathlib import Path
    html = (Path(__file__).parent.parent / "src" / "aceteam_aep" / "dashboard" / "templates" / "index.html").read_text()
    for cat in ["Finance", "IoT", "Software", "Web", "Program"]:
        assert cat in html, f"Missing category name: {cat}"
```

- [ ] **Step 2: Run all dashboard tests**

```bash
uv run pytest tests/test_dashboard_views.py tests/test_dashboard.py -v
```

Expected: All pass.

- [ ] **Step 3: Commit**

```bash
git add tests/test_dashboard_views.py
git commit -m "test(dashboard): verify policy controls section, detector names, category names"
```

---

## Task 3: Integration Test for Checkbox-Driven Policy Swap

**Files:**
- Modify: `tests/integration/test_safety_toggle.py`

- [ ] **Step 1: Add test to existing TestPolicyHotSwap class**

```python
def test_disable_detector_via_policy(self, proxy):
    """Disabling agent_threat via policy swap lets dangerous calls through."""
    # With default policy, dangerous call is blocked
    r = _call(proxy, DANGEROUS_PROMPT)
    assert r.status_code == 400

    # Disable agent_threat detector (simulates unchecking the checkbox)
    r = httpx.post(
        f"{proxy}/aep/api/safety",
        json={
            "policy": {
                "default_action": "flag",
                "block_on": ["high"],
                "flag_on": ["medium", "low"],
                "detectors": {
                    "agent_threat": {"enabled": False, "action": "block"},
                    "pii": {"enabled": True, "action": "block"},
                    "cost_anomaly": {"enabled": True, "action": "flag"},
                },
            }
        },
        timeout=5,
    )
    assert r.status_code == 200

    # Same dangerous call now passes (agent_threat disabled)
    r = _call(proxy, DANGEROUS_PROMPT)
    assert r.status_code == 200
```

- [ ] **Step 2: Run integration tests**

```bash
export OPENAI_API_KEY=$(grep '^OPENAI_API_KEY=' ../aceteam/.env | head -1 | cut -d= -f2- | tr -d "'\"")
uv run pytest tests/integration/test_safety_toggle.py::TestPolicyHotSwap::test_disable_detector_via_policy -v
```

Expected: PASS

- [ ] **Step 3: Run full integration suite**

```bash
uv run pytest tests/integration/test_safety_toggle.py -v
```

Expected: All 21 pass.

- [ ] **Step 4: Commit**

```bash
git add tests/integration/test_safety_toggle.py
git commit -m "test(integration): add detector disable via policy swap test"
```

---

## Dependency Graph

```
Task 1 (HTML/CSS/JS) ── Task 2 (unit tests) ── Task 3 (integration test)
```

All sequential — Task 2 validates Task 1's HTML, Task 3 validates the API interaction that the checkboxes trigger.

---

## Out of Scope

- WebSocket for real-time policy sync (polling at 5s is fine)
- Drag-and-drop policy reordering
- Policy presets dropdown (healthcare, finance, startup)
- Saving policy to file from dashboard
- CISO view policy controls (developer view only for now)
