diff --git a/.changeset/portable-base-paths.md b/.changeset/portable-base-paths.md new file mode 100644 index 0000000..603070e --- /dev/null +++ b/.changeset/portable-base-paths.md @@ -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. diff --git a/AGENTS.md b/AGENTS.md index 774ce44..ec8687c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/docs/config.html b/docs/config.html index 766ea90..c91a8a9 100644 --- a/docs/config.html +++ b/docs/config.html @@ -91,6 +91,7 @@
+
+ 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 deployment example for complete Nginx + configuration. +
+Machine-global runtime values are resolved in this order:
diff --git a/docs/config.md b/docs/config.md index fd4f343..8eb9282 100644 --- a/docs/config.md +++ b/docs/config.md @@ -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: diff --git a/docs/install.html b/docs/install.html index 01e98e0..4a37f49 100644 --- a/docs/install.html +++ b/docs/install.html @@ -95,6 +95,7 @@ Install through Pi WSL / manual run Remote access + Reverse proxy prefixes Federated machines Manage services Configure @@ -224,6 +225,79 @@
+ The published PI WEB client is deployment-independent. The same package works at the origin root
+ (/) or at canonical nested prefixes such as /ai/ and /test/ai/;
+ no prefix-specific rebuild or PI WEB configuration is needed.
+
+ For a root deployment, proxy / directly to http://127.0.0.1:8504 without
+ rewriting the path. For a nested deployment:
+
/ai, to /ai/. The browser uses the trailing-slash document URL as the application base./ai/ application and preserve required authentication headers and cookies.# http context
+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/ {
+ # The trailing slash strips /ai/ before forwarding.
+ 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;
+ }
+}
+
+ Use the same pattern for /test/ai/ 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 /api/ or
+ /pi-web-plugins/.
+
+ 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 start_url and
+ scope stay inside it as well.
+
diff --git a/docs/machines.html b/docs/machines.html index 38099c1..cf10fa5 100644 --- a/docs/machines.html +++ b/docs/machines.html @@ -174,7 +174,9 @@ PI WEB gateway you opened
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
- https://devbox.example.test/pi-web.
+ https://devbox.example.test/pi-web. The machine registry normalizes the trailing slash; when
+ opening that deployment directly in a browser, use its canonical https://devbox.example.test/pi-web/
+ URL and configure the proxy to redirect the slashless form.
+ 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
+ ./<plugin-id>/plugin.js and legacy plugin-root-relative references such as
+ nested/plugin.js from remote machines.
+
For portable plugin assets, prefer URLs relative to the plugin module, such as
new URL("./asset.json", import.meta.url). If a remote plugin constructs absolute asset URLs,
diff --git a/docs/plugins.md b/docs/plugins.md
index 9a353df..cbcfcd2 100644
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -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
{
@@ -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/