Skip to content

Commit bf96161

Browse files
committed
feat: add opt-in LAN dashboard access
1 parent 5051e97 commit bf96161

28 files changed

Lines changed: 451 additions & 25 deletions

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,22 @@ parallax status # check health + running projects
6666
parallax stop
6767
```
6868

69+
To access a headless machine's dashboard from a trusted internal network, opt in when starting:
70+
71+
```bash
72+
parallax start --network-access
73+
```
74+
75+
Parallax prints the network URL, such as `http://cerebro.local:9372`. Network access is
76+
unauthenticated and allows dashboard users to approve work and modify configuration, so enable it
77+
only on a trusted network. Localhost-only access remains the default.
78+
6979
## CLI
7080

7181
```bash
7282
parallax --version
7383
parallax init # first-time setup wizard
74-
parallax start [--server-api-port <port>] [--server-ui-port <port>] [--concurrency <count>]
84+
parallax start [--server-api-port <port>] [--server-ui-port <port>] [--concurrency <count>] [--network-access]
7585
parallax stop
7686
parallax status
7787
parallax open
@@ -92,7 +102,8 @@ See [docs/slack-bot.md](docs/slack-bot.md) for the full setup guide.
92102

93103
## Dashboard
94104

95-
The dashboard is accessible at `http://localhost:9372` (default):
105+
The dashboard is accessible at `http://localhost:9372` by default. With
106+
`parallax start --network-access`, it is also available through the host's network name or IP:
96107

97108
- **Tasks** — live task list with plan approval and log streaming
98109
- **Projects** — add, edit, and delete project configurations

docs/cli-reference.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Notes:
7979
Start orchestrator and dashboard in background.
8080

8181
```bash
82-
parallax start [--server-api-port <port>] [--server-ui-port <port>] [--concurrency <count>]
82+
parallax start [--server-api-port <port>] [--server-ui-port <port>] [--concurrency <count>] [--network-access]
8383
```
8484

8585
`parallax start` reads project and secret configuration from `~/.parallax/config.json`.
@@ -90,8 +90,13 @@ Examples:
9090
```bash
9191
parallax start
9292
parallax start --server-api-port 9371 --server-ui-port 9372 --concurrency 2
93+
parallax start --network-access
9394
```
9495

96+
`--network-access` binds the dashboard and API to all network interfaces and prints a LAN URL such
97+
as `http://cerebro.local:9372`. The default remains localhost-only. Network mode is unauthenticated
98+
and should only be used on a trusted internal network.
99+
95100
## parallax stop
96101

97102
Stop background processes recorded in the running manifest.
@@ -165,4 +170,5 @@ Common files:
165170

166171
- `config.json`: project and integration configuration (managed by `parallax init` and the dashboard)
167172
- `running.json`: process manifest (`orchestratorPid`, `uiPid`, ports, start timestamp)
173+
- `running.json` also records whether the current runtime enabled network access
168174
- `parallax.db`: SQLite state database

docs/getting-started.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,24 @@ What this does:
8383
- launches the background orchestrator and dashboard
8484
- reads projects and secrets from `~/.parallax/config.json`
8585

86+
### Optional: access a headless machine over the local network
87+
88+
On a trusted internal network, start Parallax with:
89+
90+
```bash
91+
parallax start --network-access
92+
```
93+
94+
The startup output includes a network dashboard URL, for example:
95+
96+
```text
97+
http://cerebro.local:9372
98+
```
99+
100+
You can also use the machine's LAN IP address. This mode has no authentication: anyone who can
101+
reach it can approve tasks and modify Parallax configuration and secrets. Without
102+
`--network-access`, both the dashboard and API remain bound to localhost.
103+
86104
## 6. Open the dashboard
87105

88106
```bash
@@ -91,6 +109,8 @@ parallax open
91109

92110
Or open `http://localhost:9372` in your browser.
93111

112+
For a remote browser, use the network URL printed by `parallax start --network-access`.
113+
94114
The dashboard has three sections (left navigation):
95115

96116
- **Tasks** — live task list, plan approval, log streaming

