Tests / Test failed: 4, passed: 744
Replace hand-written f-string + html.escape() rendering in the Host Agent
local console with a module-level Jinja2 Environment configured with
select_autoescape(["html","xml"]). XSS safety now holds by mechanism
rather than per-call discipline — every operator-controlled field
(device name, connection_info, task summary, etc.) is escaped by the
engine uniformly.
Eight templates under host_agent/web/templates/ replace the former
_chrome(), _CSS, escape(), and per-page _xxx_body() helpers: base.html
(header/nav/CSS + {% block body %}), login, dashboard (with the polling
<script> preserved byte-identically inside {% raw %}), devices, account,
history, tasks_list, and task_detail. The task-list and task-detail
templates — added by the just-landed task-execution-progress-visibility
change — were also migrated here rather than left in f-string form,
since this change removes the shared helpers they depended on.
URLs, auth/session/CSRF semantics, redirects, and /api/status JSON are
unchanged. 15 new template tests cover render-smoke, XSS probing, script
byte-identity, and no-autoescape-bypass guards. Tasks 8.1-8.6 (manual
browser verification) remain.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
74 lines
3.1 KiB
HTML
74 lines
3.1 KiB
HTML
{% extends "base.html" %}
|
|
{% block body %}
|
|
<h1>Status</h1>
|
|
<section>
|
|
<h2>Enrollment</h2>
|
|
<p>Host ID: {{ identity.host_id if identity else "" or "not enrolled" }}</p>
|
|
<p>Agent instance ID: {{ identity.agent_instance_id if identity else "" or "unknown" }}</p>
|
|
<p>Control plane: {{ config.control_plane_url }}</p>
|
|
</section>
|
|
<section>
|
|
<h2>Heartbeat</h2>
|
|
<p id="last-heartbeat">{{ heartbeat_text }}</p>
|
|
</section>
|
|
<section>
|
|
<h2>Cloud policy</h2>
|
|
<p id="host-policy">{{ policy_text }}</p>
|
|
</section>
|
|
<section>
|
|
<h2>Current assignment</h2>
|
|
<p id="current-assignment">{{ assignment_text }}</p>
|
|
<p id="current-progress">{{ progress_text }}</p>
|
|
</section>
|
|
<section>
|
|
<h2>Devices</h2>
|
|
<table>
|
|
<thead><tr><th>ID</th><th>Name</th><th>Driver</th><th>Status</th></tr></thead>
|
|
<tbody id="device-status-body">{% for device in devices %}<tr><td>{{ device.id }}</td><td>{{ device.name or "" }}</td><td>{{ device.driver_type }}</td><td>{{ device.display_status }}</td></tr>{% endfor %}</tbody>
|
|
</table>
|
|
</section>
|
|
{% raw %}<script>
|
|
(function () {
|
|
function render(data) {
|
|
var hb = data.status.last_heartbeat;
|
|
document.getElementById("last-heartbeat").textContent = hb
|
|
? (hb.ok ? "ok" : "failed") + " at " + hb.at + " (" + hb.device_count + " devices)"
|
|
: "never";
|
|
var current = data.status.current_assignment;
|
|
document.getElementById("current-assignment").textContent = current
|
|
? current.task_id + " on " + current.device_id + " (started " + current.started_at + ")"
|
|
: "none";
|
|
var progress = data.status.progress;
|
|
document.getElementById("current-progress").textContent = progress
|
|
? "step " + progress.step_index + " \u2014 " + progress.step_status + ": " + progress.summary
|
|
: "";
|
|
var policy = data.status.host_policy;
|
|
document.getElementById("host-policy").textContent = policy
|
|
? "revision " + policy.revision + "; self-submission "
|
|
+ (policy.self_submission_enabled ? "enabled" : "disabled")
|
|
+ "; max active tasks " + (policy.max_active_tasks || "unlimited")
|
|
+ "; daily token budget " + (policy.daily_token_budget || "unmetered")
|
|
: "no Cloud policy cached";
|
|
var body = document.getElementById("device-status-body");
|
|
body.innerHTML = "";
|
|
data.devices.forEach(function (device) {
|
|
var row = document.createElement("tr");
|
|
["id", "name", "driver_type", "status"].forEach(function (key) {
|
|
var cell = document.createElement("td");
|
|
cell.textContent = device[key] || "";
|
|
row.appendChild(cell);
|
|
});
|
|
body.appendChild(row);
|
|
});
|
|
}
|
|
function poll() {
|
|
fetch("/api/status", { credentials: "same-origin" })
|
|
.then(function (response) { return response.ok ? response.json() : null; })
|
|
.then(function (data) { if (data) render(data); })
|
|
.catch(function () {});
|
|
}
|
|
setInterval(poll, 5000);
|
|
})();
|
|
</script>{% endraw %}
|
|
{% endblock %}
|