Skip to content

feat(web): rebuild the dashboard UI and the API it runs on - #797

Open
KamilPesek wants to merge 13 commits into
netresearch:mainfrom
KamilPesek:feat/dashboard-redesign
Open

feat(web): rebuild the dashboard UI and the API it runs on#797
KamilPesek wants to merge 13 commits into
netresearch:mainfrom
KamilPesek:feat/dashboard-redesign

Conversation

@KamilPesek

@KamilPesek KamilPesek commented Aug 19, 2026

Copy link
Copy Markdown

Summary

Splits the single-file UI into html/template partials plus standalone app.js and
styles.css, then rebuilds the dashboard on top: teal/graphite theming, stat cards,
a searchable job table, sortable job and history tables, run output in an expandable
subrow, job history in a modal, and a custom tooltip system that survives overflow and
dialog stacking.

Backend changes the UI depends on:

  • /api/dashboard aggregates the per-tick payload so the UI polls once. Auth-gated
    like the rest of /api/, and declared in the Server.routes table so
    TestRouteAuthExpectations holds it to an auth expectation. The per-resource
    endpoints are untouched — this is additive, for polling consumers
  • recentRuns on job payloads: an additive summary of the last 10 executions
    (date, duration, failed, skipped) on /api/jobs, /api/jobs/disabled,
    /api/jobs/removed and /api/dashboard, so list views can render outcome
    history without a fetch per job. Omitted for jobs that keep no history
  • response compression via klauspost/compress/gzhttp (the only new dependency).
    The wrapper enables zstd next to gzip and prefers it at equal q-values, so
    Chrome, Edge and Firefox — which send gzip, deflate, br, zstd — receive
    Content-Encoding: zstd, and clients without zstd (Safari below 26, curl
    defaults, monitoring scripts) receive gzip. Both branches are pinned in
    web/compress_test.go
  • /live and /ready exempt from rate limiting; everything else, static assets
    and /health included, still counted. /health is token-free but expensive —
    GetHealth calls runtime.ReadMemStats on every request, which stops the world,
    and reports version and goroutine count — so exempting it would leave an
    unauthenticated, unthrottled endpoint that pauses the GC per call. Orchestrator
    probes are served by /live and /ready
  • origin gate on job update: POST /api/jobs/update now returns 403 for INI- and
    label-owned jobs, mirroring the existing delete gate (Persist manually added/edited jobs #593), and the UI disables
    both actions on those jobs
  • job config no longer leaks through the API, and editing a paused job no longer
    resumes it
  • run-job demuxes container logs instead of leaking mux frame headers
  • nextRuns/prevRuns anchored on the cron entry, not the poll time
  • OFELIA_UI_DEV_DIR serves UI assets from disk during development

Five of those bullets are pre-existing bug fixes rather than UI scaffolding. Two are
worth spelling out, because the failure mode is not obvious from the one-line summary:

  • Editing a config-owned job rewrote its origin to api/web, after which the
    delete gate no longer recognised it as config-owned — editing a label job unlocked
    deleting it.
  • CopyLogs branched on info.Config.HostConfig != nil, true for essentially
    every container, so non-TTY containers took the TTY raw-copy path and Docker's
    8-byte frame headers landed in stored job output.

And one refactor that is hardening rather than a fix, corrected from an earlier
version of this description: stripJobs now matches job collections by shape
(map[string]*struct) instead of by a hand-maintained field-name list. The list it
replaces — RunJobs, ExecJobs, ServiceJobs, LocalJobs, ComposeJobs — covers exactly
the five collections cli.Config has today, so /api/config output is unchanged by
this PR and nothing leaked. The point of the change is that a collection added later
is stripped by construction, instead of shipping every job's definition until someone
remembers to update the list.

Breaking changes

  • core.DockerProvider gains CopyContainerLogs, so any external implementation of
    that exported interface must add the method to compile. Inside the repo it is the
    +4 lines in each of the six mock providers.
  • POST /api/jobs/update returns 403 for INI- and label-owned jobs, where it
    previously returned 200. Scripts that edited config-owned jobs through the API must
    change the source config, or use POST /api/jobs/disable to suppress the job.
  • Scheduler.UpdateJob updates a disabled job in place instead of returning
    ErrJobNotFound. Callers that used the error to detect "not scheduled" must check
    the disabled state explicitly.

All three are source- or API-level breaks under SemVer §4 for the current 0.y.z line.
They carry BREAKING CHANGE: trailers in the commits and are listed under ### Changed
in the CHANGELOG.

One security note on the compression: it now covers authenticated API responses,
which is the precondition for BREACH-style length leaks. No current endpoint has the
other half of that pattern — none reflects caller-supplied input into a response that
also carries a secret — so this is not exploitable as it stands. It is recorded in
web/AGENTS.md so a future endpoint that echoes user input is checked against it.

Docs and CHANGELOG updated. Tests added for the compression middleware (gzip and
zstd), the rate-limit scope, the update gate, config stripping, UI rendering and
refresh ordering.

The change is split into 13 commits so the bug fixes can be cherry-picked into a
patch release without the UI work.

Type of Change

  • Bug fix (non-breaking)
  • New feature (non-breaking)
  • Breaking change
  • Documentation update
  • Refactoring / code quality
  • CI / build / dependencies

Checklist

  • Commits are signed (-S) and signed-off (--signoff)
  • Commits follow Conventional Commits format
  • Tests added/updated for changed behavior
  • Documentation updated (README, AGENTS.md, CHANGELOG, or inline docs)
  • CI passes (lint, tests, static analysis)

go-check / golangci-lint is red, with the same seven findings it reports on main
(six nolint:goconst directives that nolintlint now considers unused, and one
gofumpt formatting complaint in cli/doctor_docker_timeout_test.go). None of them
comes from this branch; they need a separate fix on main.

Test Plan

Verified locally:

go build ./...                          # OK
go vet ./...                            # OK
gofmt -l .                              # clean
go test ./web/... ./core/... ./cli/... ./config/...   # ok

Every one of the 13 commits builds on its own (go build ./... at each).

TestComposeJobBuildCommand fails in the golang:1.26-alpine dev container because
the image has no docker binary in PATH; it passes in CI.

The e2e suite is left to CI: it needs a host with no unrelated ofelia.*-labelled
containers, since the daemon label scan picks them up and throws off the job-count
assertions.

To exercise the UI by hand:

go build -o ofelia . && ./ofelia daemon --config config.ini
# open http://localhost:8081 with `enable-web = true` in [global]

For UI iteration without rebuilding the binary:

OFELIA_UI_DEV_DIR=./static/ui ./ofelia daemon --config config.ini

Worth clicking through: open a job's history from the name button, expand a run's
output, switch timezone and theme, search and sort the job table, and confirm that a
job defined in INI or via Docker labels shows edit and delete as disabled with a
tooltip naming the source (and that POST /api/jobs/update on it returns 403).

@KamilPesek
KamilPesek requested a review from CybotTM as a code owner August 19, 2026 06:12

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests frontend labels Aug 19, 2026
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.12418% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.19%. Comparing base (b2397a6) to head (073eab8).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
web/ui.go 60.46% 10 Missing and 7 partials ⚠️
web/server.go 85.93% 4 Missing and 5 partials ⚠️
core/docker_sdk_provider.go 76.92% 2 Missing and 1 partial ⚠️
core/scheduler.go 66.66% 1 Missing and 1 partial ⚠️
web/compress.go 71.42% 1 Missing and 1 partial ⚠️
core/adapters/docker/container.go 0.00% 0 Missing and 1 partial ⚠️
core/runjob.go 50.00% 0 Missing and 1 partial ⚠️

❌ Your patch check has failed because the patch coverage (77.12%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #797      +/-   ##
==========================================
- Coverage   89.33%   89.19%   -0.15%     
==========================================
  Files          88       91       +3     
  Lines       12220    12314      +94     
==========================================
+ Hits        10917    10983      +66     
- Misses       1003     1018      +15     
- Partials      300      313      +13     
Flag Coverage Δ
integration 89.17% <77.12%> (-0.15%) ⬇️
unittests 88.61% <77.12%> (-0.16%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@CybotTM CybotTM left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for this — it is a lot of work, and the five bug fixes carried along with the UI are the more valuable half. Reviewed at 5d88df8. Copilot is out of review quota this month and the automated Claude review is disabled for fork PRs, so this is a manual review.

What I verified

Checked out the head, built and ran the suite locally: go build ./... and go vet ./... clean, gofmt -l . empty, go test ./web/... ./core/... ./cli/... ./config/... all packages ok (e2e not run locally). All five claimed bug fixes were traced in the code: the CopyLogs branch on info.Config.HostConfig instead of info.Config.Tty, the origin rewrite that unlocked deleting a label job, the now-projected nextRuns, the resumed paused job, and the config stripping.

Breaking change

The "Breaking change" box is unchecked, and three changes here are breaking:

  • core.DockerProvider gains CopyContainerLogs — any external implementation of the exported interface fails to compile. The PR description already names this; the checkbox contradicts it.
  • POST /api/jobs/update returns 403 for INI- and label-owned jobs where it previously returned 200. A script that edited label jobs through the API stops working. It being a security fix does not change the classification.
  • Scheduler.UpdateJob now updates disabled jobs instead of returning ErrJobNotFound (core/scheduler.go:733) — exported method, changed behaviour.

recentRuns and /api/dashboard are purely additive, and the compression is negotiated with Vary: Accept-Encoding, so neither of those is a break.

In the CHANGELOG the update gate currently sits under ### Added; it belongs under a breaking/changed heading.

The stripJobs justification does not hold

The description says the old field-name list "named a field that no longer exists and missed ComposeJobs, so compose job definitions shipped to every reader of /api/config". The list it replaced was RunJobs, ExecJobs, ServiceJobs, LocalJobs, ComposeJobs. All five of those fields exist in cli/config.go:96-100, and they are the only map[string]*struct fields in Config, so the old code stripped exactly what the new code strips. The leak described here did not happen, and /api/config output does not change in this PR.

The refactor itself is fine — matching by shape is more robust against a collection added later, which is a good reason on its own. But it is presented as a security fix, and the same wording is in the commit message and will end up in the release notes. Please reword it as the hardening it is.

The compression is not gzip-only

gzhttp.NewWrapper defaults to zstdEnabled: true and preferZstd: true (gzhttp/compress.go:591-599 in the pinned v1.19.2), and on equal q-values zstd wins. Probed against the middleware as written in this PR:

Accept-Encoding: "gzip, deflate, br, zstd"  ->  Content-Encoding: "zstd"   41 bytes
Accept-Encoding: "gzip, deflate, br"        ->  Content-Encoding: "gzip"   66 bytes
Accept-Encoding: "gzip"                     ->  Content-Encoding: "gzip"   66 bytes

Chrome, Edge and Firefox send the first header, so they get zstd; Safari below 26 falls back to gzip, which is exactly what the wrapper is for. The behaviour is good. What does not match it is the naming and the documentation written in this same PR: web/gzip.go, gzipMiddleware, web/AGENTS.md:47, docs/packages/web.md:278 and the CHANGELOG entry all say gzip. And web/gzip_test.go:33 sends gzip, deflate, br and asserts gzip, so the encoding real browsers actually receive has no test at all.

Please fix in this PR

  1. Sign the commit. git commit -S — the commit is verified: false, reason: "unsigned" and the branch ruleset has required_signatures, so this cannot merge as it stands. DCO sign-off is present and green.
  2. Narrow the rate-limit exemption to /live and /ready. /health is auth-exempt (server.go:269) and HealthChecker.GetHealth calls runtime.ReadMemStats on every request (health.go:297), which stops the world. Exempting it from the limiter creates an unauthenticated, unthrottled endpoint that pauses the GC per call and reports version and goroutine count. Orchestrator probes are served by /live and /ready.
  3. Check the breaking-change box, move the update gate to a breaking section in the CHANGELOG, and mention the DockerProvider addition there.
  4. Reword the stripJobs rationale in the description and the commit message.
  5. Name zstd: web/AGENTS.md:47, docs/packages/web.md:278, the CHANGELOG entry, and add a web/gzip_test.go case with Accept-Encoding: gzip, deflate, br, zstd asserting Content-Encoding: zstd. Renaming gzip.go/gzipMiddleware to compress.go/compressMiddleware would keep the file name honest too.
  6. app.js:721: formatTime(e.date) goes into innerHTML unescaped, while line 549 escapes the same expression. Not exploitable today (the value is a slice of a date that already parsed as valid), but web/AGENTS.md:38 states the rule without exception.

Follow-ups, not this PR

These are all worth doing and none of them affects whether this change is correct. Happy to file them as issues.

  • No caching at all for static assets. Neither Cache-Control nor ETag nor Last-Modified is set anywhere in web/, and embedded files have no ModTime, so conditional requests never 304. app.js (55 kB), styles.css (29 kB) and pico.min.css (83 kB) are re-transferred and re-compressed on every reload — which is the cost middleware.go cites as the reason to keep counting assets against the limiter. gzhttp ships SuffixETag for exactly this.
  • CSP still needs script-src 'unsafe-inline' because of the pre-paint script in layout.html:9-37. renderUIPage (ui.go:100) already passes nil and documents itself as the seam for server-injected values — a per-request nonce there would close it.
  • Brotli for the static assets, precompressed at build time rather than per request. Belongs with the caching work.
  • MinSize(0) compresses 2-byte responses (gzhttp's default is 1024); it is why /live and /api/logout needed explicit content types. Worth a measurement.
  • stripControlChars (app.js:78) removes only the ESC byte, so a coloured log line still renders [0;31m as text. Dropping the whole CSI sequence would fix it.
  • <script src="app.js"> sits between <main> and <footer> (layout.html:70) but touches #footer-version. It works only because the access is inside a fetch().then(); defer would make that robust.
  • apiJob.Config still carries the full job definition to every /api/jobs reader, and now /api/dashboard too. Pre-existing, but a design question worth its own thread — the description's "job config no longer leaks through the API" reads wider than what changed, which is /api/config.

Commit structure

One commit over 49 files containing five bug fixes means those fixes cannot be cherry-picked. The log demux and the delete-gate hole are candidates for a patch release without the UI. Since the commit has to be rewritten to be signed anyway, that is a free moment to split it. Roughly:

fix(core): demux container logs instead of copying raw frames
fix(core): keep a paused job paused across UpdateJob
fix(web): anchor nextRuns/prevRuns on the cron entry
fix(web)!: refuse API updates to config-owned jobs
refactor(web): match job collections by shape in stripJobs
fix(web): exempt health probes from rate limiting
feat(web): negotiate response compression via zstd and gzip
feat(web): add /api/dashboard aggregate endpoint
feat(web): add recentRuns summary to job payloads
refactor(web): split the UI into templates, app.js and styles.css
feat(web): rebuild the dashboard UI
feat(web): serve UI assets from OFELIA_UI_DEV_DIR in development
docs: update API docs, web package docs and CHANGELOG

The ! plus a BREAKING CHANGE: trailer on the update-gate commit makes the break visible to the release tooling; the DockerProvider addition can share that trailer on the log-demux commit.

Assisted by claude-code:claude-opus-5 — Session

RunJob stored the raw reader from GetContainerLogs, so every line of a
non-TTY container's output carried Docker's 8-byte stream frame header
into the execution's stdout: the header bytes showed up in the web UI's
run output and in the mail/slack middlewares.

The demultiplexing already existed in the container adapter's CopyLogs,
but it was unreachable through the provider interface, and its TTY branch
tested `info.Config.HostConfig != nil` — never the TTY flag — so a real
TTY container took the demux path and a non-TTY container took the raw
copy. Both are fixed: CopyLogs branches on Config.Tty, and DockerProvider
gains CopyContainerLogs so RunJob can write straight into the execution's
stdout and stderr streams.

BREAKING CHANGE: core.DockerProvider gains CopyContainerLogs. External
implementations of the exported interface must add the method to compile.
Users of the provided implementations are unaffected.

Signed-off-by: Kamil Pešek <pesek.kamil@seznam.cz>
UpdateJob refused disabled jobs with ErrJobNotFound, which pushed the web
updateJobHandler into its RemoveJob+AddJob fallback. RemoveJob drops the
disabledNames entry and files the old job under Removed, so editing a
paused job silently resumed it and left a phantom row in the Removed tab
for the rest of the process's life.

Disabled jobs are now updated in place. go-cron replaces the entry on
update, so the pause is re-asserted afterwards rather than assumed to
carry over; pausing an already-paused entry is a no-op.

BREAKING CHANGE: Scheduler.UpdateJob no longer returns ErrJobNotFound for
a disabled job. Callers that used the error to detect "not scheduled"
must check the disabled state explicitly.

Signed-off-by: Kamil Pešek <pesek.kamil@seznam.cz>
computeRunTimes projected the schedule from the poll's `now`. For an
interval schedule (@every 30s) go-cron's Next(t) is t+interval with no
anchor, so every 5s poll reported a next run 30s out and the countdown in
the UI never moved toward zero.

The cron entry already carries the scheduled Next and Prev. Use them as
the first element and project the remaining ones from there; fall back to
projecting from `now` only when the entry has no time yet.

Signed-off-by: Kamil Pešek <pesek.kamil@seznam.cz>
POST /api/jobs/update accepted jobs that came from the INI file or from
Docker labels. The update overrode the job in memory until the next
config sync — and rewrote its origin to api/web, after which the delete
gate no longer recognized the job as config-owned. Editing a label job
was therefore enough to unlock deleting it, past the gate added for netresearch#593.

Updates on such jobs now answer 403 with a message naming the source,
mirroring the delete gate.

BREAKING CHANGE: POST /api/jobs/update returns 403 Forbidden for jobs
defined in INI config or Docker labels, where it previously returned 200.
Scripts that edited config-owned jobs through the API must change the
source config, or use POST /api/jobs/disable to suppress the job.

Signed-off-by: Kamil Pešek <pesek.kamil@seznam.cz>
stripJobs zeroed the job collections of the config payload by field name.
A hand-maintained name list drifts: this one had already picked up a
field that no longer exists, and a collection added to cli.Config later
would ship every job's full definition — commands, env, credentials — to
every reader of /api/config.

Match on shape instead: map[string]*struct is what every job collection
in cli.Config is, and no other field there has that shape, so a new
collection is stripped the day it is added. The stripped output is
unchanged for the current config: the five names in the old list and the
five fields matched by shape are the same set.

Signed-off-by: Kamil Pešek <pesek.kamil@seznam.cz>
The per-IP limiter counted every request, /live and /ready included. A
probe answered 429 reads as unhealthy, so a busy dashboard tab could get
the daemon restarted by its own orchestrator. Both probes are cheap — a
constant string and a scheduler-state lookup — so they bypass the limiter
now.

/health and /healthz stay counted. They are token-free like the probes,
but GetHealth calls runtime.ReadMemStats on every request, which stops
the world, and answers with the version and the goroutine count:
exempting them would leave an unauthenticated, unthrottled endpoint that
pauses the GC once per call. Orchestrators poll /live and /ready.

The existing limiter tests move to /api/test paths so the exempt set is
never crossed by accident.

Signed-off-by: Kamil Pešek <pesek.kamil@seznam.cz>
List views want a per-job outcome history — a sparkline of the last runs,
and last/avg/max durations — but the only source was the per-job history
endpoint, one request per row.

apiJob gains recentRuns: at most the ten newest executions, oldest first,
each reduced to date, duration, failed and skipped. Additive and omitted
for jobs that keep no history, so existing readers are unaffected.

Signed-off-by: Kamil Pešek <pesek.kamil@seznam.cz>
A polling client needs jobs, disabled, removed and config together, plus
the open job's history: four to five requests per tick, ~60/min per
browser tab, which exhausted the 100-requests-per-minute per-IP limit
with two dashboard tabs open and 429'd everything including the static
assets.

GET /api/dashboard answers all of it in one response, optionally with a
job's runs via ?history=<name>, so a tick costs one request and the
sections come from a single moment in time. The per-resource endpoints
are untouched — they are the documented public API that scripts and
monitoring consume; this one is additive, for polling consumers.

The history conversion moves into buildAPIHistory, shared by the
per-job endpoint and the aggregate.

Signed-off-by: Kamil Pešek <pesek.kamil@seznam.cz>
static/ui/index.html had grown to 830 lines of markup, CSS and JavaScript
in one file, which made every UI change a merge-conflict magnet and gave
the browser no way to cache the parts that rarely change.

The file is split with no behavior change: styles.css, app.js, and
templates/layout.html plus one partial per tab, assembled server-side by
uiHandler at GET /. Templates are parsed once from the embedded FS at
startup, so a broken template fails the daemon instead of every request.
Static assets keep going through http.FileServer; /templates/* is not
served — the sources are render inputs, not assets, and the render seam
is where server-injected values will land. /index.html still renders the
page so old bookmarks keep working.

Still vanilla CSS and JS with no build step and no new dependency.

Signed-off-by: Kamil Pešek <pesek.kamil@seznam.cz>
Every UI edit meant rebuilding the binary to refresh the embedded
snapshot, which is a slow loop for CSS and template work.

When OFELIA_UI_DEV_DIR names a directory, assets are read from it and the
page templates are re-parsed on every request, so an edit shows up on the
next browser reload. A template parse error answers 500 with the parse
error in the body, so the developer sees what broke without reading the
daemon log. Unset in production: the embedded assets stay the default and
are still parsed once at startup.

Signed-off-by: Kamil Pešek <pesek.kamil@seznam.cz>
The split page still showed the same bare tables: no way to find a job
among fifty, no sense of whether a job is healthy or slowing down, and
expanding a run's output reshuffled the table's columns.

The dashboard is rebuilt on top of the template split:

- stat cards for active, failing (a click filters the table) and the
  next run with a countdown, computed from the poll already made
- job search and column sorting, both rendering from cached data, so a
  keystroke or a sort click costs no request
- a result sparkline per job and last/avg/max durations, from the
  recentRuns payload
- history in a modal dialog; run output in a full-width subrow that
  survives the 5s refresh, keyed by execution timestamp and scoped to
  the job on screen
- origin badges, disabled edit/delete on config-owned jobs with a
  tooltip naming the source, delete confirmation, and toasts carrying
  the server's message instead of silent failures
- teal-on-graphite theming, SVG action icons, striped tables, a hidden
  tab that stops polling, and a footer version from /health

The page and the stylesheet pass the W3C Nu validator.

Signed-off-by: Kamil Pešek <pesek.kamil@seznam.cz>
The UI ships 167 kB of assets and polls a 5.8 kB payload every 5 seconds,
all of it uncompressed text.

Responses now go through gzhttp as the innermost middleware. The wrapper
enables zstd next to gzip and prefers it at equal q-values, so Chrome,
Edge and Firefox — which send "gzip, deflate, br, zstd" — receive zstd,
and clients without zstd fall back to gzip. First page load drops from
~140 kB to ~28 kB, a dashboard poll from 5.8 kB to 1.6 kB. Accept-Encoding
qvalues, bodiless statuses, ranged requests and content sniffing are the
library's problem, not ours.

The middleware chain moves into wrapMiddleware so the two construction
sites cannot drift apart. Handlers that call WriteHeader before their
first body write (LivenessHandler, logoutHandler) now set Content-Type
explicitly: the wrapper can only sniff a missing type on the first write,
and that sniff would otherwise run on the compressed bytes and answer
application/x-gzip.

Signed-off-by: Kamil Pešek <pesek.kamil@seznam.cz>
- docs/API.md: /api/dashboard, the recentRuns field, the 403 on updating
  a config-owned job
- docs/packages/web.md: what the rate limiter counts and what it exempts,
  and that compression negotiates zstd or gzip
- docs/CONFIGURATION.md: OFELIA_UI_DEV_DIR
- web/AGENTS.md: the UI is templates plus assets now, the polling and
  rate-limit budget, the CSP notes for external app.js
- AGENTS.md: go:embed walks a matched directory recursively, so ui/*
  covers ui/templates/ — the previous note claimed the opposite
- CHANGELOG: the Unreleased entries for all of the above, with the
  breaking API and interface changes called out under Changed

Signed-off-by: Kamil Pešek <pesek.kamil@seznam.cz>
@KamilPesek
KamilPesek force-pushed the feat/dashboard-redesign branch from 5d88df8 to 073eab8 Compare August 24, 2026 11:43
@sonarqubecloud

Copy link
Copy Markdown

@KamilPesek

Copy link
Copy Markdown
Author

Thanks — all six points are addressed.

  1. All 13 commits signed and signed-off (GitHub reports them verified).
  2. Rate-limit exemption narrowed to /live and /ready; /health and /healthz are
    counted again, with the ReadMemStats reasoning in the doc comment.
    TestRateLimiterScope asserts both directions.
  3. Breaking change box checked; the update gate moved to ### Changed as BREAKING,
    together with the DockerProvider addition and Scheduler.UpdateJob. All three
    carry BREAKING CHANGE: trailers.
  4. stripJobs reworded in the commit message and the description — hardening, not a
    leak: the old list covered exactly today's five collections, /api/config output
    is unchanged.
  5. zstd named everywhere: compress.go / compressMiddleware, AGENTS.md, package
    docs, CHANGELOG, code comments. New TestZstdCompression pins zstd for
    gzip, deflate, br, zstd and the gzip fallback without it.
  6. app.js:721 escaped — and line 819 had the same unescaped formatTime.

golangci-lint is red with the same seven findings it reports on main — none from
this branch; happy to send a separate PR for those.

@CybotTM CybotTM left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for this contribution — the commit hygiene (four standalone fixes, hardening, API, then UI) made a 50-file PR genuinely reviewable, and the security posture is solid: we audited all 16 innerHTML sinks (job output included) and every dynamic value goes through escapeHtml/textContent; /api/dashboard is covered by the existing auth gate and route-expectation test; the BREACH analysis in web/AGENTS.md checks out. Full go test ./... passes on your head. Verdict: request changes — all mechanical:

  1. Rebase onto current main and resolve the CHANGELOG.md conflict — per git merge-tree it is the only conflicting file. This also clears six of the seven golangci-lint findings plus the gofumpt one: main fixed exactly those in a9f9e8b (#800), so the PR body's "needs a separate fix on main" paragraph is stale — please refresh it.
  2. One lint finding originates on this branch: web/compress_test.go:15github.com/klauspost/compress/zstd needs its own gci import group (blank line before the netresearch/ofelia group).
  3. docs/openapi.yaml is untouched while docs/API.md grew by 110 lines — please add /api/dashboard, the recentRuns field, and the 403 on POST /api/jobs/update so spec and docs don't diverge further.
  4. Screenshots, please (light + dark) — a visual rebuild can't be assessed from the diff.
  5. Optional but valued: wrap the top-level localStorage reads (app.js:45, 243, 269; layout.html:30–32) in try/catch — with site data blocked the current code throws before first render and the dashboard stays blank.

Noted for us maintainers, not blocking you: whether to cherry-pick your four fix commits (937a2ce, 3684990, 52c7c62, f01615b) into a patch release first; sign-off on the three properly-declared breaking changes; and a follow-up issue for the pre-existing auth-before-rate-limit ordering your refactor made visible (and, by centralizing the chain in wrapMiddleware, easier to fix — thanks for that).

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

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation frontend tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants