Skip to content

feat: configure dashboard base path at runtime - #641

Open
ZAYEC77 wants to merge 2 commits into
tale:mainfrom
NullRefExcep:feature/configurable-base-path
Open

ZAYEC77 wants to merge 2 commits into
tale:mainfrom
NullRefExcep:feature/configurable-base-path

Conversation

@ZAYEC77

@ZAYEC77 ZAYEC77 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds runtime configuration of the dashboard URL prefix through server.base_path, without rebuilding Headplane. Existing installations continue to use /admin by default; custom paths such as /headplane and /tools/web are supported.

For example:

server:
  base_path: /tools/web

The setting can also be supplied through HEADPLANE_SERVER__BASE_PATH or the corresponding NixOS option. Base paths must contain non-empty URL-safe segments, start with /, and have no trailing slash. The root path / and ./.. segments are rejected.

Closes #637.

Changes

  • Apply the configured prefix consistently to application routes, static assets, authentication cookies, OIDC callback and logout URLs, browser SSH resources, live-data endpoints, and the Docker healthcheck URL.
  • Rewrite the production asset manifest at startup so the same build can serve different prefixes. Chunk imports and CSS asset URLs remain relative.
  • Pass the prefix to the browser through an app-owned HTML meta tag available before hydration, removing the client dependency on window.__reactRouterContext.
  • Add configuration examples and NixOS options, update installation and SSO documentation, and document how to run the browser regression test.

Validation

  • 80 tests passed across the affected unit suites, including base-path validation, OIDC URLs and state-cookie scope, client/server prefix access, asset-manifest rewriting, and build immutability.
  • 3 production integration tests passed using the same build under /admin, /web, and /tools/web. These cover assets and fonts, manifest discovery, redirects, login/logout cookies, live-data streaming, and healthcheck URLs.
  • The /tools/web test also runs Chromium to verify hydration, API-key login, and client navigation between Machines and Users without document reloads, JavaScript errors, or requests to /admin.
  • Lint and formatting checks passed. Chromium installation is included in CI, and the updated Nix dependency hash was verified by building the dependency cache.
  • pnpm run typecheck still reports existing errors in unchanged test files: the Vitest describe.sequential API and implicit parameter types in the filesystem test helper. No new type errors were reported in the changed files.

Compatibility

The default /admin prefix remains unchanged. Deployments choosing a custom prefix need to update their reverse-proxy paths and registered OIDC callback/logout URLs accordingly.

The server still adapts React Router's generated ServerBuild and asset manifest. Unit tests and production browser coverage guard this integration when React Router is upgraded.

@ZAYEC77
ZAYEC77 requested a review from tale as a code owner October 2, 2026 14:11
@github-actions github-actions Bot added Docs Improvements or additions to documentation UI/UX Related to the frontend UI Authentication Authentication & Permissions Config Related to Headplane specific configuration labels Oct 2, 2026

@pullfrog pullfrog 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 new issues found.

Reviewed changes

  • Runtime server.base_path config — added to the full and partial server schemas with an arktype narrow regex that rejects empty/root paths, trailing slashes, //, and ./.. segments; mirrored in config.example.yaml, nix/options.nix, and the regenerated docs/NixOS-options.md, defaulting to build-time /admin.
  • Runtime basename override — app/server/app.ts sets the process prefix and feeds React Router a patched build (configureBuild in prod) so route matching, SSR handoff, redirects, and the client's window.__reactRouterContext.basename all reflect the configured path.
  • Asset-manifest prefixing — new app/server/assets.ts rewrites assets.url, entry/route module/imports/css, and client chunk modules; Vite base is now ./ in builds and app.ts serves the rewritten manifest dynamically (no-cache) ahead of the stale on-disk copy via dynamicPaths.
  • Client prefix accessor — new ~/utils/prefix getPrefix() replaces all client __PREFIX__ uses (favicon, SSE, SSH window.open, WASM/font URLs, preload); module-level constants were moved into functions to avoid pre-hydration evaluation.
  • Server plumbing — cookie path, OIDC redirect_uri/post-logout, OIDC state cookie, listener basename, and the Docker listen-file healthcheck URL all use config.server.base_path.
  • Tests/docs — new base-path unit + platform integration tests (prod build booted under /admin, /web, /tools/web), an OIDC callback/logout unit, and updated docs/changelog.

I verified the new unit tests (75 passed), the platform integration test (3/3 passed on a real production build), lint (0 errors), and formatting. The pnpm run typecheck failures are pre-existing in untouched test files (describe.sequential on Vitest 5) and unrelated to this PR.

One carry-forward risk (not blocking): the approach depends on React Router internals — the window.__reactRouterContext.basename handoff and the spread-patchable server build object. The integration test is the right guard for that.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog 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 new issues found.

Reviewed changes

This incremental review covers commit 567e5e2f, the only commit since the prior pullfrog review at 1f5bd192.

  • Client prefix now sourced from an SSR-emitted meta tag — app/utils/prefix.ts getPrefix() reads meta[name="headplane-base-path"] in the browser instead of window.__reactRouterContext.basename, and app/root.tsx renders that tag during SSR so the runtime prefix is available before hydration and for SSE, WASM/font, SSH, and favicon URLs.
  • Asset-manifest unit tests — new tests/unit/config/assets.test.ts asserts configureBuild rebases entry/route/module/css/client URLs and leaves the source build unmutated.
  • Prefix unit tests — new tests/unit/config/prefix.test.ts covers the SSR value, the meta-tag read before hydration, and the build-time fallback.
  • Browser integration coverage — extended tests/integration/platform/base-path.test.ts with a Playwright navigation check (hydration, API-key login, client navigation, no stray /admin requests, no console errors) and installed Chromium in CI.
  • Dependency/docs — playwright devDependency + lockfile, nix/package.nix hash bump, and docs/CONTRIBUTING.md browser-test instructions.

I verified the two new unit suites (5 tests passing) and that pnpm install --frozen-lockfile --lockfile-only is consistent. The remaining typecheck failures are pre-existing describe.sequential errors in untouched files; the extra playwright module-not-found errors are local to this environment (dependencies were installed before the PR lockfile) and CI installs it via build.sh's pnpm install --frozen-lockfile.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@ZAYEC77 ZAYEC77 mentioned this pull request Oct 2, 2026

@tale tale left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I left 1 nit but there's some bigger structural issues that I don't mind cleaning up.

  • There's quite a few AI generated comments across your changes, I'd appreciate if they were trimmed up and necessary ones were hand-written.
  • We can just drop the full __PREFIX and PREFIX calculation stuff in vite.config.ts, it's still leftover for some reason
  • I'm not a fan of the tests and listener, your AI agent has written some older style TS. We are on Node 24 (soon to be 26 in a few weeks), we have modern APIs.

Comment on lines +34 to +39
const basePath = type("string").narrow(
(value, ctx) =>
(/^\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/.test(value) &&
!value.split("/").some((segment) => segment === "." || segment === "..")) ||
ctx.mustBe("an absolute path of non-empty URL-safe segments without a trailing slash"),
);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I'm fairly sure that arktype has a type available to us instead of needing to do a custom narrow. You might have to upgrade the version of arktype.

This branch has not been deployed

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

Labels

Authentication Authentication & Permissions Config Related to Headplane specific configuration Docs Improvements or additions to documentation UI/UX Related to the frontend UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update base path

2 participants