Commit Graph

546 Commits

Author SHA1 Message Date
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 ebb26463b5 feat: add scroll direction setting for macOS Natural Scrolling (#640) (#1340)
* feat(ui): add invert scroll direction toggle and fix Vite 8 CJS interop

Add scroll direction invert setting for macOS Natural Scrolling support.
Fix react-use-websocket CJS default export not resolving under Rolldown.

Fixes #640

* fix(ui): invert horizontal scroll when invertScroll is enabled

macOS Natural Scrolling inverts both axes at the OS level. The previous
change only corrected vertical scroll, leaving horizontal broken.
2026-03-28 19:38:53 +01:00
Adam Shiervani d03ecb42d8 Revert "feat(ui): add invert scroll direction toggle and fix Vite 8 CJS interop"
This reverts commit 61564e39f6.
2026-03-28 17:33:20 +01:00
Adam Shiervani 61564e39f6 feat(ui): add invert scroll direction toggle and fix Vite 8 CJS interop
Add scroll direction invert setting for macOS Natural Scrolling support.
Fix react-use-websocket CJS default export not resolving under Rolldown.

Fixes #640
2026-03-28 17:32:28 +01:00
Adam Shiervani 3610f69451 chore(ui): remove vite-tsconfig-paths plugin (#1367)
Vite 8 natively supports tsconfig path resolution via resolve.tsconfigPaths,
making the vite-tsconfig-paths plugin redundant.
2026-03-28 17:11:16 +01:00
Adam Shiervani 1809e271e6 fix: swap menu screen padding on display rotation (#637) (#1354)
The physical LCD panel (ST7789V, 300 rows on a 320-row controller) and
touch digitizer are misaligned by ~20px in the Y axis. LVGL's rotation
transform inverts the Y-to-X mapping between 90° and 270°, so the touch
calibration offset must be applied only at 90°.

Changes:
- Swap flex_screen_menu_style padding at 90°/270° (centering fix)
- Apply +20px touch Y calibration via lv_evdev_set_calibration() at 90°
- Recalibrate on rotation change in lvgl_set_rotation()
- Remove fragile framebuffer-based rotation centering e2e test
2026-03-28 16:59:10 +01:00
Adam Shiervani f393a2f9ed fix(i18n): improve translation quality across all 13 languages (#1365)
Add 63-77 missing keys per language (factory reset, MQTT sections/errors,
serial console settings, appearance hide bars). Remove 5 obsolete keys.

Fix systemic mistranslations: keyboard keys vs crypto keys, DHCP lease
terminology, unit symbols (A/W), Gateway as tech term. Language-specific
fixes include de: Kennwort→Passwort, fr: crypté→chiffré, es/sv/nb/da:
keep Wake on LAN/loopback as English, pt: Brazilian→European Portuguese,
sv: Förlängning→Tillägg, it: contratto di locazione→lease.

Add I18N_BEST_PRACTICES.md with per-language guidelines.
2026-03-28 15:55:28 +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 48eeb147eb chore(ui): migrate to Vite 8, oxlint, and oxfmt (#1362)
* chore(ui): migrate to Vite 8, oxlint, and oxfmt

Replace esbuild+Rollup with Oxc+Rolldown (Vite 8), ESLint with oxlint
(with type-aware linting), and Prettier with oxfmt. No source code
changes — formatting and lint fixes will roll out incrementally via
lint-staged as files are touched.

* fix(ui): resolve oxlint errors in existing code

Fix errors caught by oxlint: useless rename, redundant undefined on
optional params, duplicate union type constituent, and unsafe toString
on union type.
2026-03-28 12:47:40 +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 e0211302c0 fix: replace default EDID with JetKVM v1 EDID (#1341)
The old default EDID used Toshiba's manufacturer ID (TSB) from the
TC358743 capture chip, had no CEA-861 extension block, and declared
a very narrow frequency range (1-29Hz vertical). This caused
compatibility issues with NVIDIA proprietary drivers and other strict
EDID validators.

The new EDID uses a JetKVM manufacturer ID (JKV), includes a CEA-861
extension with HDMI vendor specific block and audio support, declares
proper standard timings (1920x1200, 1280x720, 640x480), and has
corrected frequency range descriptors.

Existing devices with the old default EDID are automatically migrated
on config load.
2026-03-27 16:21:03 +01:00
Adam Shiervani 5b64d7ee52 feat: add Portuguese (pt-PT) keyboard layout (#697) (#1337)
* fix: add pt-PT (Portuguese) keyboard layout (#697)

* fix(keyboard): add literal dead key entries to pt-PT layout

The dead key characters (´, `, ¨, ~, ^) were missing from the chars
map, causing them to be silently dropped when pasting text. Add entries
with deadKey: true so the paste logic sends a follow-up Space press to
produce the literal character.
2026-03-27 16:16:43 +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 493ebf56a4 fix: replace hardcoded English strings with i18n message functions (#1162) (#1322)
* fix: replace hardcoded English strings with i18n message functions (#1162)

Replace two hardcoded English strings that bypassed the Paraglide i18n system:

- MacroStepCard.tsx: regex-derived "Left"/"Right" modifier labels now use
  m.macro_modifier_left() and m.macro_modifier_right() message functions
- devices.$id.settings.advanced.tsx: version change acknowledgment checkbox
  label now uses m.advanced_version_change_acknowledged_label()

Added new i18n keys to all locale JSON files and recompiled paraglide output.

* fix(i18n): translate untranslated keys across all languages

Translate `advanced_version_change_acknowledged_label`, `macro_modifier_left`,
and `macro_modifier_right` which were left as English in all non-English locales.

* fix(ui): fix prettier formatting in MacroStepCard

---------

Co-authored-by: AI Agent <ai@coder-ai.local>
2026-03-27 13:26:37 +01:00
Adam Shiervani 5247932179 chore(ui): add husky and lint-staged for pre-commit linting (#1335)
Set up husky to install git hooks from ui/.husky and lint-staged to
run ESLint --fix on staged *.{ts,tsx} files and Prettier --write on
staged *.{ts,tsx,js,jsx,json,css,md} files before each commit.

The prepare script ensures hooks are installed automatically when
contributors run npm install or npm ci in ui/.
2026-03-25 19:05:04 +01:00
Adam Shiervani d2370a155a fix: gate public IP check behind cloud adoption status (#1328)
Prevent HTTP requests to api.jetkvm.com/cdn-cgi/trace when cloud is not
enabled. Previously, visiting the network settings page would trigger
ForceUpdate() which always called checkIPs with true/true, bypassing any
cloud adoption check.

Changes:
- Gate rpcGetPublicIPAddresses refresh behind cloud token/URL check
- Gate rpcCheckPublicIPAddresses behind cloud token/URL check
- Hide PublicIPCard in UI when cloud is not adopted
- Fetch cloud state on network settings page mount to determine visibility
2026-03-25 18:45:45 +01:00
Adam Shiervani db4452d0bb ci: redesign workflows into lint + build (#1330)
Replace 5 workflow files with 2 clean ones:

- lint.yml: Go lint and UI lint as parallel jobs with consistent
  push + pull_request triggers (fixes fork PRs never getting UI lint)
- build.yml: stripped to compile + test only, with pull_request
  trigger added (PRs now get build feedback before merge)

Removed:
- stale-issues.yml (permanent dry-run no-op)
- smoketest.yml (frequently failing device tests)
- dependabot.yml (automated dependency bumps)
- golangci-lint.yml (folded into lint.yml)
- ui-lint.yml (folded into lint.yml)

Also fixed build.yml cache restore-keys to use a proper prefix
fallback instead of duplicating the full key.
2026-03-25 18:44:34 +01:00
Thiago Vinhas 66b71385ad feat(build): add podman support for dev deploy scripts (#1303)
The build scripts hardcoded `docker` as the container runtime. This adds
auto-detection of docker or podman (preferring docker), with a
CONTAINER_CMD env var override.

On non-x86_64 hosts (e.g. Apple Silicon), podman/buildah ignores
--build-arg overrides for the BUILDPLATFORM predefined arg, so the
Dockerfile is patched in the build context to hardcode linux/amd64,
ensuring the correct base image is pulled for cross-compilation.
2026-03-24 18:59:02 +01:00
Adam Shiervani 9f2f1a20bf test(ota): add GPG signing subkey verification tests (#1325)
Add four tests covering the subkey lifecycle that mirrors production
key setups (root key + signing subkeys on YubiKeys):

- Signing with a subkey verifies successfully
- Subkeys survive parseAndValidateKeyring entity filtering
- Rotated subkeys (old and new) both verify
- Revoked subkey is rejected by SigningKeyById

These complement the rogue-entity filtering test from #1316 by
verifying that subkey operations within a trusted entity work
correctly through the full verification pipeline.
2026-03-24 16:15:22 +01:00
Alex Howells ea5f0e5f4a fix(ota): return only the matched entity from parseAndValidateKeyring (#1316)
parseAndValidateKeyring validates that at least one entity in a
fetched keyring matches the pinned root key fingerprint
(rootKeyFingerprint, gpg.go:21). On match, it returns the entire
keyring — including any additional entities the keyserver included
in its response.

This is a problem because openpgp.CheckDetachedSignature iterates
every key in the provided keyring and accepts a signature from any
of them. A compromised or malicious keyserver could return a
response containing the legitimate JetKVM release key (satisfying
the fingerprint check) alongside an attacker-controlled key. A
binary signed with the attacker key would then pass verification
in both VerifySignature and VerifySignatureFromFile, since both
pass the cached keyring directly to CheckDetachedSignature.

The fix is a single-line change: return openpgp.EntityList{entity}
instead of the full keyring when the fingerprint matches. This
ensures only the trusted key is ever used for signature verification
regardless of what a keyserver returns.

TestParseAndValidateKeyring_FiltersRogueKeys exercises this by
constructing a two-entity armored keyring (trusted + rogue),
passing it through parseAndValidateKeyring, asserting the returned
keyring contains exactly one entity with the correct fingerprint,
and confirming that CheckDetachedSignature rejects a signature
produced by the rogue key.

Reported-by: equinox0815

Signed-off-by: Alex Howells <alex@howells.me>
2026-03-24 16:13:58 +01:00
Adam Shiervani eee6d728cd feat(e2e): add OTA upgrade-from-stable test (#1324)
Add a new E2E test that downloads the latest stable production binary
from api.jetkvm.com, deploys it to the device, then OTA-upgrades to
the locally-built binary with a config reset. This catches cross-version
upgrade regressions (config migrations, schema changes) that the
existing mock-only OTA tests cannot detect.
2026-03-24 16:02:02 +01:00
Adam Shiervani 5b143578d6 refactor: move tailscale logic into internal/tailscale package (#1318)
Move types, parsing, exec, and control-URL logic from the root kvm
package into internal/tailscale/ so go test ./... no longer tries to
link ARM-only CGO libraries on x86_64 hosts.
2026-03-24 15:41:44 +01:00
Lukas Wolfsteiner 519e391595 feat(remote): add support for custom tailscale control servers (#1312)
* feat(tailscale): add custom control URL configuration & handling w/ tests
- Introduced TailscaleControlURL in the Config struct to allow configuration of the Tailscale control server.
- Added RPC handlers for getting and setting the Tailscale control URL.
- Updated TailscaleStatus to include controlURL, ensuring it reflects the configured or default value.
- Enhanced parsing and normalization of the control URL to enforce valid formats.
- Updated TailscaleCard component to manage and display the control server URL, allowing users to save changes.

* docs: update README to include optional Tailscale networking feature
Added a new section highlighting the built-in Tailscale status and control-server configuration, including support for custom Headscale-compatible endpoints.

* docs: Extend DEVELOPMENT.md with Tailscale control server details

* fix(tailscale): enhance error handling in rpcSetTailscaleControlURL

- Updated the rpcSetTailscaleControlURL function to revert the TailscaleControlURL to its previous value if saving or applying the new URL fails.
- Added a new test to ensure that the configuration is not saved when the apply command fails, verifying that the previous URL remains intact.
- Adjusted existing tests to validate the order of operations during the URL setting process.

Related to Review: https://github.com/jetkvm/kvm/pull/1312#pullrequestreview-3984856379

* refactor(tailscale): simplify control URL application logic and enhance error handling

- Renamed the test function to better reflect its purpose and updated the test cases to ensure correct command execution.
- Removed fallback logic for applying the Tailscale control URL, streamlining the error handling to return a clear error message when the "set" command fails.
- Added a new test to verify behavior when the "set" command fails, ensuring proper error reporting.

Related to Review https://github.com/jetkvm/kvm/pull/1312#pullrequestreview-3984856379

* refactor(tailscale): update control server configuration in UI & documentation

- Updated the TailscaleCard component to allow users to select between default and custom control server URLs.
- Improved state management for control server URL input based on the selected mode.
- Revised DEVELOPMENT.md to clarify control server application logic and error handling.
- Removed outdated example JSON-RPC payloads for clarity.

Related to Review: https://github.com/jetkvm/kvm/pull/1312#discussion_r2980284611

* refactor(ui): use built-in components and i18n for TailscaleCard

- Replace raw <select> with SelectMenuBasic component
- Use SettingsItem with new SM size for control server setting
- Use NestedSettingsGroup for indented custom URL input
- Add tailscale_* i18n keys to all 14 locale files with translations
- Add size prop (SM/MD) to SettingsItem for compact contexts

---------

Co-authored-by: Adam Shiervani <adam.shiervani@gmail.com>
2026-03-24 15:20:15 +01:00
Patrick Hofmann e7e1a289df MQTT Support / Home Assistant Integration (#1195)
* MQTT support

* feat(mqtt): redesign MQTT settings UI with improved UX

Restructure the MQTT settings page for clarity and usability:

UI Structure:
- Organize settings into logical sections (Auth, Home Assistant, Advanced)
- Use progressive disclosure for port (Auto/Custom) and base topic (Default/Custom)
- Move connection status badge into page header
- Conditionally show HDD debounce only when ATX extension is active
- Add inline validation for required broker field

Connection & Error Handling:
- Add test-then-save flow: Save & Reconnect validates connectivity before persisting
- Add standalone Test Connection button for dry-run validation
- Add testMqttConnection RPC with 5s timeout (no retry, no side effects)
- Surface friendly i18n-ready error messages for common failures (auth, timeout, TLS, DNS)
- Track last connection error on MQTTManager for status reporting

Copy:
- Rewrite all descriptions for clarity and brevity
- Use benefit-oriented, active-voice microcopy throughout

---------

Co-authored-by: Adam Shiervani <adam.shiervani@gmail.com>
2026-03-24 13:49:07 +01:00
Adam Shiervani 5d83caec57 fix: update stale Debian 13 and Ubuntu 24.04 ISO URLs (#1317)
- Debian 13: debian-13.0.0 → debian-13.4.0 (old URL returns 404)
- Ubuntu 24.04: 24.04.3 → 24.04.4 (old URL returns 403)
2026-03-24 10:43:38 +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
Alex Howells 032457c9e5 feat(network): add Tailscale status to Settings > Network (#1276)
* feat(network): add Tailscale status to Settings > Network

Tailscale runs on JetKVM devices but has no visibility in the web UI.
This adds a read-only status card to the network settings page that
surfaces the output of `tailscale status --json` over the existing
JSON-RPC transport.

Backend (Go):

  tailscale.go -- TailscaleStatus/TailscalePeer structs returned by a
  single RPC handler (getTailscaleStatus). isTailscaleInstalled uses
  exec.LookPath; getTailscaleStatus shells out via exec.CommandContext
  with a 10s timeout and parses the JSON response. When the binary is
  absent the handler returns {installed: false} without error. When the
  daemon is unreachable it returns {installed: true, running: false}.

  tailscale_test.go -- table-driven tests for parseTailscaleStatus
  covering Running, NeedsLogin, Stopped, Starting, malformed JSON, and
  empty object inputs. Integration-level tests for getTailscaleStatus
  verify the exec mock path and confirm the function never returns an
  error regardless of environment state.

  jsonrpc.go -- getTailscaleStatus registered in rpcHandlers.
  log.go -- tailscaleLogger subsystem added.

Frontend (TypeScript/React):

  TailscaleCard.tsx -- GridCard component that calls getTailscaleStatus
  on mount. Renders nothing when installed=false. Shows a status badge
  (Connected/Needs Login/Stopped), Tailscale IPv4/IPv6, hostname, DNS
  name, auth URL when NeedsLogin, and health warnings when present.

  stores.ts -- TailscaleStatus and TailscalePeer interfaces.

  devices.$id.settings.network.tsx -- TailscaleCard rendered after
  PublicIPCard.

* fix(ui): address review feedback on TailscaleCard

Remove the health warnings section from TailscaleCard.tsx — these
surface raw Tailscale internal diagnostics (nftables errors, routing
warnings) that are not actionable for end users and clutter the
status card.

Reduce monospace font size on IPv4 and IPv6 address spans from
text-sm (14px) to text-[13px] so the mono typeface visually matches
the surrounding 14px proportional text.

Signed-off-by: Alex Howells <alex@howells.me>

---------

Signed-off-by: Alex Howells <alex@howells.me>
2026-03-17 18:37:03 +01:00
Thiago Vinhas 23123f060c feat: Add detached window with optional toolbar and hostname display (#1187)
* feat: Add detached window with optional toolbar and hostname display

* feat: Remove padding and borders in detached window mode

- Hide dotted background pattern in detached mode
- Remove margins around video container
- Remove min-width/height constraints and border/shadow on video

* simplify: use fixed window size for detached window

* refactor: reuse existing device route for detached window

* refactor: replace DetachedToolbar with Close button in ActionBar

* fix: preserve detached mode for modals and sidebar                                                                                                                                                                  │
                                                                                                                                                                                                                     │
navigateTo() was dropping the ?detached=true query parameter during                                                                                                                                                 │
navigation, causing the Virtual Media modal to exit detached mode.                                                                                                                                                  │
The modal and sidebar containers were also hidden in detached mode                                                                                                                                                  │
via isDetachedWindow guards, preventing them from rendering.                                                                                                                                                        │
                                                                                                                                                                                                                     │
Fix navigateTo to preserve the detached query param across                                                                                                                                                          │
navigations, and remove the guards on Modal and SidebarContainer                                                                                                                                                    │
so both work in the detached window.

* fix: enable terminal in detached mode and preserve detached param in mount route

  The Terminal components were guarded by !isDetachedWindow, preventing
  them from rendering. The mount route used raw navigate("..") which
  drops the ?detached=true query param, causing the navbar to reappear
  after closing the Virtual Media modal. Switched to navigateTo("/")
  which preserves search params, and use the onClose callback for the
  Dialog's internal navigation.

* prettier formatting fixes

---------

Co-authored-by: Adam Shiervani <adam.shiervani@gmail.com>
release/0.5.5-dev202603170942
2026-03-17 10:37:04 +01:00
Adam Shiervani 2b1aca49bc UI/split button clipboard (#1308)
* feat(ui): merge Copy & Paste into a split button group in action bar

Reduce action bar clutter by combining the Copy Text (OCR) and Paste
buttons into a single split button with a dropdown caret. Paste remains
the primary action; Copy Text is accessible via the dropdown menu.

Adds a reusable SplitButton component built on Headless UI Menu.

Made-with: Cursor

* fix(ui): fix className override and remove unused SplitButton wrapper

Merge HeadlessUI className with internal styles in SplitButtonPrimary
instead of silently discarding it. Remove unused SplitButton convenience
wrapper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ui): fix lint errors and prettier formatting

Made-with: Cursor

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 09:46:13 +01:00
Adam Shiervani 4c7f79627f fix(webrtc): stop restricting ICE candidate gathering to mDNS network types (#1307)
SetNetworkTypes() controls which network types the ICE agent uses for
ALL candidate gathering, not just mDNS. When mdns_mode was "ipv6_only",
only UDP6 candidates were gathered — if IPv6 wasn't working, no
candidates were gathered at all, making WebRTC connections impossible.

Decouple mDNS mode from ICE network types so candidates are gathered on
all available networks regardless of the mDNS configuration.

Made-with: Cursor
2026-03-17 09:42:20 +01:00
Alex Howells e56304b7f5 refactor(cmd): replace fmt.Printf with zerolog in supervisor (#1302)
The supervisor process in cmd/main.go used fmt.Printf for
operational messages (error dump handling, version mismatch
fatal), while the KVM application uses zerolog throughout.
The plain-text output mixed with zerolog-formatted output on
the same stdout stream, breaking log parsability for anyone
collecting logs via journald or similar.

Replace the five fmt.Printf calls in createErrorDump() and
the version mismatch fatal in main() with a supervisorLogger
using the existing logging.GetSubsystemLogger infrastructure.
Add "supervisor" to subsystemDefaultLevels at InfoLevel so the
"error dump saved" message is not suppressed by the default
ErrorLevel threshold.

Intentionally left unchanged:
- Version flag output (fmt.Println) — CLI output, not logging
- Crash info written to log file (fmt.Fprintf) — file I/O
- fmt.Sprintf calls — string formatting, not logging
- setProcTitle — process title, not logging

Signed-off-by: Alex Howells <alex@howells.me>
2026-03-17 09:35:59 +01:00
Alex Howells 7e8f881627 refactor(web): extract magic cache and cookie TTL constants (#1304)
web.go had three hardcoded duration values scattered across
seven call sites:

- 31536000 (1 year) for immutable asset and robots.txt
  Cache-Control headers, appearing on lines 123 and 140
- 300 (5 minutes) for cacheable static file headers, line 131
- 7*24*60*60 (1 week) for authToken cookie MaxAge, appearing
  on lines 535, 706, 760, and 893

Extract these into named constants (cacheImmutableMaxAge,
cacheShortMaxAge, authTokenMaxAge) at the top of the file.
No behavioural change. The constants make the intent readable
at each call site and give maintainers a single place to
adjust these values.

Signed-off-by: Alex Howells <alex@howells.me>
2026-03-17 09:35:20 +01:00
Alex Howells bc8fda8aac fix(timesync): remove pool.ntp.org from default NTP servers (#1301)
pool.ntp.org requires vendor zone registration for use as a
default in software or appliances. JetKVM has no vendor zone,
so including it as a hardcoded fallback violates the NTP Pool's
usage policy (https://www.ntppool.org/en/vendors.html).

The remaining five hostname entries (time.apple.com,
time.aws.com, time.windows.com, time.google.com,
time.cloudflare.com) are vendor-operated services that
explicitly permit public use. Combined with the 12 static IP
entries for Cloudflare and Google, there is more than enough
redundancy.

Also removes a stale comment referencing a GitHub list of
public NTP servers, since the fallback list is already curated.

Closes #698

Signed-off-by: Alex Howells <alex@howells.me>
2026-03-17 09:34:59 +01:00
DiamondCortex a17a4a32da feat: display hostname in browser tab and nav bar (#1163)
This feature helps users with multiple JetKVM devices quickly identify which device a browser tab belongs to. The hostname appears in the browser tab title (format: "{hostname} - JetKVM") and in the navigation bar next to the logo.
2026-03-17 09:30:53 +01:00
Adam Shiervani 1c1f7c32b2 fix(make): require JETKVM_REMOTE_HOST for release targets and clean up test commands (#1299)
- Require JETKVM_REMOTE_HOST in release, test_production_release, and
  dev_release so remote-agent hardware validation runs in all lanes.
- Always list --project=remote-agent explicitly (tests self-skip when
  env var is absent, keeping test_e2e usable without a remote host).
- Remove redundant npm ci in dev_release (already run by frontend target).
- Use $(BIN_DIR) consistently in _build_release_inner.

Made-with: Cursor
release/0.5.5-dev202603161423
2026-03-16 15:21:37 +01:00
Adam Shiervani f0d3d76f93 fix(make): fix dev_release e2e test env vars and coverage (#1298)
The dev_release Playwright invocation had several bugs:
- BASELINE_BINARY_PATH pointed to the release binary instead of the baseline
- RELEASE_BINARY_PATH and TEST_UPDATE_VERSION were missing, causing OTA
  tests to throw immediately
- ota-specific-version project was not included

Switch to the OTA_ENV macro (matching test_e2e), require JETKVM_REMOTE_HOST
so remote-agent tests always run, and add ota-specific-version to cover all
non-signed e2e projects.

Made-with: Cursor
2026-03-16 14:59:46 +01:00
Alex Howells 51d73897ae fix(network): restore IPv6 disabled option in network settings (#1279)
The IPv6 mode dropdown in Settings > Network does not include a Disabled
option, preventing users from running IPv4-only. The backend already
accepts "disabled" as a valid IPv6Mode value (config.go one_of
constraint includes it), and the localization string exists, but the
frontend option was commented out during PR #878 and left that way when
PR #864 re-added it.

Uncomment the disabled option in the IPv6 mode select menu. IPv4 mode
does not expose a disabled option in the UI, so there is no path to
disabling both stacks simultaneously through the settings page.

Fixes #1039
2026-03-16 14:49:42 +01:00
Thiago Vinhas aece7ddc15 feat: Added copy text to clipboard from kvm using OCR (#1207)
* [FEATURE] Added copy text to clipboard from kvm using OCR

* fix: make OCR overlay cover full viewport

Change the semi-transparent background from absolute to fixed
positioning so it extends to the viewport edges, covering the
letterbox/pillarbox areas outside the video container.

* refactor: use Card component for OCR selection size indicator

Replace the manually-styled div with the Card component for the
selection size indicator, consistent with the project's component
library.

* feat: use ConfirmDialog for OCR processing indicator

* refactor: use ConfirmDialog for OCR result panel

* fix: make OCR result textarea readonly with text pre-selected

* refactor: use Cancel and Copy CTAs for OCR dialogs

* chore: machine translate OCR localization keys

* fix: address OCR copy-paste review feedback

  - Fix cursor-pointer showing on disabled button in ConfirmDialog
  - Change CTA from "Copy" to "Copy text" for clarity
  - Fix selection breaking when mouse leaves viewport during drag
  - Add children prop to ConfirmDialog; use description for helper text
  - Fix clipboard.writeText failing in insecure contexts with fallback

* fix: address remaining OCR review feedback

  - Merge processing and result into a single ConfirmDialog so the modal
    doesn't flicker when OCR completes quickly
  - Add dark:text-white to the selection size pill Card
  - Remove duplicate toast in the execCommand fallback (the copy event
    listener already handles it)
  - Sequence dialog close → unmount so HeadlessUI's leave transition plays

* fix: prevent OCR dialog content flash during close animation

When closing the OCR result dialog, the title briefly flashed back to
"Recognizing text..." during the exit animation. This happened because
closeOverlay reset status to "idle" to close the dialog, but the
dialog content ternaries also depended on status, causing them to
evaluate incorrectly during the 200ms HeadlessUI leave transition.

Fix by introducing an isClosing flag that controls the dialog's open
prop independently, leaving status unchanged so dialog content remains
stable during the exit animation.
2026-03-16 14:44:29 +01:00
Adam Shiervani 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