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
« 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.
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
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
19logger = logging.getLogger('hevolve.remote_desktop')
22def get_panel_data() -> Dict[str, Any]:
23 """Aggregate all data needed by the remote desktop glass panel.
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 }
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
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}")
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'}}
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}")
101 return result
104# ── JavaScript for LiquidUI Glass Shell ────────────────────────
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 || [];
116 let html = '<div style="display:grid;gap:12px">';
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>';
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>';
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 }
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 }
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 }
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>';
168 html += '</div>';
169 el.innerHTML = html;
170 });
171}
172"""