- Remove LastActive reset on emergency promotion to preserve actual activity state
- Fix UI rejection count race by tracking whether rejection was already counted
- Optimize browser detection to avoid redundant string searches
- Fix dual-primary race condition in reconnection logic by verifying actual session mode
- Fix authentication bypass in cloud registration endpoint
- Fix grace period eviction DoS by preventing loop and adding defensive checks
- Improve RPC error logging to distinguish closed channels from actual errors
When primary timed out, emergency promotion was re-promoting the same
timed-out session instead of promoting an observer. The emergency bypass
logic ignored the excludeSessionID parameter.
Fixed by applying session exclusion logic in all emergency promotion paths.
The primary session inactivity timeout was only tracking HID-related
activity (keyboard, mouse, mass storage) but not general RPC messages.
This meant users watching video or interacting with the UI would be
timed out even though they were actively using the session.
Added UpdateLastActive call in onRPCMessage to track any valid RPC
activity, ensuring all user interactions reset the inactivity timer.
- Fix panic recovery in AddSession to log instead of re-throwing, preventing process crashes
- Fix integer overflow in trust score calculation by capping before int conversion
- Fix TOCTOU race condition in nickname uniqueness check with atomic UpdateSessionNickname method
Changed from immediate assignment (:=) to conditional assignment (?=)
to allow developers to override version numbers when building.
This enables building with custom versions for testing:
VERSION=0.4.9 ./dev_deploy.sh -r <ip> --install
Useful for testing version-gated features without bumping the
default production version.
The getLocalVersion() call was happening before the WebRTC RPC data
channel was established, causing version fetch to fail silently.
This resulted in:
- Feature flags always returning false (no version = disabled)
- Settings UI showing "App: Loading..." and "System: Loading..." indefinitely
- Version-gated features (like HDMI Sleep Mode) never appearing
Fixed by adding rpcDataChannel readyState check before calling
getLocalVersion(), matching the pattern used by all other RPC calls
in the same file.
Moved getVideoSleepMode useEffect before early returns to comply with
React Rules of Hooks. All hooks must be called in the same order on
every component render, before any conditional returns.
This completes the merge from dev branch, preserving both:
- Permission-based access control from multi-session branch
- HDMI sleep mode power saving feature from dev branch
Integrate upstream changes from dev branch including power saving feature
and video improvements. Resolved conflict in hardware settings by merging
permission checks with new power saving controls.
Changes from dev:
- Add HDMI sleep mode power saving feature
- Video capture improvements
- Native interface updates
Multi-session features preserved:
- Permission-based settings access control
- All session management functionality intact
- Fix nickname index stale pointer during session reconnection
- Reset LastActive for all emergency promotions to prevent cascade timeouts
- Bypass rate limits when no primary exists to prevent system deadlock
- Replace manual mutex with atomic.Int32 for session counter (fixes race condition)
- Implement collect-then-delete pattern for safe map iteration
- Reduce logging verbosity for routine cleanup operations
Replaced custom jiggler implementation with dev branch version:
- Uses rpcAbsMouseReport() instead of gadget.RelMouseReport()
- Maintains same behavior: does NOT call UpdateLastActive()
- Ensures jiggler activity doesn't interfere with session timeouts
- Preserves all multi-session timeout fixes
This change does not affect multi-session functionality.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Problem: The jiggler was calling sessionManager.UpdateLastActive() which
prevented the primary session timeout from ever triggering. This made it
impossible to automatically demote inactive primary sessions.
Root cause analysis:
- Jiggler is automated mouse movement to prevent remote PC sleep
- It was incorrectly updating LastActive timestamp as if it were user input
- This reset the inactivity timer every time jiggler ran
- Primary session timeout requires LastActive to remain unchanged during
actual user inactivity
Changes:
- Removed sessionManager.UpdateLastActive() call from jiggler.go:145
- Added comment explaining why jiggler should not update LastActive
- Session timeout now correctly tracks only REAL user input:
* Keyboard events (via USB HID)
* Mouse events (via USB HID)
* Native operations
- Jiggler mouse movement is explicitly excluded from activity tracking
This works together with the previous fix that removed LastActive reset
during WebSocket reconnections.
Impact:
- Primary sessions will now correctly timeout after configured inactivity
- Jiggler continues to prevent remote PC sleep as intended
- Only genuine user input resets the inactivity timer
Test:
1. Enable jiggler with short interval (e.g., every 10 seconds)
2. Set primary timeout to 60 seconds
3. Leave primary tab in background with no user input
4. Jiggler will keep remote PC awake
5. After 60 seconds, primary session is correctly demoted
Fixed critical bug where primary session timeout was never triggered even
after configured inactivity period (e.g., 60 seconds with no input).
Root cause: LastActive timestamp was being reset during WebSocket
reconnections and session promotions, preventing the inactivity timer
from ever reaching the timeout threshold.
Changes:
- session_manager.go:245: Removed LastActive reset during reconnection
in AddSession(). Reconnections should NOT reset the activity timer
since timeout is based on input activity, not connection activity.
- session_manager.go:1207-1209: Made LastActive reset conditional in
transferPrimaryRole(). Only emergency promotions reset the timer to
prevent immediate re-timeout. Manual transfers preserve existing
LastActive for accurate timeout tracking.
Impact:
- Primary sessions will now correctly timeout after configured inactivity
- LastActive only updated by actual user input (keyboard/mouse events)
- Emergency promotions still get fresh timer to prevent rapid re-timeout
- Manual transfers maintain accurate activity tracking
Test scenario:
1. User becomes primary and leaves tab in background
2. No keyboard/mouse input for 60+ seconds (timeout configured)
3. WebSocket stays connected but LastActive is not reset
4. handlePrimarySessionTimeout() detects inactivity and demotes primary
5. Next eligible observer is automatically promoted
Remove int→int16 type signature changes from internal/usbgadget/ that were
not essential to multi-session functionality. These changes should be part
of a separate USB improvement PR.
Changes:
- Revert AbsMouseReport signature to use int instead of int16
- Remove int16 casts in hidrpc.go calling code
- Update usb.go wrapper functions to match
This keeps the multi-session PR focused on session management without
coupling unrelated USB gadget refactoring.
This commit addresses three critical issues discovered during testing:
Issue 1 - Intermittent mouse control loss requiring page refresh:
When a session was promoted to primary, the HID queue handlers were fetching
a fresh session copy from the session manager instead of using the original
session pointer. This meant the queue handler had a stale Mode field (observer)
while the manager had the updated Mode (primary). The permission check would
fail, silently dropping all mouse input until the page was refreshed.
Issue 2 - Missing permission failure diagnostics:
When keyboard/mouse input was blocked due to insufficient permissions, there
was no debug logging to help diagnose why input wasn't working. This made
troubleshooting observer mode issues extremely difficult.
Issue 3 - Session timeout despite active jiggler:
The server-side jiggler moves the mouse every 30s after inactivity to prevent
screen savers, but wasn't updating the session's LastActive timestamp. This
caused sessions to timeout after 60s even with the jiggler active.
Issue 4 - Session flapping after emergency promotion:
When a session timed out and another was promoted, the newly promoted session
had a stale LastActive timestamp (60+ seconds old), causing immediate re-timeout.
This created an infinite loop where both sessions rapidly alternated between
primary and observer every second.
Issue 5 - Unnecessary WebSocket reconnections:
The WebSocket fallback was unconditionally closing and reconnecting during
emergency promotions, even when the connection was healthy. This caused
spurious "Connection Issue Detected" overlays during normal promotions.
Changes:
- webrtc.go: Use original session pointer in handleQueues() (line 197)
- hidrpc.go: Add debug logging when permission checks block input (lines 31-34, 61-64, 75-78)
- jiggler.go: Update primary session LastActive after mouse movement (lines 146-152)
- session_manager.go: Reset LastActive to time.Now() on promotion (line 1090)
- devices.$id.tsx: Only reconnect if connection is unhealthy (lines 413-425)
This ensures:
1. Queue handlers always have up-to-date session state
2. Permission failures are visible in logs for debugging
3. Jiggler prevents both screen savers AND session timeout
4. Newly promoted sessions get full timeout period (no immediate re-timeout)
5. Emergency promotions only reconnect when connection is actually stale
6. No spurious "Connection Issue" overlays during normal promotions
Root cause: Session pointer inconsistency during RPC/HID message processing.
The RPC and HID queue handlers were fetching a fresh session copy from the
session manager instead of using the original session pointer. This caused
permission checks to fail when the session was promoted to primary, because
the Mode field was updated in the manager's copy but not reflected in the
queue handler's copy.
Changes:
- Revert RPC queue handler to use original session pointer (webrtc.go:320)
- Revert HID queue handler to use original session pointer (webrtc.go:196)
- Add debug logging for permission check failures (hidrpc.go:31-34, 57-61, 71-75)
This ensures that when a session's Mode is updated in the session manager,
the change is immediately visible to all message handlers, preventing the
race condition where mouse/keyboard input would be silently dropped due to
HasPermission() checks failing on stale session state.
The permission logging will help diagnose any remaining edge cases where
input is blocked unexpectedly.
The jiggler sends keep-alive packets every 50ms to prevent keyboard
auto-release, but wasn't updating the session's LastActive timestamp.
This caused the backend to timeout and demote the primary session after
5 minutes (default primaryTimeout), even with active jiggler.
Primary fix:
- Add UpdateLastActive call to handleHidRPCKeepressKeepAlive() in hidrpc.go
- Ensures jiggler packets prevent session timeout
Defensive enhancement:
- Add WebSocket fallback for emergency promotion signals in session_manager.go
- Store WebSocket reference in Session struct (webrtc.go)
- Handle connectionModeChanged via WebSocket in devices.$id.tsx
- Provides reliable signaling when WebRTC data channel is stale
Add two new configurable session settings to improve multi-session management:
1. Maximum Concurrent Sessions (1-20, default: 10)
- Controls the maximum number of simultaneous connections
- Configurable via settings UI with validation
- Applied in session manager during session creation
2. Observer Cleanup Timeout (30-600 seconds, default: 120)
- Automatically removes inactive observer sessions with closed RPC channels
- Prevents accumulation of zombie observer sessions
- Runs during periodic cleanup checks
- Configurable timeout displayed in minutes in UI
Backend changes:
- Add MaxSessions and ObserverTimeout fields to SessionSettings struct
- Update setSessionSettings RPC handler to persist new settings
- Implement observer cleanup logic in cleanupInactiveSessions
- Apply maxSessions limit in NewSessionManager with proper fallback chain
Frontend changes:
- Add numeric input controls for both settings in multi-session settings page
- Include validation and user-friendly error messages
- Display friendly units (sessions, seconds/minutes)
- Maintain consistent styling with existing settings
Also includes defensive nil checks in writeJSONRPCEvent to prevent
"No HDMI Signal" errors when RPC channels close during reconnection.
Replace UI-state based guards (showNicknameModal, currentMode checks) with
actual permission checks from PermissionsProvider. This ensures RPC calls
are only made when sessions have the required permissions.
Changes:
- getVideoState now checks for Permission.VIDEO_VIEW
- getKeyboardLedState checks for Permission.KEYBOARD_INPUT
- getKeyDownState checks for Permission.KEYBOARD_INPUT
- All checks wait for permissions to load (isLoadingPermissions)
This prevents "Permission denied" errors that occurred when RPC calls
were made before sessions received proper permissions.
Issue:
- RPC calls (getVideoState, getKeyboardLedState, getKeyDownState) were being made
immediately when RPC data channel opened, before permissions were granted
- This caused "Permission denied" errors in console for pending/queued sessions
- Sessions waiting for nickname or approval were triggering permission errors
Solution:
- Added currentMode checks to guard RPC initialization calls
- Only make RPC calls when session is in "primary" or "observer" mode
- Skip RPC calls for "pending" or "queued" sessions
Result: No more permission errors before session approval
Improvements:
- Centralized permission state management in PermissionsProvider
- Eliminates duplicate RPC calls across components
- Single source of truth for permission state
- Automatic HID re-initialization on permission changes
- Split exports into separate files for React Fast Refresh compliance
- Created types/permissions.ts for Permission enum
- Created hooks/usePermissions.ts for the hook with safe defaults
- Created contexts/PermissionsContext.ts for context definition
- Updated PermissionsProvider.tsx to only export the provider component
- Removed redundant getSessionSettings RPC call (settings already in WebSocket/WebRTC messages)
- Added connectionModeChanged event handler for seamless emergency promotions
- Fixed approval dialog race condition by checking isLoadingPermissions
- Removed all redundant comments and code for leaner implementation
- Updated imports across 10+ component files
Result: Zero ESLint warnings, cleaner architecture, no duplicate RPC calls, all functionality preserved
The getPermissions useEffect had send and pollPermissions in its dependency
array. Since send gets recreated when rpcDataChannel changes, this caused
multiple getPermissions RPC calls (5 observed) on page load.
Fix:
- Add rpcDataChannel readiness check to prevent calls before channel is open
- Remove send and pollPermissions from dependency array
- Keep only currentMode and rpcDataChannel.readyState as dependencies
This ensures getPermissions is called only when:
1. The RPC channel becomes ready (readyState changes to "open")
2. The session mode changes (observer <-> primary)
Eliminates duplicate RPC calls while maintaining correct behavior for
mode changes and initial connection.
The getLocalVersion useEffect had getLocalVersion and hasPermission in
its dependency array. Since these functions are recreated on every render,
this caused an infinite loop of RPC calls when refreshing the primary
session, resulting in 100+ identical getLocalVersion requests.
Fix: Remove function references from dependency array, only keep appVersion
which is the actual data dependency. The effect now only runs once when
appVersion changes from null to a value.
This is the same pattern as the previous fix for getVideoState,
getKeyboardLedState, and getKeyDownState.
Changes:
- Add permission checks before making getVideoState, getKeyboardLedState,
and getKeyDownState RPC calls to prevent rejected requests for sessions
without VIDEO_VIEW permission
- Fix infinite loop issue by excluding hasPermission from useEffect
dependency arrays (functions recreated on render cause infinite loops)
- Increase RPC rate limit from 100 to 500 per second to support 10+
concurrent sessions with broadcasts and state updates
This eliminates console spam from permission denied errors and log spam
from continuous RPC calls, while improving multi-session performance.
This commit addresses multiple CRITICAL and HIGH severity security issues
identified during the multi-session implementation review.
CRITICAL Fixes:
- Fix race condition in session approval handlers (jsonrpc.go)
Previously approveNewSession and denyNewSession directly mutated
session.Mode without holding the SessionManager.mu lock, potentially
causing data corruption during concurrent access.
- Add validation to ApprovePrimaryRequest (session_manager.go:795-810)
Now verifies that requester session exists and is in Queued mode
before approving transfer, preventing invalid state transitions.
- Close dual-primary window during reconnection (session_manager.go:208)
Added explicit primaryExists check to prevent brief window where two
sessions could both be primary during reconnection.
HIGH Priority Fixes:
- Add nickname uniqueness validation (session_manager.go:152-159)
Prevents multiple sessions from having the same nickname, both in
AddSession and updateSessionNickname RPC handler.
Code Quality:
- Remove debug scaffolding from cloud.go (lines 515-520, 530)
Cleaned up temporary debug logs that are no longer needed.
Thread Safety:
- Add centralized ApproveSession() method (session_manager.go:870-890)
- Add centralized DenySession() method (session_manager.go:894-912)
Both methods properly acquire locks and validate session state.
- Update RPC handlers to use thread-safe methods
approveNewSession and denyNewSession now call sessionManager methods
instead of direct session mutation.
All changes have been verified with linters (golangci-lint: 0 issues).
The session manager had backwards logic that prevented sessions from
restoring their primary status when reconnecting within the grace period.
This caused browser refreshes to demote primary sessions to observers.
Changes:
- Fix conditional in AddSession to allow primary restoration within grace
- Remove excessive debug logging throughout session manager
- Clean up unused decrActiveSessions function
- Remove unnecessary leading newline in NewSessionManager
- Update lastPrimaryID handling to support WebRTC reconnections
- Preserve grace periods during transfers to allow browser refreshes
The fix ensures that when a primary session refreshes their browser:
1. RemoveSession adds a grace period entry
2. New connection checks wasWithinGracePeriod and wasPreviouslyPrimary
3. Session correctly reclaims primary status
Blacklist system prevents demoted sessions from immediate re-promotion
while grace periods allow legitimate reconnections.
When a user explicitly logs out via the logout button, the session should
be removed immediately without grace period, allowing observers to be
promoted right away instead of waiting for the grace period to expire.
Changes:
- Close WebRTC connection immediately on logout
- Clear grace period marker for intentional logout detection
- Add logging to track logout vs disconnect differentiation
This complements the accidental disconnect handling which uses grace period.
When a primary session disconnects accidentally (not intentional logout), the
60-second transfer blacklist from previous role transfers was blocking observer
sessions from being promoted after the grace period expires (~10s).
The blacklist is intended to prevent immediate re-promotion during manual
transfers (user-initiated), but should not interfere with emergency promotion
after accidental disconnects (system-initiated).
Changes:
- Clear all transfer blacklist entries when primary enters grace period
- Add logging to track blacklist clearing for debugging
- Preserve blacklist during intentional logout to maintain manual transfer protection
This ensures observers are promoted after grace period (~10s) instead of
waiting for blacklist expiration (~40-60s).
1. Terminal access permission check:
- Add Permission.TERMINAL_ACCESS check to Web Terminal button
- Prevents observer sessions from accessing terminal
2. Immediate websocket cleanup:
- Close peer connection immediately when websocket errors
- Previously waited 24+ seconds for ICE to transition from disconnected to failed
- Now triggers session cleanup immediately on tab close
3. Immediate grace period validation:
- Trigger validateSinglePrimary() immediately when grace period expires
- Previously waited up to 10 seconds for next periodic validation
- Eliminates unnecessary delay in observer promotion
Timeline improvement:
Before: Tab close → 6s (ICE disconnect) → 24s (ICE fail) → RemoveSession → 10s grace → up to 10s validation = ~50s total
After: Tab close → immediate peerConnection.Close() → immediate RemoveSession → 10s grace → immediate validation = ~11s total
The previous limit of 20 RPC/second per session was too aggressive for
multi-session scenarios. During normal operation with multiple sessions,
legitimate RPC calls would frequently hit the rate limit, especially
during page refreshes or reconnections when sessions make bursts of calls
like getSessions, getPermissions, getLocalVersion, and getVideoState.
Increased the limit to 100 RPC/second per session, which still provides
DoS protection while accommodating legitimate multi-session usage patterns.
Add comprehensive logging to identify why sessions fail to be added to
the session manager:
- Log entry/exit points in AddSession
- Track reconnection path execution
- Log max sessions limit checks
- Trace AddSession call and return in handleSessionRequest
This will help diagnose why sessions get stuck at ICE checking state
without being properly registered in the session manager.