Coverage for integrations / remote_desktop / gui / panel.py: 100.0%

33 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-12 04:49 +0000

1""" 

2Remote Desktop Glass Panel — Native system panel for LiquidUI Glass Shell. 

3 

4Renders inside a frosted-glass panel window in the HARTOS desktop. 

5Fetches data from /api/remote-desktop/* endpoints and displays: 

6 - Device ID (large, copyable) 

7 - Engine status (RustDesk, Sunshine, Moonlight, Native) 

8 - Active sessions list 

9 - Host/Connect controls 

10 - Install recommendations 

11 

12The actual rendering is done by JavaScript in liquid_ui_service.py 

13(loadRemoteDesktopPanel function). This module provides the Python-side 

14data aggregation that the API endpoints serve. 

15""" 

16import logging 

17from typing import Any, Dict, Optional 

18 

19logger = logging.getLogger('hevolve.remote_desktop') 

20 

21 

22def get_panel_data() -> Dict[str, Any]: 

23 """Aggregate all data needed by the remote desktop glass panel. 

24 

25 Tries orchestrator first (unified view), falls back to individual modules. 

26 Returns dict consumed by the JS panel renderer and the API endpoints. 

27 """ 

28 result = { 

29 'device_id': None, 

30 'formatted_id': None, 

31 'engines': {}, 

32 'sessions': [], 

33 'install_recommendations': [], 

34 'orchestrator_active': False, 

35 } 

36 

37 # Try orchestrator first (unified status) 

38 try: 

39 from integrations.remote_desktop.orchestrator import get_orchestrator 

40 orch = get_orchestrator() 

41 status = orch.get_status() 

42 if status.get('started'): 

43 result['orchestrator_active'] = True 

44 result['device_id'] = status.get('device_id') 

45 result['formatted_id'] = status.get('formatted_id') 

46 result['sessions'] = status.get('sessions', []) 

47 # Engine status from service manager 

48 engine_data = status.get('engines', {}) 

49 result['engines'] = { 

50 name: { 

51 'available': info.get('installed', False), 

52 'running': info.get('running', False), 

53 'healthy': info.get('healthy', False), 

54 'engine': name, 

55 } 

56 for name, info in engine_data.items() 

57 if isinstance(info, dict) 

58 } 

59 return result 

60 except Exception: 

61 pass 

62 

63 # Fallback: individual module queries 

64 # Device identity 

65 try: 

66 from integrations.remote_desktop.device_id import get_device_id, format_device_id 

67 device_id = get_device_id() 

68 result['device_id'] = device_id 

69 result['formatted_id'] = format_device_id(device_id) 

70 except Exception as e: 

71 logger.debug(f"Device ID unavailable: {e}") 

72 

73 # Engine status 

74 try: 

75 from integrations.remote_desktop.engine_selector import get_all_status 

76 status = get_all_status() 

77 result['engines'] = status.get('engines', {}) 

78 result['install_recommendations'] = status.get('install_recommendations', []) 

79 except Exception as e: 

80 logger.debug(f"Engine status unavailable: {e}") 

81 result['engines'] = {'native': {'available': True, 'engine': 'native'}} 

82 

83 # Active sessions 

84 try: 

85 from integrations.remote_desktop.session_manager import get_session_manager 

86 sm = get_session_manager() 

87 sessions = sm.get_active_sessions() 

88 result['sessions'] = [ 

89 { 

90 'session_id': s.session_id, 

91 'host_device_id': s.host_device_id, 

92 'mode': s.mode.value, 

93 'state': s.state.value, 

94 'viewers': s.viewer_device_ids, 

95 } 

96 for s in sessions 

97 ] 

98 except Exception as e: 

99 logger.debug(f"Session manager unavailable: {e}") 

100 

101 return result 

102 

103 

104# ── JavaScript for LiquidUI Glass Shell ──────────────────────── 

105 

