Skip to content

fix(admin): serve the SPA shell on /admin/ as well as /admin - #528

Merged
amondnet merged 2 commits into
mainfrom
amondnet/admin-trailing-slash
Sep 11, 2026
Merged

amondnet merged 2 commits into
mainfrom
amondnet/admin-trailing-slash

Conversation

@amondnet

@amondnet amondnet commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Closes #527.

What was wrong

On a --features ui build, GET /admin/ answered a bare 404 while GET /admin and GET /admin/anything both served the SPA shell.

An axum {*path} segment must match at least one character, so the trailing-slash spelling matched neither registration that could have served it:

  • .route("/admin", get(dashboard)) — an exact path
  • .route("/admin/{*path}", get(ui::shell)) — needs a non-empty suffix

The repo already knew this rule: /admin/api and /admin/api/ are registered separately for exactly the same reason, with a comment saying so. The JSON namespace root got both spellings; the mount root got only the bare one.

The fix

Register /admin/ alongside /admin, pointing at the same dashboard() handler.

It goes in the base router rather than beside the --features ui catch-alls, so the trailing-slash form inherits the same build-dependent answer /admin already has: the shell with the feature, and without it the 404 whose body names --features ui. A slash appended by a browser or a proxy must not change which of those two an operator gets — including not downgrading the explanatory 404 to axum's empty-bodied one.

Tests

  • admin_ui::the_mount_root_with_a_trailing_slash_serves_the_spa_shell — asserts the shell, not just a 200, since a redirect or an empty page would satisfy a status-only check while still failing the operator who typed the slash.
  • router_surface::the_mount_root_without_the_ui_feature_explains_the_missing_bundle now runs over both spellings. They are two registrations answering one handler, so /admin/ could otherwise regress to an empty 404 while /admin kept its sentence. This strictly widens the existing test — every assertion it made before, it still makes.

Both were confirmed non-vacuous by deleting the new route: each fails with a real assertion (not a compile error) and passes once it is restored.

The route-inventory gates in router_surface.rs caught the new route on the first run, which is what they are for: ADMIN_PATHS (17 → 18 paths, 19 → 20 method+path pairs) and the literal-registration count (40 → 41). The pair count is verified against the table rather than asserted by hand.

No new hardening test for /admin/: both routes resolve to the same handler, so there is no second code path for headers to drift on.

Docs

ui/README.md asserted the old behavior verbatim ("/admin/ itself 404s because an axum wildcard must match at least one character") — landed in #526 and falsified by this change, so it is rewritten rather than left to rot.

Also updated: docs/admin-ui-delivery.md's current-surface table, and the endpoints reference in all four locales (the route row, the unauthenticated-route list, and the mount-fallback paragraph).


Summary by cubic

GET /admin/ now serves the SPA shell in a --features ui build, and the 404 naming --features ui without it, matching GET /admin instead of answering a bare 404. An axum {*path} segment must match at least one character, so the trailing-slash spelling previously matched neither the exact route nor the fallback.

Tests and docs

  • New test asserts the trailing-slash form serves the shell, not just a 200.
  • The no-ui-feature test now covers both spellings, since they are separate registrations.
  • Docs that asserted the old behavior are rewritten, including the endpoints reference in all four locales.

Written for commit cb905cf. Summary will update on new commits.

An axum `{*path}` segment must match at least one character, so `/admin/`
matched neither `.route("/admin", ...)` nor `.route("/admin/{*path}", ...)`
and answered a bare 404 on the dashboard's own mount root.

Register `/admin/` beside `/admin` in the base router, pointing at the same
`dashboard()` handler. Registering it there rather than beside the
`--features ui` catch-alls keeps the trailing-slash form's answer identical
to `/admin`'s in both builds -- the shell with the feature, and without it
the 404 whose body names `--features ui` rather than axum's empty one.

The repo already knew this rule: `/admin/api` and `/admin/api/` are
registered separately for exactly the same reason. The JSON namespace root
had both spellings; the mount root had only the bare one.

