feat: collaboration rooms with switchable presenter (#1328)

* feat: add collaboration rooms with switchable presenter

Rooms are a group of members watching one stage - the live SSH/RDP/VNC
session the current presenter shares. Any member can take over the
stage; the host can invite, force-stop and end the meeting. Stages
reuse session_shares (new room share type), so gating, recording,
expiry and the global sharing toggle all apply unchanged.

* feat: add stage control handoff to collaboration rooms

The presenter or host can grant any member write access to the live
stage and take it back; members can raise a hand to ask. SSH flips the
participant's permission on the live gate; RDP/VNC re-mint the viewer's
join token. Control clears on every stage switch.

* feat: guest links, role invites and invite awareness for collab rooms

- Anonymous guest link per room (host toggles/rotates), followed by
  polling the public resolve endpoint; SSH guests join over the terminal
  WS with roomGuestToken, guac guests get read-only join tokens
- Invite by role (expands to current members, snapshot semantics)
- Toast when a room you were invited to appears
- Stale stages are cleared lazily when the presenter is gone
- Telnet presenting, expired-tab fallback, documented single-instance
  and guac-kick limits
- Tests for the collab routes, room hub, share access and control flip

* fix: keep remote desktop collaboration read-only
This commit is contained in:
ZacharyZcR
2026-08-25 00:56:04 +08:00
committed by GitHub
parent d35458f78b
commit 81d79cc89b
50 changed files with 56698 additions and 81 deletions
+13
View File
@@ -27,6 +27,7 @@ import {
Plug,
ScrollText,
Sparkles,
Presentation,
Workflow,
} from "lucide-react";
import { lazy, Suspense } from "react";
@@ -57,6 +58,11 @@ const loadTerminalFeature = () =>
default: m.Terminal,
}));
const TerminalFeature = lazy(loadTerminalFeature);
const CollabRoomTab = lazy(() =>
import("@/features/collab/CollabRoomTab").then((m) => ({
default: m.CollabRoomTab,
})),
);
const LocalTerminal = lazy(() =>
import("@/features/local-terminal/LocalTerminal").then((m) => ({
default: m.LocalTerminal,
@@ -318,6 +324,8 @@ export function tabIcon(type: TabType) {
return <LayoutGrid className="size-3.5" />;
case "fleet-inventory":
return <Boxes className="size-3.5" />;
case "collab":
return <Presentation className="size-3.5" />;
case "termix-id":
return <Fingerprint className="size-3.5" />;
case "alerts":
@@ -615,6 +623,11 @@ export function renderTabContent(
<FleetInventoryTab fleetId={tab.fleetId} isVisible={isVisible} />,
);
case "collab":
return withTabSuspense(
<CollabRoomTab roomId={tab.collabRoomId} isVisible={isVisible} />,
);
case "termix-id":
return withTabSuspense(
<PanelTabFrame>