docs/troubleshooting.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,50 @@ parallax stop
5151
parallax start --server-api-port 3000 --server-ui-port 8080 --concurrency 2
5252
```
5353

54+
## Dashboard cannot be reached over the local network
55+
56+
Parallax binds to localhost unless network access is explicitly enabled:
57+
58+
```bash
59+
parallax stop
60+
parallax start --network-access
61+
```
62+
63+
Use the network URL printed at startup or by `parallax status`, for example
64+
`http://cerebro.local:9372`.
65+
66+
### Vite says the host is not allowed
67+
68+
An error such as:
69+
70+
```text
71+
Blocked request. This host ("cerebro.local") is not allowed.
72+
```
73+
74+
means the development dashboard was started without network mode. Restart the full Parallax runtime
75+
with `parallax start --network-access`; do not edit the installed `vite.config.js`.
76+
77+
### The `.local` hostname does not resolve
78+
79+
On macOS, check the Bonjour hostname:
80+
81+
```bash
82+
scutil --get LocalHostName
83+
```
84+
85+
Try `<LocalHostName>.local`, or use the Mac's LAN IP address instead. Ensure both devices are on the
86+
same network and that client isolation is disabled on the router or access point.
87+
88+
### The hostname resolves but the connection is refused
89+
90+
- Confirm `parallax status` shows a network dashboard URL.
91+
- Allow incoming connections for Node.js in **System Settings → Network → Firewall**.
92+
- Confirm ports `9371` and `9372`, or your custom API/UI ports, are not blocked by host or network
93+
firewall rules.
94+
95+
Network access has no authentication. Enable it only on a trusted internal network because remote
96+
dashboard users can approve tasks and modify configuration and secrets.
97+
5498
## Task actions fail
5599

56100
### Unknown task id

packages/cli/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ What this flow does:
3636

3737
The dashboard is at `http://localhost:9372` after `parallax start`.
3838

39+
For a trusted internal network, `parallax start --network-access` also exposes the dashboard through
40+
the machine hostname or LAN IP. This mode is unauthenticated; localhost-only access remains the
41+
default.
42+
3943
## How it works
4044

4145
- Parallax stores all configuration and runtime state under `~/.parallax`

packages/cli/src/args.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,25 @@ function parseStrictPort(args: string[], key: string, fallback: number): number
6868
}
6969

7070
export function parseStartOptions(args: string[]): StartCommandOptions {
71-
const allowedFlags = new Set(['--server-api-port', '--server-ui-port', '--concurrency'])
71+
const allowedFlags = new Set([
72+
'--server-api-port',
73+
'--server-ui-port',
74+
'--concurrency',
75+
'--network-access',
76+
])
7277
for (let index = 0; index < args.length; index += 1) {
7378
const arg = args[index]
7479
if (arg.startsWith('--')) {
7580
const flag = arg.includes('=') ? arg.split('=')[0] : arg
7681
if (!allowedFlags.has(flag)) {
7782
throw new Error(`Unsupported flag for parallax start: ${arg}`)
7883
}
84+
if (flag === '--network-access') {
85+
if (arg.includes('=')) {
86+
throw new Error('--network-access does not accept a value.')
87+
}
88+
continue
89+
}
7990
if (!arg.includes('=')) {
8091
index += 1
8192
}
@@ -98,7 +109,7 @@ export function parseStartOptions(args: string[]): StartCommandOptions {
98109
throw new Error('--server-api-port and --server-ui-port must be different.')
99110
}
100111

101-
return { apiPort, uiPort, concurrency }
112+
return { apiPort, uiPort, concurrency, networkAccess: hasFlag(args, 'network-access') }
102113
}
103114

104115
export function parseStopOptions(args: string[]): StopCommandOptions {

packages/cli/src/commands/start.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fsSync from 'node:fs'
33
import path from 'node:path'
44
import { createRequire } from 'node:module'
55
import { parseStartOptions } from '../args.js'
6+
import { buildDashboardUrl, resolveNetworkHostname } from '../network.js'
67
import {
78
isProcessAlive,
89
readFileTail,
@@ -70,6 +71,7 @@ export async function runStart(args: string[], context: CliContext) {
7071
apiPort: options.apiPort,
7172
uiPort: options.uiPort,
7273
concurrency: options.concurrency,
74+
networkAccess: options.networkAccess,
7375
})
7476
const workspaceDevMode = process.env.NODE_ENV === 'dev'
7577
const orchestratorStdoutPath = path.join(dataDir, 'orchestrator.stdout.log')
@@ -122,14 +124,20 @@ export async function runStart(args: string[], context: CliContext) {
122124
'@parallax/ui',
123125
'start',
124126
'--host',
125-
'0.0.0.0',
127+
options.networkAccess ? '0.0.0.0' : '127.0.0.1',
126128
'--port',
127129
String(options.uiPort),
128130
],
129131
context.rootDir,
130-
{
131-
VITE_PARALLAX_API_BASE: `http://localhost:${options.apiPort}`,
132-
},
132+
options.networkAccess
133+
? {
134+
VITE_PARALLAX_API_PORT: String(options.apiPort),
135+
PARALLAX_NETWORK_ACCESS: 'true',
136+
}
137+
: {
138+
VITE_PARALLAX_API_BASE: `http://localhost:${options.apiPort}`,
139+
PARALLAX_NETWORK_ACCESS: 'false',
140+
},
133141
{
134142
stdoutPath: uiStdoutPath,
135143
stderrPath: uiStderrPath,
@@ -168,6 +176,7 @@ export async function runStart(args: string[], context: CliContext) {
168176
uiPid: uiPid || undefined,
169177
apiPort: options.apiPort,
170178
uiPort: options.uiPort,
179+
networkAccess: options.networkAccess,
171180
},
172181
null,
173182
2
@@ -179,6 +188,14 @@ export async function runStart(args: string[], context: CliContext) {
179188
console.log(`${GREEN}✓ Parallax started in background.${RESET}`)
180189
console.log(`${DIM}Orchestrator PID:${RESET} ${orchestratorPid}`)
181190
console.log(`${DIM}Dashboard:${RESET} http://localhost:${options.uiPort}`)
191+
if (options.networkAccess) {
192+
console.log(
193+
`${DIM}Network dashboard:${RESET} ${buildDashboardUrl(resolveNetworkHostname(), options.uiPort)}`
194+
)
195+
console.log(
196+
`${YELLOW}Warning: network access is unauthenticated. Anyone on this trusted network can control Parallax and modify its configuration.${RESET}`
197+
)
198+
}
182199
console.log(`${DIM}Projects:${RESET} ${storedConfig.projects.length}`)
183200
console.log('')
184201
console.log('')

packages/cli/src/commands/status.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import path from 'node:path'
22
import { sleep } from '@parallax/common'
33
import { parseStatusOptions } from '../args.js'
4+
import { buildDashboardUrl, resolveNetworkHostname } from '../network.js'
45
import { startSpinner, isProcessAlive } from '../process.js'
56
import type { CliContext } from '../types.js'
67

@@ -68,6 +69,11 @@ export async function runStatus(args: string[], context: CliContext) {
6869
output.push(`${RED}✗ Parallax status: issues detected.${RESET}`)
6970
output.push(`${DIM}Orchestrator PID:${RESET} ${state.orchestratorPid}`)
7071
output.push(`${DIM}Dashboard:${RESET} http://localhost:${state.uiPort}`)
72+
if (state.networkAccess) {
73+
output.push(
74+
`${DIM}Network dashboard:${RESET} ${buildDashboardUrl(resolveNetworkHostname(), state.uiPort)}`
75+
)
76+
}
7177
output.push('')
7278
output.push(...errors)
7379
return
@@ -88,6 +94,11 @@ export async function runStatus(args: string[], context: CliContext) {
8894
output.push(`${GREEN}✓ Parallax status: healthy.${RESET}`)
8995
output.push(`${DIM}Orchestrator PID:${RESET} ${state.orchestratorPid}`)
9096
output.push(`${DIM}Dashboard:${RESET} http://localhost:${state.uiPort}`)
97+
if (state.networkAccess) {
98+
output.push(
99+
`${DIM}Network dashboard:${RESET} ${buildDashboardUrl(resolveNetworkHostname(), state.uiPort)}`
100+
)
101+
}
91102

92103
if (projects.length > 0) {
93104
output.push('')

packages/cli/src/config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,19 @@ export function parseRunningState(raw: string, source: string): RunningState {
5858
(parsed as { orchestratorPid: number }).orchestratorPid <= 0 ||
5959
(parsed as { apiPort: number }).apiPort <= 0 ||
6060
(parsed as { uiPort: number }).uiPort <= 0 ||
61+
('networkAccess' in parsed &&
62+
typeof (parsed as { networkAccess?: unknown }).networkAccess !== 'boolean') ||
6163
('uiPid' in parsed && typeof (parsed as { uiPid?: unknown }).uiPid !== 'number') ||
6264
(typeof (parsed as { uiPid?: unknown }).uiPid === 'number' &&
6365
(parsed as { uiPid: number }).uiPid <= 0)
6466
) {
6567
throw new Error(`Invalid running manifest at ${source}.`)
6668
}
6769

68-
return parsed as RunningState
70+
return {
71+
...(parsed as RunningState),
72+
networkAccess: (parsed as { networkAccess?: boolean }).networkAccess === true,
73+
}
6974
}
7075

7176
export async function loadRunningState(

packages/cli/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ async function resolveDefaultApiBase(): Promise<string> {
8484

8585
function buildEnvConfig(
8686
dataDir: string,
87-
runtime: { apiPort: number; uiPort: number; concurrency: number }
87+
runtime: { apiPort: number; uiPort: number; concurrency: number; networkAccess: boolean }
8888
) {
8989
const existingNodeOptions = process.env.NODE_OPTIONS?.trim()
9090
const sqliteWarningSuppression = '--disable-warning=ExperimentalWarning'
@@ -99,6 +99,7 @@ function buildEnvConfig(
9999
PARALLAX_SERVER_API_PORT: String(runtime.apiPort),
100100
PARALLAX_SERVER_UI_PORT: String(runtime.uiPort),
101101
PARALLAX_CONCURRENCY: String(runtime.concurrency),
102+
PARALLAX_NETWORK_ACCESS: String(runtime.networkAccess),
102103
}
103104
}
104105

0 commit comments

Comments
 (0)