Skip to content

feat(web): add PITCHFORK_WEB_PATH support for reverse proxy path prefixes#244

Merged
jdx merged 3 commits intomainfrom
feat/web-path-prefix
Feb 21, 2026
Merged

feat(web): add PITCHFORK_WEB_PATH support for reverse proxy path prefixes#244
jdx merged 3 commits intomainfrom
feat/web-path-prefix

Conversation

@jdx
Copy link
Owner

@jdx jdx commented Feb 21, 2026

Summary

  • Adds --web-path CLI flag / PITCHFORK_WEB_PATH env var to serve the web UI under a path prefix (e.g., /ps)
  • Uses Axum's .nest() to mount all routes under the prefix, with a root //{prefix}/ redirect
  • Prefixes all hardcoded paths (href=, hx-get=, hx-post=, src=, EventSource, window.location.href) in HTML output across all route files

Closes #241

Usage

# No prefix (same as before)
PITCHFORK_WEB_PORT=9001 pitchfork supervisor run

# With path prefix
PITCHFORK_WEB_PORT=9001 PITCHFORK_WEB_PATH=ps pitchfork supervisor run
# http://localhost:9001/ redirects to /ps/
# All pages, links, HTMX polling, SSE streaming, static assets work under /ps/

Test plan

  • cargo build succeeds
  • cargo nextest run passes (no regressions)
  • Manual: run without prefix — identical to current behavior
  • Manual: run with PITCHFORK_WEB_PATH=ps — all pages, links, HTMX, SSE, static assets work under /ps/
  • Manual: root / redirects to /ps/ when prefix is set

🤖 Generated with Claude Code


Note

Medium Risk
Touches core web routing and many hardcoded URL strings; mis-normalization or missed prefixes could break navigation/HTMX/SSE or static asset loading, though scope is limited to the local web UI.

Overview
Adds support for serving the web UI under a configurable path prefix via new --web-path / PITCHFORK_WEB_PATH options, plumbed from pitchfork supervisor run into SUPERVISOR.start() and the web server.

The Axum server now normalizes/validates the prefix, nests all routes under it, and redirects / to /{prefix}/ when configured; all HTML output (links, HTMX requests/polling, SSE EventSource, and static asset URLs) is updated to respect the base path. CLI usage specs/docs are updated accordingly.

Written by Cursor Bugbot for commit 823b0f5. This will update automatically on new commits. Configure here.

…ixes

Allow serving the web UI under a path prefix (e.g., /ps) for use with
reverse proxies like `tailscale serve --set-path=/ps 9001`.

Adds --web-path CLI flag / PITCHFORK_WEB_PATH env var. Uses Axum's
.nest() to mount all routes under the prefix, adds root redirect,
and prefixes all hardcoded paths in HTML output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 21, 2026 15:52
@gemini-code-assist
Copy link

Summary of Changes

Hello @jdx, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the web UI's flexibility by enabling it to operate seamlessly behind a reverse proxy or in environments where it needs to be served from a sub-path. By introducing a configurable web path prefix, the application can now correctly handle all internal and external links, HTMX requests, and SSE streams, adapting its routing and HTML output to the specified base path. This change improves deployment options and integration with existing infrastructure without altering core functionality.

Highlights

  • Web Path Prefix Support: Introduced a new --web-path CLI flag and PITCHFORK_WEB_PATH environment variable, allowing the web UI to be served under a specified path prefix (e.g., /ps).
  • Axum Routing Integration: Implemented Axum's .nest() functionality to mount all web routes under the configured path prefix, ensuring proper routing for the web UI.
  • Dynamic HTML Path Rewriting: Modified all hardcoded relative paths within HTML output (including href, hx-get, hx-post, src, EventSource, and window.location.href) to dynamically include the configured web path prefix.
  • Root Redirect: Added a root / redirect to /{prefix}/ when a web path prefix is configured, ensuring users are directed to the correct base URL.
Changelog
  • docs/cli/commands.json
    • Added the --web-path option to the CLI command definition.
  • docs/cli/supervisor/run.md
    • Documented the new --web-path CLI option for serving the web UI under a path prefix.
  • pitchfork.usage.kdl
    • Included the --web-path flag in the supervisor run command definition.
  • src/cli/supervisor/run.rs
    • Added a web_path field to the Run struct to capture the new CLI argument.
    • Modified the start method to pass the web_path to the supervisor.
  • src/supervisor/mod.rs
    • Updated the start function signature to accept an Option<String> for web_path.
    • Passed the web_path argument to the crate::web::serve function.
  • src/web/mod.rs
    • Introduced a BASE_PATH OnceLock to store the global web path prefix.
    • Added a bp() helper function to retrieve the stored base path.
    • Implemented normalize_base_path to format user-provided path prefixes consistently.
  • src/web/routes/config.rs
    • Imported the bp function.
    • Updated all internal links and asset paths in base_html to use the bp() prefix.
    • Applied the bp() prefix to links and HTMX targets in the list and edit functions.
  • src/web/routes/daemons.rs
    • Imported the bp function.
    • Updated all internal links and asset paths in base_html to use the bp() prefix.
    • Applied the bp() prefix to HTMX targets and window.location.href in daemon_row, list_content, list_partial, show, and start functions.
  • src/web/routes/index.rs
    • Imported the bp function.
    • Applied the bp() prefix to HTMX targets and window.location.href in daemon_row.
    • Updated all internal links and asset paths in the main index HTML template to use the bp() prefix.
    • Applied the bp() prefix to HTMX targets in the index function.
  • src/web/routes/logs.rs
    • Imported the bp function.
    • Updated all internal links and asset paths in base_html to use the bp() prefix.
    • Applied the bp() prefix to HTMX targets and EventSource URLs in the index and show functions.
  • src/web/server.rs
    • Modified the serve function to accept web_path.
    • Normalized the web_path and stored it in the BASE_PATH OnceLock.
    • Implemented conditional routing using Axum's .nest() to mount all routes under the base_path if specified.
    • Added a permanent redirect from / to /{base_path}/ when a base_path is configured.
    • Updated the web UI listening information to include the base_path in the log message.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

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 effectively adds support for serving the web UI under a path prefix. The changes are comprehensive, touching the CLI, supervisor, web server, and all web routes to correctly handle the new web-path configuration. The use of Axum's .nest() is appropriate for this task.

