Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/portable-base-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@jmfederico/pi-web": patch
---

Support root and nested reverse-proxy deployments with one published client, including scoped PWA assets, WebSockets, and local or federated plugins.
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ Project-specific testing rules live in `.agents/skills/testing-guide/SKILL.md`.

Use that skill whenever writing, modifying, reviewing, or planning tests, closing coverage gaps, triaging test failures, or creating test helpers/harnesses. Keep detailed testing conventions there rather than growing this top-level orientation file.

## Client application URL convention

- Build PI WEB-owned browser paths as application-relative references without a leading slash, for example `api/...` and `pi-web-plugins/...`.
- Encode every dynamic path segment with `encodeURIComponent`; encode query values, using `URLSearchParams` for multi-field queries.
- Resolve each reference exactly once at the browser boundary: ordinary JSON HTTP paths go to `request()`, direct browser APIs receive URLs from helpers backed by `resolveAppUrl()`, and WebSockets use `resolveAppWebSocketUrl()`.
- Name helpers returning unresolved application references with a `Path` suffix and helpers returning browser-ready absolute values with a `Url` suffix.
- Plugin module references must go through `resolvePluginModuleUrl()`. Its leading-slash handling is the documented rolling-compatibility exception; do not introduce other leading-root app references.
- Pre-JavaScript HTML assets use Vite `%BASE_URL%`; PWA manifest references stay `./`-relative. External links, data URLs, and module-relative plugin assets are not application paths.
- To assess deviations, search production client code for raw `fetch`, `WebSocket`, `XMLHttpRequest`, URL-bearing DOM attributes, and leading `/api` or `/pi-web-plugins` literals. Every app-owned result must follow one of the boundaries above.
- Published nested deployments require a canonical trailing slash; the reverse proxy must redirect a slashless prefix before serving the app.

## Configuration conventions

- `$PI_WEB_DATA_DIR` (`~/.pi-web` by default) contains PI WEB-managed state such as `projects.json` and `machines.json`; do not treat it as the user-editable config API.
Expand Down
17 changes: 17 additions & 0 deletions docs/config.html
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ <h1>Configure PI WEB where your agents work.</h1>
<aside class="toc" aria-label="Config page contents">
<strong>On this page</strong>
<a href="#files">Config files</a>
<a href="#deployment-paths">Deployment paths</a>
<a href="#precedence">Precedence and reloads</a>
<a href="#global-config">Global config</a>
<a href="#project-config">Project config</a>
Expand Down Expand Up @@ -133,6 +134,22 @@ <h2>Config files</h2>
</p>
</section>

<section id="deployment-paths">
<h2>Reverse-proxy deployment paths</h2>
<p>
The deployment path is not a PI WEB config-file key or environment setting. The published client is
portable: one build works at <code>/</code> and at canonical trailing-slash prefixes such as
<code>/ai/</code> or <code>/test/ai/</code>.
</p>
<p>
For a nested deployment, redirect the slashless prefix to the trailing-slash URL, strip the prefix
before forwarding to PI WEB, and proxy authenticated HTTP and WebSocket traffic through the same
location. Relative browser and PWA URLs then stay within that prefix. See the
<a href="install#reverse-proxy-prefix">reverse proxy deployment example</a> for complete Nginx
configuration.
</p>
</section>

<section id="precedence">
<h2>Precedence and reloads</h2>
<p>Machine-global runtime values are resolved in this order:</p>
Expand Down
6 changes: 6 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ Pi package settings are separate from PI WEB config. They live in Pi's package-m

If you installed services with a custom config path, rerun `pi-web install --config /path/to/config.json` after changing that path or after upgrading from a version that only applied the custom path to the web service. This regenerates service files so the web/API and session daemon use the same `PI_WEB_CONFIG`.

## Reverse-proxy deployment paths

