Skip to content

improvments - #81

Open
alexwbaule wants to merge 8 commits into
librespeed:masterfrom
alexwbaule:feature/new-interface
Open

improvments#81
alexwbaule wants to merge 8 commits into
librespeed:masterfrom
alexwbaule:feature/new-interface

Conversation

@alexwbaule

Copy link
Copy Markdown

Summary

This PR modernizes the LibreSpeed Go backend with a new results view page, a redesigned admin
interface, improved test accuracy, stable device identification, and several reliability fixes.


Speed Test Improvements

  • Equalized download/upload duration: both phases now use 2s grace time, 6 parallel streams,
    and 20s max time (time_auto: true), eliminating the previous asymmetry where upload ran
    significantly shorter than download
  • Real-time chart fix: the chart no longer starts at t=0 with a false zero — the timestamp
    anchor (dlT0/ulT0) is only set when the first non-zero speed sample arrives, so the graph
    always starts from the actual measurement point

Results Share Page (/results/view)

  • New HTML results page at /results/view?id=UUID showing a full breakdown of a test result
  • Displays: download/upload/ping/jitter metrics, real-time speed chart (canvas), letter grade
    (A–F), latency-under-load, and ISP/location info parsed from JSON into a structured grid
  • Grade calculated client-side and stored with the test record: factors in download, upload, ping,
    jitter, packet loss, and latency under load
  • Share modal: the share button opens a dialog showing the PNG result image; the copy field
    shows the /results/view URL. Both the main share button and the history table share buttons
    behave identically

Stable Device Identification

  • Browser generates a stable clientId stored in localStorage (UUIDv4), combined with a
    SHA-256 fingerprint of userAgent + language + hardwareConcurrency + deviceMemory + platform + screenResolution + timezone
  • The combined clientId:fingerprint16 is sent with each test via the client_id telemetry field
  • Allows grouping tests by device in the admin interface even when the IP address changes

Admin Statistics Interface (/stats)

Complete redesign of the admin page:

  • Card-based layout for test results, grouped by device (ClientID)
  • Clickable result cards open a modal with the /results/view page embedded (iframe)
  • Real pagination: replaced the Last100/L1000 hack with proper FetchAll(offset, limit) +
    Count() across all database drivers
  • Advanced search with per-field operators:
    • Text contains: UUID, IP address
    • Numeric with operator ( = > <): Download, Upload, Ping, Jitter
    • Date with operator: filter by test date (YYYY-MM-DD)
    • Active filter badge shows match count; pagination preserves filter state across pages
  • Device grouping: tests grouped by ClientID; devices without an ID shown as
    "Unknown Devices"
  • Config loading fix: var conf = config.LoadedConfig() at package level was evaluated before
    main() loaded settings.toml, causing stats to always show "Statistics Disabled" regardless
    of the configured password — moved to inside the handler function
  • Logout button styling fix: a stray } closed the <style> block early, making all CSS
    rules after that point invisible to the browser

Database Layer

  • Added FetchAll(offset, limit int) and Count() to the DataAccess interface
  • Implemented across all drivers: BoltDB, PostgreSQL, MySQL, SQLite, MSSQL, memory, none
  • BoltDB field leak fix: TelemetryData struct was declared outside the scan loop in
    FetchLast100; since json.Unmarshal does not zero absent fields, values from one record
    leaked into the next — moved declaration inside the loop

Schema Extensions

New fields added to TelemetryData:

Field Description
GradeData JSON: {grade, criteria}
ChartData JSON: {dl: [{t,v}], ul: [{t,v}]}
LatencyUnderload Ping measured during active load (ms)
PingDuringTest JSON: {dl: [...], ul: [...]} — pings per phase
ClientID Browser-generated stable device identifier

