feat(remote): host-unreachable banner, manual wake, and native MAC wake-on-LAN

The reactive wake (typing into a session whose host slept) left the state invisible:
nothing told the user the machine was asleep, and with no wake target configured
there was nothing to do about it. Adds:

- RemoteHost.wakeMac (comma-separated) - Codeman builds and broadcasts the magic
  packet itself (UDP port 9), so the common case needs no external script. The
  existing wakeCommand stays as the explicit override.
- GET /api/sessions/:id/reachability - probes (throttled, cached, and it never
  wakes) and reports HOW the host can be woken, or that nothing is configured.
- POST /api/sessions/:id/wake - wakes, waits, reattaches the pane and flushes
  buffered input; 400 with a routable message when no target is configured.
- The amber host-unreachable banner + its 'Wake' / 'Configure WoL' action, and a
  small config dialog that saves via PUT /api/remote-hosts/:id.
- RemoteWakeDeps.resolveRemote: host config is re-resolved for LIVE sessions
  (throttled + cached), so saving the dialog takes effect without a restart.
This commit is contained in:
Randalix
2026-09-15 14:20:31 +02:00
parent 3f0bfde54a
commit 8b5a13435a
17 changed files with 959 additions and 77 deletions
+10
View File
@@ -3071,6 +3071,9 @@ Object.assign(CodemanApp.prototype, {
const identityFile = document.getElementById('remoteHostIdentityFile').value.trim();
const socksProxy = document.getElementById('remoteHostSocksProxy').value.trim();
const jumpHost = document.getElementById('remoteHostJumpHost').value.trim();
// Wake-on-LAN: keep in sync with `_readRemoteHostFromForm` (the Discover path).
const wakeMac = document.getElementById('remoteHostWakeMac').value.trim();
const wakeCommand = document.getElementById('remoteHostWakeCommand').value.trim();
const extraSshOptions = document.getElementById('remoteHostExtraSshOptions').value
.split('\n')
.map(line => line.trim())
@@ -3108,6 +3111,8 @@ Object.assign(CodemanApp.prototype, {
...(socksProxy ? { socksProxy } : {}),
...(jumpHost ? { jumpHost } : {}),
...(extraSshOptions.length ? { extraSshOptions } : {}),
...(wakeMac ? { wakeMac } : {}),
...(wakeCommand ? { wakeCommand } : {}),
...(codexCommand ? { commands: { codex: codexCommand } } : {}),
};
const hostRes = await fetch('/api/remote-hosts', {
@@ -3568,6 +3573,9 @@ Object.assign(CodemanApp.prototype, {
const socksProxy = document.getElementById('remoteHostSocksProxy').value.trim();
const jumpHost = document.getElementById('remoteHostJumpHost').value.trim();
const codexCommand = document.getElementById('remoteHostCodexCommand').value.trim();
// Wake-on-LAN: keep in sync with `linkRemoteCase`'s inline payload.
const wakeMac = document.getElementById('remoteHostWakeMac').value.trim();
const wakeCommand = document.getElementById('remoteHostWakeCommand').value.trim();
const extraSshOptions = document.getElementById('remoteHostExtraSshOptions').value
.split('\n')
.map(line => line.trim())
@@ -3587,6 +3595,8 @@ Object.assign(CodemanApp.prototype, {
...(socksProxy ? { socksProxy } : {}),
...(jumpHost ? { jumpHost } : {}),
...(extraSshOptions.length ? { extraSshOptions } : {}),
...(wakeMac ? { wakeMac } : {}),
...(wakeCommand ? { wakeCommand } : {}),
...(codexCommand ? { commands: { codex: codexCommand } } : {}),
};
},