Redesign the console backup into a complete .tar.gz disaster-recovery bundle that can be restored on a fresh machine to bring the entire server back online. Adds zero-dependency tar.gz writer/reader (backupArchive.js), logical dumpAllTables/importAllTables for SQLite and PostgreSQL, and packs the console database, auth.db, .env, .session_secret, branding uploads, and Go server identity (Ed25519 keys, .api_key, db_v2.sqlite3) plus a recovery README. Restore auto-detects archive vs legacy JSON snapshot, supports per-component selection (database/uploads/secrets/.env/Go DB) with restart-required signaling, and warns that the archive contains secrets. EN/PL/ZH i18n added.
This commit was made possible thanks to Insolve.
Add console wallpaper, login page branding (title/subtitle/background/footer), global agent download page branding, footer/copyright with 'Powered by BetterDesk' toggle, and custom CSS injection. Includes background image upload route (8MB), sanitization for colors/gradients/CSS, dynamic theme CSS for app and login pages, sidebar attribution, and EN/PL/ZH i18n keys.
This commit was made possible thanks to Insolve.
Issue #148 reporter @Kreol13 confirmed LDAP test connection works but
actual login fails with 'user not found in database'. Root cause: the
Node.js console only delegates unknown-user logins to the Go server when
BETTERDESK_AUTH_AUTOCREATE=true is set explicitly (security audit fix
H-01). When LDAP/OIDC is configured via the UI, that env var is not set,
so LDAP/OIDC users who don't have a local account are rejected before
Go gets a chance to authenticate them.
Go server:
- New public endpoint GET /api/auth/sso/status returning
{ldap_enabled, oidc_enabled, any_enabled}.
- Restored missing POST /api/auth/oidc/exchange route registration and
authMiddleware whitelist (regression from earlier OIDC hardening).
Node.js console:
- authService.getGoSSOStatus() — cached 60s lookup of Go SSO state.
- authenticate() now auto-provisions LDAP/OIDC-authenticated users when
any SSO provider is enabled on Go, in addition to the existing
BETTERDESK_AUTH_AUTOCREATE opt-in. Default role drops from 'admin' to
'viewer' for safer first-login provisioning when Go does not return
an explicit role mapping. Logs name which provider triggered
provisioning for audit clarity.
The H-01 concern (compromised Go server auto-provisioning admins) is
mitigated because enabling LDAP/OIDC requires an admin with the
server.config permission to explicitly opt in via the Authentication
settings tab.
This commit was made possible thanks to Insolve.
The in-app updater computed changed files via the GitHub compare API, whose 'files' array is capped at 300 entries. Large updates were truncated, so changed Go callee files (codec/ws.go, peer/map.go, auth/ldap.go, auth/oidc.go) were never downloaded, leaving inconsistent on-disk source that failed 'go build' with 'undefined' errors. ensureServerSource() also short-circuited whenever go.mod existed, so the full-resync safety net never ran.
Fixes: ensureServerSource() gains a force option that performs a full, consistent source resync (git clone / tree API) regardless of go.mod presence; called with force from applyUpdate's server block and from rebuildServerBinary (the 'Rebuild server binary' button).
Installer parity (same #158 class): betterdesk.sh, betterdesk.ps1 and betterdesk-docker.sh copied the source *directory* into the destination, which nested the new tree inside the stale dir when the pre-update rename/mv failed on a locked file. They now copy *contents* into a guaranteed-existing dir while preserving data/ and node_modules/.
This commit was made possible thanks to Insolve.
Phases 1-4 of the Go-server centralization plan plus optional-TLS transport.
Go server:
- db: HelpRequest model + SQLite/PostgreSQL stores (help_requests_*.go), GetDeviceOrgID.
- cdap: handleHelpRequest/handleChatMessage handlers, SendChatToDevice delivery.
- api: REST help endpoints (help_handlers.go), publish help_request/chat_message events.
Node.js panel:
- bd-api.routes.js: drop local in-memory Maps, proxy all help/chat/notification
endpoints to the Go server (read-proxy) with status/id/timestamp normalization.
Agent (native Go + Tauri sidecar):
- config.go/agent.go: optional EnforceTLS, ServerCertPin (SPKI pin), TLSInsecureSkipVerify
with env overlays and dialOptions() cert pinning via VerifyPeerCertificate.
- HTTP (ws://) stays a fully supported transport: TLS enforcement is an explicit
operator opt-in (never auto-derived from the URL scheme). The agent logs a warning
recommending wss:// for untrusted networks instead of blocking the connection.
- config.rs/sidecar.rs: propagate enforce_tls + server_cert_pin from AgentConfig
through SidecarConfig to the Go agent config; warn on plaintext ws:// to remote hosts.
This commit was made possible thanks to Insolve.
Implement RustDesk-desktop-style display switching and virtual display management in the web remote client.
- client.js: rewrite switchMonitor to send SwitchDisplay + CaptureDisplays messages with keyframe refresh; getMonitors marks primary (origin 0,0) and current display; add getCurrentDisplay, _processPeerInfo, _parseVirtualDisplaySupport, getVirtualDisplaySupport, toggleVirtualDisplay; switchDisplay receive handler tracks current display.
- remote.js: add i18n helper, peer_info/display_switched/virtual_display_toggled listeners, refreshMonitorButton; rewrite updateMonitorMenu with monitor list plus rustdesk_idd (4 checkboxes) and amyuni_idd (+/- counter) virtual display controls and plug-out-all.
- remote.css: styles for divider, active monitor/virtual items, counter controls.
- i18n: add virtual_displays, virtual_display, plug_out_all keys to en/pl/zh.
This commit was made possible thanks to Insolve.
Both the expanded pill and the compact handle use backdrop-filter, which creates separate stacking contexts. With auto z-index, paint order followed DOM order, so the handle (rendered last) covered the dropdown menus that extend down into its region — the dropdown z-index:60 only applied inside the pill's own context. Give the pill z-index:2 and the handle z-index:1 so dropdowns appear in front of the handle and stay clickable.
This commit was made possible thanks to Insolve.
The expanded toolbar pill used overflow-x:auto, which per the CSS spec forces overflow-y to auto as well. This clipped the absolutely-positioned dropdown menus (Actions, Display, Monitors) that open below the pill (top:100%), making them invisible and unclickable. Switch the pill to overflow:visible and let its buttons wrap (flex-wrap) so the bar stays bounded on small screens without a scroll container that traps the dropdowns.
This commit was made possible thanks to Insolve.
Redesign the web remote toolbar into a RustDesk-style floating pill. The compact handle (move / fullscreen / expand) is always visible and the action pill no longer auto-opens on hover - it expands only on an explicit click of the expand button. The left handle drags the toolbar horizontally along one axis. The 'back to devices' control is now a button that closes the script-opened rdclient tab (re-focusing the opener) instead of navigating this tab to /devices, which previously spawned duplicate web-panel tabs. closeSession uses the same returnToDevices() helper. Adds move_toolbar/toggle_menu i18n keys (EN/PL/ZH) and an updated cross-platform feature matrix.
This commit was made possible thanks to Insolve.
Complete the web console locale set so all 26 language files share the EN/PL baseline with no missing keys, extra keys, empty values, or English fallback values.
Keep strict i18n audit behavior and disabled auto-fix flow so incomplete translations are surfaced for manual review instead of being filled with English fallback text.
Validated with the strict web-nodejs i18n audit, JSON parsing for all locale files, placeholder preservation checks, and VS Code diagnostics.
This commit was made possible thanks to Insolve.
Bump golang.org/x/crypto to v0.52.0 and x/sys to v0.45.0 in betterdesk-server, x/sys to v0.45.0 in betterdesk-agent, and aiohttp to >=3.10.11 in the rest-webhook bridge. Validated clean via govulncheck.
Add an explicit Go server rebuild path to the in-app updater so security/library updates actually reach the running binary: updateService now marks the server binary stale when the source changed but a non-critical rebuild/deploy step failed, exposes getServerBinaryStatus() and rebuildServerBinary(), and surfaces a warning banner plus a Rebuild button in the settings update panel. New REST endpoints GET/POST /api/settings/updates/server-binary/{status,rebuild} guarded by server.config permission. i18n keys added across all locales.
This commit was made possible thanks to Insolve.
serializeMessage used Message.create() which does NOT convert string enum names to numeric values. A KeyEvent with controlKey:'Backspace' (or Space/Tab/Return/arrows) and mode:'Legacy' was encoded as control_key=0 (Unknown), so every non-character key was silently dropped by the peer while numeric chr letter keys worked. Switched to Message.fromObject() which converts enum name strings to their numeric values and preserves Uint8Array byte fields (clipboard content, file blocks, password, public keys). Verified round-trip: Backspace->2, Space->30, mode Legacy->0, bytes/login/file-block/mouse intact.
This commit was made possible thanks to Insolve.
Fixes four reported web remote client defects:
- FPS stats stuck at 0: _handleDecodedFrame (WebCodecs/HTTPS path) now records feed timestamps so getStats() reports real FPS (previously only the JMuxer fallback fed the array).
- Top toolbar never auto-hid: mousemove handler no longer re-shows the bar on every move (removed '|| toolbarVisible'), so it hides 3s after the pointer leaves the top edge.
- Keyboard support: rewritten Legacy-mode key handling with full-Unicode codePointAt, AltGr awareness (strips phantom Ctrl+Alt for printable glyphs so accented/national chars type correctly), and proper down/press/up repeat semantics matching RustDesk.
- Clipboard: local clipboard is now auto-pushed to the remote on window focus and viewer mousedown, so a plain Ctrl+V on the remote pastes local content like native RustDesk.
This commit was made possible thanks to Insolve.
The controlled RustDesk peer periodically sends TestDelay{from_client:false} as a QoS probe and measures its round-trip to size both target bitrate and framerate. We were replying with a fresh buildTestDelay() (new timestamp), so the peer computed a bogus delay and throttled the stream down to ~1 fps. Now we echo the probe back verbatim (same time, from_client stays false), restoring the full 24-30 fps stream like the native client.
This commit was made possible thanks to Insolve.
RDVideo.getSupportedCodecs() probed only with hardwareAcceleration:'prefer-hardware', so on machines without hardware decode for VP9/AV1/VP8/H265 every codec except Auto was reported unsupported. That both grayed out the whole codec menu and made the client advertise only H264 in its SupportedDecoding at login, so the peer never offered another codec. Now probe prefer-hardware, then prefer-software, then no-preference and treat a codec as supported if ANY variant works.
This commit was made possible thanks to Insolve.
Remove the user-mode / rootless install and privilege-separation scaffolding that was added in the previous commit — the feature needs more testing before shipping. Reverts: USER_MODE, RUN_AS_ROOT, ensure_service_user, set_service_ownership, systemd User=/Group= directives, all --user/--rootless/--run-as-root CLI flags, and related help text. Restores API_PORT default to 21114.
server.js: fix startup banner — show 'Disabled' (not 'Served by Go server') when apiEnabled is false, and correct the Go API fallback URL from port 21121 to 21114.
This commit was made possible thanks to Insolve.
Add a codec selector (Auto/VP9/AV1/H264/VP8/H265) to the remote viewer display menu so users can request a GPU-friendly codec from the peer. setCodec() re-advertises SupportedDecoding abilities with the chosen prefer codec via OptionMessage and forces a fresh keyframe. Browser decode abilities are probed (VideoDecoder.isConfigSupported) and unsupported codecs are disabled in the menu and advertised honestly at login. Raise default image quality from Balanced to Best at both client-creation sites and pause adaptive auto-quality once the user makes a manual quality/codec choice so it no longer overrides them.
This commit was made possible thanks to Insolve.
WebCodecs VideoDecoder must receive a keyframe as the first chunk after configure(); a leading delta frame throws and the decoder produces no output. Because delta frames kept resetting the last-frame timer, the 3s stall-recovery never fired and the keyframe was never requested, leaving a permanent black screen (and 0x0 remote size, which also broke input mapping).
video.js: drop delta frames until a keyframe arrives (_needKeyframe gate after configure), use a dedicated monotonic input counter for chunk timestamps, drop deltas when the decode queue backs up, and rebuild the decoder with a software-decoding fallback + keyframe request on decoder error. init() now probes codec support without forcing hardware acceleration so software-only environments still pass.
client.js: wire video.onNeedKeyframe to send refresh_video, request an initial keyframe at session start, and extend stall-recovery to also fire when peer frames arrive but the decoder yields no output for 2s.
This commit was made possible thanks to Insolve.
Brings in GPU multi-codec video for the CDAP rdclient (HTTP H.264 via MSE, HTTPS AV1/VP9/H264 via WebCodecs), agent-client runtime branding, and the prebuilt Windows sidecar binary.
This commit was made possible thanks to Insolve.
Enables hardware-accelerated video for the CDAP/OS-agent rdclient path instead of being stuck on 1-2fps MJPEG, including over plain HTTP (no HTTPS/domain/cert required).
Browser viewers (web-nodejs): cdap-adapter.js and cdap-desktop.js now decode video via the shared RDVideo pipeline. WebCodecs (AV1/VP9/H264, hardware-accelerated) is used in secure contexts (HTTPS/localhost); over plain HTTP, H.264 is decoded through JMuxer/MSE using the browser's native (usually GPU) decoder. Viewers advertise decodableCodecs() in the desktop_start init payload so the agent sends a real GPU stream; MJPEG/WebP remain safe fallbacks. cdap-device.ejs preloads jmuxer + RDVideo.
Go agent (betterdesk-agent): codec.go + codec_framing.go add multi-codec engine with GPU encoder probing and selection order AV1 -> VP9 -> H264 -> WebP; desktop.go intersects the operator-advertised codec list with local encoder ability and emits honest desktop_meta (format + codec_string); config.go adds codec config fields.
Go server (betterdesk-server): cdap_handlers.go + cdap/desktop.go thread the codecs[] / video_codec fields through desktop_start so operator codec advertisement reaches the agent.
Tauri agent-client: config.rs/sidecar.rs/commands.rs codec plumbing, SettingsPanel codec dropdown + en/pl/zh i18n, refreshed agent sidecar binary.
Note: AV1/VP9 still require WebCodecs (HTTPS/localhost); H.264-over-MSE covers the plain-HTTP case. Self-signed HTTPS (installer SSL menu option C) unlocks all codecs. Fully backward compatible.
This commit was made possible thanks to Insolve.
Add a device verification/enrollment workflow so new registrations are
held for operator review instead of connecting silently.
Go server (signal + api):
- Signal-mode pending: in 'managed' mode, unknown stock RustDesk clients
are queued (pending_device_<id>) instead of being silently rejected.
- Rich approve: handleApproveDevice accepts display_name, sync_mode and
normalized tags; handleRejectDevice supports an optional ban.
Node.js console:
- registrations.ejs approve modal (name, sync mode, tags, folder) and
reject modal with ban option; betterdeskApi + routes wire display_name,
sync_mode, tags and folder assignment through to the Go server.
- EN/PL/ZH i18n for all new strings.
Installers (managed default for FRESH installs only; existing installs
stay on the Go default 'open' or their DB-persisted mode):
- betterdesk.sh / betterdesk.ps1 write ENROLLMENT_MODE=managed to the
server env only when no existing database is detected (FRESH_INSTALL).
- Docker single + multi container entrypoints detect fresh volumes via a
sentinel plus id_ed25519/db_v2.sqlite3 presence and default to managed;
ENROLLMENT_MODE is now passed through supervisord and all compose files.
This commit was made possible thanks to Insolve.
Includes: alpha bundle generator, cross-platform build pipeline, RustDesk-client API endpoints on Go server (Phase A), privilege separation across all installers (systemd hardening, NSSM virtual accounts, Docker verified).
This commit was made possible thanks to Insolve.
Privilege separation across all installers so the long-running services no longer run with full administrative rights:
betterdesk.sh: installer keeps root but systemd units now run as a dedicated unprivileged 'betterdesk' system account by default (auto-created via ensure_service_user). Added full systemd hardening for the Go server (NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, ReadWritePaths) and light hardening for the Node.js console. chown migrates existing root-owned data to the service account on update. Opt-out via --run-as-root / BETTERDESK_RUN_AS_ROOT=1; custom account via BETTERDESK_SERVICE_USER. Minimal mode covered too.
betterdesk.ps1: NSSM services now run under their per-service low-privilege virtual accounts (NT SERVICE\<service>) instead of LocalSystem, with scoped icacls grants on the install/data dirs (Set-ServiceLeastPrivilege helper). Applied to the Go server, Node.js console and minimal-mode service. Opt-out via -RunAsRoot / BETTERDESK_RUN_AS_ROOT=1.
Docker: verified already privilege-separated (supervisord drops both programs to user=betterdesk; multi-container images drop via su-exec).
Also bundles in-progress changes to the Go server API, Node.js console services and Docker compose/Dockerfiles.
This commit was made possible thanks to Insolve.
The policies/attestation page routes used a local requireAdmin that only accepted the literal role 'admin' and redirected rejected users to /dashboard. Under the new 6-role RBAC (Phase 52b), fresh-install admins have role 'super_admin', so the check failed and users were redirected to /dashboard, which has no route and renders the 404 page.
Replaced the local middleware in policies.routes.js with the shared RBAC-aware requireAuth/requireAdmin from middleware/auth.js (accepts super_admin/admin/global_admin, renders errors/403 for others instead of a 404 redirect). Fixed the same outdated role==='admin' checks in reports.routes.js, tenants.routes.js, and cdap-studio.routes.js using isSuperAdminRole.
This commit was made possible thanks to Insolve.
Go server:
- mergeAdminTagsIntoAB: strip banned/deleted peers from AB data
- handleClientGroupList: exclude banned peers from tag groups
- handleClientPeersList: add device_name fallback to peer ID, add online field
- handleGetPeer/handleListPeers: return status as int (1/0) with status_text
- handleUsersWithClientFallback: only return users with assigned devices
Node.js console:
- mergeAddressBookData: filter banned devices from AB merge
- buildSyncedAddressBook: set includeDevices=false to prevent ghost AB entries
- normalisePeer: use status_text fallback for status_tier
This commit was made possible thanks to Insolve.
The SSO commit (188991d) changed authenticate() to delegate auth to the Go server first, falling back to local-only for admin roles in 'emergency mode'. This broke login for all users whose accounts exist only in auth.db (Node.js) but not in Go server's database — which is the default for ALL-IN-ONE installs. Go server returned 401 for unknown users, and the new code treated that as a hard rejection with no local fallback. Restored the original local-first flow: check auth.db first, verify password locally, use Go server only as a fallback when local password fails (LDAP/password change sync) or when user doesn't exist locally (opt-in auto-create).
This commit was made possible thanks to Insolve.
auth.routes.js: strip trailing /api segment from BETTERDESK_API_URL when building the absolute OIDC authorize redirect, otherwise the env value (which intentionally ends with /api for axios baseURL) caused doubled /api/api/auth/oidc/authorize URLs.
betterdeskApi.js: testLDAPConnection and testOIDCDiscovery now extract e.response.data.error from axios failures, so the Settings -> SSO test buttons show real Go-side messages (e.g. 'LDAP host is required', DNS errors) instead of the generic 'Request failed with status code 400'.
This commit was made possible thanks to Insolve.
updateService.js: distinguish critical vs non-critical failures in SHA tracking. Server binary compile/download failures are non-critical — SHA is saved so the same update is not shown again on restart. update-cli.js: match same logic, non-critical failures don't set exit code 1. betterdesk.sh: new update_from_github() with git clone + tarball fallback, 3-method menu. betterdesk.ps1: new Update-FromGitHub with git clone + ZIP fallback, 3-method menu. betterdesk-docker.sh: new update_docker_from_github() with 2-method menu.
This commit was made possible thanks to Insolve.
Fleet, scaling, cross-platform were missing from management category. Toolkit was missing from tools category. This caused the sidebar flyout to not open and the user card to jump to the top when visiting these pages.
This commit was made possible thanks to Insolve.
UI: group chip buttons changed to icon-only (matching folder chips), Create Group tile styling unified with Create Folder, old .group-chip-action CSS replaced with unified .chip-action class.
Go server: peerResponse and singlePeerResponse now return status as int (1=active, 0=disabled) instead of string, added status_text for admin panel backward compat. Fixes RustDesk client crash 'type String is not a subtype of type int?'.
Go server: new handleUsersWithClientFallback — detects RustDesk client requests to /api/users and returns current user without requiring user.view permission. Fixes disappearing folders/groups caused by _getUsers() 403 short-circuiting _pull().
Node.js: normalisePeer updated to use status_text fallback for status_tier.
Branding: RustDesk Server Management -> BetterDesk Server Management across themes, i18n, settings. Console version bumped to 3.0.0.
This commit was made possible thanks to Insolve.
Removes browsing-topics=() directive which caused console warnings in browsers
that don't support this non-standard feature.
This commit was made possible thanks to Insolve.
- Remove string 'id' from group payload (caused Dart 'String is not subtype of int?' crash)
- Add team.peers array with device IDs, access_perm, sort to group response
- Change device filter from online-only to all non-banned/non-disabled peers
- Fix peer format: nested info object, int status, bool online (matching Go server)
- Enable includeDevices for admin/operator AB sync (60 peers now visible)
- Collect peer_ids from device_group_members and device_folder_assignments
This commit was made possible thanks to Insolve.
- Add menu option T (Toggle HTTP/HTTPS) to betterdesk.sh and betterdesk.ps1
- Add --protocol http|https CLI flag (bash) and -Protocol param (PS1)
- Toggle updates .env, systemd/NSSM services, and Go server TLS flags
- Auto-generate self-signed cert when switching to HTTPS if none exists
- Go API (:21114) always stays HTTP (internal Node.js<->Go communication)
- Fix betterdeskApi.js: conditional httpsAgent/httpAgent based on URL scheme
- Fix deviceStatusPush.js: TLS options only applied for wss:// connections
This commit was made possible thanks to Insolve.
5 root causes identified and fixed:
- H1: /api/peers returned status as string 'ONLINE' — RustDesk PeerPayload expects int (1=active, 0=disabled). New handleClientPeersList() detects client requests (?accessible/?pageSize) and returns {total,data} envelope with PeerPayload format (nested info map, int status).
- H2: mergeAdminTagsIntoAB() used typed struct → silently dropped tag_colors field. Rewritten with map[string]any to preserve all AB fields.
- H3: /api/device-group/accessible missing (404) — RustDesk GroupModel._getDeviceGroups() calls this endpoint. Added route pointing to handleClientGroupList.
- H4: /api/users returned plain array with wrong field names — RustDesk expects {total,data} with UserPayload (name, display_name, status:int, is_admin:bool). New handleClientUsersList() returns correct format.
- L1: .group-chip.chip-add CSS didn't fully override base 3-column grid. Added explicit grid/centering/dashed-border overrides.
Also updated handleClientGroupPeers (/api/peers/list) to use PeerPayload format with nested info map for consistency.
This commit was made possible thanks to Insolve.
Sidebar footer previously rendered the raw DB role string (e.g. 'global_admin'). Now resolves user.role to users.role_<role> translation key with snake_case->Title Case fallback when key is missing.
Node.js (no recompile required):
- H-04: RUSTDESK_API_DISABLE_TOTP now requires explicit _ACKNOWLEDGED flag
- M-03: drop Referer-based skip from apiLimiter; add dedicated widgetLimiter
- M-06: gate /api/system/info, /logs/recent, /database/stats, /docker/containers, /speed-test behind requirePermission('metrics.view')
- L-01: startup banner now warns when TRUST_PROXY is off in production / errors when TOTP bypass is set without acknowledgement
Scripts / Docker:
- M-04: betterdesk.sh migration tool invocation switched from eval(cmd-string) to bash array exec
- M-05: all admin / PostgreSQL password generators switched from openssl rand -base64+tr+head to openssl rand -hex 16 (full entropy)
- L-02: docker-compose.yml / single.yml / quick.yml services gain security_opt: no-new-privileges and cap_drop: ALL
Documentation:
- I-04: add SECURITY.md (supported versions, reporting channels, SLA, scope, hardening defaults)
Go server (requires rebuild on host: cd betterdesk-server && go build ./...):
- H-03: /metrics now gated by METRICS_IP_ALLOWLIST / METRICS_PUBLIC; per-username login + 2FA rate-limit added on top of per-IP
- M-07: enrollment (/api/devices/register*) and branding (GET /api/branding) endpoints rate-limited per IP
- I-02: bd-mgmt WebSocket gets SetReadLimit(16 MiB) to bound memory
- L-04: auth middleware skips noisy public probes and redacts /peers/{id} segments
This commit was made possible thanks to Insolve.
- Updated Polish and Chinese language files to reflect changes in update descriptions and confirmations.
- Modified settings.js to automatically include server updates in the update process.
- Simplified the update installation API to automatically handle server updates without user intervention.
- Improved updateService.js to ensure all supported components are updated together, enhancing reliability.
- Added logic to check and install the Go toolchain as needed during updates, ensuring compatibility.
- Enhanced error handling and logging during the update process for better visibility of issues.
Make device group edit/delete actions visible, add direct user-group management entry points from the device group ACL modal, and allow device group editors to load user groups for ACL assignment.
Refs #140.