Server & Reliability

  • HTTP→HTTPS redirect listener: new redirect_from config option starts a plain-HTTP listener
    on the specified port (e.g. 80) and issues a 301 redirect to the main server. Uses http://
    or https:// based on whether enable_tls is set; omits port from the URL when it matches
    the scheme default (80/443)
  • Suppressed expected disconnect errors: http2: stream closed, broken pipe,
    connection reset by peer, and context canceled are normal when a browser aborts
    download/upload streams at test end — added isClientGone() check so these no longer
    appear as errors in the log
  • /results/view route ordering fix: route was registered after the /results/ wildcard,
    causing 404s — moved before the wildcard

@maddie maddie left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Nice work on this PR, especially isClientGone(), device grouping, and proper pagination. Found a few things that need fixing before merge:

1. SQL drivers don't persist the 5 new schema fields

The TelemetryData struct added GradeData, ChartData, LatencyUnderload, PingDuringTest, and ClientID, but the INSERT statements in all 4 SQL drivers weren't updated:

  • database/mysql/mysql.go:31-32 — still only 11 columns
  • database/postgresql/postgresql.go:31-32 — same
  • database/sqlite/sqlite.go:29-43 — CREATE TABLE also only has old columns
  • database/mssql/mssql.go:43-47 — same

This means client_id (sent by the worker and read in Record()) is silently dropped on SQL backends. Device grouping on /stats won't work. Also, SELECT * + Scan on existing databases will fail because column count doesn't match.

BoltDB and Memory are fine since they serialize the full struct.

2. Chart/grade/latency-under-load data never reaches the server

  • speedtest_worker.js sendTelemetry() — only appends client_id to FormData, doesn't include dlChartData, ulChartData, or latencyUnderload
  • telemetry.go Record() — only reads client_id, not grade/chart/underload fields
  • results/view.go:437-438 — chart renders hardcoded demo data, not {{ .ChartData }}

3. Stray } breaks CSS in stats template

results/stats.go:557 — extra closing brace right after .badge-filtered block. Everything from .logout-btn downward won't parse. Same class of bug the PR description mentions fixing in the logout button.

4. Session key regenerated every restart

results/stats.go:160-162securecookie.GenerateRandomKey(32) runs at init, so a new key is generated every time the process starts. All existing sessions invalidated on restart; multi-instance deployments will have constant re-login prompts. Should be a fixed key from config.

5. config.LoadedConfig() called twice

telemetry.go lines 175 and 193 — the second call can reuse the first result.

Would be happy to re-review once the SQL column sync and telemetry data flow are addressed.

… stability

SQL drivers (PostgreSQL, MySQL, SQLite, MSSQL):
- Add grade_data, chart_data, latency_underload, ping_during_test, client_id
  to INSERT statements — fields were silently dropped on all SQL backends
- Replace SELECT * with explicit column list using COALESCE for backward
  compatibility with existing databases
- Remove all SQL string concatenation — every query is now a complete literal
  with bind parameters only
- SQLite: add new columns to CREATE TABLE and auto-migrate existing databases
  via ALTER TABLE ADD COLUMN (error on duplicate column is intentionally ignored)

Telemetry data flow:
- speedtest_worker.js: add computeGrade() and send grade_data, chart_data,
  latency_underload, ping_during_test in sendTelemetry() FormData and fallback
  postData — these were computed but never reaching the server
- telemetry.go: read and persist the four new fields from the POST body
- results/view.go: render real ChartData via template.JS (no more hardcoded
  sample data); parse GradeData JSON and display actual grade letter

Session key stability (results/stats.go):
- Replace securecookie.GenerateRandomKey(32) with SHA-256 of the configured
  stats password, initialized via sync.Once — previously every process restart
  invalidated all active sessions

Other fixes:
- telemetry.go: remove duplicate config.LoadedConfig() call, reuse conf
- web/web.go: HTTP redirect now uses https:// only when enable_tls is true,
  falls back to http:// otherwise
- results/stats.go: remove stray closing brace that silently terminated the
  <style> block early, causing all CSS rules after .badge-filtered to be ignored
@alexwbaule

Copy link
Copy Markdown
Author

@maddie , fixed all things that you comment on review.

@maddie maddie left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — re-check on 5ab1cc6 (base: 0bb00da)

