Add a process-local security audit trail to the Windows server's web
console, mirroring the Go server's event names and reply shape. The ring
keeps the most recent 1000 events since process start and is never
persisted.
- New query_audit WebSocket command, admin-only, with time/event/actor/
IP/device/keyword/warn-only filters and pagination; the reply is the
audit_result payload the existing Audit Log page already understands.
The challenge advertises caps.auditLog so browsers enable the button.
- Record logins (success, failure, throttling, salt probes), WebSocket
connect/close, screen and terminal session start/stop/denials,
clipboard transfers (byte counts only), RDP resets, and admin user
mutations.
- Repeated failed logins and username probes are de-duplicated per
connection so scanners cannot flood the ring.
- Audit writes never hold m_ClientsMutex: disconnect paths snapshot the
client identity before the map entry is erased, and offline-device
stops are audited after the clients lock is released.
Co-Authored-By: deepseek-flash
Add a third view to the web UI, the screen wall, which shows several
devices at once in one tab with 1x1 / 2x2 / 3x3 / 4x4 / 5x5 layouts and a
device sidebar on the left. Tiles are monitoring-only; clicking one
enlarges that device into the existing single-screen page for control, and
Back returns to the wall. The same device can be added several times: all
tiles of one device share a single decoder and a single stream.
The wall decodes at most 9 streams on desktop and 4 on touch devices.
Tiles beyond the budget keep receiving frames but have their decoder
closed and show the last frame plus a Paused badge; they are promoted
automatically when a live tile goes away. Subscriptions are reference
counted on the client (streams map with owner sets), so a stream is only
connected on the 0 -> 1 transition and disconnected on 1 -> 0, and the
wall and the single-screen page can share one device without tearing the
stream down.
Server side, WebClient now holds a set of watched devices instead of a
single id. A new multiWatch command flag selects set semantics, while
clients without it keep the old replace-all behaviour, so an old frontend
against the new server is unchanged. connect and disconnect responses echo
id and req so the frontend can route them, input commands carry an id and
are resolved against the watch set (dropped when the set has several
devices and no id), and the per-client watch set is capped at 32.
For fast recovery after a pause or when joining late, the server caches
each device's raw IDR payload and replays it on connect and on a new
keyframe command, then asks the client to force a new IDR. The client gets
COMMAND_SCREEN_KEYFRAME_REQ = 169 and an atomic request flag that is only
cleared after an IDR actually came out of the encoder, so a failed capture
or EAGAIN does not lose the request. The frontend retries the keyframe
request up to three times; an old client that ignores the command simply
recovers on the next natural GOP.
Also fix the keyframe cache double wrapping: BroadcastH264Frame used to
cache the full packet header and HandleConnect wrapped it again. The cache
now stores data + 9, len - 9 and every sender wraps it through
BuildFramePacket, and the cache is cleared on cold start and on resolution
change. StopRemoteDesktop is always called outside m_ClientsMutex, with
the ids collected under the lock first.
Co-Authored-By: deepseek-flash
Add keyboard-shortcut (Ctrl+C / Ctrl+X / Ctrl+V) text clipboard transfer
between the browser and the remote desktop, reusing the clipboard protocol
that already exists in the native viewer (COMMAND_SCREEN_GET_CLIPBOARD=24,
COMMAND_SCREEN_SET_CLIPBOARD=25, TOKEN_CLIPBOARD_TEXT=118) so the client
needs no change.
Local -> remote: pressing Ctrl+V reads the browser clipboard, pushes it to
the remote clipboard (UTF-8 -> GBK, matching the client's CF_TEXT/ANSI
handling) and then synthesizes a remote Ctrl+V so the pasted text is the
newly delivered one.
Remote -> local: Ctrl+C or Ctrl+X is forwarded to the remote as usual and,
after a short delay, the remote clipboard is pulled back and written to the
browser clipboard for a later local Ctrl+V.
Both the MFC and Go servers get the same three clipboard signaling paths
(clipboard_set, clipboard_get, clipboard_text) so the shared single-page
frontend behaves identically on either backend.
Co-Authored-By: deepseek-v4-pro
Complete the bidirectional mutual exclusion between MCP remote control
and human Web viewing (§8.2). Direction 1 (remote_open rejected while a
human session holds the screen sub-connection) already existed; this adds
direction 2: a human Web viewer is now rejected while an MCP session owns
the device.
Add a m_McpTriggeredDevices marker (mirroring m_MfcTriggeredDevices) that
is set when an MCP session is created (BeginScreenCtrlOpen) and cleared at
every session-erasure site (CloseScreenCtrlSession, SweepIdleScreenCtrl,
OnScreenControlClosed, EndScreenCtrlAction), so the marker cannot go stale
and permanently block humans. HandleConnect checks IsMcpTriggered before
mutating client state or starting the remote desktop.
Co-Authored-By: deepseek-v4-pro
Add the remote_mouse tool to the MCP remote control surface, injecting
mouse events (move / down / up / click / right_click / middle_click /
drag / scroll) into an established remote_open session. Normalized 0..1
coordinates are mapped to physical pixels via the session's captured
resolution and clamped to screen bounds. Scroll follows the existing web
console's wheel sign convention (positive delta scrolls down) and is
vertical-only.
Extract BuildMouseMsg64 into WebService.h as a shared helper, reused by
both the web console's HandleMouse and the new MCP handler so the two
injection paths cannot drift.
Co-Authored-By: deepseek-v4-pro
Implement milestone M2b of the MCP remote-control design: inject keyboard
events through an existing screen sub-connection.
- Extract the MSG64 keyboard construction from WebService::HandleKey into a
shared inline BuildKeyMsg64 helper in WebService.h, and have HandleKey use
it (behaviour-preserving) so the Web and MCP paths cannot drift.
- Add BuildRemoteKeyboard with four actions: key_down / key_up / key_press
(key name -> VK via MapKeyNameToVk, plus CTRL/ALT/SHIFT/WIN modifiers) and
type (per-character VkKeyScanA mapping, ASCII only, newline/tab -> Enter/Tab).
The batch is sent as one [COMMAND_SCREEN_CONTROL][MSG64*N] packet over the
screen sub-connection under the existing Begin/EndScreenCtrlAction busy
discipline, then audited via WM_SHOWERRORMSG.
Non-ASCII text is rejected (-32602) and must go through remote_clipboard +
Ctrl+V (milestone M4), matching the design's clipboard path for CJK input.
Co-Authored-By: deepseek-v4-pro
Add M1 of MCP remote control (docs/Mcp_RemoteControl_Design.md): the
remote_open / remote_close tools plus the ScreenCtrlSession state machine.
remote_open establishes a hidden screen sub-connection by reusing
WebService::StartRemoteDesktop (COMMAND_SCREEN_SPY -> CScreenSpyDlg ->
RegisterScreenContext), polls for the sub-connection plus its physical
resolution (TOKEN_BITMAPINFO -> NotifyResolutionChange -> GetScreenSize),
then records the session (single device, single session, reverse-mapped
subCtx for OfflineProc cleanup) and returns {session_id, screen_w,
screen_h}. remote_close validates session_id and tears down the
sub-connection idempotently. A McpRemoteControl settings checkbox (default
off, requires McpReadonly=0) gates the tools; every open/close is audited
via WM_SHOWERRORMSG.
Gating: multi-monitor hosts are rejected with -32008 (phase 1 supports only
single monitor, where Observe=main screen and Act=virtual desktop coincide);
the monitor count comes from the client heartbeat RES_RESOLUTION ("N:W*H").
Known limitations (deferred to the injection milestones): mutual exclusion
with human remote-desktop viewing is one-directional in M1 (a human who
joins during an MCP session can tear it down on disconnect), and subCtx is
not yet dereferenced so no liveness re-check is needed until
remote_mouse/remote_keyboard.
Co-Authored-By: deepseek-v4-pro