106PANEL_JS = """ 

107function loadRemoteDesktopPanel(el, apis) { 

108 Promise.all(apis.map(u=>fetch(BACKEND+u,{signal:AbortSignal.timeout(5000)}).then(r=>r.json()).catch(()=>({})))) 

109 .then(([status,engines,sessions])=>{ 

110 const did = status.formatted_id || 'Unknown'; 

111 const deviceId = status.device_id || ''; 

112 const engineList = status.engines || engines.engines || {}; 

113 const sess = (sessions.sessions || status.active_sessions || []); 

114 const recs = engines.install_recommendations || status.install_recommendations || []; 

115 

116 let html = '<div style="display:grid;gap:12px">'; 

117 

118 // Header + Device ID 

119 html += '<div style="display:flex;justify-content:space-between;align-items:center">'; 

120 html += '<span style="font-weight:var(--hart-heading-weight);font-size:var(--hart-heading-size);color:var(--hart-heading)">Remote Desktop</span>'; 

121 html += '<span class="mi material-icons-round" style="font-size:20px;color:var(--hart-active)">connected_tv</span>'; 

122 html += '</div>'; 

123 

124 // Device ID (large, copyable) 

125 html += '<div style="padding:16px;border-radius:12px;background:var(--hart-surface);text-align:center;cursor:pointer" onclick="navigator.clipboard.writeText(\\''+deviceId+'\\').then(()=>this.querySelector(\\'.copy-hint\\').textContent=\\'Copied!\\')" title="Click to copy">'; 

126 html += '<div style="font-size:11px;color:var(--hart-muted);margin-bottom:4px">Your Device ID</div>'; 

127 html += '<div style="font-size:24px;font-weight:700;letter-spacing:3px;color:var(--hart-heading)">'+did+'</div>'; 

128 html += '<div class="copy-hint" style="font-size:10px;color:var(--hart-muted);margin-top:4px">Click to copy</div>'; 

129 html += '</div>'; 

130 

131 // Engines 

132 html += '<div style="font-weight:600;font-size:12px;color:var(--hart-muted);text-transform:uppercase;letter-spacing:1px">Engines</div>'; 

133 for(const [name,info] of Object.entries(engineList)) { 

134 const avail = info.available; 

135 const color = avail ? 'var(--hart-active)' : 'var(--hart-muted)'; 

136 const icon = avail ? 'check_circle' : 'cancel'; 

137 html += statusRow(icon, name.charAt(0).toUpperCase()+name.slice(1), avail?'Available':'Not installed', color); 

138 } 

139 

140 // Sessions 

141 if(sess.length > 0) { 

142 html += '<div style="font-weight:600;font-size:12px;color:var(--hart-muted);text-transform:uppercase;letter-spacing:1px;margin-top:4px">Active Sessions ('+sess.length+')</div>'; 

143 for(const s of sess) { 

144 html += '<div style="padding:8px;border-radius:8px;background:var(--hart-surface);display:flex;justify-content:space-between;align-items:center">'; 

145 html += '<span style="font-size:12px">'+s.session_id.substring(0,8)+' — '+s.mode+'</span>'; 

146 html += '<span style="font-size:11px;color:var(--hart-active)">'+s.state+'</span>'; 

147 html += '</div>'; 

148 } 

149 } 

150 

151 // Install recommendations 

152 if(recs.length > 0) { 

153 html += '<div style="font-weight:600;font-size:12px;color:var(--hart-muted);text-transform:uppercase;letter-spacing:1px;margin-top:4px">Recommended</div>'; 

154 for(const r of recs) { 

155 html += '<div style="padding:8px;border-radius:8px;background:var(--hart-surface)">'; 

156 html += '<div style="font-size:12px;font-weight:600">'+r.engine+'</div>'; 

157 html += '<div style="font-size:11px;color:var(--hart-muted)">'+r.reason+'</div>'; 

158 html += '</div>'; 

159 } 

160 } 

161 

162 // Action buttons 

163 html += '<div style="display:flex;gap:8px;margin-top:8px">'; 

164 html += '<button onclick="fetch(BACKEND+\\'/api/remote-desktop/host\\',{method:\\'POST\\',headers:{\\'Content-Type\\':\\'application/json\\'},body:JSON.stringify({engine:\\'auto\\'})}).then(r=>r.json()).then(d=>{alert(\\'Hosting started!\\\\nDevice ID: \\'+d.formatted_id+\\'\\\\nPassword: \\'+d.password)})" style="flex:1;padding:10px;border:none;border-radius:8px;background:var(--hart-active);color:white;font-weight:600;cursor:pointer">Host</button>'; 

165 html += '<button onclick="const id=prompt(\\'Enter Device ID:\\');if(id){const pw=prompt(\\'Password:\\');if(pw)fetch(BACKEND+\\'/api/remote-desktop/connect\\',{method:\\'POST\\',headers:{\\'Content-Type\\':\\'application/json\\'},body:JSON.stringify({device_id:id,password:pw})}).then(r=>r.json()).then(d=>alert(d.message||d.error||JSON.stringify(d)))}" style="flex:1;padding:10px;border:none;border-radius:8px;background:var(--hart-surface);color:var(--hart-heading);font-weight:600;cursor:pointer;border:1px solid var(--hart-border)">Connect</button>'; 

166 html += '</div>'; 

167 

168 html += '</div>'; 

169 el.innerHTML = html; 

170 }); 

171} 

172"""