Closes enhancement 1 of issue #207 (raised by @tuxpowered).
What changed
Clicking a row in the Settings → Deployment → Recent Executions table now opens a modal showing the full execution: timestamp, hook name + id, domain, event, dry-run flag, success / exit code, duration, error (when present), and the captured command, stdout and stderr streams. The backend was already recording all of these in data/deploy_history.jsonl (modules/core/deployer.py:165-179) and the /api/deploy/history endpoint was already serving the full record — the UI was rendering only 6 of the 13 fields.
Implementation
templates/partials/settings_deploy.html— each<tr>in the history table is nowrole="button" tabindex="0"with@clickand@keydown.enter|space.preventbindings toshowExecutionDetail(entry). Hover affordance +aria-labelper row carrying the hook name and domain. A new modal block at the end of the deploy panel uses the standard_modal.htmlmacro (size="lg",icon="fa-terminal"). Body is gated by<template x-if="selectedExecution">so bindings are skipped until a row is clicked.static/js/settings-deploy.js— addsselectedExecution: nulldata andshowExecutionDetail(entry)which stashes the entry and callsCertMate.modal.open('executionDetailModal'). Read-only; no extra fetch.- No backend change.
Security
All modal fields render via x-text (DOM textContent), never x-html. Operator-controlled command / stdout / stderr / error / hook_name strings cannot inject HTML or script tags. The data route is still @require_role('admin')-gated. Streams are truncated to 4096 chars at write time in the deployer; a footer line in the modal calls this out so a viewer is not surprised by clipped output.
Verification
Full pytest: 876 passed, 14 skipped, 2 xfailed, 0 failed in 104s.
Built a v2.6.11 image + isolated fixture dir, seeded data/deploy_history.jsonl with 4 varied entries including an XSS canary (<script>alert('xss')</script><img src=x onerror=alert(2)> in stdout). Headless Playwright smoke against the running container asserted:
| Assertion | Result |
|---|---|
| 4 rows rendered | OK |
row[0] role=button, tabindex=0
| OK |
modal initial class has hidden
| OK |
modal after row click: hidden removed
| OK |
stdout pre.textContent contains literal payload
| OK |
<script> children inside modal DOM
| 0 |
window.alert called during render
| never |
| Esc closes modal | OK |
| Enter on focused row opens modal | OK |
Out of scope (separate follow-ups against #207)
- Relabel "Hook name" → "path to script or cmd" + "Description:" prefix on the example field.
- Dedicated webhook configuration flow (URL + method + auth + freeform JSON payload + variable selector), distinct from the shell-script hook flow.