From bb1f32d36bc7825c828e9c1c6179149e83a44418 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Sun, 4 Oct 2026 01:50:09 +0000 Subject: [PATCH] Target names, URLs and log lines reach the page as text (closes #29) 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 --- README.md | 6 +++-- TODO.md | 9 ++++++++ src/main.js | 50 ++++++++++++++++++++++++++++++------------ test/unit/main.test.js | 27 +++++++++++++++++++++++ 4 files changed, 76 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b4e4de7..d0ddfeb 100644 --- a/README.md +++ b/README.md @@ -104,8 +104,10 @@ files are served, with no backend required. The application is a single-page app built with Vite and Tailwind CSS v4. All code lives in `src/main.js` with a class-based architecture: -- **`CONFIG`**: Frozen configuration object (update interval, timeouts, axis - ticks, etc.) +- **`CONFIG`**: Configuration object (update interval, timeouts, axis ticks, + etc.). The interval menu sets `updateInterval`, the one value the page writes + into `CONFIG`; the timeouts, the time the history spans and the x-axis ticks + are computed from it - **`HostState`**: Per-host state management — history buffer, latency tracking, status transitions - **`AppState`**: Top-level state container — WAN hosts, local hosts, pause diff --git a/TODO.md b/TODO.md index 5f4e5c6..fd266ca 100644 --- a/TODO.md +++ b/TODO.md @@ -23,6 +23,15 @@ latest run passes. # Completed Steps +- 2026-10-04: a target's name and URL and a debug log message show as the + characters they are and are never read as HTML (issue #29): a host row escapes + the name and URL it writes into its markup, and the debug log sets each line + as text. A unit test checks a target whose name and URL hold `<`, `>`, `"`, + `&` and `'`. `README.md` no longer calls `CONFIG` frozen: the interval menu + sets its `updateInterval`, and the values computed from it follow. `AppState` + declares the recovery probe's two properties, the sparkline axis functions + lose the parameters they did not use, and the comment on a target's history + names both kinds of entry it holds. Nothing the page does changed - 2026-10-04: the backend serves Prometheus metrics (issue #94). With `METRICS_USERNAME` and `METRICS_PASSWORD` both set, it records request duration and response size through `go-http-metrics` and serves them, with diff --git a/src/main.js b/src/main.js index 2793022..0483ed7 100644 --- a/src/main.js +++ b/src/main.js @@ -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) {