Skip to content

Commit 4d2d58b

Browse files
committed
External sites update in place instead of reloading the whole page
While a scan or a telemetry read was running, the site list, a site's page and a scan's page reloaded themselves every fifteen seconds, which jumped the page and lost what was ticked. They now fetch themselves in the background and swap only the parts that change: on a site, the scanning banner, the counters, the scan list and the AI read; on the list, each site's telemetry and scan status and the counters; on a scan, its status, counts and what has run. When nothing is running any more the page reloads once, so the results, the screenshot and the buttons come in. Nothing changes while a dialog is open, and Stop and Delete keep asking before they act.
1 parent 09a4ed4 commit 4d2d58b

4 files changed

Lines changed: 77 additions & 21 deletions

File tree

‎app/web/static/external.js‎

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -226,18 +226,66 @@
226226
Array.prototype.forEach.call(pickers, setup);
227227
})();
228228

229-
/* Pages that are waiting on something reload themselves, but never while a
230-
* dialog is open: that would throw away whatever somebody was typing. */
229+
/* Pages that are waiting on something keep themselves up to date without
230+
* reloading. Every few seconds the page is fetched again in the background
231+
* and only the parts marked data-live are swapped for their new versions, so
232+
* the rest of the page, what is ticked and where it is scrolled to, stay put.
233+
*
234+
* When the new copy says nothing is running any more, the page reloads once,
235+
* so the finished results, the screenshot and the buttons all come in. It
236+
* never touches the page while a dialog is open. */
231237
(function () {
232238
"use strict";
233239
var meta = document.querySelector('meta[name="gcr-refresh"]');
234240
if (!meta) { return; }
235241
var seconds = Math.max(5, parseInt(meta.content, 10) || 15);
236-
setInterval(function () {
237-
var open = Array.prototype.some.call(
242+
var busy = false;
243+
244+
function dialogOpen() {
245+
return Array.prototype.some.call(
238246
document.querySelectorAll(".modal-backdrop"), function (n) { return !n.hidden; });
239-
if (!open && !document.hidden) { window.location.reload(); }
240-
}, seconds * 1000);
247+
}
248+
249+
function swap(fresh) {
250+
document.querySelectorAll("[data-live]").forEach(function (old) {
251+
var key = old.getAttribute("data-live");
252+
var next = fresh.querySelector('[data-live="' + CSS.escape(key) + '"]');
253+
if (!next || next.innerHTML === old.innerHTML) { return; }
254+
old.innerHTML = next.innerHTML;
255+
old.className = next.className;
256+
/* What came in was not there when the page set up its confirmations. */
257+
old.querySelectorAll("form[data-confirm]").forEach(function (f) { f.setAttribute("data-swapped", "1"); });
258+
});
259+
}
260+
261+
function tick() {
262+
if (busy || document.hidden || dialogOpen()) { return; }
263+
busy = true;
264+
fetch(window.location.pathname + window.location.search,
265+
{credentials: "same-origin", headers: {"X-Requested-With": "live"}})
266+
.then(function (r) { if (!r.ok) { throw new Error(r.status); } return r.text(); })
267+
.then(function (html) {
268+
var fresh = new DOMParser().parseFromString(html, "text/html");
269+
if (!fresh.querySelector('meta[name="gcr-refresh"]')) {
270+
window.location.reload();
271+
return;
272+
}
273+
swap(fresh);
274+
})
275+
.catch(function () { /* try again next time */ })
276+
.then(function () { busy = false; });
277+
}
278+
279+
/* The confirmation a swapped in Stop or Delete button needs. */
280+
document.addEventListener("submit", function (event) {
281+
var form = event.target;
282+
if (form.getAttribute && form.getAttribute("data-swapped") === "1" &&
283+
!window.confirm(form.getAttribute("data-confirm"))) {
284+
event.preventDefault();
285+
}
286+
}, true);
287+
288+
setInterval(tick, seconds * 1000);
241289
})();
242290

243291
/* The list: tick all, a count of what is ticked, actions that need a tick,

‎app/web/templates/external.html‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{% extends "base.html" %}
22
{% from "_external_fields.html" import detail_fields %}
33
{% block title %}External sites · Git Code Review{% endblock %}
4-
{% block head %}{% if running or scanning %}<meta name="gcr-refresh" content="15">{% endif %}{% endblock %}
4+
{% block head %}{% if running or scanning %}<meta name="gcr-refresh" content="10">{% endif %}{% endblock %}
55
{% block content %}
66
<div class="page-head">
77
<div><h1>External sites</h1>
@@ -30,7 +30,7 @@
3030
</div>
3131

3232
{% set here = base ~ '/external' %}
33-
<div class="stats">
33+
<div class="stats" data-live="list-stats">
3434
<div class="stat"><div class="n">{{ totals.sites }}</div><div class="l">Sites</div></div>
3535
<div class="stat"><div class="n">{{ totals.live }}</div><div class="l">Answering</div></div>
3636
<a class="stat warn" href="{{ here }}?dast=never"><div class="n">{{ totals.never }}</div><div class="l">Never scanned</div></a>
@@ -39,8 +39,10 @@
3939
<a class="stat" href="{{ here }}?scope=mapped"><div class="n">{{ totals.mapped }}</div><div class="l">Mapped to code</div></a>
4040
<a class="stat" href="{{ here }}?scope=no-scan"><div class="n">{{ totals.no_scan }}</div><div class="l">No scan</div></a>
4141
</div>
42-
{% if running %}<div class="notice info"><div>Reading telemetry for {{ running }} site{{ '' if running == 1 else 's' }}. This page refreshes until they finish.</div></div>{% endif %}
43-
{% if scanning %}<div class="notice info"><div>Scanning {{ scanning }} site{{ '' if scanning == 1 else 's' }} now. {{ max_concurrent }} engine scan{{ '' if max_concurrent == 1 else 's' }} run at a time, set in Settings; the rest wait their turn. This page refreshes until they finish.</div></div>{% endif %}
42+
<div data-live="list-notices">
43+
{% if running %}<div class="notice info"><div>Reading telemetry for {{ running }} site{{ '' if running == 1 else 's' }}. The list updates as they finish.</div></div>{% endif %}
44+
{% if scanning %}<div class="notice info"><div>Scanning {{ scanning }} site{{ '' if scanning == 1 else 's' }} now. {{ max_concurrent }} engine scan{{ '' if max_concurrent == 1 else 's' }} run at a time, set in Settings; the rest wait their turn. The list updates as they run.</div></div>{% endif %}
45+
</div>
4446

4547
<form method="get" action="{{ here }}" class="filters">
4648
<div class="field" style="min-width:240px"><label for="q">Search</label>
@@ -103,7 +105,7 @@
103105
{% if a.external_auth_needed %}<span class="badge info" title="External auth needed">ext auth</span>{% endif %}
104106
{% if a.whitelist_needed %}<span class="badge pending" title="Whitelist needed">whitelist</span>{% endif %}</div>
105107
{% if a.executive_owner or a.technical_owner %}<div class="small faint">{{ a.executive_owner }}{% if a.executive_owner and a.technical_owner %}; {% endif %}{{ a.technical_owner }}</div>{% endif %}</td>
106-
<td class="small">
108+
<td class="small" data-live="tele-{{ a.id }}">
107109
{% if a.telemetry_status == 'running' %}<span class="badge pending">reading</span>
108110
{% elif not a.telemetry_at %}<span class="faint">not read yet</span>
109111
{% else %}
@@ -117,7 +119,7 @@
117119
<div class="faint">read {{ a.telemetry_at | dt('%Y-%m-%d') }}</div>
118120
{% endif %}
119121
{% if a.telemetry_status == 'failed' %}<div class="faint" title="{{ a.telemetry_error }}">last read failed</div>{% endif %}</td>
120-
<td class="small">
122+
<td class="small" data-live="scan-{{ a.id }}">
121123
{% if r.scan %}
122124
<div style="margin-bottom:.35rem"><a class="badge pending" href="{{ base }}/external/{{ a.id }}/scans/{{ r.scan.id }}">{{ 'queued' if r.scan.status == 'queued' else 'scanning' }}{% if r.scan.profile %}, {{ r.scan.profile }}{% endif %}</a>
123125
{% if r.scan.step %}<div class="faint">{{ r.scan.step }}</div>{% endif %}

‎app/web/templates/external_scan.html‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{% extends "base.html" %}
22
{% block title %}Scan of {{ asset.host }} &middot; Git Code Review{% endblock %}
3-
{% block head %}{% if busy %}<meta name="gcr-refresh" content="15">{% endif %}{% endblock %}
3+
{% block head %}{% if busy %}<meta name="gcr-refresh" content="8">{% endif %}{% endblock %}
44
{% block content %}
55
{% set here = base ~ '/external/' ~ asset.id ~ '/scans/' ~ scan.id %}
66
<div class="page-head">
@@ -34,19 +34,22 @@
3434
</div>
3535
</div>
3636

37+
<div data-live="scan-status">
3738
{% if scan.status in ('queued','running') %}
38-
<div class="notice info"><div>{{ scan.status | capitalize }}{% if scan.step %}: {{ scan.step }}{% endif %}. This page refreshes while it runs.</div></div>
39+
<div class="notice info"><div>{{ scan.status | capitalize }}{% if scan.step %}: {{ scan.step }}{% endif %}. This updates as it runs.</div></div>
3940
{% elif scan.status == 'failed' %}
4041
<div class="notice bad"><div>{{ scan.error or 'It failed.' }}</div></div>
4142
{% endif %}
43+
</div>
4244

43-
<div class="stats">
45+
<div class="stats" data-live="scan-stats">
4446
<div class="stat {{ 'bad' if view.severity in ('critical','high') else ('ok' if scan.status == 'done' and view.severity in ('', 'none') else '') }}"><div class="n" style="{{ 'font-size:1.1rem' if view.severity in ('', 'none') }}">{% if scan.status != 'done' %}&ndash;{% elif view.severity in ('', 'none') %}no findings{% else %}{{ view.severity }}{% endif %}</div><div class="l">Worst found</div></div>
4547
{% for level in ['critical','high','medium','low','info'] %}
4648
<a class="stat {{ 'bad' if level in ('critical','high') and view.counts[level] else '' }}" href="{{ here }}?sev={{ level }}"><div class="n">{{ view.counts[level] }}</div><div class="l">{{ level }}</div></a>
4749
{% endfor %}
4850
</div>
4951

52+
<div data-live="scan-engines">
5053
{% if view.engines %}
5154
<div class="card">
5255
<div class="card-head">What ran</div>
@@ -63,6 +66,7 @@
6366
</div>
6467
{% endif %}
6568

69+
</div>
6670
{% set ai = view.ai %}
6771
{% if ai.status %}
6872
<div class="card">

‎app/web/templates/external_site.html‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{% extends "base.html" %}
22
{% from "_external_fields.html" import detail_fields %}
33
{% block title %}{{ asset.host }} &middot; Git Code Review{% endblock %}
4-
{% block head %}{% if busy %}<meta name="gcr-refresh" content="15">{% endif %}{% endblock %}
4+
{% block head %}{% if busy %}<meta name="gcr-refresh" content="8">{% endif %}{% endblock %}
55
{% block content %}
66
{% set tele_headers = tele.headers or {} %}
77
<div class="page-head">
@@ -27,10 +27,11 @@
2727
</div>
2828
</div>
2929

30+
<div data-live="site-notices">
3031
{% if active_scan %}
3132
<div class="notice info"><div style="display:flex;gap:.6rem;align-items:center;flex-wrap:wrap">
3233
<span><strong>{{ 'Queued' if active_scan.status == 'queued' else 'Scanning' }}</strong>{% if active_scan.profile %}, {{ active_scan.profile }} profile{% endif %}
33-
on {{ active_scan.server_name }}{% if active_scan.step %}: {{ active_scan.step }}{% endif %}. This page refreshes until it ends.</span>
34+
on {{ active_scan.server_name }}{% if active_scan.step %}: {{ active_scan.step }}{% endif %}. This updates as it runs.</span>
3435
<a class="btn sm" href="{{ base }}/external/{{ asset.id }}/scans/{{ active_scan.id }}">Watch it</a>
3536
{% if active_scan.source != 'recorded' %}
3637
<form method="post" action="{{ base }}/external/{{ asset.id }}/scans/{{ active_scan.id }}/stop" style="display:inline"
@@ -40,8 +41,9 @@
4041
<button class="btn sm danger" type="submit">Stop</button></form>{% endif %}
4142
</div></div>
4243
{% endif %}
43-
{% if asset.telemetry_status == 'running' %}<div class="notice info"><div>Reading telemetry. This page refreshes until it is done.</div></div>{% endif %}
44+
{% if asset.telemetry_status == 'running' %}<div class="notice info"><div>Reading telemetry. The page shows it when it is done.</div></div>{% endif %}
4445
{% if asset.telemetry_status == 'failed' %}<div class="notice bad"><div>The last telemetry read failed: {{ asset.telemetry_error }}</div></div>{% endif %}
46+
</div>
4547

4648
{# The front page first, large, the way the site report opened each host. #}
4749
<div class="card">
@@ -60,7 +62,7 @@
6062
</div>
6163
</div>
6264

63-
<div class="stats">
65+
<div class="stats" data-live="site-stats">
6466
<div class="stat"><div class="n">{% if asset.grade and asset.grade != 'N/A' %}<span class="grade {{ asset.grade }}">{{ asset.grade }}</span>{% else %}&ndash;{% endif %}</div><div class="l">Web hygiene{% if asset.score %} ({{ asset.score }}){% endif %}</div></div>
6567
<div class="stat {{ 'bad' if asset.last_dast_severity in ('critical','high') else ('ok' if asset.last_dast_at and asset.last_dast_severity in ('', 'none') else '') }}"><div class="n" style="{{ 'font-size:1.1rem' if not asset.last_dast_at or asset.last_dast_severity in ('', 'none') }}">{% if not asset.last_dast_at %}never{% elif asset.last_dast_severity in ('', 'none') %}no findings{% else %}{{ asset.last_dast_severity }}{% endif %}</div><div class="l">Last dynamic scan{% if asset.last_dast_at %}, {{ asset.last_dast_at | dt('%Y-%m-%d') }}{% endif %}</div></div>
6668
<div class="stat {{ 'bad' if risk.level in ('critical','high') else ('warn' if risk.level == 'medium' else '') }}"><div class="n">{{ risk.level }}</div><div class="l">Combined risk</div></div>
@@ -156,7 +158,7 @@
156158
</div>
157159

158160
<div>
159-
<div class="card">
161+
<div class="card" data-live="site-ai">
160162
<div class="card-head">AI risk read
161163
{% if asset.ai_input_tokens or asset.ai_output_tokens %}<span class="faint small" style="font-weight:400">{{ asset.ai_input_tokens | thousands }} in, {{ asset.ai_output_tokens | thousands }} out{% if asset.ai_cost %}, {{ asset.ai_cost }}{% endif %} so far, counted on the <a href="{{ base }}/ai?tab=cost">AI cost page</a></span>{% endif %}</div>
162164
<div class="card-body">
@@ -176,7 +178,7 @@
176178
</div>
177179
</div>
178180

179-
<div class="card">
181+
<div class="card" data-live="site-scans">
180182
<div class="card-head">Dynamic scans
181183
{% if scan_total %}<span class="faint small" style="font-weight:400">{{ scan_total }} in all, newest first</span>{% endif %}</div>
182184
{% if scan_views %}

0 commit comments

Comments
 (0)