Closes #527.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request registers the trailing-slash version of the admin mount root (/admin/) alongside the exact route (/admin) to prevent requests with a trailing slash from falling through to a bare 404, as wildcard segments in axum cannot match empty strings. The changes include routing updates in src/admin/mod.rs, comprehensive documentation updates across multiple languages, and corresponding test coverage additions in tests/admin_ui.rs and tests/router_surface.rs. There are no review comments provided, so I have no feedback to evaluate.

@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 9 files

Architecture diagram
sequenceDiagram
    participant Browser as Browser / Proxy
    participant Router as Axum Router
    participant Dashboard as dashboard handler
    participant Shell as UI Shell (index.html)
    participant Feature404 as Feature-Naming 404

    Note over Browser,Router: GET /admin/ request (trailing slash)

    Browser->>Router: GET /admin/
    alt --features ui build
        Router->>Dashboard: CHANGED: /admin/ route maps to dashboard()
        Dashboard-->>Shell: Serve SPA shell (text/html)
        Shell-->>Router: index.html with /admin/assets/ links
        Router-->>Browser: 200 OK (text/html)
    else default build (no ui feature)
        Router->>Dashboard: CHANGED: /admin/ route maps to dashboard()
        Dashboard-->>Feature404: 404 with "--features ui" body
        Feature404-->>Router: explanatory 404
        Router-->>Browser: 404 (with feature-naming body)
    end

    Note over Browser,Router: GET /admin request (exact path)

    Browser->>Router: GET /admin
    alt --features ui build
        Router->>Dashboard: /admin route maps to dashboard()
        Dashboard-->>Shell: Serve SPA shell (text/html)
        Shell-->>Router: index.html with /admin/assets/ links
        Router-->>Browser: 200 OK (text/html)
    else default build (no ui feature)
        Router->>Dashboard: /admin route maps to dashboard()
        Dashboard-->>Feature404: 404 with "--features ui" body
        Feature404-->>Router: explanatory 404
        Router-->>Browser: 404 (with feature-naming body)
    end

    Note over Router: Both /admin and /admin/ registered via .route()<br>in admin_router() base router

    Note over Browser,Router: Route inventory gates verify counts<br>ADMIN_PATHS: 18 paths, 20 method+path pairs
Loading

Re-trigger cubic

@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the new route shares the established handler and is covered across UI and non-UI behavior.

Summary

  • Routes both spellings through the existing dashboard handler.
  • Adds focused coverage for the SPA shell and the explanatory non-UI 404.
  • Updates route inventories, delivery documentation, endpoint references, and maintained translations.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[GET /admin or /admin/] --> B{server.admin configured?}
    B -- No --> C[Route tree absent: 404]
    B -- Yes --> D[dashboard handler]
    D --> E{Built with ui feature?}
    E -- Yes --> F[Serve embedded SPA shell]
    E -- No --> G[404 naming --features ui]
Loading

Reviews (1) · Last reviewed commit: "fix(admin): serve the SPA shell on /admi..."

… line

The #526 index line still claimed the new cfg(not(ui)) router test never runs
in CI. That claim was corrected in the note itself during #526 but not in
MEMORY.md, which is the file loaded first -- so the stale version was the one
being read. It is run, by the default-build step that PR added.

The #528 docs-review note cited a nonexistent `ADMIN_ROUTES` fixture; the
constant is `ADMIN_PATHS`.
@sonarqubecloud

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 4 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@codspeed

codspeed Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 83 untouched benchmarks


Comparing amondnet/admin-trailing-slash (cb905cf) with main (29ff675)

Open in CodSpeed

@amondnet
amondnet merged commit c7bc752 into main Sep 11, 2026
15 checks passed
@amondnet
amondnet deleted the amondnet/admin-trailing-slash branch September 11, 2026 17:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GET /admin/ (trailing slash) returns 404 instead of the SPA shell

1 participant