The deployment path is not a PI WEB config-file key or environment setting. The published client is portable: one build works at `/` and at canonical trailing-slash prefixes such as `/ai/` or `/test/ai/`.

For a nested deployment, redirect the slashless prefix to the trailing-slash URL, strip the prefix before forwarding to PI WEB, and proxy authenticated HTTP and WebSocket traffic through the same location. Relative browser and PWA URLs then stay within that prefix. See the [reverse proxy installation guide](https://pi-web.dev/install#reverse-proxy-prefix) for a complete Nginx example.

## Precedence and reloads

Machine-global runtime values are resolved as:
Expand Down
74 changes: 74 additions & 0 deletions docs/install.html
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ <h1>Get PI WEB running where your agents work.</h1>
<a href="#pi-package">Install through Pi</a>
<a href="#manual-run">WSL / manual run</a>
<a href="#remote-access">Remote access</a>
<a href="#reverse-proxy-prefix">Reverse proxy prefixes</a>
<a href="#federated-machines">Federated machines</a>
<a href="#manage-services">Manage services</a>
<a href="#configure">Configure</a>
Expand Down Expand Up @@ -224,6 +225,79 @@ <h2>Remote access</h2>
</div>
</section>

<section id="reverse-proxy-prefix">
<h2>Reverse proxy root and path-prefix deployments</h2>
<p>
The published PI WEB client is deployment-independent. The same package works at the origin root
(<code>/</code>) or at canonical nested prefixes such as <code>/ai/</code> and <code>/test/ai/</code>;
no prefix-specific rebuild or PI WEB configuration is needed.
</p>
<p>
For a root deployment, proxy <code>/</code> directly to <code>http://127.0.0.1:8504</code> without
rewriting the path. For a nested deployment:
</p>
<ol>
<li>Redirect the slashless prefix, such as <code>/ai</code>, to <code>/ai/</code>. The browser uses the trailing-slash document URL as the application base.</li>
<li>Strip the prefix before forwarding. PI WEB continues to serve root paths on its localhost listener.</li>
<li>Apply authentication to the whole served <code>/ai/</code> application and preserve required authentication headers and cookies.</li>
<li>Forward WebSocket upgrades through the same location as HTTP, API, image, PWA, and plugin traffic.</li>
</ol>
<div class="code-card">
<div class="copy-row">
<strong>Nginx path-prefix proxy</strong>
<button class="copy-button" data-copy="#nginx-prefix-proxy">Copy</button>
</div>
<pre id="nginx-prefix-proxy"><code><span class="comment"># http context</span>
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

server {
listen 443 ssl;
server_name pi.example.com;

ssl_certificate /etc/letsencrypt/live/pi.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/pi.example.com/privkey.pem;

auth_basic "PI WEB";
auth_basic_user_file /etc/nginx/pi-web.htpasswd;

location = /ai {
return 308 /ai/$is_args$args;
}

location ^~ /ai/ {
<span class="comment"># The trailing slash strips /ai/ before forwarding.</span>
proxy_pass http://127.0.0.1:8504/;
proxy_http_version 1.1;

proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Authorization $http_authorization;
proxy_set_header Cookie $http_cookie;

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 1h;
}
}</code></pre>
</div>
<p>
Use the same pattern for <code>/test/ai/</code> by changing both Nginx locations. If your proxy uses
bearer tokens, SSO, or another authentication mechanism, keep that policy on the prefixed application
location and continue forwarding the headers or cookies it requires; the slashless redirect serves no
PI WEB content. Do not create unprotected exceptions for <code>/api/</code> or
<code>/pi-web-plugins/</code>.
</p>
<p>
Once the proxy follows this contract, relative client assets, images, PWA assets, API calls, local and
federated plugins, and WebSocket URLs stay inside the prefix. Installed PWA <code>start_url</code> and
scope stay inside it as well.
</p>
</section>

<section id="federated-machines">
<h2>Federated machines</h2>
<p>
Expand Down
4 changes: 3 additions & 1 deletion docs/machines.html
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ <h2>Set up another runtime</h2>
<p>
Prefer a private path such as NetBird, Tailscale, WireGuard, private LAN, SSH tunnel, or an authenticated reverse
proxy. If the remote is behind a path prefix, include that prefix in the machine URL, for example
<code>https://devbox.example.test/pi-web</code>.
<code>https://devbox.example.test/pi-web</code>. The machine registry normalizes the trailing slash; when
opening that deployment directly in a browser, use its canonical <code>https://devbox.example.test/pi-web/</code>
URL and configure the proxy to redirect the slashless form.
</p>
<div class="callout danger">
Do not expose PI WEB directly to the public internet. Register machines only over trusted network paths
Expand Down
8 changes: 8 additions & 0 deletions docs/plugins.html
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,14 @@ <h2>Remote machine plugins</h2>
UI should come from the selected PI WEB instance; on remote machines, the gateway copy is hidden unless
the remote machine exposes its own copy.
</p>
<p>
Current PI WEB manifests publish leading application-root module references. The browser keeps them
inside the current application base, so local and federated plugins follow root or nested reverse-proxy
deployments without a prefix-specific build while remaining compatible with existing gateways.
Federated gateways also accept manifest-relative references such as
<code>./&lt;plugin-id&gt;/plugin.js</code> and legacy plugin-root-relative references such as
<code>nested/plugin.js</code> from remote machines.
</p>
<p>
For portable plugin assets, prefer URLs relative to the plugin module, such as
<code>new URL("./asset.json", import.meta.url)</code>. If a remote plugin constructs absolute asset URLs,
Expand Down
6 changes: 4 additions & 2 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ Rules:

### Manifest and assets

The manifest contains each discovered plugin module:
The manifest contains each discovered plugin module. Current PI WEB releases emit `module` as a leading application-root reference:

```json
{
Expand All @@ -341,9 +341,11 @@ The manifest contains each discovered plugin module:
}
```

The browser maps leading application-root references into the current application base, so the same manifest works at the origin root or under a reverse-proxy path prefix. Keeping this output format also lets gateways from existing PI WEB releases consume plugins from an upgraded remote machine. For compatibility, federated gateways additionally accept explicit manifest-relative references such as `./my-plugin/pi-web-plugin.js` and legacy plugin-root-relative references such as `nested/pi-web-plugin.js`; all accepted forms are rewritten to deployment-portable, gateway-relative references.

`source` describes where the plugin came from (`bundled`, `local`, or the Pi package source). `scope` is `bundled`, `local`, `user`, or `project`. `machineSpecific` controls whether the gateway copy is valid for remote machines or only each selected machine's own copy can appear.

A plugin can fetch its own static assets with URLs under:
At an origin-root deployment, a plugin's static assets are available under:

```text
/pi-web-plugins/<plugin-id>/<path-inside-plugin-root>
Expand Down
6 changes: 3 additions & 3 deletions src/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>PI WEB</title>
<meta name="theme-color" content="#0d1117" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="%BASE_URL%favicon.svg" />
<link rel="apple-touch-icon" href="%BASE_URL%apple-touch-icon.png" />
<link rel="manifest" href="%BASE_URL%manifest.webmanifest" />
<style>
:root {
color-scheme: dark;
Expand Down
8 changes: 4 additions & 4 deletions src/client/public/manifest.webmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@
"name": "PI WEB",
"short_name": "PI WEB",
"description": "Remote web UI and browser control plane for persistent Pi Coding Agent sessions.",
"start_url": "/",
"scope": "/",
"start_url": "./",
"scope": "./",
"display": "standalone",
"background_color": "#0d1117",
"theme_color": "#0d1117",
"icons": [
{
"src": "/pwa-icon-192.png",
"src": "./pwa-icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/pwa-icon-512.png",
"src": "./pwa-icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
Expand Down
Loading