Thanks for the fixes. Re-reviewed the full diff against origin/master — three of the five points are genuinely fixed, two are only partially fixed, and the re-review surfaced one stored-XSS, one frontend regression, and a few smaller items.

Previously raised

1. SQL drivers don't persist the 5 new schema fields — partially fixed. All four drivers now carry the 16 columns in INSERT/SELECT, and SQLite gets a CREATE TABLE + migration. But the three bootstrap schemas were not updated:

  • database/mysql/telemetry_mysql.sql
  • database/postgresql/telemetry_postgresql.sql
  • database/mssql/telemetry_mssql.sql

All three still create the old 13-column table (no grade_data, chart_data, latency_underload, ping_during_test, client_id), and these drivers have no runtime migration. README step 4 tells users to import exactly these files, so fresh installs on MySQL/PostgreSQL/MSSQL will fail on every INSERT and SELECT — telemetry recording returns 500 and stats breaks. COALESCE(...,'') handles NULL, not missing columns, so the backward-compat claim doesn't hold on those backends. Please update the three .sql files (and either add an idempotent migration or document the ALTERs for existing DBs).

2. Chart/grade/latency-under-load data never reaches the server — fixed. Worker sends all five fields on both telemetry paths; Record() reads them; the view renders real ChartData.

3. Stray } breaks stats CSS — fixed (removed in 5ab1cc6).

4. Session key regenerated every restart — fixed. Now derived from the stats password (sha256("speedtest-stats:"+StatsPassword)) with sync.Once — stable across restarts and multi-instance deployments.

5. config.LoadedConfig() called twice — partially fixed. RedactIP reuses the first result, but telemetry.go:236 (EnableIDObfuscation) still calls it a second time. Harmless in practice — just reuse the local result for consistency.

New findings

6. P1 — Stored XSS via chart_data. Record() stores r.FormValue("chart_data") verbatim (telemetry.go:219), and ViewPage injects it as raw JS:

chartJS := template.JS(record.ChartData)   // results/view.go:88

POST /results/telemetry and GET /results/view are both unauthenticated, and the stats page embeds /results/view in a same-origin iframe — so anyone can submit a crafted chart_data for an arbitrary UUID, and every visitor (including an admin with a live session) executes their script. Fix: json.Unmarshal into {dl:[{t,v}], ul:[{t,v}]} and reject invalid shapes before storing; only then pass it through template.JS (or render from the parsed JSON instead of injecting raw text).

7. P1 — index-modern.html is broken by the index.js rewrite. The rewritten web/assets/javascript/index.js targets the new DOM ids (dl-gauge, ul-gauge, ip-display, result-dl), but index-modern.html (unchanged, still the modern design behind README's ?design=new) uses the old ids (download-gauge, upload-gauge, …). On the first IP sample document.getElementById('ip-display').innerHTML throws on null; the rAF render loop dies and the gauges stay blank. Either port index-modern.html to the new ids or make index.js fall back to the old ones.

8. P2 — web/assets/design-switch.js is now dead code. New index.html no longer references it (zero references in web/), so the ?design=new toggle silently stops working. Delete the file or restore the reference.

9. P2 — Gauge drawing logic is duplicated. G_START/G_SWEEP, valueToAngle, buildLogTicks, drawGauge, etc. exist both in index.html's inline script (~701–773) and in javascript/index.js; the two copies have already started drifting. Consolidate into one.

10. P3 — SQLite runs blind ALTERs and swallows errors. database/sqlite/sqlite.go:53-57 unconditionally runs ALTER TABLE ... ADD COLUMN for the 5 new columns on every startup and ignores all errors. On a fresh DB each ALTER fails by design; on a broken migration a real error is hidden. Check PRAGMA table_info(speedtest_users) before altering.

11. P3 — seen map in results/stats.go (~333–341) is redundant. len(seen) always equals len(groups) — use len(groups) directly.

12. P3 — PingDuringTest is write-only. The worker reports it, all drivers store it, nothing reads or renders it. Either consume it on the view page or drop the column.

Happy to re-review once the XSS, schema files, and index-modern fixes are in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants