@@ -577,24 +577,175 @@ viewer.send_input({"action": "type", "text": "hello"})
577577viewer.disconnect()
578578```
579579
580- GUI: ** Remote Desktop** tab with two sub-tabs.
581-
582- - ** Host** — token field with a * Generate* button, security warning
583- about the bind address, start / stop controls, refreshing port +
584- viewer-count status, and a 4 fps preview pane below the controls so
585- the user being remoted sees what viewers see.
586- - ** Viewer** — address / port / token form, * Connect* / * Disconnect* ,
587- and a custom frame-display widget that paints incoming JPEG frames
588- scaled with ` KeepAspectRatio ` . Mouse / wheel / key events on the
589- display are remapped from widget coordinates back to the remote
590- screen's pixel space using the latest frame's dimensions, then
591- forwarded as ` INPUT ` messages.
580+ GUI: ** Remote Desktop** tab opens to the ** Quick Connect** screen
581+ (AnyDesk-style) by default — huge Host ID on one side, a single input
582+ that accepts ` host:port ` , ` ws:// ` , ` wss:// ` , or a 9-digit Host ID on
583+ the other, with * Connect* and * Start hosting* as the two primary
584+ buttons. Recent connections are remembered across sessions. Advanced
585+ per-transport sub-tabs (legacy TCP / WS host + viewer, WebRTC host +
586+ viewer with manual SDP / custom codecs / TLS pinning) stay one click
587+ away. WebRTC sub-tabs lazy-load so a stock install without the
588+ ` [webrtc] ` extra still opens the tab.
592589
593590> ⚠️ Anyone with the host: port and token gets full mouse / keyboard
594591> control of the host machine. Default bind is ` 127.0.0.1 ` ; expose
595592> externally only via SSH tunnel or TLS front-end. The token is the
596593> only line of defence — treat it like a password.
597594
595+ ** Quick Connect headless API.** The transport coordinator that backs
596+ the GUI input box is also exported, so scripts can dispatch the same
597+ way:
598+
599+ ``` python
600+ from je_auto_control import parse_remote_desktop_target
601+ parse_remote_desktop_target(" 192.168.1.10:5555" )
602+ # ConnectTarget(kind='tcp', host='192.168.1.10', port=5555, ...)
603+ parse_remote_desktop_target(" ws://hub:8765/desk" )
604+ # ConnectTarget(kind='ws', host='hub', port=8765, path='/desk')
605+ parse_remote_desktop_target(" 123-456-789" )
606+ # ConnectTarget(kind='webrtc_id', host_id='123456789')
607+ ```
608+
609+ ** Connection approval + view-only mode.** Optional callback gates
610+ every incoming session AnyDesk-style. Returning ` "view_only" ` admits
611+ the viewer but drops their ` INPUT ` messages; returning a falsy value
612+ (or raising) sends ` AUTH_FAIL ` "rejected by host":
613+
614+ ``` python
615+ from je_auto_control import RemoteDesktopHost, PendingViewer
616+
617+ def gate (p : PendingViewer) -> str :
618+ if p.address[0 ].startswith(" 10." ):
619+ return " view_only"
620+ return " full" # or True
621+
622+ host = RemoteDesktopHost(token = " tok" , on_pending_viewer = gate)
623+ ```
624+
625+ ** IP allowlist (CIDR + exact IPs).** Reject peers outside the
626+ configured ranges * before* TLS / auth runs, so attackers can't probe
627+ further:
628+
629+ ``` python
630+ host = RemoteDesktopHost(
631+ token = " tok" , ip_allowlist = [" 10.0.0.0/8" , " 192.168.1.100" ],
632+ )
633+ ```
634+
635+ ** One-time share codes** — extra tokens that self-destruct on first
636+ successful auth, ideal for client-support workflows:
637+
638+ ``` python
639+ host = RemoteDesktopHost(token = " tok" , single_use_tokens = [" abc123" ])
640+ host.add_single_use_token(" 9k4ndx" ) # rotate at runtime
641+ host.revoke_single_use_token(" abc123" ) # cancel before it's used
642+ ```
643+
644+ ** TOTP 2FA (RFC 6238, stdlib only).** Layer a 6-digit OTP on top of
645+ the token; host accepts ±1 step of clock drift:
646+
647+ ``` python
648+ from je_auto_control.utils.remote_desktop.totp import (
649+ generate_secret, generate_code, provisioning_uri,
650+ )
651+ secret = generate_secret()
652+ print (provisioning_uri(secret, account = " alice" )) # otpauth:// URI for QR
653+
654+ host = RemoteDesktopHost(token = " tok" , totp_secret = secret)
655+ viewer = RemoteDesktopViewer(
656+ host = ... , token = " tok" , totp_code = generate_code(secret),
657+ )
658+ ```
659+
660+ ** Multi-monitor selection.** Capture one specific monitor instead of
661+ the combined virtual desktop:
662+
663+ ``` python
664+ from je_auto_control import list_host_monitors, RemoteDesktopHost
665+ print (list_host_monitors())
666+ # [{'index': 0, 'is_combined': True, ...},
667+ # {'index': 1, 'left': 0, 'top': 0, ...},
668+ # {'index': 2, 'left': 1920, ...}]
669+ host = RemoteDesktopHost(token = " tok" , monitor_index = 1 )
670+ ```
671+
672+ ** Remote cursor overlay.** Host broadcasts cursor position at 30 Hz
673+ (deduped on still desktops); the viewer's popup window draws an arrow
674+ on top of the JPEG stream so you can see exactly where the host's
675+ pointer is. Disable via ` enable_cursor_broadcast=False ` .
676+
677+ ** Multi-viewer collaborative cursors + chat.** Two new message types
678+ (` CHAT ` and ` CURSOR ` with ` viewer_id ` ). Use a ` MultiViewerHost ` to
679+ relay one viewer's pointer to the others; pair with the chat channel
680+ for ad-hoc text between operators:
681+
682+ ``` python
683+ host = RemoteDesktopHost(
684+ token = " tok" , on_chat = lambda sender , text : print (sender, " :" , text),
685+ )
686+ host.broadcast_chat(" session starts in 30s" )
687+ host.broadcast_viewer_cursor(" alice" , 200 , 300 )
688+
689+ viewer = RemoteDesktopViewer(
690+ host = ... , on_chat = lambda s , t : ... ,
691+ on_viewer_cursor = lambda vid , x , y : ... ,
692+ )
693+ viewer.send_chat(" ack" )
694+ ```
695+
696+ ** Relative mouse mode (FPS / CAD).** New input action that sends
697+ deltas instead of absolute coordinates:
698+
699+ ``` python
700+ viewer.send_input({" action" : " mouse_move_relative" , " dx" : 5 , " dy" : - 3 })
701+ ```
702+
703+ ** Motion-aware capture.** The capture loop now hashes each encoded
704+ JPEG; identical frames are skipped, so a static desktop produces
705+ ~ zero bandwidth. New viewers are seeded with the latest frame on auth
706+ so they never see a black popup.
707+
708+ ** Live stats** (FPS / kbps / totals over a 3-second window):
709+
710+ ``` python
711+ viewer.stats()
712+ # {'fps': 24.3, 'kbps': 4801.2, 'frames': 720.0, 'bytes': 1.8e7, 'uptime': 30.2}
713+ ```
714+
715+ ** JPEG sequence recorder (no PyAV needed).** TCP-path session
716+ capture: each frame written to disk plus ` manifest.json ` so it can
717+ be replayed at original cadence:
718+
719+ ``` python
720+ from je_auto_control.utils.remote_desktop.jpeg_recorder import (
721+ JpegSequenceRecorder,
722+ )
723+ rec = JpegSequenceRecorder(" ~/recordings/2026-05-23" )
724+ rec.start()
725+ viewer = RemoteDesktopViewer(host = ... , on_frame = rec.record_frame)
726+ # ... session ...
727+ rec.stop() # writes manifest.json next to the .jpg files
728+ ```
729+
730+ ** TCP relay (WebRTC fallback).** When P2P fails (strict NAT, mobile
731+ CGNAT, hotel Wi-Fi), both peers connect outbound to a relay and
732+ exchange a shared 32-byte session ID; the relay pipes bytes between
733+ them. Same module ships an ` encode_handshake(role, session_id) `
734+ helper for clients:
735+
736+ ``` python
737+ from je_auto_control.utils.remote_desktop.relay import RelayServer
738+ relay = RelayServer(bind = " 0.0.0.0" , port = 9000 ) # NOSONAR # public relay
739+ relay.start()
740+ ```
741+
742+ ** Service installer (unattended host).** `python -m
743+ je_auto_control.utils.remote_desktop.host_service ...`
744+ exposes ` configure ` / ` init ` / ` run ` plus per-platform installers:
745+ ` install-windows-service ` / ` uninstall-windows-service ` (pywin32),
746+ ` generate-launchd ` / ` uninstall-launchd ` , ` generate-systemd ` /
747+ ` uninstall-systemd ` .
748+
598749** Encrypted transports + alternate protocols.** Pass an ` ssl_context `
599750to either ` RemoteDesktopHost ` or ` RemoteDesktopViewer ` to wrap every
600751connection in TLS. For firewall-friendly access, use the in-tree
0 commit comments