fix(web-ui): security hardening, SPA nav, and modularization

Ship the in-progress ES-module refactor of the web-ui (new static/modules/
layout, Usage/Settings pages, uplot-based dashboard) alongside a round of
security and UX fixes:

- main.go: add CSP + X-Frame-Options: DENY + X-Content-Type-Options:
  nosniff + Referrer-Policy middleware on every response; WS CheckOrigin
  now requires Origin host to match Host (blocks cross-site WebSocket
  hijacking); upgrade client before dialing upstream so origin check
  runs first; fatal on unparseable AGENTMON_QUERY_BASE.
- app.js: delegated click handler intercepts same-origin <a> clicks for
  SPA navigation (prev. every nav link caused a full page reload,
  dropping WS + in-memory state); delegated .copy-btn[data-copy]
  handler replaces inline onclick=; removed window.navigate /
  window.copyToClipboard globals and the duplicated handleGlobalSearch.
- modules/nav-signal.js: per-route AbortController so in-flight fetches
  are cancelled when the user navigates away, preventing stale toasts
  and wasted renders.
- modules/api.js: honours the nav signal by default; AbortError is
  silent.
- modules/router.js: resets the nav controller on every route; dropped
  the fixed 80ms transition delay; breadcrumbs no longer emit inline
  onclick= (delegated handler picks them up).
- modules/utils.js: renderCopyButton emits data-copy=\"...\" instead of
  nesting a JS string inside an HTML attribute — fixes an XSS where
  values containing ' broke out via &#39; decoding.

Verified: go build clean; `node --check` clean on all modified modules;
manual curl probes confirm security headers present on every response
and WS upgrade returns 403 for cross-origin/missing Origin while 101
for same-origin.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
William Valentin
2026-04-23 15:36:12 -07:00
parent 41b7165800
commit 184aa5e6cb
20 changed files with 5129 additions and 4216 deletions
+3 -5
View File
@@ -17,14 +17,12 @@
<div class="header-logo">
<h1><a href="/">agentmon<span class="logo-dot"></span></a></h1>
</div>
<nav><a href="/">Dashboard<span class="nav-badge" id="nav-error-badge"></span></a><a href="/sessions">Sessions</a><a href="/agents">Agents</a><a href="/infrastructure">Infra</a></nav>
<nav><a href="/">Dashboard<span class="nav-badge" id="nav-error-badge"></span></a><a href="/sessions">Sessions</a><a href="/agents">Agents</a><a href="/infrastructure">Infra</a><a href="/usage">Usage</a><a href="/settings">Settings</a></nav>
<div class="header-right">
<div class="header-search">
<input type="text" id="global-search" placeholder="Search ID..." spellcheck="false" autocomplete="off">
<kbd>/</kbd>
<button class="cmd-k-hint" id="cmd-k-hint" title="Command palette" type="button">
<kbd>⌘K</kbd>
</button>
<button class="cmd-k-hint" id="cmd-k-hint" title="Command palette" type="button"></button>
</div>
<span class="ws-dot" id="ws-dot" title="Disconnected"></span>
<button class="theme-toggle" id="theme-toggle" aria-label="Toggle theme"></button>
@@ -35,6 +33,6 @@
<p>Loading...</p>
</main>
<script src="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.iife.min.js"></script>
<script src="/static/app.js"></script>
<script type="module" src="/static/app.js"></script>
</body>
</html>