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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ between releases do not reach them. Remote-PC runtime bundles are versioned sepa
## [Unreleased]

### Added
- **Settings → Phone & devices → Portal** gives this PC a public HTTPS address through
[Portal](https://github.com/gosuda/portal-tunnel), with no account and no domain of your own.
With Portal v2.6.1 or later installed, **Start** runs it on a relay you enter, dropping any
`Tailscale-User-Login` header a visitor sends; it starts only with `HERDR_WEB_TOKEN` set. The
address and its QR code show on every signed-in device, any device that can type into your
terminals can stop it, and an address left on comes back when the app restarts.
([#746](https://github.com/devswha/herdr-web-ui/pull/746) by @rabbitson87)
- **Two tabs side by side** on a desktop, from different workspaces or PCs: drag a tab down out
of its strip, or a sidebar row over the pane area, and drop it on the left or right half. A guide
shows the two halves as soon as the drag starts and keeps its side steady at the centre; Escape
Expand Down
10 changes: 8 additions & 2 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1225,8 +1225,9 @@ One set for both themes: the card is island black wherever it shows.
lines, Markdown width), **Terminal** (font size, 80 columns in a split, font family, wheel
speed, input mode, Key bar), **Alerts**, **Voice input**, **Subscription usage**,
**Shortcuts** (the platform-resolved global bindings, then **Paging** with a 0–100 line overlap stepper; one line by default, zero for a full viewport), **Phone & devices** (the phone address,
Keep screen on, Install; then paired devices), **Remote PCs**, **About** (Updates, herdr,
the repository links). A button that points at Updates opens the dialog on About.
Keep screen on, Install; then Portal, where the server offers it; then paired devices),
**Remote PCs**, **About** (Updates, herdr, the repository links). A button that points at
Updates opens the dialog on About.
- Every page is built from the same parts (`components/SettingsControls.tsx`). A group is an
optional caption (`--fs-sm`, `--fw-semibold`, `--text-dim`, sentence case) over one card:
`--bg-elevated`, hairline `--border`, `--radius-lg`, its rows parted by hairlines and never
Expand Down Expand Up @@ -1254,6 +1255,11 @@ One set for both themes: the card is island black wherever it shows.
workspace-list toggle. Under the row, once the server has answered, the bridge auto-update switch.
- Install (a row of the Phone group) reflects installed, promptable or browser-instructions state;
About links the repository.
- Portal, a group of its own between Phone and Devices where the server offers it: while it runs,
the address and QR code as the Phone group draws them over a Relay row with Stop; otherwise one
hint for what blocks it (no token, serve-only, no `portal` or an old one, with a link to get it)
or a wide Relay row whose text field and Start stack on a phone. Starting and stopping are one
dim status line; a failure is the server's sentence over Portal's own lines, folded.
- The running versions are always written, since the sidebar carries none. Updates opens with
**Running vX.Y.Z (commit)**: the server's version and commit, or the client's own build version
before the server answers and where it names neither. While the server runs another version than
Expand Down
11 changes: 8 additions & 3 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ herdr plugin uninstall devswha.herdr-web-ui
tailscale serve --https=<port> off # the port the installer printed, if it served the app
```

The installer's herdr, Bun and Node stay, since other tools may use them: `~/.local/bin/herdr`, `~/.bun`, and `~/.local/share/herdr-web-ui/node` with its link `~/.local/bin/node`. Push keys and paired devices are in `~/.config/herdr-web-ui`.
The installer's herdr, Bun and Node stay, since other tools may use them: `~/.local/bin/herdr`, `~/.bun`, and `~/.local/share/herdr-web-ui/node` with its link `~/.local/bin/node`. Push keys, paired devices and the identity file that owns the app's Portal address are in `~/.config/herdr-web-ui`.

## Supported agents

Expand Down Expand Up @@ -413,6 +413,10 @@ Take `<relay>` from `portal list`; it has to run v2.6.1 or later. A `portal` pro

Portal prints `service ready at https://<name>-<address>.<relay>:443`, where `<address>` is the identity file's 40-character address; with or without `:443` it is the same address. That address belongs to the identity, so nobody else can take it, even while Portal is stopped; keep the identity file private and keep using it, since a new identity gives a new address. Check it as above: `https://<name>-<address>.<relay>/api/session`, opened from another device without signing in, must answer 401. Then open it on the phone and sign in with the token, or pair the phone from **Settings → Phone & devices → Pair a device** on the PC. Use that address, not the shorter `https://<name>.<relay>`, which another identity can claim while Portal is stopped.

**Settings → Phone & devices → Portal** runs that command for you once Portal v2.6.1 or later is installed on this PC ([Portal's install script](https://github.com/gosuda/portal-tunnel#quick-start) puts `portal` in `~/.local/bin`): enter a relay and press **Start**. It starts only with `HERDR_WEB_TOKEN` set and `HERDR_WEB_TAILSCALE_SERVE_ONLY` off, and asks the relay for its version first. The address and its QR code show on every signed-in device, any device that can type into your terminals can stop it, and one left on comes back when the app restarts. The app keeps an identity file of its own in `HERDR_WEB_STATE_DIR`, so its address stays the same from one start to the next and differs from the one a `portal` you run yourself has.

At startup, the app waits for any recorded Portal process to stop before answering requests. If `portal/state.json` under `HERDR_WEB_STATE_DIR` is damaged, or the recorded process cannot be identified or stopped, requests fail closed rather than risk exposing a server restarted without a token. Check the server log. After confirming that this app's Portal process has stopped, repair or remove only `portal/state.json` and restart the app. Keep `portal/identity.json` to preserve the address; never discard the state record while its tunnel may still be running. Servers on different ports need separate state directories to control their own Portal addresses.

**Sign out** in the header or command palette clears this browser's token and device cookies; terminal sessions and agents keep running. It is shown for token or device authentication, not automatic local or Tailscale access.

If `devices.json` under `HERDR_WEB_STATE_DIR` (default `~/.config/herdr-web-ui`) is corrupt or unreadable, the server keeps unrecognized external clients out and preserves the file. Local access and the owner's trusted Tailscale login without a configured token, and a valid token, still work. Settings → Phone & devices and the server log explain the error. Restore a valid registry from backup or fix its permissions, then restart; pairing and device changes remain disabled until it is repaired.
Expand All @@ -438,7 +442,7 @@ Observe connections cannot take a pane, and a displaced bridge never takes it ba
| `HERDR_WEB_CSP` | unset (enforced) | `report-only` sends the policy as `Content-Security-Policy-Report-Only`: the browser reports what it would block and the app keeps working. Used to measure the policy after a change |
| `HERDR_WEB_TAILSCALE_OWNER` | this PC's Tailscale login | The Tailscale login that gets in through `tailscale serve` without pairing. Set it on a PC whose Tailscale node is tagged, which has no login of its own |
| `HERDR_WEB_TAILSCALE_SERVE_ONLY` | unset (off) | `1` declares `tailscale serve` the only way anything reaches this port. Then the owner's own device gets in through serve without a code, on a tailnet one login owns with no tagged node. Enable it only when no public reverse proxy, tunnel or other forwarding server exposes this port: a visitor through one would otherwise get the owner's access |
| `HERDR_WEB_STATE_DIR` | `~/.config/herdr-web-ui` | Push keys, device subscriptions, PC registrations and update builds |
| `HERDR_WEB_STATE_DIR` | `~/.config/herdr-web-ui` | Push keys, device subscriptions, PC registrations, update builds and the app's Portal identity |
| `HERDR_WEB_OPENAI_API_KEY` | unset | OpenAI API key for [voice input](#voice-input). Set here, it cannot be changed from the app |
| `HERDR_WEB_OPENAI_BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible API root for voice input |
| `HERDR_WEB_AUTO_UPDATE` | `0` | `1` installs new releases without asking |
Expand Down Expand Up @@ -581,7 +585,7 @@ No, but a phone needs two things Tailscale gives at once: a way to reach the PC
- **An SSH tunnel from the phone** (Termux, Blink): `ssh -L 7317:127.0.0.1:7317 <pc>`, then open `http://localhost:7317` on the phone. Browsers treat localhost as secure, so installing and alerts should work while the tunnel is up (not verified on iOS yet). The phone still has to reach the PC over SSH.
- **A VPN into your home** (WireGuard, ZeroTier, a router VPN): the LAN address works in the browser, but a plain `http://` address can neither install the app nor receive alerts.
- **A reverse proxy with a real certificate** on a domain you own, with a token set and the proxy sending `X-Forwarded-For`: [Behind a reverse proxy](#behind-a-reverse-proxy) has Caddy and nginx examples to copy. This exposes the server to the internet, so read [Access and safety](#access-and-safety) first.
- **A public address with Portal**, a tunnel with no account and no domain of your own, with a long random token set: [Behind a reverse proxy](#behind-a-reverse-proxy) has the command. This exposes the server to the internet too.
- **A public address with Portal**, a tunnel with no account and no domain of your own, with a long random token set: [Behind a reverse proxy](#behind-a-reverse-proxy) has the command, and **Settings → Phone & devices → Portal** runs it for you. This exposes the server to the internet too.
</details>

<details>
Expand All @@ -594,6 +598,7 @@ Optional features can send data off the PC:
- [Subscription usage](#subscription-usage), when enabled, sends each provider's credentials to that provider's usage endpoint.
- Updates and remote-PC setup fetch releases or configured runtime bundles over the network; remote panes are reached over SSH.
- Enabled push alerts go through the browser vendor's push service as encrypted notifications.
- [Portal](#behind-a-reverse-proxy), while it runs, carries every connection to the app through the relay you chose: the relay sees connection addresses, timing and volume, and holds the certificate for the address.

See [Access and safety](#access-and-safety) for who can connect to the app.
</details>
Expand Down
1 change: 1 addition & 0 deletions scripts/ci-browser.sh
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ run_script scripts/pane-paging-demo-regression.ts
run_script scripts/key-bar-customization-demo-regression.ts
run_script scripts/key-bar-hold-repeat-regression.ts
run_script scripts/settings-pages-demo-regression.ts
run_script scripts/portal-regression.ts
run_script scripts/deep-link-demo-regression.ts
run_script scripts/chat-history-browser-qa.ts
run_script scripts/chat-fallback-browser-qa.ts
Expand Down
3 changes: 2 additions & 1 deletion scripts/ci-browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ describe("browser lane", () => {
it("summarizes every script and exits zero on success without enabling local evidence", async () => {
const { text, code, evidence } = await lane();
expect(code).toBe(0);
expect(rows(text)).toHaveLength(43);
expect(rows(text)).toHaveLength(44);
expect(rows(text).map((row) => row.script)).toContain("scripts/portal-regression.ts");
expect(rows(text).map((row) => row.script)).toContain("scripts/agy-chat-browser-qa.ts");
expect(rows(text).map((row) => row.script)).toContain("scripts/pending-pane-switch-regression.ts");
expect(rows(text).map((row) => row.script)).toContain("scripts/lan-exposure-notice-regression.ts");
Expand Down
Loading
Loading