src/main.js is 1262 lines / ~49 kB — the entire application in one side-effecting entry module. Verified on main at fbfe1df.
1. innerHTML interpolation of values that are about to become user-configurable
Eight innerHTML assignment sites: lines 589, 705, 707, 715, 866, 896, 900, 1043. Two interpolate host data directly into raw HTML:
src/main.js:559 — ${host.name}
src/main.js:564 — ${host.url}
src/main.js:871 — ${entry.message} into the debug log
No escaping helper exists anywhere in the file.
This is not exploitable today — every host is a hardcoded constant in WAN_HOSTS. But README.md:163 plans "configurable host list (environment variable or config file)", which converts all three into live injection sinks the moment it lands. Fixing it now is cheap; fixing it after the config feature ships means auditing every call site under time pressure.
2. CONFIG is documented as frozen and is mutated at runtime
src/main.js:10 declares it const with no Object.freeze. src/main.js:1202 does CONFIG.updateInterval = newInterval; when the user changes the interval dropdown. README.md:61 calls it a "Frozen configuration object". (The README half is tracked in #24; the code half is here — either freeze it and derive the mutable interval separately, or stop calling it frozen.)
3. Undeclared instance property
AppState's constructor (src/main.js:264-272) declares wan, local, paused, tickCount. _recoveryProbeId is never declared there but is read at :999, written at :1009, and read/cleared at :1023-1025. The first read is against undefined.
4. Duplicated logic
HostState.medianLatency() (:246-256) and the inline median inside AppState.wanStats() (:292-297) are the same algorithm written twice.
minLatency / maxLatency / averageLatency (:226-244) each independently re-filter this.history — three passes where one would do.
The status-element class string "status-text text-xs ... col-span-2 mt-5" is written out verbatim five times: lines 724, 728, 732, 736, 1048.
5. Magic numbers that the CONFIG object exists to hold
CONFIG was created to centralize constants, yet these are hardcoded inline: history cap 1000 (:113); gateway probe timeout 1500 (:173); health thresholds 10, 4, 4, 1000, 3 (:332-337); latency colour thresholds 50/100/200/500duplicated across two functions with parallel hardcoded hex and class values (:392-396, :402-406); sparkline margins (:412); canary count 4 (:1005); recovery interval 500 (:1019); resize delay 100 (:1255); sort cadence 2/10 (:973).
The duplicated colour thresholds are the worst of these — changing a threshold requires editing two functions in lockstep or the figure colour and the sparkline colour silently disagree.
6. Nesting depth
CODE_STYLEGUIDE.md: "Avoid nesting if... invert the condition and use return to exit early." Three-plus levels at :472-484, :494-510, :514-524, :1033-1056 (four sequential if blocks with a nested if at :1054), :1064-1079, and :169-193 (four levels inside detectGateway).
7. Minor
Comma-chained declarations at :470-471, :500-501, :516-517, inconsistent with the one-per-statement style used everywhere else.
SparklineRenderer._drawXAxis(ctx, w, h, m, cw) at :458 never uses w.
pushSample writes error: result.error (:212) while pushPaused writes paused: true (:222) — two different shapes in one buffer, discriminated ad-hoc at :476.
64 lines exceed 80 columns, all inside HTML template literals prettier will not break (e.g. :548 is a 300+ char SVG path, :593 a 400+ char <h1>).
Definition of done
An HTML-escaping helper exists and is applied to every interpolation of host name, host URL, and debug-log message. Prefer building DOM nodes with textContent over innerHTML string assembly where the change is local; where innerHTML stays, the interpolated values must be escaped.
CONFIG and the code agree on mutability. Either Object.freeze it and hold the user-adjustable interval elsewhere, or drop the "frozen" claim. State which you chose.
_recoveryProbeId is declared in the constructor.
The median implementation exists once. The three history-filtering stat functions share one pass.
The repeated status class string is a single named constant.
The magic numbers listed in item 5 move into CONFIG. The latency colour thresholds are defined once and both the figure-colour and sparkline-colour functions read from that single definition.
The unused w parameter is removed; the two history-entry shapes are reconciled or explicitly documented as a tagged union.
make check passes.
TODO.md updated in the same commit.
Commit title ends with (closes #N).
Scope boundary — read this
Full modularization of the file is NOT in scope for this issue. Natural module boundaries already exist as comment banners and a future split would be roughly: config.js (:3-141), log.js (:106-124), format.js (:143-161), net.js (:163-194, :349-386), state.js (:196-347), colors.js (:388-407), sparkline.js (:409-536), ui.js (:538-925), main.js (:927-1262). That is a large, high-churn change and it belongs in its own commit after the correctness fixes above have landed and after #21 gives us tests to refactor against.
Do the safety and duplication fixes here. Extract a module only where item 5 or #21 genuinely requires it, and say so in the PR.
Deliberately not on the 1.0.0 milestone: none of this blocks a tag, and the injection sinks are latent rather than live. It should land before the configurable-host-list feature does, whenever that is scheduled.
Implementation requirements
Follow CODE_STYLEGUIDE_JS.md and the general styleguide's early-return guidance.
Behaviour must not change. This is a correctness-and-clarity pass, not a redesign — the UI should look and behave identically afterwards.
Verify by building and running the image and confirming sparklines, pinning, the interval dropdown, the debug panel, and the recovery probe all still work. Report what you exercised.
Do not touch the backend or any build/CI file.
make targets and script/ entrypoints only.
No attribution trailers in the commit message.
## Problem
`src/main.js` is 1262 lines / ~49 kB — the entire application in one side-effecting entry module. Verified on `main` at `fbfe1df`.
### 1. `innerHTML` interpolation of values that are about to become user-configurable
Eight `innerHTML` assignment sites: lines 589, 705, 707, 715, 866, 896, 900, 1043. Two interpolate host data directly into raw HTML:
- `src/main.js:559` — `${host.name}`
- `src/main.js:564` — `${host.url}`
- `src/main.js:871` — `${entry.message}` into the debug log
No escaping helper exists anywhere in the file.
**This is not exploitable today** — every host is a hardcoded constant in `WAN_HOSTS`. But `README.md:163` plans "configurable host list (environment variable or config file)", which converts all three into live injection sinks the moment it lands. Fixing it now is cheap; fixing it after the config feature ships means auditing every call site under time pressure.
### 2. `CONFIG` is documented as frozen and is mutated at runtime
`src/main.js:10` declares it `const` with **no `Object.freeze`**. `src/main.js:1202` does `CONFIG.updateInterval = newInterval;` when the user changes the interval dropdown. `README.md:61` calls it a "Frozen configuration object". (The README half is tracked in #24; the code half is here — either freeze it and derive the mutable interval separately, or stop calling it frozen.)
### 3. Undeclared instance property
`AppState`'s constructor (`src/main.js:264-272`) declares `wan`, `local`, `paused`, `tickCount`. `_recoveryProbeId` is never declared there but is read at `:999`, written at `:1009`, and read/cleared at `:1023-1025`. The first read is against `undefined`.
### 4. Duplicated logic
- `HostState.medianLatency()` (`:246-256`) and the inline median inside `AppState.wanStats()` (`:292-297`) are the same algorithm written twice.
- `minLatency` / `maxLatency` / `averageLatency` (`:226-244`) each independently re-filter `this.history` — three passes where one would do.
- The status-element class string `"status-text text-xs ... col-span-2 mt-5"` is written out verbatim **five times**: lines 724, 728, 732, 736, 1048.
### 5. Magic numbers that the CONFIG object exists to hold
`CONFIG` was created to centralize constants, yet these are hardcoded inline: history cap `1000` (`:113`); gateway probe timeout `1500` (`:173`); health thresholds `10, 4, 4, 1000, 3` (`:332-337`); latency colour thresholds `50/100/200/500` **duplicated across two functions** with parallel hardcoded hex and class values (`:392-396`, `:402-406`); sparkline margins (`:412`); canary count `4` (`:1005`); recovery interval `500` (`:1019`); resize delay `100` (`:1255`); sort cadence `2`/`10` (`:973`).
The duplicated colour thresholds are the worst of these — changing a threshold requires editing two functions in lockstep or the figure colour and the sparkline colour silently disagree.
### 6. Nesting depth
`CODE_STYLEGUIDE.md`: "Avoid nesting `if`... invert the condition and use `return` to exit early." Three-plus levels at `:472-484`, `:494-510`, `:514-524`, `:1033-1056` (four sequential `if` blocks with a nested `if` at `:1054`), `:1064-1079`, and `:169-193` (four levels inside `detectGateway`).
### 7. Minor
- Comma-chained declarations at `:470-471`, `:500-501`, `:516-517`, inconsistent with the one-per-statement style used everywhere else.
- `SparklineRenderer._drawXAxis(ctx, w, h, m, cw)` at `:458` never uses `w`.
- `pushSample` writes `error: result.error` (`:212`) while `pushPaused` writes `paused: true` (`:222`) — two different shapes in one buffer, discriminated ad-hoc at `:476`.
- 64 lines exceed 80 columns, all inside HTML template literals prettier will not break (e.g. `:548` is a 300+ char SVG path, `:593` a 400+ char `<h1>`).
## Definition of done
- [ ] An HTML-escaping helper exists and is applied to every interpolation of host name, host URL, and debug-log message. Prefer building DOM nodes with `textContent` over `innerHTML` string assembly where the change is local; where `innerHTML` stays, the interpolated values must be escaped.
- [ ] `CONFIG` and the code agree on mutability. Either `Object.freeze` it and hold the user-adjustable interval elsewhere, or drop the "frozen" claim. State which you chose.
- [ ] `_recoveryProbeId` is declared in the constructor.
- [ ] The median implementation exists once. The three history-filtering stat functions share one pass.
- [ ] The repeated status class string is a single named constant.
- [ ] The magic numbers listed in item 5 move into `CONFIG`. The latency colour thresholds are defined **once** and both the figure-colour and sparkline-colour functions read from that single definition.
- [ ] The unused `w` parameter is removed; the two history-entry shapes are reconciled or explicitly documented as a tagged union.
- [ ] `make check` passes.
- [ ] `TODO.md` updated in the same commit.
- [ ] Commit title ends with ` (closes #N)`.
## Scope boundary — read this
**Full modularization of the file is NOT in scope for this issue.** Natural module boundaries already exist as comment banners and a future split would be roughly: `config.js` (`:3-141`), `log.js` (`:106-124`), `format.js` (`:143-161`), `net.js` (`:163-194`, `:349-386`), `state.js` (`:196-347`), `colors.js` (`:388-407`), `sparkline.js` (`:409-536`), `ui.js` (`:538-925`), `main.js` (`:927-1262`). That is a large, high-churn change and it belongs in its own commit **after** the correctness fixes above have landed and after #21 gives us tests to refactor against.
Do the safety and duplication fixes here. Extract a module only where item 5 or #21 genuinely requires it, and say so in the PR.
Deliberately not on the `1.0.0` milestone: none of this blocks a tag, and the injection sinks are latent rather than live. It should land before the configurable-host-list feature does, whenever that is scheduled.
## Implementation requirements
- Follow `CODE_STYLEGUIDE_JS.md` and the general styleguide's early-return guidance.
- Behaviour must not change. This is a correctness-and-clarity pass, not a redesign — the UI should look and behave identically afterwards.
- Verify by building and running the image and confirming sparklines, pinning, the interval dropdown, the debug panel, and the recovery probe all still work. Report what you exercised.
- Do not touch the backend or any build/CI file.
- `make` targets and `script/` entrypoints only.
- No attribution trailers in the commit message.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
src/main.jsis 1262 lines / ~49 kB — the entire application in one side-effecting entry module. Verified onmainatfbfe1df.1.
innerHTMLinterpolation of values that are about to become user-configurableEight
innerHTMLassignment sites: lines 589, 705, 707, 715, 866, 896, 900, 1043. Two interpolate host data directly into raw HTML:src/main.js:559—${host.name}src/main.js:564—${host.url}src/main.js:871—${entry.message}into the debug logNo escaping helper exists anywhere in the file.
This is not exploitable today — every host is a hardcoded constant in
WAN_HOSTS. ButREADME.md:163plans "configurable host list (environment variable or config file)", which converts all three into live injection sinks the moment it lands. Fixing it now is cheap; fixing it after the config feature ships means auditing every call site under time pressure.2.
CONFIGis documented as frozen and is mutated at runtimesrc/main.js:10declares itconstwith noObject.freeze.src/main.js:1202doesCONFIG.updateInterval = newInterval;when the user changes the interval dropdown.README.md:61calls it a "Frozen configuration object". (The README half is tracked in #24; the code half is here — either freeze it and derive the mutable interval separately, or stop calling it frozen.)3. Undeclared instance property
AppState's constructor (src/main.js:264-272) declareswan,local,paused,tickCount._recoveryProbeIdis never declared there but is read at:999, written at:1009, and read/cleared at:1023-1025. The first read is againstundefined.4. Duplicated logic
HostState.medianLatency()(:246-256) and the inline median insideAppState.wanStats()(:292-297) are the same algorithm written twice.minLatency/maxLatency/averageLatency(:226-244) each independently re-filterthis.history— three passes where one would do."status-text text-xs ... col-span-2 mt-5"is written out verbatim five times: lines 724, 728, 732, 736, 1048.5. Magic numbers that the CONFIG object exists to hold
CONFIGwas created to centralize constants, yet these are hardcoded inline: history cap1000(:113); gateway probe timeout1500(:173); health thresholds10, 4, 4, 1000, 3(:332-337); latency colour thresholds50/100/200/500duplicated across two functions with parallel hardcoded hex and class values (:392-396,:402-406); sparkline margins (:412); canary count4(:1005); recovery interval500(:1019); resize delay100(:1255); sort cadence2/10(:973).The duplicated colour thresholds are the worst of these — changing a threshold requires editing two functions in lockstep or the figure colour and the sparkline colour silently disagree.
6. Nesting depth
CODE_STYLEGUIDE.md: "Avoid nestingif... invert the condition and usereturnto exit early." Three-plus levels at:472-484,:494-510,:514-524,:1033-1056(four sequentialifblocks with a nestedifat:1054),:1064-1079, and:169-193(four levels insidedetectGateway).7. Minor
:470-471,:500-501,:516-517, inconsistent with the one-per-statement style used everywhere else.SparklineRenderer._drawXAxis(ctx, w, h, m, cw)at:458never usesw.pushSamplewriteserror: result.error(:212) whilepushPausedwritespaused: true(:222) — two different shapes in one buffer, discriminated ad-hoc at:476.:548is a 300+ char SVG path,:593a 400+ char<h1>).Definition of done
textContentoverinnerHTMLstring assembly where the change is local; whereinnerHTMLstays, the interpolated values must be escaped.CONFIGand the code agree on mutability. EitherObject.freezeit and hold the user-adjustable interval elsewhere, or drop the "frozen" claim. State which you chose._recoveryProbeIdis declared in the constructor.CONFIG. The latency colour thresholds are defined once and both the figure-colour and sparkline-colour functions read from that single definition.wparameter is removed; the two history-entry shapes are reconciled or explicitly documented as a tagged union.make checkpasses.TODO.mdupdated in the same commit.(closes #N).Scope boundary — read this
Full modularization of the file is NOT in scope for this issue. Natural module boundaries already exist as comment banners and a future split would be roughly:
config.js(:3-141),log.js(:106-124),format.js(:143-161),net.js(:163-194,:349-386),state.js(:196-347),colors.js(:388-407),sparkline.js(:409-536),ui.js(:538-925),main.js(:927-1262). That is a large, high-churn change and it belongs in its own commit after the correctness fixes above have landed and after #21 gives us tests to refactor against.Do the safety and duplication fixes here. Extract a module only where item 5 or #21 genuinely requires it, and say so in the PR.
Deliberately not on the
1.0.0milestone: none of this blocks a tag, and the injection sinks are latent rather than live. It should land before the configurable-host-list feature does, whenever that is scheduled.Implementation requirements
CODE_STYLEGUIDE_JS.mdand the general styleguide's early-return guidance.maketargets andscript/entrypoints only.