Target names, URLs and log lines reach the page as text (closes #29)
check / check (push) Successful in 3m8s
check / check (push) Successful in 3m8s
A host row escapes the name and URL it writes into its markup with a new escapeHTML function, and the debug log builds each line as an element whose text is set, so neither is read as HTML once targets can be configured. A unit test builds the row of a target whose name and URL hold < > " & and ' and checks each comes out escaped; hostRowHTML is exported for it. README.md stops calling CONFIG frozen: the interval menu sets updateInterval, and the timeouts, history span and axis ticks are computed from it. AppState declares _recoveryProbeId and _recoveryProbeChecks, the sparkline axis functions drop the parameters they never used, and HostState's history comment names both entry shapes. Model: opus-5-5
This commit was merged in pull request #105.
This commit is contained in:
+36
-14
@@ -8,6 +8,8 @@
|
||||
// display their real value in the latency figure. The history buffer holds
|
||||
// maxHistoryPoints samples (historyDuration / updateInterval).
|
||||
// reportInterval is how often collected samples are POSTed to the backend.
|
||||
// The interval menu changes updateInterval while the page runs; the
|
||||
// getters compute their values from it each time they are read.
|
||||
export const CONFIG = {
|
||||
updateInterval: 3000,
|
||||
maxHistoryPoints: 100,
|
||||
@@ -201,7 +203,10 @@ export class HostState {
|
||||
constructor(host, pinned = false) {
|
||||
this.name = host.name;
|
||||
this.url = host.url;
|
||||
this.history = []; // { timestamp, latency, paused }
|
||||
// Each entry is either a check's result, { timestamp, latency,
|
||||
// error }, or a round skipped while paused, { timestamp,
|
||||
// latency: null, paused: true }.
|
||||
this.history = [];
|
||||
this.lastLatency = null;
|
||||
this.status = "pending"; // 'online' | 'offline' | 'error' | 'pending'
|
||||
this.pinned = pinned;
|
||||
@@ -271,6 +276,10 @@ export class AppState {
|
||||
this.local = localHosts.map((h) => new HostState(h));
|
||||
this.paused = false;
|
||||
this.tickCount = 0;
|
||||
// The recovery probe's timer, null while it is not running, and the
|
||||
// checks it started last.
|
||||
this._recoveryProbeId = null;
|
||||
this._recoveryProbeChecks = null;
|
||||
}
|
||||
|
||||
get allHosts() {
|
||||
@@ -580,8 +589,8 @@ class SparklineRenderer {
|
||||
const ch = h - m.top - m.bottom;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
SparklineRenderer._drawYAxis(ctx, w, h, m, ch);
|
||||
SparklineRenderer._drawXAxis(ctx, w, h, m, cw);
|
||||
SparklineRenderer._drawYAxis(ctx, w, m, ch);
|
||||
SparklineRenderer._drawXAxis(ctx, h, m, cw);
|
||||
|
||||
const len = history.length;
|
||||
const pw = cw / (CONFIG.maxHistoryPoints - 1);
|
||||
@@ -597,7 +606,7 @@ class SparklineRenderer {
|
||||
SparklineRenderer._drawTip(ctx, history, getX, getY);
|
||||
}
|
||||
|
||||
static _drawYAxis(ctx, w, h, m, ch) {
|
||||
static _drawYAxis(ctx, w, m, ch) {
|
||||
ctx.font = "300 12px monospace";
|
||||
ctx.textAlign = "right";
|
||||
ctx.textBaseline = "middle";
|
||||
@@ -614,7 +623,7 @@ class SparklineRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
static _drawXAxis(ctx, w, h, m, cw) {
|
||||
static _drawXAxis(ctx, h, m, cw) {
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "top";
|
||||
for (const tick of CONFIG.xAxisTicks) {
|
||||
@@ -702,7 +711,18 @@ class SparklineRenderer {
|
||||
// horizontally.
|
||||
const STATUS_TEXT_CLASS = "status-text text-xs text-right col-span-2 mt-5";
|
||||
|
||||
function hostRowHTML(host, index, showPin = true) {
|
||||
// Escapes text for HTML, so it shows as written inside an element or a
|
||||
// quoted attribute and is never read as markup.
|
||||
function escapeHTML(text) {
|
||||
return text
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
export function hostRowHTML(host, index, showPin = true) {
|
||||
const pinColor = host.pinned
|
||||
? "text-blue-500"
|
||||
: "text-gray-600 hover:text-gray-400";
|
||||
@@ -721,12 +741,12 @@ function hostRowHTML(host, index, showPin = true) {
|
||||
<div class="w-[420px] flex-shrink-0 grid grid-cols-[minmax(0,1fr)_auto] items-center">
|
||||
<div class="flex items-center gap-2 min-w-[200px]">
|
||||
<div class="w-3 h-3 rounded-full flex-shrink-0 bg-[#6b7280]"></div>
|
||||
<span class="font-medium text-white truncate">${host.name}</span>
|
||||
<span class="font-medium text-white truncate">${escapeHTML(host.name)}</span>
|
||||
</div>
|
||||
<div class="latency-value text-4xl font-bold tabular-nums text-right mt-3" data-host="${index}">
|
||||
<span class="text-gray-500">---</span>
|
||||
</div>
|
||||
<a href="${host.url}" target="_blank" rel="noopener" class="text-xs text-gray-500 truncate block col-span-2 -mt-2">${host.url}</a>
|
||||
<a href="${escapeHTML(host.url)}" target="_blank" rel="noopener" class="text-xs text-gray-500 truncate block col-span-2 -mt-2">${escapeHTML(host.url)}</a>
|
||||
<div class="${STATUS_TEXT_CLASS} text-gray-500" data-host="${index}">waiting...</div>
|
||||
</div>
|
||||
<div class="flex-grow sparkline-container rounded overflow-hidden border border-gray-700/30">
|
||||
@@ -1042,14 +1062,16 @@ function renderDebugLog() {
|
||||
info: "text-gray-300",
|
||||
debug: "text-gray-500",
|
||||
};
|
||||
el.innerHTML = debugLog
|
||||
.map((entry) => {
|
||||
el.replaceChildren(
|
||||
...debugLog.map((entry) => {
|
||||
const ts = formatUTCTimestamp(entry.timestamp);
|
||||
const cls = levelColors[entry.level] || "text-gray-400";
|
||||
const lvl = entry.level.toUpperCase().padEnd(7);
|
||||
return `<div class="${cls}">${ts} ${lvl} ${entry.message}</div>`;
|
||||
})
|
||||
.join("");
|
||||
const line = document.createElement("div");
|
||||
line.className = levelColors[entry.level] || "text-gray-400";
|
||||
line.textContent = `${ts} ${lvl} ${entry.message}`;
|
||||
return line;
|
||||
}),
|
||||
);
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user