Skip to content

feat(ui): Add static asset caching and pre-compressed serving#5102

Draft
SoloJacobs wants to merge 10 commits intoprometheus:mainfrom
SoloJacobs:example-ui
Draft

feat(ui): Add static asset caching and pre-compressed serving#5102
SoloJacobs wants to merge 10 commits intoprometheus:mainfrom
SoloJacobs:example-ui

Conversation

@SoloJacobs
Copy link
Copy Markdown
Contributor

@SoloJacobs SoloJacobs commented Mar 22, 2026

This PR uses vite to build the Elm ui. This has a number of consequences, which are detailed in the commit messages.

This is an overview over the files provided by Alertmanager v0.31.1 via the ui, which might be handy for debugging:

URL Content-Type
/favicon.ico image/vnd.microsoft.icon
/ text/html; charset=utf-8
/script.js text/javascript; charset=utf-8
/lib/bootstrap-4.6.2-dist/css/bootstrap.min.css text/css; charset=utf-8
/lib/bootstrap-4.6.2-dist/css/bootstrap.min.css.map text/plain; charset=utf-8
/lib/elm-datepicker/css/elm-datepicker.css text/css; charset=utf-8
/lib/font-awesome-4.7.0/css/font-awesome.css text/css; charset=utf-8
/lib/font-awesome-4.7.0/css/font-awesome.min.css text/css; charset=utf-8
/lib/font-awesome-4.7.0/fonts/FontAwesome.otf application/vnd.oasis.opendocument.formula-template
/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.eot application/vnd.ms-fontobject
/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.svg image/svg+xml
/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf font/ttf
/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.woff font/woff
/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2 font/woff2

Pull Request Checklist

  • Is this a new feature?
    • I have added tests that test the new feature's functionality
      (Compression is tested and we are also testing the Cache-Control headers. However, we don't have a fulle end-to-end test of which requests a browser would actually send.)
  • Does this change affect performance?
    • I have provided benchmarks comparison that shows performance is improved or is not degraded
      (I checked the binary sizes, and vite will show the web artifacts.)
  • Is this a breaking change?
    • Yes, you have to now locally build the ui. This can be done via a work space:
    go work use /home/solomonjacobs/git/alertmanager
  • I have added/updated the required documentation
  • I have signed-off my commits
  • I will follow best practices for contributing to this project

Which user-facing changes does this PR introduce?

* Cache static assets with Cache-Control
* Serve pre-compressed assets (gzip + brotli)
* `go get github.com/prometheus/alertmanager/ui` is no longer possible

Summary by CodeRabbit

  • New Features

    • Frontend served from an embedded build that prefers Brotli/gzip precompressed assets for faster loads.
    • Operational endpoints (metrics, health, reload, debug/pprof) now exposed via a dedicated operations route.
  • Chores

    • Frontend migrated to a Vite/npm build and lockfile-driven installs; CI updated to use project Node version and improved npm caching.
    • Local Docker-based dev/build flows removed; templating/email build switched to local Node.js.
    • UI assets and icon stylesheet handling simplified; startup now uses a JS entry that restores settings from browser storage.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replace Docker-based frontend/tooling with npm/Vite local builds; remove bundled Font Awesome CSS; embed and serve prebuilt app/dist with encoding-aware content negotiation and caching; extract operational/debug endpoints into a new weboperations package; update CI to use root .nvmrc and npm caching; adjust Makefile and main wiring.

Changes

Cohort / File(s) Summary
CI Workflow Configuration
/.github/workflows/ci.yml, /.github/workflows/ui-ci.yml
Use ./.nvmrc for Node setup; enable npm caching and add cache-dependency paths; UI CI switches install to npm ci and includes .nvmrc in cache key.
Top-level Makefile
Makefile
Add .PHONY: ui-elm; make build, lint, test, and assets depend on ui-elm; replace direct script.js generation rule with $(FRONTEND_DIR)/src/Data: api/v2/openapi.yaml delegation.
Template/email tooling
template/Makefile, template/package.json, template/.gitignore
Remove Docker-based email-image flow; add template/package.json with juice devDep; add node_modules target using npm ci; run node ./inline-css to produce email.tmpl; ignore node_modules/.
UI Docker/tooling removal
ui/Dockerfile, ui/app/Makefile, ui/app/CONTRIBUTING.md
Delete UI Dockerfile and Docker wrappers; Makefile uses npm ci/npx and npm run build with .build_stamp; update docs to prefer npm workflow and make test.
Frontend package & gitignore
ui/app/package.json, ui/app/.gitignore
Add Vite/Elm/bootstrap/font-awesome deps and build script; ignore node_modules/ and /.build_stamp.
Removed bundled assets
ui/app/lib/.../font-awesome.css, ui/app/lib/.../font-awesome.min.css
Delete bundled Font Awesome CSS files (remove all .fa* rules and font-face glyph mappings).
Frontend entry & build config
ui/app/index.html, ui/app/src/main.js, ui/app/vite.config.mjs
Replace inline Elm bootstrap with module entry (/src/main.js); add JS entry that imports CSS, initializes Elm with flags and persistence ports; add Vite config with Elm plugin and gzip/br compression generation.
Elm app simplification
ui/app/src/Types.elm, ui/app/src/Updates.elm, ui/app/src/Views.elm, ui/app/src/Main.elm
Remove runtime CSS-loading model fields/messages/handlers; simplify Model init and Views to no longer fetch/inject CSS at runtime.
Web asset serving
ui/web.go, ui/web_test.go
Serve embedded app/dist; add content-type mapping, encoding-aware selection (prefer .br then .gz, in-memory decompress when identity requested), Vary: Accept-Encoding, immutable caching for /assets/*, and updated tests validating encodings and cache headers; Register signature simplified to Register(r *route.Router).
Web operations package
weboperations/weboperations.go, weboperations/weboperations_test.go
New weboperations package with Register(r *route.Router, reloadCh chan<- chan error) registering /metrics, POST /-/reload (per-request chan), /-/healthy//-/ready (GET/HEAD), and proxy `GET
Main wiring & cmd changes
cmd/alertmanager/main.go
UI registration call changed to ui.Register(router) and operational endpoints wired by weboperations.Register(router, webReload) afterwards.
Docs
README.md
Replace legacy go install GOPATH instructions with source build instructions requiring Go and Node: clone repo, make build, then run ./alertmanager --config.file=<your_file>.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Client (Browser)
    participant Server as Alertmanager (ui/web.go)
    participant Assets as Embedded app/dist
    participant Decompress as Decompressor

    Browser->>Server: GET /assets/app.js\nAccept-Encoding: br, gzip
    activate Server
    Server->>Assets: Lookup /assets/app.js
    Assets-->>Server: file exists
    alt Brotli variant exists
        Server->>Assets: Read app.js.br
        Assets-->>Server: br bytes
        Server-->>Browser: 200\nContent-Encoding: br\nVary: Accept-Encoding
    else Gzip variant exists
        Server->>Assets: Read app.js.gz
        Assets-->>Server: gz bytes
        Server-->>Browser: 200\nContent-Encoding: gzip\nVary: Accept-Encoding
    else Only gz stored but client requests identity
        Server->>Assets: Read app.js.gz
        Assets-->>Server: gz bytes
        Server->>Decompress: decompress gz in-memory
        Decompress-->>Server: uncompressed bytes
        Server-->>Browser: 200\n[uncompressed bytes]
    end
    deactivate Server
Loading
sequenceDiagram
    participant User as Browser
    participant Index as index.html
    participant MainJS as /src/main.js
    participant Elm as Elm.Main
    participant Storage as localStorage

    User->>Index: Load /
    Index->>MainJS: load module
    MainJS->>Storage: read firstDayOfWeek, groupExpandAll, defaultCreator
    Storage-->>MainJS: return values
    MainJS->>Elm: init(flags)
    Elm-->>MainJS: subscribe ports
    Elm->>MainJS: persistFirstDayOfWeek(value)
    MainJS->>Storage: write firstDayOfWeek
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I hopped from Docker to a Vite-filled glen,
npm trimmed my bundles, Brotli sped the den,
Fonts unstitched, the dist now rides the binary,
Webops hum health and reloads, bright and merry,
I nibble code and watch the bytes run free!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat(ui): Add static asset caching and pre-compressed serving' directly and specifically describes the main changes: caching static assets with Cache-Control headers and serving pre-compressed assets (gzip + brotli). It matches the primary objectives of the PR.
Description check ✅ Passed The PR description addresses most template requirements: it includes a detailed summary of changes, mentions tests were added for compression and Cache-Control behavior, documents breaking changes (local UI build now required, go get no longer works), provides updated documentation, confirms signed-off commits, and includes release notes. However, the description lacks explicit mention of issue numbers in the 'Fixes #' format and doesn't specify which template sections were completed vs skipped.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@SoloJacobs SoloJacobs force-pushed the example-ui branch 9 times, most recently from feb0289 to 92c42fb Compare March 22, 2026 11:29
@SoloJacobs SoloJacobs marked this pull request as ready for review March 22, 2026 11:46
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (1)
ui/app/CONTRIBUTING.md (1)

11-21: Clarify command execution directory for new contributors.

At Line 14 and Line 20, explicitly stating these commands should run from ui/app would reduce ambiguity.

📝 Suggested doc tweak
 To set up your environment and install all dependencies, run:
 
 ```sh
+# from ui/app
 npm ci

To run the tests:

+# from ui/app
make test
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @ui/app/CONTRIBUTING.md around lines 11 - 21, Update the CONTRIBUTING.md so
the two command examples explicitly state they must be executed from the ui/app
directory: add a short note like "from ui/app" immediately above the npm ci
example and the make test example (the sections showing the npm ci and make test commands) so contributors know the correct working directory when running
those commands.


</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:

  • Around line 60-67: The workflow calls npm in the container (step id
    cache-paths) before ensuring Node/npm exists; either ensure the container image
    includes Node/npm or explicitly install Node/npm inside the job before the
    cache-paths step so npm config get cache succeeds; update the job to add an
    installation step (or switch the container image) that provides Node/npm, and
    keep the actions/cache@v4 step and path (${{ steps.cache-paths.outputs.npm-cache
    }}) after that installation so the cache lookup is reliable in the container.

In @Makefile:

  • Around line 30-46: The ui Elm generated Data files are only a prerequisite of
    the assets target, so build/test/lint that depend on the ui-elm target can run
    without src/Data present; update the Makefile so the ui-elm target depends on
    $(FRONTEND_DIR)/src/Data (i.e., change the ui-elm rule to "ui-elm:
    $(FRONTEND_DIR)/src/Data" or add that prerequisite) so that invoking
    build/test/lint will ensure the OpenAPI-generated Data modules exist before
    running "cd $(FRONTEND_DIR) &amp;&amp; $(MAKE) build" (you can then remove the redundant
    dependency from the assets rule if desired).

In @template/Makefile:

  • Around line 4-6: The Makefile target email.tmpl lists a prerequisite
    inline-css.js but the recipe calls node ./inline-css (missing the .js
    extension); update the recipe for the email.tmpl target to explicitly run the
    same file named in the prerequisite (use node ./inline-css.js) or alternatively
    change the prerequisite to match the executed script name—ensure the symbols
    inline-css.js (prerequisite) and the recipe invocation (node ./inline-css.js)
    are consistent.

In @ui/app/Makefile:

  • Around line 41-44: The .build_stamp rule currently only depends on
    $(ELM_FILES), index.html, and vite.config.mjs, so changes to the JS/CSS entry or
    package config won't trigger rebuilds; update the .build_stamp dependency list
    to include package.json, src/main.js (or your actual JS entry), and
    src/assets/elm-datepicker.css (or other asset files) so that changes to those
    files cause npm run build to run and .build_stamp to be updated; modify the
    Makefile rule for .build_stamp to add these filenames alongside $(ELM_FILES),
    index.html, and vite.config.mjs.
  • Around line 48-54: The Makefile recipe uses DOCKER_RUN_CURRENT_USER to start a
    container named openapi-gen but lacks failure handling, allowing subsequent
    docker cp operations to silently copy from an old container; update the recipe
    (the block that runs openapi-gen, trap, and docker cp) to abort on error by
    enabling set -e (or chain commands with &&) immediately before the docker run,
    or check the docker run exit status and only proceed if it succeeded, and ensure
    the temporary container is removed on both success and failure (keep the trap
    but only run docker cp when the run returned success) so openapi-gen failures
    cannot cause stale-copy corruption.

In @ui/app/vite.config.mjs:

  • Around line 5-19: The Vite config uses defineConfig(exported object) but lacks
    a relative base, causing built asset URLs to be absolute; update the object
    passed to defineConfig (the export default defineConfig call) to include base:
    "./" at top level so assets are emitted as relative URLs and resolve correctly
    when ui.Register mounts the app on a prefixed path; ensure this change is
    applied alongside the existing plugins (elm(), compression(...)) in the
    vite.config module.

In @ui/web_test.go:

  • Around line 152-159: The helper checkCachingHeaders currently treats two
    different Cache-Control policies as interchangeable and never asserts Vary, so
    update checkCachingHeaders to accept an explicit expectedCacheControl string and
    a wantVary bool, assert that res.Header().Get("Cache-Control") equals
    expectedCacheControl (not Contains), and when wantVary is true assert
    res.Header().Get("Vary") == "Accept-Encoding" (or empty when false); then update
    callers (tests that exercise "/", "/favicon.ico", and "/assets/*") to pass
    "no-cache, no-store, must-revalidate" for root/favicon, "public,
    max-age=31536000, immutable" for assets, and set wantVary appropriately based on
    whether the response is compressed.

In @ui/web.go:

  • Around line 185-187: The handler for r.Get("/assets/*path") sets a long-lived
    Cache-Control header before calling serve, which causes 404/500 responses to be
    cached; change the flow so the Cache-Control header is only added for successful
    asset responses—either move the header assignment into serve and set it only
    when the file is found and served (i.e., when you would write a 200 OK), or have
    the handler check existence (using the same http.FileSystem/Open or os.Stat
    logic serve uses) and only set Cache-Control after confirming the file exists;
    ensure the 404 and error paths (the logic in serve that writes 404/500) do not
    set the immutable max-age header.

In @weboperations/weboperations.go:

  • Around line 31-38: The POST /-/reload handler can hang; change the local reply
    channel to a buffered channel (errc := make(chan error, 1)), remove the defer
    close(errc) (handler must not close a channel it doesn't own), and replace the
    direct send and receive with context-aware selects that also listen on
    req.Context().Done(); specifically, use select { case reloadCh <- errc: ... case
    <-req.Context().Done(): return } for queuing the reload and another select {
    case err := <-errc: ... case <-req.Context().Done(): return } for waiting for
    the reply so both enqueue and reply are cancellation-safe.

Nitpick comments:
In @ui/app/CONTRIBUTING.md:

  • Around line 11-21: Update the CONTRIBUTING.md so the two command examples
    explicitly state they must be executed from the ui/app directory: add a short
    note like "from ui/app" immediately above the npm ci example and the make test example (the sections showing the npm ci and make test commands) so
    contributors know the correct working directory when running those commands.

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `77ceba66-ab1f-485f-8bf8-870d44f0e778`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5f3188d4ef00ec05a1f75b8f1b0ddbbd7730ead6 and 92c42fbea6a5c61cd019546728e31a98e00169bb.

</details>

<details>
<summary>⛔ Files ignored due to path filters (10)</summary>

* `template/package-lock.json` is excluded by `!**/package-lock.json`
* `ui/app/lib/bootstrap-4.6.2-dist/css/bootstrap.min.css.map` is excluded by `!**/*.map`
* `ui/app/lib/font-awesome-4.7.0/fonts/FontAwesome.otf` is excluded by `!**/*.otf`
* `ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.eot` is excluded by `!**/*.eot`
* `ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.svg` is excluded by `!**/*.svg`
* `ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf` is excluded by `!**/*.ttf`
* `ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.woff` is excluded by `!**/*.woff`
* `ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2` is excluded by `!**/*.woff2`
* `ui/app/package-lock.json` is excluded by `!**/package-lock.json`
* `ui/app/public/favicon.ico` is excluded by `!**/*.ico`

</details>

<details>
<summary>📒 Files selected for processing (31)</summary>

* `.github/workflows/ci.yml`
* `.github/workflows/ui-ci.yml`
* `.nvmrc`
* `Makefile`
* `README.md`
* `cmd/alertmanager/main.go`
* `template/.gitignore`
* `template/Dockerfile`
* `template/Makefile`
* `template/package.json`
* `ui/Dockerfile`
* `ui/app/.gitignore`
* `ui/app/CONTRIBUTING.md`
* `ui/app/Makefile`
* `ui/app/index.html`
* `ui/app/lib/bootstrap-4.6.2-dist/css/bootstrap.min.css`
* `ui/app/lib/font-awesome-4.7.0/css/font-awesome.css`
* `ui/app/lib/font-awesome-4.7.0/css/font-awesome.min.css`
* `ui/app/package.json`
* `ui/app/script.js`
* `ui/app/src/Main.elm`
* `ui/app/src/Types.elm`
* `ui/app/src/Updates.elm`
* `ui/app/src/Views.elm`
* `ui/app/src/assets/elm-datepicker.css`
* `ui/app/src/main.js`
* `ui/app/vite.config.mjs`
* `ui/web.go`
* `ui/web_test.go`
* `weboperations/weboperations.go`
* `weboperations/weboperations_test.go`

</details>

<details>
<summary>💤 Files with no reviewable changes (7)</summary>

* ui/app/src/Main.elm
* ui/app/src/Updates.elm
* ui/Dockerfile
* template/Dockerfile
* ui/app/lib/font-awesome-4.7.0/css/font-awesome.min.css
* ui/app/src/Types.elm
* ui/app/lib/font-awesome-4.7.0/css/font-awesome.css

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
ui/app/Makefile (1)

12-12: Add PHONY declarations for all and test targets.

The static analysis tool correctly flags that all (Line 12) and test (Line 24) should be declared as .PHONY targets since they don't produce files with those names. This prevents unexpected behavior if files named all or test are ever created.

Suggested fix
-.PHONY: build
+.PHONY: all build test
 build: .build_stamp

Also applies to: 38-39, 58-58

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/Makefile` at line 12, Add a .PHONY declaration for the make targets
that do not produce files (at minimum include all and test) so make won’t treat
those names as real files; update the Makefile to add a line like “.PHONY: all
test” (and include any other non-file-producing targets referenced elsewhere in
the file corresponding to the 38-39 and 58-58 occurrences) placed near the top
with other meta-targets.
ui/web.go (1)

139-139: Type assertion f.(io.ReadSeeker) may panic if the embedded FS changes.

While embed.FS files currently implement io.ReadSeeker, this is an unchecked type assertion. A defensive check would prevent a runtime panic if Go's embed implementation changes in the future.

Defensive type assertion example
-http.ServeContent(w, req, filePath, time.Time{}, f.(io.ReadSeeker))
+rs, ok := f.(io.ReadSeeker)
+if !ok {
+    http.Error(w, "internal error: file not seekable", http.StatusInternalServerError)
+    return
+}
+http.ServeContent(w, req, filePath, time.Time{}, rs)

Also applies to: 149-149, 172-172

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/web.go` at line 139, The direct type assertion f.(io.ReadSeeker) can
panic; change to a safe check and fallback: attempt a type assertion (rs, ok :=
f.(io.ReadSeeker)) and call http.ServeContent with rs if ok, otherwise read the
file into memory (io.ReadAll(f)), wrap the bytes with bytes.NewReader(...)
(which implements io.ReadSeeker) and pass that to http.ServeContent; ensure f is
closed after open/read. Apply the same defensive pattern to the other
occurrences referenced (the other http.ServeContent calls).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@ui/app/Makefile`:
- Line 12: Add a .PHONY declaration for the make targets that do not produce
files (at minimum include all and test) so make won’t treat those names as real
files; update the Makefile to add a line like “.PHONY: all test” (and include
any other non-file-producing targets referenced elsewhere in the file
corresponding to the 38-39 and 58-58 occurrences) placed near the top with other
meta-targets.

In `@ui/web.go`:
- Line 139: The direct type assertion f.(io.ReadSeeker) can panic; change to a
safe check and fallback: attempt a type assertion (rs, ok := f.(io.ReadSeeker))
and call http.ServeContent with rs if ok, otherwise read the file into memory
(io.ReadAll(f)), wrap the bytes with bytes.NewReader(...) (which implements
io.ReadSeeker) and pass that to http.ServeContent; ensure f is closed after
open/read. Apply the same defensive pattern to the other occurrences referenced
(the other http.ServeContent calls).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5ecf2950-c3cf-4bf6-8ed8-3160292886d6

📥 Commits

Reviewing files that changed from the base of the PR and between 92c42fb and 2f4c59b.

⛔ Files ignored due to path filters (9)
  • ui/app/lib/bootstrap-4.6.2-dist/css/bootstrap.min.css.map is excluded by !**/*.map
  • ui/app/lib/font-awesome-4.7.0/fonts/FontAwesome.otf is excluded by !**/*.otf
  • ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.eot is excluded by !**/*.eot
  • ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.svg is excluded by !**/*.svg
  • ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf is excluded by !**/*.ttf
  • ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.woff is excluded by !**/*.woff
  • ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2 is excluded by !**/*.woff2
  • ui/app/package-lock.json is excluded by !**/package-lock.json
  • ui/app/public/favicon.ico is excluded by !**/*.ico
📒 Files selected for processing (25)
  • .github/workflows/ci.yml
  • .github/workflows/ui-ci.yml
  • .nvmrc
  • Makefile
  • README.md
  • cmd/alertmanager/main.go
  • ui/app/.gitignore
  • ui/app/Makefile
  • ui/app/index.html
  • ui/app/lib/bootstrap-4.6.2-dist/css/bootstrap.min.css
  • ui/app/lib/font-awesome-4.7.0/css/font-awesome.css
  • ui/app/lib/font-awesome-4.7.0/css/font-awesome.min.css
  • ui/app/package.json
  • ui/app/script.js
  • ui/app/src/Main.elm
  • ui/app/src/Types.elm
  • ui/app/src/Updates.elm
  • ui/app/src/Views.elm
  • ui/app/src/assets/elm-datepicker.css
  • ui/app/src/main.js
  • ui/app/vite.config.mjs
  • ui/web.go
  • ui/web_test.go
  • weboperations/weboperations.go
  • weboperations/weboperations_test.go
💤 Files with no reviewable changes (5)
  • ui/app/src/Updates.elm
  • ui/app/src/Main.elm
  • ui/app/src/Types.elm
  • ui/app/lib/font-awesome-4.7.0/css/font-awesome.min.css
  • ui/app/lib/font-awesome-4.7.0/css/font-awesome.css
✅ Files skipped from review due to trivial changes (3)
  • ui/app/.gitignore
  • ui/app/package.json
  • weboperations/weboperations_test.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • .github/workflows/ui-ci.yml
  • cmd/alertmanager/main.go
  • README.md
  • ui/app/vite.config.mjs
  • .github/workflows/ci.yml
  • ui/app/src/main.js
  • ui/web_test.go
  • Makefile

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
ui/web_test.go (1)

85-123: Add Content-Type assertions while you're already walking every embedded file.

This suite is the safety net for the new hard-coded fileTypes map, but it only checks status, caching, and content encoding. A MIME typo in ui/web.go would still return 200 here and slip through.

Also applies to: 174-215

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/web_test.go` around lines 85 - 123, walkEmbeddedFiles currently builds
expected encodings per URL but doesn't assert Content-Type; update it (and the
test loop that uses its result) to include and check the expected MIME for each
file: while iterating fileTypes (via fileTypes[ext] and fileType.varyEncoding)
capture fileType.mime and either return it alongside the encodings from
walkEmbeddedFiles or assert it immediately; then in the request assertions (the
test block that iterates the returned URLs/encodings around lines 174-215)
verify the response header "Content-Type" equals fileType.mime for each
url/encoding (use the same fileTypes lookup by extension to derive expected
MIME). Ensure references to encGzip, encBrotli, encNone remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Makefile`:
- Around line 39-46: The assets target currently lists the phony ui-elm
prerequisite first which runs the Vite build against checked-in ui/app/src/Data
before $(FRONTEND_DIR)/src/Data is regenerated; change the prerequisite order so
the generated artifact $(FRONTEND_DIR)/src/Data (with its dependency on
api/v2/openapi.yaml) is listed before ui-elm (and keep template/email.tmpl),
ensuring Make updates src/Data first and only then runs the phony ui-elm build;
update the assets rule to depend on $(FRONTEND_DIR)/src/Data before ui-elm and
leave the existing $(FRONTEND_DIR)/src/Data: rule intact.

In `@ui/app/Makefile`:
- Around line 35-36: The Makefile target dev-server currently runs Elm Reactor
which cannot handle bundler-dependent imports in main.js; update the dev-server
recipe to start Vite instead (e.g., run the project's Vite dev command such as
"npx vite" or the equivalent npm/pnpm/yarn script like "npm run dev") so CSS and
npm package imports and the Elm build are served by the bundler; replace the
"npx elm reactor" invocation in the dev-server rule with the Vite start command
and ensure any package manager script name used matches the project's
package.json.

In `@ui/web.go`:
- Around line 64-96: selectEncoding currently conflates "no encoding (identity)
allowed" with "identity explicitly rejected"; add tracking for identity by
introducing an identity variable (initialized to effectUnseen) like
brotli/gzip/wildcard and set it when encountering "identity" (or
"identity;q=..."). At the end, if identity==effectReject or
(wildcard==effectReject && identity==effectUnseen) return a distinct sentinel
(e.g., encNotAcceptable or encRejectIdentity) instead of encNone; update callers
that use selectEncoding (and the encoding type) to handle this new sentinel and
respond with a 406 or similar. Ensure you reference selectEncoding, encoding,
encNone, brotli/gzip/wildcard, and the new identity and encNotAcceptable symbols
in your changes.
- Around line 123-127: The handler currently treats unknown file extensions by
returning http.Error with StatusInternalServerError; change this so that when
the extension lookup (ext := strings.ToLower(path.Ext(filePath)); fileType, ok
:= fileTypes[ext]) fails (ok == false) you call http.NotFound(w, r) and return
instead of sending an internal server error, so unknown/unsupported asset
requests (e.g. /assets/foo.txt) fall through as a 404 like other missing assets;
locate this logic around the ext/fileTypes check in the request handler (the
block using ext, fileTypes and http.Error) and replace the error response with
http.NotFound.
- Around line 30-31: The embed directive "//go:embed app/dist" referenced by the
variable "asset embed.FS" points to a non-existent folder and breaks
compilation; fix it by either committing a placeholder assets directory with the
expected files (so the embed pattern has matches) or change the embed directive
to reference an existing path/pattern that is present in the repository (or gate
the embed behind a build tag and provide a developer-friendly stub) so that the
variable "asset" is only built when matching files exist.

---

Nitpick comments:
In `@ui/web_test.go`:
- Around line 85-123: walkEmbeddedFiles currently builds expected encodings per
URL but doesn't assert Content-Type; update it (and the test loop that uses its
result) to include and check the expected MIME for each file: while iterating
fileTypes (via fileTypes[ext] and fileType.varyEncoding) capture fileType.mime
and either return it alongside the encodings from walkEmbeddedFiles or assert it
immediately; then in the request assertions (the test block that iterates the
returned URLs/encodings around lines 174-215) verify the response header
"Content-Type" equals fileType.mime for each url/encoding (use the same
fileTypes lookup by extension to derive expected MIME). Ensure references to
encGzip, encBrotli, encNone remain unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e22df37d-5a23-43f7-a16c-5a7367be2bdc

📥 Commits

Reviewing files that changed from the base of the PR and between 2f4c59b and 1440993.

⛔ Files ignored due to path filters (9)
  • ui/app/lib/bootstrap-4.6.2-dist/css/bootstrap.min.css.map is excluded by !**/*.map
  • ui/app/lib/font-awesome-4.7.0/fonts/FontAwesome.otf is excluded by !**/*.otf
  • ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.eot is excluded by !**/*.eot
  • ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.svg is excluded by !**/*.svg
  • ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf is excluded by !**/*.ttf
  • ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.woff is excluded by !**/*.woff
  • ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2 is excluded by !**/*.woff2
  • ui/app/package-lock.json is excluded by !**/package-lock.json
  • ui/app/public/favicon.ico is excluded by !**/*.ico
📒 Files selected for processing (19)
  • Makefile
  • README.md
  • ui/app/.gitignore
  • ui/app/Makefile
  • ui/app/index.html
  • ui/app/lib/bootstrap-4.6.2-dist/css/bootstrap.min.css
  • ui/app/lib/font-awesome-4.7.0/css/font-awesome.css
  • ui/app/lib/font-awesome-4.7.0/css/font-awesome.min.css
  • ui/app/package.json
  • ui/app/script.js
  • ui/app/src/Main.elm
  • ui/app/src/Types.elm
  • ui/app/src/Updates.elm
  • ui/app/src/Views.elm
  • ui/app/src/assets/elm-datepicker.css
  • ui/app/src/main.js
  • ui/app/vite.config.mjs
  • ui/web.go
  • ui/web_test.go
💤 Files with no reviewable changes (5)
  • ui/app/src/Main.elm
  • ui/app/src/Types.elm
  • ui/app/lib/font-awesome-4.7.0/css/font-awesome.min.css
  • ui/app/lib/font-awesome-4.7.0/css/font-awesome.css
  • ui/app/src/Updates.elm
✅ Files skipped from review due to trivial changes (3)
  • ui/app/.gitignore
  • ui/app/package.json
  • README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • ui/app/vite.config.mjs
  • ui/app/src/main.js

@SoloJacobs SoloJacobs marked this pull request as draft March 22, 2026 16:23
@SoloJacobs SoloJacobs added waiting-for-user-input The Pull Request or Issue is waiting for input from the user that reported it or created it. component/ui and removed waiting-for-user-input The Pull Request or Issue is waiting for input from the user that reported it or created it. labels Mar 22, 2026
There are a few advantages to this setup:

* Dependabot can now help with keeping versions recent.
* The setup is more idiomatic and simpler. In particular, our build
  steps are more deterministic due to the inclusion of
  `package-lock.json`.
* We have to get rid of the Docker based approach for the mantime ui, so
  this change makes things more uniform across the repo.
* The command `promu crossbuild` does not support running Docker. With
  this change, we could delete `email.tmpl` from our repo, and it would
  not affect our build process in any way.

The disadvantage is that this setup requires a local toolchain (stored
in `template/node_modules`) to update `email.tmpl`.

Why does this PR not delete `email.tmpl`? This file is small, and `go`
developers do not need to install any `node` tools to run.

```sh
go get github.com/prometheus/alertmanager/template
```

Signed-off-by: Solomon Jacobs <solomonjacobs@protonmail.com>
There are a few advantages to this setup:

* Dependabot can now help with keeping versions recent.
* The setup is more idiomatic and simpler. In particular, our build
  steps are more deterministic due to the inclusion of
  `package-lock.json`.
* We have to get rid of the Docker based approach for the mantime ui, so
  this change makes things more uniform across the repo.
* The command `promu crossbuild` does not support running Docker. With
  this change, we could delete `ui/app/script.js` from our repo, and it
  would not affect our build process in any way.
* This change should help users, which are building `podman` with
  `Alertmanager`.

The disadvantage is that this setup requires a local toolchain (stored
in `ui/app/node_modules`) to update `ui/app/script.js`.

The file `ui/app/script.js` will be removed from the repo in a future
commit. This will break the following invocation:

```sh
go get github.com/prometheus/alertmanager/ui
```

The dependency `openapitools/openapi-generator-cli` could not be
removed, since it depends on `Java`.

Signed-off-by: Solomon Jacobs <solomonjacobs@protonmail.com>
This (together with moving to `npm ci`) should hopefully get
rid of this sporadic failure we have:

```
Errors occurred while compiling your configuration for elm-review. I need your
configuration to compile in order to know how to analyze your files. Hopefully
the compiler error below will help you figure out how to fix it.

make: *** [Makefile:32: test] Error 1
```

At the very least, this makes as better citizens of the internet.

Note: `nvm` picks up its configuration, even if running from a
subdirectory.

Signed-off-by: Solomon Jacobs <solomonjacobs@protonmail.com>
In the future, we want to remove the ui build artefacts from the
repository. This will break the usages of

```sh
go get github.com/prometheus/alertmanager/ui
```

That is exactly as expected, since the ui can't function correctly
without the ui artefacts.

This change attempts to make the transition a bit less painful.

Signed-off-by: Solomon Jacobs <solomonjacobs@protonmail.com>
Previously, bootstrap and font-awesome were manually vendored under
`ui/app/lib/`. They are now fetched via `npm`. Vite bundles these two
and the elm-datepicker at build time into a single file.

URL changes:
- /script.js → /assets/index-[hash].js (content-hashed)
- /lib/bootstrap-*/css/bootstrap.min.css{,.map} → merged into /assets/index-[hash].css
- /lib/elm-datepicker/css/elm-datepicker.css → merged into /assets/index-[hash].css
- /lib/font-awesome-*/css/font-awesome{,.min}.css → merged into /assets/index-[hash].css
- /lib/font-awesome-*/fonts/{woff,woff2,ttf,eot,svg} → /assets/fontawesome-webfont-[hash].{ext}

FontAwesome.otf was removed entirely, this is not a web font.

Files under /assets/ are served with "Cache-Control: public,
max-age=31536000, immutable" (content-hashed filenames make them safe to
cache forever).