My main feedback is focused on improving maintainability by reducing code duplication that has been highlighted by these changes. Specifically, the daemon_row and base_html functions are duplicated across multiple route files. Consolidating these into shared components would make the codebase easier to manage in the long run. Please see my detailed comments for suggestions.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds support for serving the Pitchfork web UI under a configurable path prefix to enable deployment behind reverse proxies with path-based routing. The implementation uses Axum's .nest() functionality to mount all routes under an optional prefix (e.g., /ps/), with automatic redirection from the root path when a prefix is configured.

Changes:

  • Added PITCHFORK_WEB_PATH environment variable and --web-path CLI flag for configuring the path prefix
  • Implemented path prefix handling using Axum's router nesting with a redirect from root to the prefixed path
  • Updated all hardcoded paths in HTML templates and JavaScript code across all route handlers to use the configurable base path

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/web/mod.rs Adds BASE_PATH global and helper functions for path normalization and retrieval
src/web/server.rs Implements router nesting with base path prefix and root redirect
src/web/routes/index.rs Updates all hardcoded paths in dashboard to use bp() helper
src/web/routes/logs.rs Updates all hardcoded paths in logs pages to use bp() helper
src/web/routes/daemons.rs Updates all hardcoded paths in daemon management to use bp() helper
src/web/routes/config.rs Updates all hardcoded paths in config editor to use bp() helper
src/supervisor/mod.rs Threads web_path parameter to web server startup
src/cli/supervisor/run.rs Adds web_path CLI argument and passes to supervisor
pitchfork.usage.kdl Adds web-path flag specification
docs/cli/supervisor/run.md Documents the new --web-path flag
docs/cli/commands.json Adds web-path flag to generated CLI documentation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

let app = Router::new()
pub async fn serve(port: u16, web_path: Option<String>) -> Result<()> {
let base_path = super::normalize_base_path(web_path.as_deref());
let _ = super::BASE_PATH.set(base_path.clone());
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

The result of BASE_PATH.set() is being silently ignored. If serve() is called multiple times (e.g., in tests or if the supervisor restarts), the second call will fail to set the BASE_PATH but this will be silently ignored, potentially causing bp() to return a stale or incorrect value. Consider using .expect() with a clear message, or document that serve() must only be called once per process.

Suggested change
let _ = super::BASE_PATH.set(base_path.clone());
super::BASE_PATH
.set(base_path.clone())
.expect("BASE_PATH was already set; serve() must only be called once per process");

Copilot uses AI. Check for mistakes.
src/web/mod.rs Outdated
Comment on lines +18 to +30
pub(crate) fn normalize_base_path(path: Option<&str>) -> String {
match path {
None => String::new(),
Some(p) => {
let trimmed = p.trim().trim_matches('/');
if trimmed.is_empty() {
String::new()
} else {
format!("/{trimmed}")
}
}
}
}
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

The normalize_base_path function doesn't validate that the path contains only valid URL path characters. User input like "ps/../admin" or "ps//test" or paths with special characters could create invalid or unexpected routes. Consider validating that the normalized path matches a pattern like /^/[a-zA-Z0-9_-]+$/ to ensure it's a simple, safe path segment.

Copilot uses AI. Check for mistakes.
- Use .expect() on BASE_PATH.set() instead of silently ignoring failure
- Validate web path contains only safe characters (alphanumeric, hyphens,
  underscores) to prevent path traversal or invalid routes
- Use temporary redirect (307) instead of permanent (308) for root
  redirect since the path prefix is a configurable runtime setting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON, but a Cloud Agent failed to start.

normalize_base_path now returns Result instead of using assert!.
Since serve() runs inside tokio::spawn, a panic would silently abort
the task and bypass the error!() handler in the supervisor. Returning
an error ensures invalid PITCHFORK_WEB_PATH values are properly logged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 21, 2026 16:15
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@jdx jdx merged commit 31e7f59 into main Feb 21, 2026
9 checks passed
@jdx jdx deleted the feat/web-path-prefix branch February 21, 2026 16:30
@jdx jdx mentioned this pull request Feb 21, 2026
jdx added a commit that referenced this pull request Feb 21, 2026
## 🤖 New release

* `pitchfork-cli`: 1.5.0 -> 1.6.0

<details><summary><i><b>Changelog</b></i></summary><p>

<blockquote>

## [1.6.0](v1.5.0...v1.6.0) -
2026-02-21

### Added

- *(web)* add PITCHFORK_WEB_PATH support for reverse proxy path prefixes
([#244](#244))
- add daemon lifecycle hooks and retry count env vars
([#245](#245))

### Fixed

- pass cwd to ready_cmd spawning
([#243](#243))
</blockquote>


</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Release-only version/documentation updates; no functional code changes
are included in this diff.
> 
> **Overview**
> Prepares the `pitchfork-cli` **v1.6.0** release by bumping the
crate/version metadata from `1.5.0` to `1.6.0` (including `Cargo.toml`,
`Cargo.lock`, and generated CLI docs/spec files).
> 
> Updates `CHANGELOG.md` with the new `1.6.0` entry summarizing the
release’s added features and the `ready_cmd` working-directory fix.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e00649e. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants