41 Commits
Author SHA1 Message Date
Adam Shiervani de90126017 Fix e2e flakyness 2026-06-10 11:33:14 +02:00
Adam Shiervani d5d473bdbd Fix e2e flakyness 2026-06-09 20:50:45 +02:00
Adam Shiervani 800c0cbb53 Add USB audio gadget permission setting (#1479)
* fix disabled audio gadget

* add usb audio device setting

* polish usb audio disabled hint

* stabilize disconnect key release test

* clear audio when usb audio is disabled

* guard audio config with usb gadget
2026-05-29 08:57:10 +02:00
Adam Shiervani f332ceefbf feat: add idle host display setting (#1476)
* feat: hide host display while idle

* feat: add idle display setting

* fix: handle host display advertisement errors

* fix: serialize host display advertisement decisions

* fix: suppress transient HDMI startup error

* docs: explain HDMI startup grace period

* fix: avoid redundant EDID reapply on sessions

* fix: reset video playing state on reconnect
2026-05-25 11:52:22 +02:00
Adam Shiervani c0fcfcba61 Stabilize remote agent input E2E tests (#1477) 2026-05-25 11:51:45 +02:00
Adam Shiervani 90750bf609 Experimental Audio Support (#1475)
* WIP: USB audio capture via UAC1 gadget with G.722 + PCMU encoding

Adds ALSA capture from the USB UAC1 gadget, G.722/PCMU encoding, a WebRTC
audio track, and an e2e remote-agent flow that plays a tone on the remote
host and verifies it reaches the browser.

Snapshot of codex-driven implementation before simplification.

* Simplify USB audio path: drop residual scaffolding from long session

Backend:
- audio.go: drop source rotation + "no-data reopen with next device" loop.
  One source (UAC1Gadget; falls back to hw:1,0 only if sysfs lookup fails).
- internal/audio: remove unused Reader interface and unavailableCapture stub.
  Stub now returns the concrete type with an error.
- webrtc.go: inline single-use resolveAudioCodec helper; DRY video/audio RTCP
  drain into drainRTCP; fold startSessionAudio into the connect callback.

Frontend:
- devices.$id.tsx: drop remoteMediaStreamRef track-merging. Backend tracks
  share stream ID "kvm", so pion delivers them in one MediaStream — just
  assign event.streams[0].
- WebRTCVideo.tsx: replace dynamic per-track <audio> creation + ref array
  with a single hidden <audio> bound to mediaStream.

Remote agent:
- Drop PipeWire/wpctl detection path; plughw: works directly.
- Drop killStaleAudioToneProcesses pkill workaround; the (cmd, cancel, done)
  trio collapses to a single *exec.Cmd field with Start/Kill/Wait.

E2E:
- ra-audio.spec.ts: drop attachAudioDiagnostics scaffold and openReadyPage
  duplicate. Spec is now linear: setup → wait for track → diff stats → tone.

Net: ~355 LOC removed.

* Fix onSessionConnected race; replace onFirst/CurrentSessionConnected split

The previous codex split routed audio start through onCurrentSessionConnected,
gated on session == currentSession. But currentSession is assigned by the
caller (web.go, cloud.go) AFTER ExchangeOffer returns, while
OnICEConnectionStateChange can fire from inside ExchangeOffer or shortly
after — racing the assignment. When the race hits, the equality check fails,
the callback is skipped, and audio never starts.

Pass session into the callback directly so the per-session setup uses the
session in hand, not whatever currentSession happens to point to at that
instant. Keep stopVideoSleepModeTicker on the count-edge (still only on
first-session) and let onSessionConnected handle the rest unconditionally.

* Pin WebRTC playout-delay to zero so receivers don't ratchet under stress

Chrome's adaptive receive-side jitter buffer grows under stress (e.g.
playing a video on the controlled machine) and does not reliably shrink
back; the Connection Stats "Playback Delay" graph used to climb to
~300 ms and stay there until the page was reloaded.

The trigger is the USB UAC1 audio path, not video motion per se — once
real audio starts flowing, Chrome's AV-sync layer pulls the video
jitter buffer up to whatever the audio path settles at, and the ratchet
locks in. Receiver-side hints (jitterBufferTarget, playoutDelayHint,
setMinimumJitterBufferDelay) cap the steady state but don't release a
buffer that has already grown.

Fix: register the WebRTC playout-delay RTP header extension on both
audio and video and stamp min=max=0 on every outgoing packet via a
pion interceptor. Chrome treats this as an authoritative override of
its adaptive logic and keeps both buffers at the decoder floor through
and after stress, with no peer-connection rebuild needed.

Test: drive the host display with a real audio+video file via
gst-launch playbin (audio routed through PipeWire to the USB UAC1
sink) and assert receive-side video delay stays bounded both during
and after playback.

* Simplify audio capture loop and ALSA reader

- Drop short-read zero-fill in ALSA reader; return ErrNoAudioData so the
  capture loop emits no frame for the cycle instead of half-silent audio.
- Replace ErrNoAudioData = io.ErrNoProgress (wrong semantic) with a domain
  sentinel and remove the unused idleReads debug counter.
- Encoders sum all source samples before one divide — better precision,
  fewer ops; clampS16 and sampleS16 helpers gone.
- Resolve audio codec inline in runAudioCapture; drop the audioCodecForTrack
  wrapper. Caller checks AudioTrack != nil so startAudio no longer accepts
  nil as a stop signal.
- Use C.GoString instead of hand-rolled cString helper.
- Add a why-comment on the separate <audio> element (video stays muted).

* Trim playoutdelay per-symbol comments

Keep the package-level "why" (Chrome's one-way jitter buffer); drop
restate-the-signature comments on Factory, NewFactory, NewInterceptor,
and BindLocalStream.

* e2e: extract shared audio helpers and drop dev-only override

- Lift ensureNoPasswordViaAPI and waitForAudioStream into helpers.ts (the
  audio spec was inlining both, the latter as a copy of waitForVideoStream).
- ra-audio.spec.ts shrinks from 78 to 55 lines.
- Remove the JETKVM_AUDIO_DEVICE override in the remote agent: it fabricated
  an AudioDeviceInfo with is_jetkvm=true regardless of what device the env
  var pointed at, silently lying to the spec's assertion. Audio device
  discovery via aplay + /proc/asound/.../usbid is reliable; if no JetKVM
  device is present the spec already skips.

* Reopen ALSA capture on persistent read errors

The C-side recovers EPIPE/ESTRPIPE via snd_pcm_recover; the errors that
surface to Go (EBADFD, ENODEV, …) usually mean the handle is dead —
typically a USB gadget rebuild or host reattach mid-session, which used
to leave audio silent until the session disconnected.

After 5 consecutive non-idle read errors, close and reopen the capture
with exponential backoff (100 ms → 2 s cap). Initial open uses the same
helper so we keep retrying instead of giving up if the gadget isn't ready
yet. Re-resolves the card each attempt so a USB re-enumeration that
shifts the card number is picked up automatically.

* Add Audio settings page with Enable Audio toggle (experimental)

Audio is opt-in via device config. New Audio nav entry in Settings sits
next to Video, with a single "Enable Audio" item marked Experimental
(mirrors HTTPS Mode in Access).

Backend:
- Config.AudioEnabled (default false), persisted to /userdata/kvm_config.json
- getAudioConfig / setAudioConfig JSON-RPC handlers
- webrtc.go: extract attachAudioTrack helper; skip track creation when
  disabled or when the offer advertises no supported codec. The SDP
  answer leaves the audio m-line inactive, so flipping the toggle
  requires a fresh connection (page reload).

Frontend:
- New devices.$id.settings.audio.tsx — fetches state via getAudioConfig,
  saves via setAudioConfig, optimistic UI with rollback on error.
- devices.$id.tsx always offers audio in the SDP; backend decides.
- en.json + 13 locale files: 5 keys each (audio_*, settings_audio) with
  proper translations honoring per-language formality.

E2E:
- ra-audio.spec.ts: connect, enable via RPC, reload, verify audio energy.
  Restores disabled state in a finally block so other specs aren't
  affected. 9 s on kvm-2 + .180.

* Reload page after enabling audio so the browser prompts for autoplay

Re-negotiation only happens on a fresh WebRTC session, and the autoplay
overlay needs a user gesture to play the new audio track. A simple reload
covers both — the toggle's user click acts as the gesture, the new offer
includes audio, and the overlay surfaces normally.

Disable path is unchanged (audio stops naturally on the next connect).

* Reload page on disable too so audio stops immediately

* Trim audio_* copy across all 14 locales

Page header and item description previously said roughly the same thing
in long form. Now: page-level describes the topic ("Stream audio from
the host to your browser"); item-level is a terse one-liner ("Stream
HDMI audio from the host."). Drops the "Requires a fresh connection"
clause — the page auto-reloads on toggle, so it's no longer accurate.

Per-language tone follows I18N_BEST_PRACTICES.md: formal Sie/vous/usted/
вы/chi (de/fr/es/ru/cy), informal du (sv/nb/da), polite です/ます (ja),
infinitive (it), European Portuguese (pt).

* Fold audio e2e into the standard remote-agent project

Drop the dedicated test_audio_e2e Makefile target and the separate
remote-agent-audio Playwright project. The remote-agent project now
matches every ra-*.spec.ts under e2e/remote-agent, so make test_e2e
runs ra-audio.spec.ts alongside ra-all.spec.ts in the same worker.

* Fall back to bare-track MediaStream when ontrack streams[] is empty

Hit a state where pc.getReceivers() showed live video and audio tracks
but useRTCStore.mediaStream stayed undefined — the SDP answer arrived
without a=msid, so event.streams[0] was undefined and
setMediaStream(undefined) left the store empty even though RTP was
flowing. Only a hard reload recovered.

Now: when the event carries a stream, use it as before. When it doesn't,
get-or-create a MediaStream and append the track. Re-using the existing
store value across both ontrack invocations keeps audio + video on the
same MediaStream so the autoplay/video pipeline downstream is unchanged.

* Close peer connection before reload-on-toggle

Firefox's soft reload doesn't always tear down the RTCPeerConnection,
which leaves the post-reload page in a half-renegotiated state: tracks
arrive on receivers but never attach to a MediaStream, so video stays
stuck on "Loading…" (or the page falls back to the pre-connect blue
background) until a hard refresh. Closing the PC explicitly before
reload guarantees a clean start.

* Aggregate ontrack into one canonical MediaStream

The answer SDP from pion omits a=msid for the audio track in some
configurations (visible on Firefox: video keeps its msid, audio doesn't).
The previous handler called setMediaStream(event.streams[0]) on each
ontrack, so:

  1. video ontrack → setMediaStream(streamA)  [has video]
  2. audio ontrack → setMediaStream(streamB)  [synthetic, audio only]

streamB replaces streamA, video disappears. Hard refresh only "fixed" it
incidentally — the same SDP would break the next negotiation too.

Now: ignore event.streams[0], maintain one canonical MediaStream in the
store, and addTrack into it on every ontrack. Browsers render tracks
added to a live MediaStream that's already attached to srcObject, so
both audio and video stay attached regardless of which order ontrack
fires or whether the SDP carried msid.

* Only render <audio> element when device audio is enabled

The backend keeps the m=audio section in the SDP even when audio is
disabled (just inactive direction), so Firefox still attaches a muted
audio track to the MediaStream. The autoplay <audio> element then
triggers Firefox's "block audio" policy on a stream that will never
actually play any sound.

Fetch getAudioConfig once the RPC channel is up, then conditionally
render the <audio> element. No autoplay prompt when audio is off.

* Drop stale mediaStream on reconnect; release audio capture when owner session ends

* Back off briefly on idle ALSA reads to avoid CPU spin

* Re-wire <audio> when track arrives late; gate VideoStart to first session
2026-05-22 10:43:30 +02:00
Adam Shiervani e3595cdea2 Add explicit USB wake control (#1471) 2026-05-22 10:22:48 +02:00
Adam Shiervani 5427b80248 test(e2e): preserve remote agent diagnostics (#1457)
* test(e2e): lower s3 wake version gate

* test(e2e): preserve device logs across restarts

Capture failing device logs before later restarts can truncate last.log, and keep a rotated copy available during global teardown.
2026-05-19 10:34:32 +02:00
Adam Shiervani 6419d049a2 Fix webrtc session shutdown race (#1468) 2026-05-18 10:22:15 +02:00
Adam Shiervani b878ba9e20 fix: align S3 wake test version gate (#1458) 2026-05-12 10:27:14 +02:00
Adam Shiervani df5dbea431 fix(keyboard): keep modifiers out of auto-release (#1438)
* fix(keyboard): keep modifiers out of auto-release
Prevent per-key auto-release from dropping held modifiers during jitter while keeping explicit cleanup paths covered by E2E tests.

* fix(keyboard): keep modifiers out of auto-release
Prevent per-key auto-release from dropping held modifiers during jitter while keeping explicit cleanup paths covered by E2E tests.

* chore(keyboard): trim autorelease comments

Keep comments focused on keyboard behavior and remove branch-specific narrative from the tests.

* fix(keyboard): reset keepalive timing on key state changes

Reset session keepalive timing on every keyboard state change so stale gaps do not poison later holds under modifiers.
2026-05-01 14:48:25 +02:00
Adam Shiervani 73d78ef984 fix(test): tighten remote-agent wake gating
Retry relative-only horizontal wheel input after USB re-enumeration and require system 0.2.9 for S3 wake tests so hardware-dependent cases only run where the wake path is expected to work.
2026-04-08 12:39:41 +02:00
Adam Shiervani 1c478b1264 fix(test): harden remote agent serial console checks
Improve the remote-agent e2e helpers to escape SSH commands, time out stuck SSH calls, and retry transient failures. Replace fixed ttyACM sleeps with polling so serial console tests wait for the actual remote host state.
2026-04-07 14:09:01 +02:00
Adam Shiervani a53fd6e731 fix(test): use relative mouse wake for S3 suspend (#1402)
Use relative mouse reports for the S3 wake e2e path because absolute mouse movement is not a reliable wake source on some hosts. This keeps the test aligned with a wake path that passes against the current JetKVM and remote host setup.
2026-04-07 13:06:43 +02:00
Adam Shiervani 49f30d9dde test(e2e): add S3 suspend/wake tests via USB remote wakeup (#1392)
Add two e2e tests that verify JetKVM can wake a host from S3 sleep
using HID input (keyboard and mouse). Tests trigger suspend via
`echo mem > /sys/power/state`, wait for the host to become
unreachable, then send HID reports and verify recovery.

Both tests skip gracefully when system firmware is below 0.2.8
(missing wakeup_on_write kernel support) or when the remote host
doesn't support S3 deep sleep.

Also adds a `semverGte` helper for version comparisons.
2026-04-02 15:01:14 +02:00
Adam Shiervani 73a67e88ab fix(mouse): prevent double wheel scroll by sending to only one HID device (#1390)
rpcWheelReport was unconditionally sending wheel events to both the
absolute and relative mouse HID devices. The host OS sees two separate
USB HID devices each producing a scroll event, so it processes both,
doubling the effective scroll distance (e.g. 6 lines instead of 3 on
Windows).

Send wheel events to the absolute mouse device when enabled, falling
back to the relative mouse device otherwise. This mirrors how pointer
reports already work (absMouseReport vs relMouseReport are separate).

Add an E2E test that verifies exactly one wheel event is received per
wheelReport RPC call when both mouse devices are enabled.
2026-04-01 10:58:58 +02:00
Adam Shiervani 87eac39529 fix(keyboard): prevent modifier key auto-release during typing (#1386) (#1387)
Key-repeat events (fired at ~30Hz by the browser for held keys) were
cancelling and restarting the keepalive interval on every keydown. Since
the repeat rate (~33ms) is shorter than the keepalive period (50ms), the
keepalive tick could never fire. When a second key was pressed and the
modifier's repeat stopped, the modifier's 100ms auto-release timer
expired with no keepalive to extend it.

Fix: start the keepalive interval on first key press and leave it
running undisturbed until all keys are released. Track held keys
client-side via a Set to know when to start/stop the interval.

Also adds six e2e tests covering:
- Key-repeat simulation (rapid repeated presses without releases)
- Modifier held across rapid tap burst (20 keys at 50ms spacing)
- Multiple simultaneous modifiers (Ctrl+Shift+key)
- Reversed release order (modifier up before non-modifier)
- AltGr (AltRight) held while tapping
- Modifier held while tapping 10 keys over 10 seconds

Closes #1386
2026-04-01 10:03:26 +02:00
Adam Shiervani 41ea6b93ca fix(test): fix e2e flakiness from hook timeouts, EDID drops, and USB recovery (#1382)
OTA beforeAll/afterAll hooks were using the global 60s timeout while
performing multi-reboot sequences that need minutes. Add explicit
test.setTimeout(420000) inside each hook.

setEDID RPC can drop the WebSocket on some devices during HDMI re-link.
Tolerate RPC timeouts, reconnect page + WebRTC afterwards, and retry
agent resolution checks with a 15s polling loop.

EBUSY CDROM test used sysfs traversal that broke across kernel/host
variations. Switch to lsblk and skip gracefully when the host doesn't
enumerate USB mass storage as sr* devices.

USB recovery test left the device bricked (initUsbGadget crash loop)
when auto-recovery failed. Add try/catch that re-binds the UDC and
reboots on failure.

OTA reconnectAfterReboot retries increased from 15 to 30 (65s → 95s)
to handle slower devices.
2026-03-30 19:26:09 +02:00
Adam Shiervani c7344ed673 fix(test): fix e2e flakiness from HDMI signal not detected during beforeAll (#1377)
The remote host display may enter DPMS sleep during the build/deploy
phase, causing the KVM to report "No HDMI signal detected" when the
remote-agent tests start. Wake the display via DPMS before waiting,
increase the timeout from 15s to 30s, and add a page-reload retry
if the WebRTC session connected before the signal was detected.

Authored-by: Adam Shiervani <adam.shiervani@gmail.com>
2026-03-30 00:16:05 +02:00
Adam Shiervani 785c9c1b7c fix(test): fix e2e flakiness from RPC timeouts and stale session dialogs (#1376)
setEDID blocks ~7.5s for HDMI renegotiation, which could exceed the
hardcoded 10s RPC timeout. Add configurable timeout to sendJsonRpc and
use 20s for all setEDID calls. Also handle the "Use Here" session dialog
after page reload in beforeAll, and wait for video stream before LED tests.
2026-03-29 23:25:45 +02:00
Adam Shiervani 2167272ef1 fix(keyboard): serialise keyboard state mutations to eliminate race (#1369)
* fix(keyboard): serialise keyboard state mutations to eliminate race

keypressReport performed a read-modify-write on keysDownState across
three separate lock acquisitions, allowing concurrent callers (e.g. two
auto-release timers, or an auto-release racing with a session-disconnect
clear) to interleave and produce lost updates — leaving stale keys in
the device state after disconnect.

Replace the per-operation keyboardWriteHidFileLock with a keyboardMutex
that covers the entire read-compute-write-update sequence in both
keypressReport and KeyboardReport. This makes all keyboard state
mutations serialisable, eliminating the race by construction rather than
patching individual call sites.

Also cancel pending auto-release timers on session close (avoids
unnecessary HID writes) and query device state directly via JSON-RPC
in the E2E test (removes Zustand store timing dependency).

* fix(lint): align goimports formatting in config.go
2026-03-29 01:51:49 +01:00
Adam Shiervani 8612779ab8 fix(e2e): kill remote-agent before scp to avoid ETXTBSY (#1368)
Linux prevents overwriting a running binary. Move the pkill step
before the scp so the binary is stopped before we copy the new one,
then start it without the redundant kill+sleep.
2026-03-29 00:16:38 +01:00
Adam Shiervani c08f14ff3f fix: send mouse button state changes via reliable WebRTC channel (#695) (#1338)
* fix: send mouse button state changes via reliable WebRTC channel (#695)

When holding one mouse button and pressing another without moving the mouse,
only pointerdown/pointerup events fire (no mousemove to self-correct). These
button-only state changes were sent via the unreliable WebRTC data channel
(maxRetransmits: 0), and lost packets were never recovered.

Changes:
- useHidRpc.ts: Track last button state and send button changes via the
  reliable channel. Movement-only events continue using the unreliable
  channel for low latency, since lost movement packets self-correct via
  subsequent mousemove events.
- e2e/remote-agent/main.go: Fix omitempty on InputEvent.Value so button
  release events (value=0) are included in JSON responses.
- ra-all.spec.ts: Add E2E test that holds left mouse button, presses and
  releases right mouse button, and verifies all 4 button events arrive
  on the remote host (20 iterations).

* fix: rebuild remote agent when source changes to prevent stale deploys

ensureDeployed() skipped rebuild and redeploy when the agent was already
running, causing the omitempty fix on InputEvent.Value to never reach
the remote host. Now compares source mtime against binary mtime and
forces redeploy when a rebuild occurs.
2026-03-28 23:14:02 +01:00
Adam Shiervani b3634cf463 test: add keyboard keepalive and auto-release e2e tests (#1346)
Add 8 baseline tests for the keepalive/auto-release code path with
timings matched to the actual Go constants (100ms DefaultAutoReleaseDuration,
50ms keepalive interval, 100ms baseExtension).

Tests cover: held key survival with keepalives, auto-release without
keepalives, window blur release, arrow key hold, modifier+key combos,
simultaneous keys, rapid tap cycles, and long (2s) holds.
2026-03-28 20:56:37 +01:00
Adam Shiervani 203c6ae6fd fix: recover HID chardev after DWC3 rebind race on RV1106 (#1366)
* fix: reset USB gadget when virtual media unmount fails with EBUSY (#834)

When unmountImageLocked() gets EBUSY from the kernel (host OS still
accessing the virtual disk via PREVENT MEDIUM REMOVAL), fall back to
gadget.RebindUsb(true) to force-disconnect the host, then retry the
unmount. Uses RebindUsb directly instead of UpdateGadgetConfig to avoid
hitting the same EBUSY when writing configfs attributes before rebind.

After rebind, properly reopen keyboard HID file (ResetHIDFiles + sleep +
OpenKeyboardHidFile) matching the pattern in setMassStorageMode().

Also propagate unmount errors to RPC callers and only clear
currentVirtualMediaState after the unmount actually succeeds.

Adds E2E test that mounts an ISO on the remote host to trigger PREVENT
MEDIUM REMOVAL, then verifies unmount succeeds and keyboard recovers.

* fix: recover HID chardev after DWC3 rebind race on RV1106

The DWC3 USB controller on the RV1106 has a race condition where rapid
unbind→bind of the UDC can permanently corrupt HID chardev state —
/dev/hidg0 returns ENXIO even though the device node exists and the UDC
shows "configured". This can be triggered by UpdateGadgetConfig's
transaction rebind and by host-initiated USB device resets during mass
storage media changes.

Three-part fix:

1. rebindUsb(): after binding, verify /dev/hidg0 is openable. If not,
   unbind again with a 100ms pause for kernel cleanup, then rebind.

2. setMassStorageMode(): pre-set recovery timer before UpdateGadgetConfig
   to prevent the poller from interfering. After the 1s sleep, if
   OpenKeyboardHidFile fails, do a corrective RebindUsb + retry.

3. checkUSBState() poller: when a state transition occurs and
   OpenKeyboardHidFile fails, trigger a corrective rebind to recover
   from host-initiated USB resets that corrupt the chardev.

* fix: suppress USB recovery poller before rebind in unmount and mode-change paths

The auto-recovery poller could see transient "not attached" UDC state
during RebindUsb and trigger a competing rebind, corrupting HID chardev
state. Add setUSBRecoveryTimer calls before the rebind in
unmountImageLocked and before the corrective rebind in setMassStorageMode.

* test: replace blind sleep with two-phase wait in factory-reset e2e test

Wait for device to become unreachable before polling for it to come back,
preventing false passes from stale pre-reset responses.

* refactor: simplify branch — extract helpers, remove duplication, fix flaky tests

- Extract rebindAndRecoverHID() in Go to deduplicate USB recovery sequences
- Remove redundant setUSBRecoveryTimer() call after UpdateGadgetConfig()
- Extract waitForKeyboardReady() helper replacing 5 duplicate retry loops
- Consolidate 3 duplicate remoteExec definitions into single remoteHostExec()
- Use shared SSH_OPTS from helpers.ts instead of hardcoded SSH options
- Fix remote agent omitempty on mouse X/Y causing undefined in TypeScript
- Poll keys-down state in disconnect test to avoid race condition

* fix: remove dead IsHidgChardevHealthy export, reset HID files before rebind

- Remove unused exported IsHidgChardevHealthy wrapper (only the unexported
  isHidgChardevHealthy is called, inside rebindUsb)
- Move ResetHIDFiles() before RebindUsb in checkUSBState so stale file
  handles are closed even if the rebind fails — prevents silent mouse
  write failures on dead inodes after a successful unbind + failed bind
2026-03-28 20:56:21 +01:00
Adam Shiervani 9bf63aacfb fix: reset USB gadget when virtual media unmount fails with EBUSY (#1331)
When unmountImageLocked() gets EBUSY from the kernel (host OS still
accessing the virtual disk via PREVENT MEDIUM REMOVAL), fall back to
gadget.RebindUsb(true) to force-disconnect the host, then retry the
unmount. Uses RebindUsb directly instead of UpdateGadgetConfig to avoid
hitting the same EBUSY when writing configfs attributes before rebind.

After rebind, properly reopen keyboard HID file (ResetHIDFiles + sleep +
OpenKeyboardHidFile) matching the pattern in setMassStorageMode().

Also propagate unmount errors to RPC callers and only clear
currentVirtualMediaState after the unmount actually succeeds.

Adds E2E test that mounts an ISO on the remote host to trigger PREVENT
MEDIUM REMOVAL, then verifies unmount succeeds and keyboard recovers.
2026-03-28 20:51:05 +01:00
Adam Shiervani 76e748a820 fix: USB HID startup recovery, unreliable channel fallback, and e2e test stability (#1364)
* fix: USB HID startup recovery and e2e test stability

- fix(usb): always rebind UDC on Init() to guarantee clean HID
  function driver state. After factory reset + reboot, the configfs
  entries may exist from the previous boot but the kernel's internal
  HID function attachment is broken (/dev/hidg0 returns ENXIO).
  The changeset resolver skipped the bind because the UDC file
  content matched — but content match != working. Rebinding on
  every startup is cheap (brief USB re-enumeration) and guarantees
  a clean state.

- fix(ui): fall back to reliable HID channel when unreliable WebRTC
  data channel is not yet established. Prevents silent mouse event
  drops during the brief window after page reload.

- fix(e2e): suppress SSH known-hosts warnings with LogLevel=ERROR,
  replace zsh-incompatible glob patterns with find(1), fix nested
  SSH quoting, increase USB rebind timeouts, add keyboard warmup
  after EDID changes, reorder tests for stability.

* test: remove Polish diacritics, WoL broadcast, and factory reset UI tests

* fix(test): assert keyboard recovery after EDID restore instead of silently passing

The retry loop captured no result and had no assertion, so a timeout
would let the test pass without verifying HID actually recovered.
2026-03-28 13:40:04 +01:00
Adam Shiervani 72d27ac85e feat: add custom broadcast IP option to Wake-on-LAN (#1238) (#1345)
* fix: add custom broadcast IP option to Wake-on-LAN (#1238)

Add support for specifying a custom subnet broadcast IP when sending
WOL magic packets, enabling wake across different subnets.

Backend:
- Add broadcastIP optional parameter to rpcSendWOLMagicPacket
- Add OptionalParams support to RPCHandler for params with zero defaults
- Pass broadcastIP query param through HTTP handler

UI:
- Add broadcast address dropdown (Auto/Custom) to WOL dialog
- Show subnet broadcast IP input when Custom is selected
- Pass broadcastIP to RPC call when custom mode is active

* fix: move broadcast address field to add form only, default to Auto (#1238)

* fix(ui): simplify WoL broadcast dropdown and indent custom field

- Rename "Auto (global broadcast)" to "Auto" in the broadcast address
  dropdown
- Wrap the custom subnet IP input in a nested indent with left border,
  matching the settings page pattern (NestedSettingsGroup style)

* fix(i18n): use localization system for WoL broadcast address labels

Replace hardcoded English strings with m.xxx() calls in the broadcast
address UI and add the 4 new keys to all 14 locale files.
2026-03-28 12:54:57 +01:00
Adam Shiervani edaa86c0d3 feat: add USB CDC-ACM serial console gadget (#726) (#1352)
* fix: add USB serial console toggle to hardware settings (#726)

* fix: add USB CDC-ACM serial console gadget function (#726)

Add serial_console.go with acm.usb0 gadget config item following the
mass_storage pattern. Add SerialConsole bool to Devices struct and wire
it through config.go enable check and jsonrpc.go setUsbDeviceState.

The existing UI toggle in UsbDeviceSetting.tsx (with localization
messages) now calls through to the backend correctly.

When enabled, the KVM device creates /dev/ttyGS0 and the target host
sees a CDC-ACM serial device (/dev/ttyACM*). When disabled, the
symlink is removed from the USB gadget config and the host no longer
enumerates the ACM interface.

* fix: add CDC-ACM Console terminal UI for USB serial gadget (#726)

* fix: merge terminal buttons into split button and rename CDC-ACM to USB Serial Console (#726)

Combine KVM Terminal and USB Serial Console into a split button when both
are present, make USB serial console state reactive via zustand store so
the action bar updates without a page refresh, and fix the split button
chevron not respecting the disabled state.
2026-03-27 23:09:39 +01:00
Adam Shiervani 711158ab18 fix: modifier key auto-release and keyboard reset on disconnect (#641) (#1339)
* fix: modifier key auto-release and keyboard reset on disconnect (#641)

- Fix performAutoRelease() to check state.Modifier bitmask for modifier
  keys (0xE0-0xE7) instead of only checking state.Keys array, which never
  contained modifiers
- Release all keys (send all-keys-up HID report) when WebRTC session
  disconnects to prevent stuck keys
- Add keyboard state reset in onLastSessionDisconnected() as safety net

* test(e2e): add modifier auto-release and disconnect key-release tests

- Add test verifying modifier keys (Ctrl, Shift, Alt) auto-release after
  timeout using direct JSON-RPC to bypass browser keepalive
- Add test verifying all held keys are released when WebRTC session
  disconnects, checking both host-side events and device-side state
- Add getKeysDownState helper to e2e helpers
2026-03-27 22:38:12 +01:00
Adam Shiervani f05ef925f8 feat: add Polish (Polski) keyboard layout for paste support (#566) (#1348) 2026-03-27 22:18:51 +01:00
Adam Shiervani e735f7d228 fix(video): align VENC virtual width/height to 16 bytes for non-standard resolutions (#699) (#1347)
The RV1106 hardware H.264 encoder (VENC) requires virtual width and height
to be aligned to 16-pixel boundaries. Resolutions like 1366x768 (where 1366
is not divisible by 16) caused the encoder to fail silently, producing no
video output and triggering an infinite retry loop.

Changed RK_ALIGN_2 to RK_ALIGN_16 for u32VirWidth and u32VirHeight in both
the VENC attribute initialization and frame submission paths.
2026-03-27 16:51:29 +01:00
Adam Shiervani 2fa35124f2 feat(ui): add toggles to hide header and status bars (#1333) (#1343)
* fix: add toggles to show/hide header bar and status bar in Appearance settings (#1333)

* fix: invert panel visibility toggles to hide header/status bars (#1333)
2026-03-27 16:14:14 +01:00
Adam Shiervani 8f890565ce fix: reset HID file handles after disk-mode virtual media mount (#560) (#1349)
* fix: reset HID file handles after mass storage mode change triggers USB rebind (#560)

* fix: add E2E regression test for disk-mode virtual media keyboard loss (#560)

* fix: include config backup file in factory reset paths

The backup config (.bak) created by SaveBackupConfig() during config
validation failures was not removed during factory reset, potentially
leaking sensitive user data (MQTT credentials, network settings).
2026-03-27 16:00:20 +01:00
Adam Shiervani b3ce335922 feat: add factory reset to replace config-only reset (#529) (#1355)
* fix: implement factory reset replacing config-only reset (#529)

- Add rpcFactoryReset that removes all user data (config, images,
  TLS certs, SSH keys, serial settings, crash dumps) and reboots
- Remove rpcResetConfig RPC handler (keep internal resetConfig for
  OTA and native event use)
- Replace Reset Config UI with Factory Reset button (danger theme)
  and confirmation dialog in Settings > Advanced
- Update localization: add factory reset keys, remove reset config keys
- Add E2E test verifying factory reset UI and dialog copy
- Update ra-all factory reset test to restore SSH keys after reset

* fix: ensure factory reset reboots even when path removal fails

The early return on error exited rpcFactoryReset before reaching the
goroutine that triggers hwReboot, leaving the device partially wiped
with no reboot. Log the warning instead and always fall through to
the reboot.

* fix: remove hardcoded screenshot path from factory reset e2e test
2026-03-27 15:52:52 +01:00
Adam Shiervani 71db3b91ab fix: add settling delay when waking HDMI capture chip from sleep (#519) (#1356)
* fix: add settling delay in VideoStart() when waking from sleep mode (#519)

* test: improve e2e test reliability and reduce flakiness

- Replace fire-sleep-assert with polling in wheel scroll tests
- Add retry loops for macro test and RPC setup after USB re-enumeration
- Add waitForRpcReady helper to handle session dialogs and stale pages
- Remove flaky paste modal UI test (redundant with keyboard scan tests)
- Preserve SSH keys and dev mode across config resets in setup/teardown
- Add saveSSHDevState/restoreSSHDevState helpers

* fix: trim sysfs whitespace in getSleepMode() so sleep detection works

Linux sysfs attributes include a trailing newline, so comparing raw
content with "1" always returned false. Use strings.TrimSpace to match
the convention used elsewhere in the codebase.
2026-03-27 15:30:29 +01:00
Adam Shiervani fc64d6d2f3 feat: add 4th and 5th button support for absolute mouse (#416) (#1357) 2026-03-27 14:22:22 +01:00
Adam Shiervani cf7215411d feat: add horizontal mouse wheel scrolling support (#415) (#1358)
* fix: add horizontal mouse wheel (AC Pan) scroll support (#415)

- HID descriptors: add AC Pan (Usage 0x0238, Consumer Page) to both
  absolute and relative mouse descriptors for horizontal scroll
- Backend: extend AbsMouseWheelReport to accept wheelX, add
  RelMouseWheelReport with both axes, update report_length
- RPC: add wheelX parameter to wheelReport binding
- Frontend: read deltaX in wheel handler with same clamping/inversion
  and throttling as vertical scroll
- E2E: add wheel scroll test verifying both vertical (REL_WHEEL) and
  horizontal (REL_HWHEEL) events reach the remote host

* style: fix goimports alignment in RelMouseReport

* fix: don't negate horizontal scroll direction in wheelReport

The clampWheel helper was negating the result for both axes, but only
vertical scrolling needs inversion (browser deltaY and HID Wheel use
opposite sign conventions). Horizontal scrolling (deltaX / AC Pan)
shares the same convention (positive = right), so negation reversed the
direction on the target machine.

* fix: wire RelMouseWheelReport into RPC and add wheel scroll e2e tests

rpcWheelReport only called AbsMouseWheelReport, so wheel scrolling was
silently broken in relative-only mouse mode. Now calls both Abs and Rel
wheel report methods (each guards on its own enabledDevices flag).

Adds e2e tests for vertical/horizontal wheel scroll in default mode and
relative-only mode. Bumps beforeAll waitForInputDevices timeout to 30s
and keyboard LED test expectKeyPress timeouts to 5s to reduce flakiness.
2026-03-27 14:10:55 +01:00
Adam Shiervani c176ee20f9 fix: prevent cursor jumping to top-left on window blur (#392) (#1359)
* fix: prevent cursor jumping to top-left on window blur (#392)

On window blur/visibilitychange, resetMousePosition was sending
sendAbsMouseMovement(0, 0, 0), which moved the target cursor to the
top-left corner. This could trigger hot-corner actions on the target.

Track the last sent absolute position in a ref and use it in
resetMousePosition to only release mouse buttons (buttons=0) without
changing the cursor position.

* fix(e2e): add SSH retry logic and keepalives for high-latency links

Consolidate SSH options into a shared SSH_OPTS constant with increased
ConnectTimeout (10→30s), ServerAliveInterval, and ServerAliveCountMax.
Add retry logic (3 attempts with backoff) to sshExec for transient
connection errors (reset, refused, timed out, no route).

* fix(e2e): reset device config in global teardown

Always reset the device config and restart the app after a test run so
the device is left in a clean state regardless of pass/fail.
2026-03-27 13:41:27 +01:00
Adam Shiervani a6d56d7820 fix(e2e): re-read caps_lock LED state each retry iteration (#1319)
The retry loop captured initialCaps once before entering the loop.
If a try-tap failed to reach the host but the undo-tap in the catch
block succeeded, the host's actual caps state diverged from
initialCaps. Every subsequent iteration then toggled in the wrong
direction, guaranteeing the 15s deadline was exhausted.

Fix: re-read the LED state at the top of each iteration so the
expected value always reflects reality.

Made-with: Cursor
2026-03-23 17:22:42 +01:00
Adam ShiervaniandClaude Opus 4.6 15dc380062 fix: auto-recover USB gadget when host power-cycles (#1297)
* fix: auto-recover USB gadget when host reconnects (#128)

When the USB host reboots or disconnects, the UDC state becomes
"not attached" and never recovers. Add automatic recovery that detects
this state and rebinds the USB gadget, with rate limiting to avoid
thrashing. Also refactor keyboard HID file handling to support
force-reopen after rebind.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: move USB recovery logic to internal/usbgadget package

Extract ShouldAttemptUSBRecovery and its retry interval constant into
the internal/usbgadget package so the logic is testable without
importing the top-level kvm package. Reset the recovery timer in
updateUsbRelatedConfig to prevent auto-recovery from interfering
during the host's USB re-enumeration window after a deliberate
config change.

Made-with: Cursor

* feat(e2e): add JSON-RPC data channel support to test hooks

Expose the WebRTC RPC data channel through test hooks and add a
sendJsonRpc helper that sends a JSON-RPC request over the channel
and resolves via callback with timeout handling. This enables e2e
tests to invoke backend RPC methods directly.

Made-with: Cursor

* refactor(e2e): restructure tests with remote-agent suite

Move device-level e2e tests (config-reset, EDID, HTTPS, HDMI sleep,
LED, mouse, USB attach timing, USB device) into a consolidated
remote-agent suite that runs through a Go-based agent binary. Add a
separate Playwright project for remote-agent tests.

- Remove standalone spec files now covered by ra-all.spec.ts
- Rename login-rate-limit to zz-login-rate-limit so it runs last
  (avoids needing a post-test reboot to clear rate-limit state)
- Reorder welcome-password tests so validation runs first, reusing
  the onboarding state and saving an SSH reset cycle

Made-with: Cursor

* refactor(e2e): clean up test helpers and reduce duplication

Consolidate all test helpers into a single helpers.ts file, removing
the separate ota-helpers.ts. This gives every test file a single import
source and eliminates duplicated code across the test suite.

Key changes:
- Merge ota-helpers.ts into helpers.ts (mock server, binary deployment,
  device config, env var validation, triggerUpdate, withTempSignature)
- Remove duplicated rpc/restartAppViaSSH/waitForDeviceReady functions
  from ra-all.spec.ts in favour of shared imports
- Extract loginAndOpenSettings helper in settings-local-auth tests
- Extract getOTAEnvVars, toPreReleaseVersion, triggerUpdate, and
  withTempSignature to reduce boilerplate across OTA tests
- Remove unused verifyMouseWorks function and dead variables
- Strip redundant JSDoc that just restated type signatures
- Remove duplicated per-project use config from playwright.config.ts
  (already inherited from top-level)
- Convert dynamic imports in sshExec to top-level imports

Made-with: Cursor

* refactor(e2e): move binary deployment into Playwright globalSetup

Replace the shell-script deployment logic with Playwright's
globalSetup/globalTeardown hooks. When BASELINE_BINARY_PATH is set,
globalSetup deploys the binary, resets device config, reboots, and
captures pre-test logs. globalTeardown captures post-test logs.

This keeps the deployment lifecycle inside Playwright where it belongs,
and reduces test_core_e2e.sh to a thin wrapper that sets env vars.

Made-with: Cursor

* fix: retry HID file reopen after USB gadget rebind

After rebinding the DWC3 USB controller, the kernel needs a moment to
create the /dev/hidg* device nodes. The previous code attempted to
reopen the keyboard HID file immediately after rebind, which raced
with the kernel and failed with "no such device or address".

Add a retry loop (up to 10 attempts, 200ms apart) to wait for the
device nodes to appear before reopening the keyboard HID file.

Made-with: Cursor

* fix: harden USB gadget recovery after UDC unbind

Reset stale HID gadget handles after rebind, suppress transient HID-open errors during detach windows, and fall back to full gadget reconfiguration when simple UDC rebind does not restore keyboard HID promptly. Strengthen the remote-agent USB recovery E2E to verify both keyboard and mouse input recover after unbind with retry tolerance for host-side input node churn.

Made-with: Cursor

* refactor: simplify USB HID error handling and reduce hot-path overhead

- Use errors.Is with syscall.Errno instead of string matching in
  IsHIDTemporarilyUnavailableError (robust, zero-alloc)
- Cache USB state in usbReadyForHidReports instead of reading sysfs
  on every HID report
- Extract rpcHidReport wrapper to deduplicate 5 rpc*Report functions
- Fix openWithTimeout goroutine/fd leak on timeout
- Add USBStateNotAttached/USBStateUnknown constants, replace literals
- Deduplicate discoverJetKVMDevices by delegating to listInputDevices

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: run remote-agent e2e tests when JETKVM_REMOTE_HOST is provided

Made-with: Cursor

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 12:54:20 +01:00