In a previous PR (prometheus#5028), `Alertmanager` stopped sending "Last-Modified:
Thu, 01 Jan 1970 00:00:01 GMT". I introduced this behaviour by accident,
but consider this correct behaviour. The findings are documented via a
test.

This change removes various build artifacts, most importantly
`ui/app/script.js`. The following command no longer works in isolation:

```sh
go get github.com/prometheus/alertmanager/ui
```

Users now must build `ui/app/dist` from source.

The MIME types are now hard-coded, removing the dependency on the OS
MIME database. In practice, this would only affect `FontAwesome.otf`,
which we no longer serve anyway.

Signed-off-by: Solomon Jacobs <solomonjacobs@protonmail.com>
@SoloJacobs SoloJacobs marked this pull request as ready for review March 23, 2026 22:09
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/app/Makefile`:
- Line 12: The Makefile's phony targets are missing: declare the targets all and
test as .PHONY to prevent accidental file name collisions (add a .PHONY: all
test line); also consider consolidating with the existing .PHONY declaration for
build (reference the targets "all", "test", and "build") so all phony targets
are declared together for maintainability.

In `@ui/app/src/main.js`:
- Around line 10-12: Wrap JSON.parse calls for the persisted flags to prevent
boot-time SyntaxError: replace direct
JSON.parse(localStorage.getItem('firstDayOfWeek')) and
JSON.parse(localStorage.getItem('groupExpandAll')) with a safe parse (either a
small try/catch around each parse or a shared safeParse helper) that returns a
sensible default (e.g., null/false) when parsing fails or value is null; keep
defaultCreator as-is (localStorage.getItem('defaultCreator')) and ensure the
code uses the fallback values when storage is invalid.

In `@ui/web_test.go`:
- Around line 71-77: The test loop is fetching the index URL (`urlPath`) instead
of each asset (`assetPath`), so change the call in the t.Run body to use
assetPath with fetchWithEncoding(router, assetPath, encNone) and ensure the
goroutine captures the correct iteration value (e.g., keep the existing
assetPath := path.Join(...) or rebind inside the loop before t.Run) so each
subtest actually verifies the individual asset.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f5df670c-37fc-47fe-bcdd-567e76850ff2

📥 Commits

Reviewing files that changed from the base of the PR and between 1440993 and 8d38061.

⛔ Files ignored due to path filters (10)
  • template/package-lock.json is excluded by !**/package-lock.json
  • ui/app/lib/bootstrap-4.6.2-dist/css/bootstrap.min.css.map is excluded by !**/*.map
  • ui/app/lib/font-awesome-4.7.0/fonts/FontAwesome.otf is excluded by !**/*.otf
  • ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.eot is excluded by !**/*.eot
  • ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.svg is excluded by !**/*.svg
  • ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf is excluded by !**/*.ttf
  • ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.woff is excluded by !**/*.woff
  • ui/app/lib/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2 is excluded by !**/*.woff2
  • ui/app/package-lock.json is excluded by !**/package-lock.json
  • ui/app/public/favicon.ico is excluded by !**/*.ico
📒 Files selected for processing (31)
  • .github/workflows/ci.yml
  • .github/workflows/ui-ci.yml
  • .nvmrc
  • Makefile
  • README.md
  • cmd/alertmanager/main.go
  • template/.gitignore
  • template/Dockerfile
  • template/Makefile
  • template/package.json
  • ui/Dockerfile
  • ui/app/.gitignore
  • ui/app/CONTRIBUTING.md
  • ui/app/Makefile
  • ui/app/index.html
  • ui/app/lib/bootstrap-4.6.2-dist/css/bootstrap.min.css
  • ui/app/lib/font-awesome-4.7.0/css/font-awesome.css
  • ui/app/lib/font-awesome-4.7.0/css/font-awesome.min.css
  • ui/app/package.json
  • ui/app/script.js
  • ui/app/src/Main.elm
  • ui/app/src/Types.elm
  • ui/app/src/Updates.elm
  • ui/app/src/Views.elm
  • ui/app/src/assets/elm-datepicker.css
  • ui/app/src/main.js
  • ui/app/vite.config.mjs
  • ui/web.go
  • ui/web_test.go
  • weboperations/weboperations.go
  • weboperations/weboperations_test.go
💤 Files with no reviewable changes (7)
  • template/Dockerfile
  • ui/app/src/Updates.elm
  • ui/app/src/Main.elm
  • ui/Dockerfile
  • ui/app/src/Types.elm
  • ui/app/lib/font-awesome-4.7.0/css/font-awesome.css
  • ui/app/lib/font-awesome-4.7.0/css/font-awesome.min.css
✅ Files skipped from review due to trivial changes (5)
  • template/.gitignore
  • ui/app/.gitignore
  • ui/app/CONTRIBUTING.md
  • template/package.json
  • ui/app/package.json
🚧 Files skipped from review as they are similar to previous changes (8)
  • cmd/alertmanager/main.go
  • .github/workflows/ci.yml
  • ui/app/vite.config.mjs
  • weboperations/weboperations.go
  • ui/app/index.html
  • .github/workflows/ui-ci.yml
  • README.md
  • Makefile

Content-Encoding now varies per request based on Accept-Encoding.

Assets are stored only in compressed form (.gz level 9, .br quality 11),
which decreases the binary size from ~42.8 MB (v0.31.1) to ~39.7 MB.

Clients that accept neither gzip nor brotli receive a response
decompressed in-memory from the .gz variant on every request.

Adds vite-plugin-compression2 as a build dependency.

Signed-off-by: Solomon Jacobs <solomonjacobs@protonmail.com>
The GOPATH layout was only necessary before Go modules were introduced in Go 1.11.

Signed-off-by: Solomon Jacobs <solomonjacobs@protonmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
ui/web.go (1)

187-188: Consider explicit path validation for defense-in-depth clarity.

Lines 187–188 construct the file path via path.Join("assets", route.Param(...)). While path.Join will collapse ../ segments (e.g., ../index.htmlindex.html), the subsequent appFS.Open() call on line 139/145/151/157 is protected by fs.Sub(asset, "app/dist") on line 118, which prevents directory escape. However, this protection is implicit and fragile; if the code were refactored away from fs.Sub, the traversal path becomes exploitable.

Adding explicit validation at the route handler boundary (checking for .. or using fs.ValidPath()) would make the safety explicit and maintainable:

Optional improvement
 r.Get("/assets/*path", func(w http.ResponseWriter, req *http.Request) {
+	assetPath := route.Param(req.Context(), "path")
+	if !fs.ValidPath(assetPath) {
+		http.NotFound(w, req)
+		return
+	}
-	serve(w, req, path.Join("assets", route.Param(req.Context(), "path")), true)
+	serve(w, req, path.Join("assets", assetPath), true)
 })

A test case for /assets/../index.html would also prevent regression.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/web.go` around lines 187 - 188, The handler building asset paths uses
path.Join("assets", route.Param(req.Context(), "path")) (in the route GET
"/assets/*path" and serve function) but relies implicitly on fs.Sub/appFS.Open
for safety; add explicit validation at the route boundary by rejecting path
parameters containing ".." or using fs.ValidPath() before calling serve,
returning a 400/403 for invalid values to ensure defense-in-depth; update the
serve invocation to only receive validated, cleanedPath and add a
unit/integration test that requests "/assets/../index.html" to assert the server
rejects it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/web_test.go`:
- Around line 62-75: fetchIndexAndAssets builds asset URLs with path.Join which
breaks for absolute asset references; replace that logic by parsing the base URL
(url.Parse) from urlPath and resolving each asset reference with
base.ResolveReference(url.Parse(match[1])) so absolute and relative asset
href/src values are handled correctly. Update fetchIndexAndAssets to build
assetPath using url.Parse + ResolveReference, ensure error handling for Parse
failures, and add the necessary import for net/url; keep the existing
fetchWithEncoding calls and test assertions unchanged.

---

Nitpick comments:
In `@ui/web.go`:
- Around line 187-188: The handler building asset paths uses path.Join("assets",
route.Param(req.Context(), "path")) (in the route GET "/assets/*path" and serve
function) but relies implicitly on fs.Sub/appFS.Open for safety; add explicit
validation at the route boundary by rejecting path parameters containing ".." or
using fs.ValidPath() before calling serve, returning a 400/403 for invalid
values to ensure defense-in-depth; update the serve invocation to only receive
validated, cleanedPath and add a unit/integration test that requests
"/assets/../index.html" to assert the server rejects it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f3376ef2-c892-486f-a3df-4268a7ab0687

📥 Commits

Reviewing files that changed from the base of the PR and between 8d38061 and 418a0a6.

⛔ Files ignored due to path filters (1)
  • ui/app/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • README.md
  • ui/app/.gitignore
  • ui/app/Makefile
  • ui/app/package.json
  • ui/app/vite.config.mjs
  • ui/web.go
  • ui/web_test.go
✅ Files skipped from review due to trivial changes (3)
  • ui/app/.gitignore
  • ui/app/vite.config.mjs
  • ui/app/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • ui/app/Makefile

Comment on lines +62 to +75
func fetchIndexAndAssets(t *testing.T, router *route.Router, urlPath string) {
t.Helper()
res := fetchWithEncoding(router, urlPath, encNone)
require.Equal(t, http.StatusOK, res.Code)

require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), "/debug/pprof/", "pprof page did not load with expected content when using a route prefix")
re := regexp.MustCompile(`(?:src|href)="([^"]+)"`)
matches := re.FindAllStringSubmatch(res.Body.String(), -1)
require.NotEmpty(t, matches, "No assets (src/href) found in index.html. Is the build empty?")

