Persist every device that has ever logged in to the Go server in a small
JSON store (devices.json, overridable with YAMA_DEVICES_FILE and capped by
YAMA_DEVICES_MAX, default 1000 newest). The store follows the users.json
convention: atomic temp-file write with fsync and rename, a 500 ms debounce
for bursts of reconnects, first_seen preserved across logins, and pruning
that evicts offline records before devices that are still connected.
Devices connected at shutdown are stamped offline before the final flush so
last_seen stays accurate across restarts.
The server advertises an offlineDevices capability and device_list now
appends historical devices that are not online, marked with online:false and
last_seen. The web UI shows an Offline checkbox after Total/Showing (state
kept in localStorage), renders offline cards dimmed and dashed, and always
lists them in the screen-wall sidebar. Older servers without the capability
keep the online-only list.
Grid and List cards gain a delete button after the terminal button, admin
only, behind a confirmation dialog. Deleting an offline device removes its
history record; deleting an online device first sends the single-byte
TOKEN_UNINSTALL command (matching the MFC OnOnlineUninstall flow) and then
removes the record too, so the device does not come back as a dead offline
entry. The agent's own disconnect then runs the existing cleanup path:
screen and terminal sub-connections are closed, the hub entry is removed,
and browsers receive device_offline to release their watch sets, wall
sessions, and stream references. Wall mode exposes no delete action.
Tests cover history persistence/pruning/recovery, offline ordering, the
device_list merge, and all three delete outcomes (offline removal, online
uninstall with a single 0x3F byte, and viewer rejection).
Co-Authored-By: deepseek-flash
Bring the Go web backend up to parity with the MFC server's screen wall.
A browser tab can now stream several devices over one WebSocket: the
single `watching` field becomes a per-connection watch set guarded by the
hub mutex, capped at 32 devices per socket. The challenge advertises
`caps.multiWatch` and `caps.keyframeRequest`, binary frames stay keyed by
the low 32 bits of the device ID, and connect/disconnect replies echo the
device id and request id so the wall can route concurrent responses.
Input commands (mouse, key, clipboard, rdp_reset) resolve their target
from the watch set: an explicit id must be a current subscription, and an
absent id is accepted only while exactly one device is watched. Legacy
frontends that do not send `multiWatch` keep replace-all semantics, so a
repeated device switch cannot grow the set without bound. Offline
devices are removed from every watch set, and teardown is computed from a
snapshot taken outside the hub lock to avoid re-entrant locking.
Add the `keyframe` command: it replays the cached IDR, then asks the
device to emit a fresh one, rate-limited to one request per device per
second. This exposed a Go-only corruption bug: the client answers the
request with TOKEN_KEYFRAME (134), which the dispatcher treated as a
no-op. That token only appears on forced IDRs because the client's GOP is
effectively infinite, so the dropped IDR left the browser decoding
P-frames against a reference chain it never received, producing
persistent garbling. Relay it as a keyframe instead. Also drop the
cached IDR when the resolution changes, mirroring the C++ server, so a
late joiner cannot receive an old-geometry keyframe.
Add tests for the watch-set transitions and for relaying TOKEN_KEYFRAME.
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
A screen or terminal sub-connection was keyed solely by a MasterID that any
TCP peer may supply, so any internet host could claim an online device's
MasterID and bind a sub-conn in its place, hijacking the live screen stream
or a pending shell session. The ConnAuth signature cannot be verified on the
server because its key lives in a vendored client library, so the trust
anchor here is the source IP: sub-conns are always opened device-to-server
from the same host as the login conn. BindScreenConn and BindTerminalConn
now reject a sub-conn whose peer IP differs from the device's recorded
main-connection PeerIP.
Co-Authored-By: deepseek-v4-pro