// Test GET request to pprof heap endpoint
req = httptest.NewRequest("GET", routePrefix+"/debug/pprof/heap", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
for _, match := range matches {
assetPath := path.Join(urlPath, match[1])
t.Run(assetPath, func(t *testing.T) {
res := fetchWithEncoding(router, assetPath, encNone)
require.Equal(t, http.StatusOK, res.Code)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine ui/web_test.go
find . -name "web_test.go" -path "*/ui/*" 2>/dev/null | head -5

Repository: prometheus/alertmanager

Length of output: 83


🏁 Script executed:

# Get the file size to see if we can read it entirely
wc -l ui/web_test.go 2>/dev/null || echo "File not found or error"

Repository: prometheus/alertmanager

Length of output: 85


🏁 Script executed:

# Read the specific section mentioned in the review (lines 55-100 for context)
sed -n '55,100p' ui/web_test.go 2>/dev/null

Repository: prometheus/alertmanager

Length of output: 1432


🏁 Script executed:

# Also check the imports at the top of the file
head -20 ui/web_test.go

Repository: prometheus/alertmanager

Length of output: 725


🏁 Script executed:

# Check what assets/references are actually in the HTML
# First, let me look at ui/web.go to see how the HTML is generated
head -50 ui/web.go

Repository: prometheus/alertmanager

Length of output: 1439


🏁 Script executed:

# Check if there's HTML template content that shows what href/src are generated
grep -n "href\|src=" ui/web.go | head -20

Repository: prometheus/alertmanager

Length of output: 49


🏁 Script executed:

# Let me search for the HTML template more thoroughly
grep -r "assets\|app\.js" ui/ --include="*.go" | grep -E "(href|src|template)" | head -20

Repository: prometheus/alertmanager

Length of output: 176


🏁 Script executed:

# Let's understand the structure - check if there's a template file
find ui -type f \( -name "*.html" -o -name "*.tmpl" \)

Repository: prometheus/alertmanager

Length of output: 109


🏁 Script executed:

# Check what path.Join actually does with the scenario described
python3 << 'EOF'
import os.path

# Simulate the behavior
urlPath = "/alertmanager/"
assetRef = "/assets/app.js"

# What does path.Join do? In Go, path.Join has different behavior from os.path.join
# Let's test the Go semantics
# path.Join "/alertmanager/" and "/assets/app.js"
# According to Go docs, absolute paths override

# In Go's path.Join:
# - Leading slashes make a path absolute
# - path.Join("/a", "/b") returns "/b" (second absolute path wins)
# - But in this test, urlPath has trailing slash

result = os.path.join("/alertmanager/", "/assets/app.js")
print(f"os.path.join result: {result}")

# Go's path.Join behavior with trailing slash
# path.Join("/alertmanager/", "/assets/app.js") removes trailing slash first
# Then: path.Join("/alertmanager", "/assets/app.js") returns "/assets/app.js"
# Because "/assets/app.js" is absolute, it resets the path

print(f"Expected Go path.Join behavior: /assets/app.js")
print(f"This means the test tries to fetch /assets/app.js, not /alertmanager/assets/app.js")
EOF

Repository: prometheus/alertmanager

Length of output: 232


🏁 Script executed:

# Now let's check if the HTML actually contains absolute paths or relative paths
# by looking at any build output or checking how assets are referenced
grep -r "href=\|src=" ui/ --include="*.go" -B2 -A2 | head -40

Repository: prometheus/alertmanager

Length of output: 49


🏁 Script executed:

# Check the HTML files to see what asset references are in them
cat ui/app/index.html

Repository: prometheus/alertmanager

Length of output: 471


🏁 Script executed:

# Also check the other one
cat ui/mantine-ui/index.html

Repository: prometheus/alertmanager

Length of output: 483


🏁 Script executed:

# Let me verify Go's path.Join semantics more explicitly with a test
python3 << 'EOF'
# Go's path.Join behavior according to the documentation:
# - It returns the shortest path name equivalent to path by purely lexical processing.
# - It eliminates . and .. elements
# - Leading slashes in subsequent arguments cause the path to be reset

# Let's trace through the exact scenario:
# urlPath = "/alertmanager/"
# match[1] = "/assets/app.js"

# Go path.Join("/alertmanager/", "/assets/app.js")
# Step 1: Clean up both arguments
# Clean("/alertmanager/") -> "/alertmanager"
# "/assets/app.js" starts with "/" so it's treated as absolute

# Result: Since the second argument starts with "/" (is absolute),
# it replaces the entire path, returning "/assets/app.js"

# So the test would try to fetch "/assets/app.js", not "/alertmanager/assets/app.js"

print("Go path.Join semantics:")
print('  path.Join("/alertmanager/", "/assets/app.js") -> "/assets/app.js"')
print("")
print("This means:")
print("  - With "/" route prefix, test fetches /assets/app.js (correct)")
print("  - With /alertmanager/ route prefix, test ALSO fetches /assets/app.js (WRONG!)")
print("")
print("The reviewer's concern might be about a DIFFERENT scenario:")
print("  - If the HTML had relative paths like href=\"assets/app.js\"")
print("  - Then path.Join(\"/alertmanager/\", \"assets/app.js\") -> \"/alertmanager/assets/app.js\"")
print("  - Which would be correct for the prefix case")
print("")
print("The real issue depends on what the HTML actually contains.")
EOF

Repository: prometheus/alertmanager

Length of output: 324


🏁 Script executed:

# Let's check if the app/dist directory has built HTML or if it's just source
# Find any index.html files in the embedded assets
find ui/app -name "index.html" -exec head -20 {} \;

Repository: prometheus/alertmanager

Length of output: 471


🏁 Script executed:

# Check if there's a build process or vite config that might show asset paths
find . -name "vite.config.*" -o -name "package.json" | xargs ls -la 2>/dev/null

Repository: prometheus/alertmanager

Length of output: 443


🏁 Script executed:

# Let's look at what files are actually in app/dist
ls -la ui/app/dist/ 2>/dev/null || echo "app/dist not found (expected - build artifact)"

Repository: prometheus/alertmanager

Length of output: 113


🏁 Script executed:

# Since it's embedded, let's check if there are any built artifacts in git
git ls-files | grep -E "app/dist|app/index" | head -10

Repository: prometheus/alertmanager

Length of output: 84


🏁 Script executed:

# Check the vite configurations to understand how the build process handles asset paths
cat ui/app/vite.config.mjs

Repository: prometheus/alertmanager

Length of output: 689


🏁 Script executed:

# Also check the other one
cat ui/mantine-ui/vite.config.mjs

Repository: prometheus/alertmanager

Length of output: 469


🏁 Script executed:

# Now let's trace through what actually happens with the test
# The key question: what are the BUILT asset references in app/dist?
# Since we can't see app/dist, let's check git to see if it's committed

git log --oneline ui/app/dist/index.html 2>/dev/null | head -5 || echo "Not in git history"

Repository: prometheus/alertmanager

Length of output: 49


🏁 Script executed:

# Check if app/dist is in gitignore
grep -r "app/dist\|dist/" .gitignore 2>/dev/null

Repository: prometheus/alertmanager

Length of output: 49


🏁 Script executed:

# Let's understand the test scenario more carefully by looking at how Register works
grep -n "func Register" ui/web.go -A 20

Repository: prometheus/alertmanager

Length of output: 876


🏁 Script executed:

# Check how the router serves these files
grep -n "WithPrefix\|ServeHTTP\|Handler" ui/web.go -B 2 -A 5 | head -50

Repository: prometheus/alertmanager

Length of output: 49


🏁 Script executed:

# Let's trace through exactly what would happen with the current code
# The test extracts "/src/main.js" from the HTML
# Then calls path.Join("/alertmanager/", "/src/main.js")

go run << 'EOF'
package main
import (
	"fmt"
	"path"
)
func main() {
	urlPath := "/alertmanager/"
	absPath := "/src/main.js"
	result := path.Join(urlPath, absPath)
	fmt.Printf("path.Join(%q, %q) = %q\n", urlPath, absPath, result)
	
	// With relative path
	relPath := "favicon.ico"
	result2 := path.Join(urlPath, relPath)
	fmt.Printf("path.Join(%q, %q) = %q\n", urlPath, relPath, result2)
}
EOF

Repository: prometheus/alertmanager

Length of output: 89


🏁 Script executed:

# Let's check if the tests are currently passing
git log --oneline -10 -- ui/web_test.go

Repository: prometheus/alertmanager

Length of output: 115


🏁 Script executed:

# Check if there's a CI configuration that runs these tests
find . -name "*.yml" -o -name "*.yaml" | xargs grep -l "web_test\|go test.*ui" 2>/dev/null | head -3

Repository: prometheus/alertmanager

Length of output: 49


🏁 Script executed:

# Let's look at what the Router.WithPrefix actually does
# Check the route package
find . -path "*/prometheus/common/route*" -o -name "route.go" | head -5

Repository: prometheus/alertmanager

Length of output: 86


🏁 Script executed:

# If route is external, let's check if it's in go.mod
grep -A 2 "prometheus/common" go.mod | head -5

Repository: prometheus/alertmanager

Length of output: 188


🏁 Script executed:

# Check what actually gets built - look for any CI artifacts or build info
git log --all --oneline --grep="build\|ui\|dist" -- ui/app/dist 2>/dev/null | head -5

Repository: prometheus/alertmanager

Length of output: 49


🏁 Script executed:

# Most importantly, let's verify the actual behavior
# by checking if ui/app/dist is committed to the repository
git ls-tree HEAD ui/app/dist 2>/dev/null | head -10

Repository: prometheus/alertmanager

Length of output: 49


🏁 Script executed:

# The key insight: with base: "./" in vite, what does it actually produce?
# Let's check if there's a Makefile or build script that shows how this builds
find . -name "Makefile" -o -name "makefile" | xargs grep -l "ui\|app" 2>/dev/null

Repository: prometheus/alertmanager

Length of output: 129


🏁 Script executed:

# Check the Makefile for UI build targets
grep -n "ui\|app/dist" Makefile | head -20

Repository: prometheus/alertmanager

Length of output: 534


🏁 Script executed:

# Let's understand: does the test actually pass or fail currently?
# Check if there are any recent PRs or issues about this
git log --all --oneline --decorate | grep -i "test\|ui\|web\|asset" | head -10

Repository: prometheus/alertmanager

Length of output: 835


🏁 Script executed:

# Most importantly, let's check if url.Parse approach would work correctly
python3 << 'PYEOF'
from urllib.parse import urljoin, urlparse

# Scenario 1: Current code with absolute paths
urlPath = "/alertmanager/"
assetRef = "/src/main.js"

# path.Join behavior in Go
# path.Join("/alertmanager/", "/src/main.js") -> "/src/main.js" (second absolute path wins)
go_path_join_result = "/src/main.js"
print(f"Current behavior: path.Join('{urlPath}', '{assetRef}') -> '{go_path_join_result}'")
print(f"  This would try to fetch: {go_path_join_result}")
print()

# Scenario 2: URL resolution approach (proposed fix)
# Parse base URL
base = urlparse(urlPath)
base_with_scheme = f"http://localhost{urlPath}"  # Add scheme for urljoin to work

# URL resolution
resolved = urljoin(base_with_scheme, assetRef)
print(f"URL resolution: urljoin('{base_with_scheme}', '{assetRef}') -> '{resolved}'")
# Extract path only
resolved_path = urlparse(resolved).path
print(f"  Extracted path: {resolved_path}")
print()

# Scenario 3: With relative paths (if vite base: "./" works correctly)
assetRef_relative = "src/main.js"
go_path_join_relative = "/alertmanager/src/main.js"  # path.Join behavior
print(f"If built as relative: path.Join('{urlPath}', '{assetRef_relative}') -> '{go_path_join_relative}'")
print(f"  This would try to fetch: {go_path_join_relative}")
print()

print("ANALYSIS:")
print("- If vite builds with base:./ and converts /src/main.js to src/main.js -> current code works")
print("- If vite keeps absolute paths -> current code is broken, proposal would fix it")
PYEOF

Repository: prometheus/alertmanager

Length of output: 660


🏁 Script executed:

# Check if there are any related issues or discussions in git history
git log --all --source --grep="route.*prefix\|prefix.*route" --oneline | head -10

Repository: prometheus/alertmanager

Length of output: 598


🏁 Script executed:

# Let's check when the WithPrefix feature was added
git log --all --oneline -S "WithPrefix" -- ui/web_test.go | head -5

Repository: prometheus/alertmanager

Length of output: 195


🏁 Script executed:

# Search for any recent discussion about this issue in comments
grep -r "route.prefix\|base.*href\|href.*prefix" . --include="*.go" --include="*.md" 2>/dev/null | head -10

Repository: prometheus/alertmanager

Length of output: 1111


🏁 Script executed:

# Let's check the actual UI/Mantine Makefile to see how it builds
cat ui/app/Makefile

Repository: prometheus/alertmanager

Length of output: 1801


🏁 Script executed:

# Also check mantine-ui
cat ui/mantine-ui/Makefile

Repository: prometheus/alertmanager

Length of output: 121


🏁 Script executed:

# The critical question: does vite with base:./ actually produce relative paths?
# Let's check if there's any documentation or examples in the codebase
grep -r "base.*\./" ui/ --include="*.mjs" --include="*.js"

Repository: prometheus/alertmanager

Length of output: 157


🏁 Script executed:

# Let's look at the recent pprof fix mentioned in git history
# as it seems related to route prefix issues
git show 5362823d --stat

Repository: prometheus/alertmanager

Length of output: 2410


🏁 Script executed:

# Check that specific commit to see what was broken with route prefixes
git show 5362823d | head -100

Repository: prometheus/alertmanager

Length of output: 3825


🏁 Script executed:

# Let's trace the actual problem more carefully
# Check the route package being used - what does WithPrefix actually do?
# It's from prometheus/common
# Let's check how it modifies routes

grep -n "WithPrefix\|Prefix" ui/web_test.go -B 2 -A 5

Repository: prometheus/alertmanager

Length of output: 418


🏁 Script executed:

# The critical question: with base: "./", does vite output relative or absolute paths?
# Let's check if there are any build artifacts or documentation about this

# Search for any comments or documentation about how assets are served
grep -r "base.*\./" . --include="*.mjs" --include="*.md" --include="*.go" 2>/dev/null | grep -v node_modules

Repository: prometheus/alertmanager

Length of output: 159


🏁 Script executed:

# Let's check what the register function actually does and how routes are matched
sed -n '115,175p' ui/web.go

Repository: prometheus/alertmanager

Length of output: 2148


🏁 Script executed:

# Check if tests currently pass - look for test run configurations
find . -name ".github" -type d -exec cat {}'/workflows/*.yml' \; 2>/dev/null | grep -A 20 "web_test\|go test" | head -30

Repository: prometheus/alertmanager

Length of output: 49


🏁 Script executed:

# Most importantly, let's understand exactly what the issue is by checking
# what would happen with the proposed fix
python3 << 'PYEOF'
from urllib.parse import urlparse, urljoin

# Test scenario from the review
urlPath = "/alertmanager/"

# Absolute asset reference (what's in the HTML)
absRef = "/src/main.js"

# Current approach with path.Join
print("=== CURRENT APPROACH ===")
print(f"path.Join('{urlPath}', '{absRef}') in Go -> '/src/main.js'")
print(f"Test would fetch: /src/main.js (WRONG!)")
print()

# Proposed approach with URL resolution
print("=== PROPOSED APPROACH ===")
# Parse base URL
baseURL = urlparse(urlPath)
# Create full URL for urljoin to work
fullBase = f"http://localhost{urlPath}"
resolved = urljoin(fullBase, absRef)
print(f"urljoin('{fullBase}', '{absRef}') -> {resolved}")
print(f"Extracted path: {urlparse(resolved).path}")
print()

# Relative asset reference (if vite uses base: "./" correctly)
relRef = "src/main.js"
print("=== IF VITE OUTPUTS RELATIVE PATHS ===")
print(f"path.Join('{urlPath}', '{relRef}') in Go -> '/alertmanager/src/main.js'")
print(f"Test would fetch: /alertmanager/src/main.js (CORRECT)")
print()

# With proposed approach and relative paths
resolved2 = urljoin(fullBase, relRef)
print(f"urljoin('{fullBase}', '{relRef}') -> {resolved2}")
print(f"Extracted path: {urlparse(resolved2).path}")

PYEOF

Repository: prometheus/alertmanager

Length of output: 664


🏁 Script executed:

# The key: check if the route.Router's WithPrefix actually affects how paths are matched
# Let's search for the route package source if available locally, or check how it's used

# Check if there's any special handling for routes with prefixes
grep -n "Get\|Post\|Handler\|ServeHTTP" ui/web.go | head -20

Repository: prometheus/alertmanager

Length of output: 350


🏁 Script executed:

# Let's verify if the test TestIndexAssetsAreServedPrefix would actually fail with absolute paths
# by checking how the route prefix interacts with the handler

sed -n '115,145p' ui/web.go

Repository: prometheus/alertmanager

Length of output: 1132


🏁 Script executed:

# Check if the tests are currently passing in CI
git log --all --oneline | head -30

Repository: prometheus/alertmanager

Length of output: 2239


🏁 Script executed:

# Let's check if there's any indication the test is working or broken
# by looking for test results or issues
grep -r "TestIndexAssetsAreServed" . --include="*.go" --include="*.md" 2>/dev/null

Repository: prometheus/alertmanager

Length of output: 287


🏁 Script executed:

# Let me check what Vite with base: "./" actually produces
# by looking at any documentation or build outputs

# Check if there's a GitHub Actions workflow that builds and tests this
find . -name "*.yml" -o -name "*.yaml" | xargs grep -l "ui/app\|frontend" 2>/dev/null | head -3

Repository: prometheus/alertmanager

Length of output: 93


🏁 Script executed:

# Let's look at the full context of how handlers are registered
sed -n '179,220p' ui/web.go

Repository: prometheus/alertmanager

Length of output: 786


🏁 Script executed:

# Now the critical check: does the route matching work with WithPrefix?
# If register is called with route.New().WithPrefix("/alertmanager"),
# then r.Get("/assets/*path") becomes r.Get("/alertmanager/assets/*path")

# Let me verify the test actually tries to fetch the prefixed path
sed -n '86,92p' ui/web_test.go

Repository: prometheus/alertmanager

Length of output: 312


🏁 Script executed:

# Check the CI workflow to see if tests are passing
cat .github/workflows/ci.yml | grep -A 30 "test\|web"

Repository: prometheus/alertmanager

Length of output: 2378


🏁 Script executed:

# Let me look for any build output or see if we can understand what Vite actually produces
# by checking if there are any clues in the codebase

# Search for any references to how the built assets should look
grep -r "href=\|src=" ui/app --include="*.mjs" --include="*.tsx" --include="*.ts" 2>/dev/null | head -10

Repository: prometheus/alertmanager

Length of output: 49


🏁 Script executed:

# Check if vite's base option actually rewrites paths or just sets the base
# Let's look for any vite documentation or comments about this
grep -r "base.*relative\|relative.*path" . --include="*.mjs" --include="*.md" 2>/dev/null

Repository: prometheus/alertmanager

Length of output: 49


🏁 Script executed:

# The crucial question: does Vite with base: "./" actually output relative paths
# in the built HTML, or does it leave absolute paths and rely on <base> tag?

# Check if there's a <base> tag handling anywhere
grep -r "<base\|base.*href" ui/ --include="*.html" --include="*.tsx" --include="*.ts" 2>/dev/null

Repository: prometheus/alertmanager

Length of output: 49


🏁 Script executed:

# Let me verify the concern by checking what route.Router actually does
# by looking at how URLs are matched when WithPrefix is used

# The key: when route.New().WithPrefix("/alertmanager") is used,
# does it match /alertmanager/* or just /* ?

# Let's trace through a test scenario manually
python3 << 'PYEOF'
# The test does:
# 1. router := route.New().WithPrefix("/alertmanager")
# 2. Register(router)
# 3. fetchIndexAndAssets(t, router, "/alertmanager/")

# In Register, it calls:
# r.Get("/", ...)           -> becomes /alertmanager/ with prefix
# r.Get("/assets/*path", ...)->becomes /alertmanager/assets/* with prefix

# fetchIndexAndAssets:
# 1. fetches /alertmanager/ -> gets HTML with <script src="/src/main.js">
# 2. extracts /src/main.js
# 3. calls path.Join("/alertmanager/", "/src/main.js") -> /src/main.js
# 4. tries to fetch /src/main.js
# 5. BUT the handler is registered for /alertmanager/assets/*, not /src/*

# So the test SHOULD fail when trying to fetch /src/main.js
# because there's no handler for it!

# UNLESS: the HTML actually has relative paths
# e.g., <script src="src/main.js">
# Then: path.Join("/alertmanager/", "src/main.js") -> /alertmanager/src/main.js
# But there's no handler for /alertmanager/src/* either!

# UNLESS: vite's base: "./" somehow makes the paths work
# Or: the built app/dist has special structure

print("Analysis:")
print("1. Handlers registered: /alertmanager/, /alertmanager/favicon.ico, /alertmanager/assets/*")
print("2. HTML sources have absolute paths: /src/main.js, /src/favicon.svg")
print("3. path.Join('/alertmanager/', '/src/main.js') = /src/main.js")
print("4. Fetching /src/main.js should 404 because handler is at /alertmanager/assets/*")
print()
print("Conclusion: Test should fail UNLESS:")
print("  - HTML is in app/dist with RELATIVE paths (vite rewriting)")
print("  - OR there's special handling we haven't seen")
PYEOF

Repository: prometheus/alertmanager

Length of output: 524


🏁 Script executed:

# Let me check the actual built content by looking at what gets embedded
# The key is app/dist - let's see if we can infer its structure

# Check if there's any doc about the build output
grep -r "app/dist\|assets\|src/main" . --include="*.md" --include="*.txt" 2>/dev/null | head -10

Repository: prometheus/alertmanager

Length of output: 404


🏁 Script executed:

# Let me check the actual build output by looking at what npm build produces
# Check the package.json to see the build script
cat ui/app/package.json

Repository: prometheus/alertmanager

Length of output: 492


🏁 Script executed:

# Also check mantine-ui to understand both apps
cat ui/mantine-ui/package.json

Repository: prometheus/alertmanager

Length of output: 1931


🏁 Script executed:

# The key insight: if the tests are passing in CI, and they do
# route.New().WithPrefix("/alertmanager") + path.Join, then
# the built HTML must have paths that work with this combination

# Let me trace through what MUST be true for tests to pass:
python3 << 'PYEOF'
# For TestIndexAssetsAreServedPrefix to pass:
# 1. Fetch /alertmanager/ -> must get HTML with asset refs
# 2. Extract refs like "xyz"
# 3. path.Join("/alertmanager/", "xyz") -> some path
# 4. Fetch that path -> must return 200

# Handlers registered with WithPrefix("/alertmanager"):
# - /alertmanager/
# - /alertmanager/favicon.ico
# - /alertmanager/assets/*

# What must be in the HTML for the test to pass?
# - favicon.ico -> path.Join("/alertmanager/", "favicon.ico") = "/alertmanager/favicon.ico" ✓
# - /assets/something -> path.Join("/alertmanager/", "/assets/something") = "/assets/something" ✗
# - assets/something -> path.Join("/alertmanager/", "assets/something") = "/alertmanager/assets/something" ✓

print("For test to pass, HTML must contain RELATIVE paths like:")
print("  - favicon.ico")
print("  - assets/something")
print("  NOT absolute paths like:")
print("  - /favicon.ico (would become /favicon.ico, no handler)")
print("  - /assets/something (would become /assets/something, no handler)")
print()
print("Vite's base: './' likely converts absolute paths to relative during build")
print("This makes the test pass, which is correct!")
print()
print("The reviewer's concern might be:")
print("  - If vite configuration ever breaks and produces absolute paths")
print("  - The test might still pass (with wrong assets)")
print("  - OR if there are edge cases")
PYEOF

Repository: prometheus/alertmanager

Length of output: 611


🏁 Script executed:

# Let's check if there are any recent issues or PRs about asset handling
git log --all --oneline --grep="asset\|path\|prefix" -- ui/web_test.go | head -10

Repository: prometheus/alertmanager

Length of output: 208


🏁 Script executed:

# Check when web_test.go was created
git log --all --follow --oneline -- ui/web_test.go | tail -5

Repository: prometheus/alertmanager

Length of output: 257


🏁 Script executed:

# Let's look at the commit that added these tests
git show c53fff68 --stat

Repository: prometheus/alertmanager

Length of output: 436


🏁 Script executed:

# Check the actual content of that commit
git show c53fff68

Repository: prometheus/alertmanager

Length of output: 1606


🏁 Script executed:

# Now let's verify if the current test is actually working correctly
# by understanding what Vite with base: "./" actually does

# Create a small test to understand Vite's behavior
python3 << 'PYEOF'
# According to Vite documentation, when base is set to "./":
# - It's a relative base URL
# - Script/link references in HTML are rewritten to be relative to the base
# - So /src/main.js becomes just src/main.js in the built HTML
# - This makes the assets work correctly with route prefixes

# Verification scenario:
# 1. HTML source has: <script src="/src/main.js">
# 2. Vite builds with base: "./"
# 3. Built HTML should have: <script src="src/main.js"> (relative)
# 4. Test fetches /alertmanager/ -> gets HTML
# 5. Test extracts "src/main.js"
# 6. Test calls path.Join("/alertmanager/", "src/main.js") -> "/alertmanager/src/main.js"
# 7. Test fetches /alertmanager/src/main.js -> ???

# BUT handlers are registered for /alertmanager/assets/*, not /alertmanager/src/*
# So this still doesn't work!

# Unless... the built app/dist structure is different from the source
# OR the HTML is being generated differently

print("The mystery: what's actually in app/dist?")
print("Source HTML has /src/main.js")
print("But handlers are for /assets/*")
print("Vite must be doing something to map /src/ to /assets/")

PYEOF

Repository: prometheus/alertmanager

Length of output: 222


🏁 Script executed:

# Let me check if there's any transformation from /src to /assets
grep -r "src.*assets\|assets.*src" ui/app --include="*.mjs" --include="*.tsx" --include="*.ts" 2>/dev/null | head -10

Repository: prometheus/alertmanager

Length of output: 49


🏁 Script executed:

# Check if the /assets route actually works for serving app code
# by looking at how the register function maps paths
sed -n '187,195p' ui/web.go

Repository: prometheus/alertmanager

Length of output: 385


🏁 Script executed:

# So the handler is: /assets/*path maps to path.Join("assets", route.Param(...))
# This means /alertmanager/assets/foo -> serves assets/foo from appFS

# But the HTML has /src/main.js, not /assets/main.js
# Unless Vite rewrites it differently...

# Let me check if there's index.html in app/dist after build
# Or if the files are served differently
git show c53fff68 | head -200

Repository: prometheus/alertmanager

Length of output: 1606


🏁 Script executed:

# Wait, the test TestWebRoutes from c53fff68 calls Register with 3 parameters
# But the current Register function only takes 1 parameter!
# Let me check the actual current Register signature

grep -n "^func Register" ui/web.go

Repository: prometheus/alertmanager

Length of output: 103


🏁 Script executed:

# Let me see the full Register function definition
sed -n '115,125p' ui/web.go

Repository: prometheus/alertmanager

Length of output: 493


🏁 Script executed:

# It seems the file might have changed. Let me check the actual current state
# by looking at lines around the Register function
sed -n '110,220p' ui/web.go

Repository: prometheus/alertmanager

Length of output: 2955


🏁 Script executed:

# There's a discrepancy. The test from c53fff68 calls Register with 3 args
# but I saw only 1 arg. Let me check the current file again carefully

head -150 ui/web.go | tail -50

Repository: prometheus/alertmanager

Length of output: 1640


🏁 Script executed:

# Let me get a complete picture of the current ui/web.go around Register
wc -l ui/web.go

Repository: prometheus/alertmanager

Length of output: 80


🏁 Script executed:

# The file is 200+ lines. Let me get the full Register function
grep -n "^func Register" ui/web.go -A 50

Repository: prometheus/alertmanager

Length of output: 2029


🏁 Script executed:

# Let me check if mantine-ui has a vite.config
cat ui/mantine-ui/vite.config.mjs 2>/dev/null || echo "File not found"

Repository: prometheus/alertmanager

Length of output: 469


🏁 Script executed:

# Check the current HEAD to see what UI is being used
git log --oneline -1

Repository: prometheus/alertmanager

Length of output: 115


🏁 Script executed:

# The key question: does the current test actually pass?
# Let me check if there are any recent test failures or PRs about this
git log --all --oneline -- ui/web_test.go | head -15

Repository: prometheus/alertmanager

Length of output: 257


🏁 Script executed:

# Let me verify the actual scenario by understanding app/dist structure
# Since we can't see the built artifacts, let me check if there's any documentation
grep -r "app/dist\|assets" . --include="*.md" | grep -i "structure\|path\|route" | head -10

Repository: prometheus/alertmanager

Length of output: 137


🏁 Script executed:

# Most critical: let me check if the test is actually testing what the review claims
# by reading the full fetchIndexAndAssets function as currently implemented

sed -n '62,92p' ui/web_test.go

Repository: prometheus/alertmanager

Length of output: 1063


🏁 Script executed:

# Check if mantine-ui or elm-ui app is the one being built
grep -r "app/dist" . --include="*.go" | head -5

Repository: prometheus/alertmanager

Length of output: 333


🏁 Script executed:

# Check if there's any build step that moves/renames files from src to assets
grep -r "src.*assets\|mv.*src\|cp.*src" . --include="Makefile" --include="*.sh" 2>/dev/null

Repository: prometheus/alertmanager

Length of output: 237


🏁 Script executed:

# Most importantly: verify what's actually in the app/dist after building
# by checking if the test would actually pass with absolute paths
python3 << 'PYEOF'
# The critical insight:
# If HTML has: <script src="/src/main.js">
# And vite.config has base: "./", vite REWRITES it to: <script src="src/main.js">
# Then path.Join("/alertmanager/", "src/main.js") = "/alertmanager/src/main.js"

# But handlers serve /assets/*, not /src/*
# So either:
# 1. Built files are in dist/assets/, not dist/src/
# 2. Or there's a /src/* handler we haven't seen

# Let me check if the register function has more routes
print("Need to verify: are there handlers for /src/* routes?")
print("Or are all asset files in assets/ subdirectory of app/dist?")
PYEOF

Repository: prometheus/alertmanager

Length of output: 180


🏁 Script executed:

# Check if there are other route handlers beyond what we've seen
grep "r.Get\|r.Post\|r.Delete\|r.Put" ui/web.go

Repository: prometheus/alertmanager

Length of output: 334


🏁 Script executed:

# Final verification: check what the current test actually extracts from the HTML
# by checking if there are any absolute paths that would break

# Let me look at what vite outputs for mantine-ui which doesn't have base: "./"
python3 << 'PYEOF'
# mantine-ui has NO base option, which means it defaults to "/"
# This means it will keep absolute paths like /src/main.tsx
# And the test would try to fetch: path.Join("/alertmanager/", "/src/main.tsx") = "/src/main.tsx"
# But there's no handler for /src/*, so it would 404

# elm-ui (ui/app) has base: "./" which means:
# 1. It rewrites /src/main.js to relative paths
# 2. Vite might also move files to different locations

# The question: which app is actually being tested?
print("Critical: which UI framework is actually embedded in app/dist?")
print("- elm-ui with base: './' (relative paths, would work)")
print("- mantine-ui without base (absolute paths, would fail)")
PYEOF

Repository: prometheus/alertmanager

Length of output: 238


🏁 Script executed:

# Check which app is the primary one
grep -r "mantine-ui\|elm.*ui\|app/dist" . --include="Makefile" --include="*.go" | grep -v node_modules | grep -v ".git"

Repository: prometheus/alertmanager

Length of output: 371


🏁 Script executed:

# The Makefile showed FRONTEND_DIR = $(BIN_DIR)/ui/app
# So elm-ui (ui/app) is the one being used
# Which has base: "./" that makes relative paths

# But let me verify the source HTML actually has those absolute paths
grep -o 'src="[^"]*"\|href="[^"]*"' ui/app/index.html

Repository: prometheus/alertmanager

Length of output: 104


🏁 Script executed:

# Perfect - now I have all the information needed
# The review comment's concern is valid, but the current code works due to vite config
# Let me prepare the rewritten review

echo "Verification complete"

Repository: prometheus/alertmanager

Length of output: 88


The test currently works due to vite's base: "./" configuration, but relies on fragile assumptions about build output.

fetchIndexAndAssets uses path.Join to construct URLs, which resets the path when the asset reference is absolute (e.g., path.Join("/alertmanager/", "/src/main.js") becomes /src/main.js). The test passes because vite's base: "./" setting in ui/app/vite.config.mjs converts absolute paths to relative ones during the build, making path.Join behave correctly by coincidence rather than by design.

If the vite configuration changes, the build process shifts, or absolute paths accidentally slip through, the test could silently pass while assets fail in production. URL resolution should use proper URL semantics (via url.Parse and ResolveReference) rather than path manipulation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/web_test.go` around lines 62 - 75, fetchIndexAndAssets builds asset URLs
with path.Join which breaks for absolute asset references; replace that logic by
parsing the base URL (url.Parse) from urlPath and resolving each asset reference
with base.ResolveReference(url.Parse(match[1])) so absolute and relative asset
href/src values are handled correctly. Update fetchIndexAndAssets to build
assetPath using url.Parse + ResolveReference, ensure error handling for Parse
failures, and add the necessary import for net/url; keep the existing
fetchWithEncoding calls and test assertions unchanged.

This makes it so `make` will no longer skip targets, if files such as
`ui/app/all` exist. More importantly, it will stop this from comming up
in AI code reviews.

Signed-off-by: Solomon Jacobs <solomonjacobs@protonmail.com>
Previously, the Makefile used `:=` for the TEMPOPENAPI variable, causing
`mktemp` to run at parse time on every `make` invocation. The temp dir
was only cleaned up inside the `src/Data` recipe.

In the GitHub workflow, this left a root-owned `700` directory on the
host, which blocks `hashFiles` in post-job cleanups. Because of this, it
was previously not possible to invoke any `ui/app/Makefile` command
during the `test` stage.

Signed-off-by: Solomon Jacobs <solomonjacobs@protonmail.com>
Signed-off-by: Solomon Jacobs <solomonjacobs@protonmail.com>
@SoloJacobs SoloJacobs marked this pull request as draft March 26, 2026 21:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant