reachy-mini 1.2.5rc1__py3-none-any.whl → 1.2.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. reachy_mini/apps/app.py +24 -21
  2. reachy_mini/apps/manager.py +17 -3
  3. reachy_mini/apps/sources/hf_auth.py +92 -0
  4. reachy_mini/apps/sources/hf_space.py +1 -1
  5. reachy_mini/apps/sources/local_common_venv.py +199 -24
  6. reachy_mini/apps/templates/main.py.j2 +4 -3
  7. reachy_mini/daemon/app/dashboard/static/js/apps.js +9 -1
  8. reachy_mini/daemon/app/dashboard/static/js/appstore.js +228 -0
  9. reachy_mini/daemon/app/dashboard/static/js/logs.js +148 -0
  10. reachy_mini/daemon/app/dashboard/templates/logs.html +37 -0
  11. reachy_mini/daemon/app/dashboard/templates/sections/appstore.html +92 -0
  12. reachy_mini/daemon/app/dashboard/templates/sections/cache.html +82 -0
  13. reachy_mini/daemon/app/dashboard/templates/sections/daemon.html +5 -0
  14. reachy_mini/daemon/app/dashboard/templates/settings.html +1 -0
  15. reachy_mini/daemon/app/main.py +172 -7
  16. reachy_mini/daemon/app/models.py +8 -0
  17. reachy_mini/daemon/app/routers/apps.py +56 -0
  18. reachy_mini/daemon/app/routers/cache.py +58 -0
  19. reachy_mini/daemon/app/routers/hf_auth.py +57 -0
  20. reachy_mini/daemon/app/routers/logs.py +124 -0
  21. reachy_mini/daemon/app/routers/state.py +25 -1
  22. reachy_mini/daemon/app/routers/wifi_config.py +75 -0
  23. reachy_mini/daemon/app/services/bluetooth/bluetooth_service.py +1 -1
  24. reachy_mini/daemon/app/services/bluetooth/commands/WIFI_RESET.sh +8 -0
  25. reachy_mini/daemon/app/services/wireless/launcher.sh +8 -2
  26. reachy_mini/daemon/app/services/wireless/reachy-mini-daemon.service +13 -0
  27. reachy_mini/daemon/backend/abstract.py +29 -9
  28. reachy_mini/daemon/backend/mockup_sim/__init__.py +12 -0
  29. reachy_mini/daemon/backend/mockup_sim/backend.py +176 -0
  30. reachy_mini/daemon/backend/mujoco/backend.py +0 -5
  31. reachy_mini/daemon/backend/robot/backend.py +78 -5
  32. reachy_mini/daemon/daemon.py +46 -7
  33. reachy_mini/daemon/utils.py +71 -15
  34. reachy_mini/io/zenoh_client.py +26 -0
  35. reachy_mini/io/zenoh_server.py +10 -6
  36. reachy_mini/kinematics/nn_kinematics.py +2 -2
  37. reachy_mini/kinematics/placo_kinematics.py +15 -15
  38. reachy_mini/media/__init__.py +55 -1
  39. reachy_mini/media/audio_base.py +185 -13
  40. reachy_mini/media/audio_control_utils.py +60 -5
  41. reachy_mini/media/audio_gstreamer.py +97 -16
  42. reachy_mini/media/audio_sounddevice.py +120 -19
  43. reachy_mini/media/audio_utils.py +110 -5
  44. reachy_mini/media/camera_base.py +182 -11
  45. reachy_mini/media/camera_constants.py +132 -4
  46. reachy_mini/media/camera_gstreamer.py +42 -2
  47. reachy_mini/media/camera_opencv.py +83 -5
  48. reachy_mini/media/camera_utils.py +95 -7
  49. reachy_mini/media/media_manager.py +139 -6
  50. reachy_mini/media/webrtc_client_gstreamer.py +142 -13
  51. reachy_mini/media/webrtc_daemon.py +72 -7
  52. reachy_mini/motion/recorded_move.py +76 -2
  53. reachy_mini/reachy_mini.py +196 -40
  54. reachy_mini/tools/reflash_motors.py +1 -1
  55. reachy_mini/tools/scan_motors.py +86 -0
  56. reachy_mini/tools/setup_motor.py +49 -31
  57. reachy_mini/utils/interpolation.py +1 -1
  58. reachy_mini/utils/wireless_version/startup_check.py +278 -21
  59. reachy_mini/utils/wireless_version/update.py +44 -1
  60. {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/METADATA +7 -6
  61. {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/RECORD +65 -53
  62. {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/WHEEL +0 -0
  63. {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/entry_points.txt +0 -0
  64. {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/licenses/LICENSE +0 -0
  65. {reachy_mini-1.2.5rc1.dist-info → reachy_mini-1.2.11.dist-info}/top_level.txt +0 -0
@@ -152,6 +152,231 @@ const hfAppsStore = {
152
152
  installedApps.refreshAppList();
153
153
  };
154
154
  },
155
+
156
+ // Advanced functionality for private spaces
157
+ advanced: {
158
+ isAuthenticated: false,
159
+ username: null,
160
+
161
+ init: async () => {
162
+ // Check if we're on wireless version
163
+ try {
164
+ const status = await fetch('/api/daemon/status').then(r => r.json());
165
+ const isWireless = status.wireless_version;
166
+
167
+ if (!isWireless) {
168
+ // Don't show advanced section on non-wireless
169
+ return;
170
+ }
171
+
172
+ // Show the advanced section
173
+ document.getElementById('hf-advanced-section').classList.remove('hidden');
174
+
175
+ // Set up event listeners
176
+ document.getElementById('hf-advanced-toggle').onclick = hfAppsStore.advanced.toggleSection;
177
+ document.getElementById('hf-login-button').onclick = hfAppsStore.advanced.login;
178
+ document.getElementById('hf-logout-button').onclick = hfAppsStore.advanced.logout;
179
+ document.getElementById('hf-install-private-button').onclick = hfAppsStore.advanced.installPrivateSpace;
180
+
181
+ // Add Enter key support for token input
182
+ document.getElementById('hf-token-input').addEventListener('keypress', (e) => {
183
+ if (e.key === 'Enter') {
184
+ hfAppsStore.advanced.login();
185
+ }
186
+ });
187
+
188
+ // Add Enter key support for space ID input
189
+ document.getElementById('hf-space-id-input').addEventListener('keypress', (e) => {
190
+ if (e.key === 'Enter') {
191
+ hfAppsStore.advanced.installPrivateSpace();
192
+ }
193
+ });
194
+
195
+ // Check authentication status
196
+ await hfAppsStore.advanced.checkAuthStatus();
197
+ } catch (error) {
198
+ console.error('Error initializing advanced section:', error);
199
+ }
200
+ },
201
+
202
+ toggleSection: () => {
203
+ const content = document.getElementById('hf-advanced-content');
204
+ const chevron = document.getElementById('hf-advanced-chevron');
205
+
206
+ if (content.classList.contains('hidden')) {
207
+ content.classList.remove('hidden');
208
+ chevron.style.transform = 'rotate(90deg)';
209
+ } else {
210
+ content.classList.add('hidden');
211
+ chevron.style.transform = 'rotate(0deg)';
212
+ }
213
+ },
214
+
215
+ checkAuthStatus: async () => {
216
+ try {
217
+ const response = await fetch('/api/hf-auth/status');
218
+ const data = await response.json();
219
+
220
+ hfAppsStore.advanced.isAuthenticated = data.is_logged_in;
221
+ hfAppsStore.advanced.username = data.username;
222
+
223
+ hfAppsStore.advanced.updateAuthUI();
224
+ } catch (error) {
225
+ console.error('Error checking auth status:', error);
226
+ }
227
+ },
228
+
229
+ updateAuthUI: () => {
230
+ const indicator = document.getElementById('hf-auth-indicator');
231
+ const authText = document.getElementById('hf-auth-text');
232
+ const loginForm = document.getElementById('hf-login-form');
233
+ const loggedInView = document.getElementById('hf-logged-in-view');
234
+ const usernameSpan = document.getElementById('hf-username');
235
+
236
+ if (hfAppsStore.advanced.isAuthenticated) {
237
+ // Authenticated state
238
+ indicator.classList.remove('bg-gray-400');
239
+ indicator.classList.add('bg-green-500');
240
+ authText.textContent = 'Authenticated';
241
+ loginForm.classList.add('hidden');
242
+ loggedInView.classList.remove('hidden');
243
+ usernameSpan.textContent = hfAppsStore.advanced.username || 'Unknown';
244
+ } else {
245
+ // Not authenticated state
246
+ indicator.classList.remove('bg-green-500');
247
+ indicator.classList.add('bg-gray-400');
248
+ authText.textContent = 'Not authenticated';
249
+ loginForm.classList.remove('hidden');
250
+ loggedInView.classList.add('hidden');
251
+ }
252
+ },
253
+
254
+ login: async () => {
255
+ const tokenInput = document.getElementById('hf-token-input');
256
+ const errorDiv = document.getElementById('hf-login-error');
257
+ const loginButton = document.getElementById('hf-login-button');
258
+
259
+ const token = tokenInput.value.trim();
260
+
261
+ if (!token) {
262
+ errorDiv.textContent = 'Please enter a token';
263
+ errorDiv.classList.remove('hidden');
264
+ return;
265
+ }
266
+
267
+ // Disable button during request
268
+ loginButton.disabled = true;
269
+ loginButton.textContent = 'Logging in...';
270
+ errorDiv.classList.add('hidden');
271
+
272
+ try {
273
+ const response = await fetch('/api/hf-auth/save-token', {
274
+ method: 'POST',
275
+ headers: { 'Content-Type': 'application/json' },
276
+ body: JSON.stringify({ token })
277
+ });
278
+
279
+ if (!response.ok) {
280
+ const error = await response.json();
281
+ throw new Error(error.detail || 'Login failed');
282
+ }
283
+
284
+ const data = await response.json();
285
+
286
+ // Clear token input
287
+ tokenInput.value = '';
288
+
289
+ // Update state
290
+ hfAppsStore.advanced.isAuthenticated = true;
291
+ hfAppsStore.advanced.username = data.username;
292
+ hfAppsStore.advanced.updateAuthUI();
293
+
294
+ } catch (error) {
295
+ errorDiv.textContent = error.message;
296
+ errorDiv.classList.remove('hidden');
297
+ } finally {
298
+ loginButton.disabled = false;
299
+ loginButton.textContent = 'Login';
300
+ }
301
+ },
302
+
303
+ logout: async () => {
304
+ try {
305
+ await fetch('/api/hf-auth/token', { method: 'DELETE' });
306
+
307
+ hfAppsStore.advanced.isAuthenticated = false;
308
+ hfAppsStore.advanced.username = null;
309
+ hfAppsStore.advanced.updateAuthUI();
310
+
311
+ } catch (error) {
312
+ console.error('Error logging out:', error);
313
+ }
314
+ },
315
+
316
+ installPrivateSpace: async () => {
317
+ const spaceIdInput = document.getElementById('hf-space-id-input');
318
+ const errorDiv = document.getElementById('hf-private-install-error');
319
+ const installButton = document.getElementById('hf-install-private-button');
320
+
321
+ const spaceId = spaceIdInput.value.trim();
322
+
323
+ if (!spaceId) {
324
+ errorDiv.textContent = 'Please enter a space ID';
325
+ errorDiv.classList.remove('hidden');
326
+ return;
327
+ }
328
+
329
+ // Validate format (should be "username/space-name")
330
+ if (!spaceId.includes('/')) {
331
+ errorDiv.textContent = 'Space ID should be in format: username/space-name';
332
+ errorDiv.classList.remove('hidden');
333
+ return;
334
+ }
335
+
336
+ // Disable button during request
337
+ installButton.disabled = true;
338
+ installButton.textContent = 'Installing...';
339
+ errorDiv.classList.add('hidden');
340
+
341
+ try {
342
+ const response = await fetch('/api/apps/install-private-space', {
343
+ method: 'POST',
344
+ headers: { 'Content-Type': 'application/json' },
345
+ body: JSON.stringify({ space_id: spaceId })
346
+ });
347
+
348
+ if (!response.ok) {
349
+ const error = await response.json();
350
+ throw new Error(error.detail || 'Installation failed');
351
+ }
352
+
353
+ const data = await response.json();
354
+ const jobId = data.job_id;
355
+
356
+ // Clear input
357
+ spaceIdInput.value = '';
358
+
359
+ // Show installation modal (reuse existing modal)
360
+ const spaceName = spaceId.split('/')[1];
361
+ hfAppsStore.appInstallLogHandler(spaceName, jobId);
362
+
363
+ } catch (error) {
364
+ if (error.message.includes('authenticate') || error.message.includes('401')) {
365
+ // Token expired or invalid - show login form
366
+ errorDiv.textContent = 'Authentication expired. Please login again.';
367
+ hfAppsStore.advanced.isAuthenticated = false;
368
+ hfAppsStore.advanced.username = null;
369
+ hfAppsStore.advanced.updateAuthUI();
370
+ } else {
371
+ errorDiv.textContent = error.message;
372
+ }
373
+ errorDiv.classList.remove('hidden');
374
+ } finally {
375
+ installButton.disabled = false;
376
+ installButton.textContent = 'Install Private Space';
377
+ }
378
+ },
379
+ },
155
380
  };
156
381
 
157
382
 
@@ -164,4 +389,7 @@ window.addEventListener('load', async () => {
164
389
  });
165
390
  }
166
391
  await hfAppsStore.refreshAppList();
392
+
393
+ // Initialize advanced section for private spaces
394
+ await hfAppsStore.advanced.init();
167
395
  });
@@ -0,0 +1,148 @@
1
+ const daemonLogs = {
2
+ ws: null,
3
+ isConnected: false,
4
+
5
+ connectWebSocket: () => {
6
+ const logsDiv = document.getElementById('daemon-logs-content');
7
+ const statusDiv = document.getElementById('logs-status');
8
+
9
+ if (!logsDiv || !statusDiv) {
10
+ console.error('Logs elements not found');
11
+ return;
12
+ }
13
+
14
+ // Create WebSocket connection
15
+ daemonLogs.ws = new WebSocket(`ws://${location.host}/logs/ws/daemon`);
16
+
17
+ daemonLogs.ws.onopen = () => {
18
+ daemonLogs.isConnected = true;
19
+ statusDiv.textContent = 'Connected';
20
+ statusDiv.className = 'text-sm text-green-600 font-semibold';
21
+ };
22
+
23
+ daemonLogs.ws.onmessage = (event) => {
24
+ // Ignore empty keepalive messages
25
+ if (event.data && event.data.trim()) {
26
+ daemonLogs.appendLog(event.data);
27
+ }
28
+ };
29
+
30
+ daemonLogs.ws.onclose = () => {
31
+ daemonLogs.isConnected = false;
32
+ statusDiv.textContent = 'Disconnected';
33
+ statusDiv.className = 'text-sm text-red-600 font-semibold';
34
+
35
+ // Attempt reconnection after 2 seconds
36
+ setTimeout(() => {
37
+ if (!daemonLogs.isConnected) {
38
+ console.log('Attempting to reconnect to logs WebSocket...');
39
+ daemonLogs.connectWebSocket();
40
+ }
41
+ }, 2000);
42
+ };
43
+
44
+ daemonLogs.ws.onerror = (error) => {
45
+ console.error('WebSocket error:', error);
46
+ statusDiv.textContent = 'Connection Error';
47
+ statusDiv.className = 'text-sm text-red-600 font-semibold';
48
+ };
49
+ },
50
+
51
+ appendLog: (line) => {
52
+ const logsDiv = document.getElementById('daemon-logs-content');
53
+
54
+ if (!logsDiv) {
55
+ return;
56
+ }
57
+
58
+ // Check if user is scrolled to bottom before adding new log
59
+ const isScrolledToBottom = logsDiv.scrollHeight - logsDiv.scrollTop <= logsDiv.clientHeight + 50;
60
+
61
+ // Create new log line element
62
+ const logLine = document.createElement('div');
63
+ logLine.className = 'text-gray-300 leading-tight';
64
+
65
+ // Highlight error and warning lines
66
+ if (line.includes('ERROR') || line.includes('Error') || line.includes('error')) {
67
+ logLine.className = 'text-red-400 font-semibold leading-tight';
68
+ } else if (line.includes('WARNING') || line.includes('Warning') || line.includes('warning')) {
69
+ logLine.className = 'text-yellow-400 leading-tight';
70
+ } else if (line.includes('INFO')) {
71
+ logLine.className = 'text-green-400 leading-tight';
72
+ }
73
+
74
+ logLine.textContent = line;
75
+ logsDiv.appendChild(logLine);
76
+
77
+ // Only auto-scroll if user was already at the bottom
78
+ if (isScrolledToBottom) {
79
+ requestAnimationFrame(() => {
80
+ logsDiv.scrollTop = logsDiv.scrollHeight;
81
+ });
82
+ }
83
+ },
84
+
85
+ clearLogs: () => {
86
+ const logsDiv = document.getElementById('daemon-logs-content');
87
+
88
+ if (logsDiv) {
89
+ logsDiv.innerHTML = '';
90
+ }
91
+ },
92
+
93
+ copyLogs: () => {
94
+ const logsDiv = document.getElementById('daemon-logs-content');
95
+ const buttonText = document.getElementById('copy-button-text');
96
+
97
+ if (!logsDiv) {
98
+ return;
99
+ }
100
+
101
+ // Get all log text
102
+ const logText = logsDiv.innerText;
103
+
104
+ // Create temporary textarea for copying (works without HTTPS)
105
+ const textarea = document.createElement('textarea');
106
+ textarea.value = logText;
107
+ textarea.style.position = 'fixed';
108
+ textarea.style.opacity = '0';
109
+ document.body.appendChild(textarea);
110
+
111
+ try {
112
+ // Select and copy
113
+ textarea.select();
114
+ textarea.setSelectionRange(0, 99999); // For mobile devices
115
+ const successful = document.execCommand('copy');
116
+
117
+ if (successful && buttonText) {
118
+ buttonText.textContent = 'Copied!';
119
+ setTimeout(() => {
120
+ buttonText.textContent = 'Copy';
121
+ }, 2000);
122
+ } else if (buttonText) {
123
+ buttonText.textContent = 'Failed';
124
+ setTimeout(() => {
125
+ buttonText.textContent = 'Copy';
126
+ }, 2000);
127
+ }
128
+ } catch (err) {
129
+ console.error('Failed to copy logs:', err);
130
+ if (buttonText) {
131
+ buttonText.textContent = 'Failed';
132
+ setTimeout(() => {
133
+ buttonText.textContent = 'Copy';
134
+ }, 2000);
135
+ }
136
+ } finally {
137
+ document.body.removeChild(textarea);
138
+ }
139
+ },
140
+
141
+ disconnect: () => {
142
+ if (daemonLogs.ws) {
143
+ daemonLogs.ws.close();
144
+ daemonLogs.ws = null;
145
+ daemonLogs.isConnected = false;
146
+ }
147
+ }
148
+ };
@@ -0,0 +1,37 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+
5
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
6
+ <div class="flex items-center justify-between mb-4">
7
+ <div class="flex items-center gap-3">
8
+ <h1 class="text-xl font-semibold text-gray-900 dark:text-white">Daemon Logs</h1>
9
+ <span id="logs-status" class="text-sm text-gray-500">Disconnected</span>
10
+ </div>
11
+ <div class="flex gap-2">
12
+ <button onclick="daemonLogs.copyLogs()" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
13
+ <span id="copy-button-text">Copy</span>
14
+ </button>
15
+ <button onclick="daemonLogs.clearLogs()" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 rounded dark:bg-gray-600 dark:hover:bg-gray-500 dark:text-white">
16
+ Clear
17
+ </button>
18
+ </div>
19
+ </div>
20
+
21
+ <div id="daemon-logs-content"
22
+ class="overflow-y-auto h-[calc(100vh-200px)] bg-gray-900 border border-gray-600 rounded p-3 font-mono text-xs text-gray-100 leading-relaxed">
23
+ <!-- Logs will be appended here by JavaScript -->
24
+ </div>
25
+ </div>
26
+
27
+ {% endblock %}
28
+
29
+ {% block extra_js %}
30
+ <script src="/static/js/logs.js"></script>
31
+ <script>
32
+ // Auto-connect when page loads
33
+ window.addEventListener('load', () => {
34
+ daemonLogs.connectWebSocket();
35
+ });
36
+ </script>
37
+ {% endblock %}
@@ -32,4 +32,96 @@
32
32
  <div class="">
33
33
  <ul id="hf-available-apps" class="overflow-y-auto max-h-96">Loading apps from 🤗...</ul>
34
34
  </div>
35
+
36
+ <!-- Advanced section for private spaces (wireless only) -->
37
+ <div id="hf-advanced-section" class="mt-6 hidden">
38
+ <!-- Collapsible header -->
39
+ <button
40
+ id="hf-advanced-toggle"
41
+ class="flex items-center justify-between w-full text-left font-medium text-gray-700 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white transition-colors py-2"
42
+ type="button"
43
+ >
44
+ <span class="flex items-center gap-2 text-base">
45
+ <svg id="hf-advanced-chevron" class="w-5 h-5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
46
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
47
+ </svg>
48
+ Advanced: Install private space
49
+ </span>
50
+ </button>
51
+
52
+ <!-- Collapsible content -->
53
+ <div id="hf-advanced-content" class="hidden mt-4 p-6 bg-gray-100 rounded-lg border-2 border-gray-300" style="background-color: #f3f4f6; border-color: #d1d5db;">
54
+
55
+ <!-- Auth status display -->
56
+ <div id="hf-auth-status" class="flex items-center gap-3 mb-6 pb-4 border-b-2 border-gray-300" style="border-color: #d1d5db;">
57
+ <div id="hf-auth-indicator" class="w-3 h-3 rounded-full bg-gray-400"></div>
58
+ <span id="hf-auth-text" class="text-base font-semibold" style="color: #374151;">Not authenticated</span>
59
+ </div>
60
+
61
+ <!-- Login form (shown when not authenticated) -->
62
+ <div id="hf-login-form" class="space-y-4">
63
+ <div>
64
+ <label for="hf-token-input" class="block text-lg font-bold mb-3" style="color: #111827;">
65
+ HuggingFace Token
66
+ </label>
67
+ <input
68
+ type="password"
69
+ id="hf-token-input"
70
+ placeholder="hf_..."
71
+ style="width: 100%; min-height: 48px; padding: 12px 16px; font-size: 16px; font-family: monospace; border: 2px solid #9ca3af; border-radius: 8px; background-color: #ffffff; color: #111827;"
72
+ class="focus:outline-none focus:ring-2 focus:ring-red-500"
73
+ />
74
+ <p class="mt-3 text-sm" style="color: #6b7280;">
75
+ Get your token from <a href="https://huggingface.co/settings/tokens" target="_blank" class="hover:underline font-medium" style="color: #dc2626;">huggingface.co/settings/tokens</a>
76
+ </p>
77
+ </div>
78
+ <button
79
+ id="hf-login-button"
80
+ style="width: 100%; min-height: 48px; padding: 12px 24px; font-size: 18px; font-weight: 600; background-color: #dc2626; color: #ffffff; border: none; border-radius: 8px; cursor: pointer;"
81
+ class="hover:bg-red-700 transition-colors shadow-md"
82
+ >
83
+ Login to HuggingFace
84
+ </button>
85
+ <div id="hf-login-error" class="hidden text-base font-medium p-3 rounded-lg border-2" style="color: #dc2626; background-color: #fee2e2; border-color: #fecaca;"></div>
86
+ </div>
87
+
88
+ <!-- Logged in view (shown when authenticated) -->
89
+ <div id="hf-logged-in-view" class="hidden space-y-5">
90
+ <div class="flex items-center justify-between p-4 rounded-lg border-2" style="background-color: #d1fae5; border-color: #86efac;">
91
+ <span class="text-base" style="color: #065f46;">
92
+ Logged in as <span id="hf-username" class="font-bold" style="color: #064e3b;"></span>
93
+ </span>
94
+ <button
95
+ id="hf-logout-button"
96
+ class="text-base hover:underline font-medium"
97
+ style="color: #dc2626;"
98
+ >
99
+ Logout
100
+ </button>
101
+ </div>
102
+
103
+ <!-- Private space installation -->
104
+ <div>
105
+ <label for="hf-space-id-input" class="block text-lg font-bold mb-3" style="color: #111827;">
106
+ Private Space ID
107
+ </label>
108
+ <input
109
+ type="text"
110
+ id="hf-space-id-input"
111
+ placeholder="username/space-name"
112
+ style="width: 100%; min-height: 48px; padding: 12px 16px; font-size: 16px; font-family: monospace; border: 2px solid #9ca3af; border-radius: 8px; background-color: #ffffff; color: #111827;"
113
+ class="focus:outline-none focus:ring-2 focus:ring-red-500"
114
+ />
115
+ </div>
116
+ <button
117
+ id="hf-install-private-button"
118
+ style="width: 100%; min-height: 48px; padding: 12px 24px; font-size: 18px; font-weight: 600; background-color: #ffffff; color: #dc2626; border: 2px solid #dc2626; border-radius: 8px; cursor: pointer;"
119
+ class="hover:bg-red-600 hover:text-white transition-colors shadow-md"
120
+ >
121
+ Install Private Space
122
+ </button>
123
+ <div id="hf-private-install-error" class="hidden text-base font-medium p-3 rounded-lg border-2" style="color: #dc2626; background-color: #fee2e2; border-color: #fecaca;"></div>
124
+ </div>
125
+ </div>
126
+ </div>
35
127
  </section>
@@ -0,0 +1,82 @@
1
+ <div class="app-section">
2
+ <div class="app-section-title">Cache Management</div>
3
+
4
+ <div class="mt-2">
5
+ <button id="clear-hf-cache-btn"
6
+ onclick="clearHFCache()"
7
+ class="px-4 py-2 rounded-lg shadow bg-red-500 hover:bg-red-600"
8
+ style="color: white;">
9
+ Clear HuggingFace Cache
10
+ </button>
11
+ <span id="cache-status" class="ml-3 text-sm"></span>
12
+ </div>
13
+ <div class="mt-2">
14
+ <button id="reset-apps"
15
+ onclick="resetApps()"
16
+ class="px-4 py-2 rounded-lg shadow bg-red-500 hover:bg-red-600"
17
+ style="color: white;">
18
+ Reset Applications
19
+ </button>
20
+ <span id="reset-status" class="ml-3 text-sm"></span>
21
+ </div>
22
+ </div>
23
+
24
+ <script>
25
+ async function clearHFCache() {
26
+ const btn = document.getElementById('clear-hf-cache-btn');
27
+ const status = document.getElementById('cache-status');
28
+
29
+ btn.disabled = true;
30
+ btn.textContent = 'Clearing...';
31
+ status.textContent = '';
32
+
33
+ try {
34
+ const response = await fetch('/cache/clear-hf', { method: 'POST' });
35
+ const data = await response.json();
36
+
37
+ if (response.ok) {
38
+ status.textContent = '✓ Cache cleared';
39
+ status.className = 'ml-3 text-sm text-green-600';
40
+ } else {
41
+ status.textContent = '✗ ' + (data.detail || 'Failed');
42
+ status.className = 'ml-3 text-sm text-red-600';
43
+ }
44
+ } catch (error) {
45
+ status.textContent = '✗ Error: ' + error.message;
46
+ status.className = 'ml-3 text-sm text-red-600';
47
+ } finally {
48
+ btn.disabled = false;
49
+ btn.textContent = 'Clear HuggingFace Cache';
50
+ setTimeout(() => { status.textContent = ''; }, 3000);
51
+ }
52
+ }
53
+
54
+ async function resetApps() {
55
+ const btn = document.getElementById('reset-apps');
56
+ const status = document.getElementById('reset-status');
57
+
58
+ btn.disabled = true;
59
+ btn.textContent = 'Resetting...';
60
+ status.textContent = '';
61
+
62
+ try {
63
+ const response = await fetch('/cache/reset-apps', { method: 'POST' });
64
+ const data = await response.json();
65
+
66
+ if (response.ok) {
67
+ status.textContent = '✓ Apps reset';
68
+ status.className = 'ml-3 text-sm text-green-600';
69
+ } else {
70
+ status.textContent = '✗ ' + (data.detail || 'Failed');
71
+ status.className = 'ml-3 text-sm text-red-600';
72
+ }
73
+ } catch (error) {
74
+ status.textContent = '✗ Error: ' + error.message;
75
+ status.className = 'ml-3 text-sm text-red-600';
76
+ } finally {
77
+ btn.disabled = false;
78
+ btn.textContent = 'Reset Applications';
79
+ setTimeout(() => { status.textContent = ''; }, 3000);
80
+ }
81
+ }
82
+ </script>
@@ -35,6 +35,11 @@
35
35
  </div>
36
36
  {% if args.wireless_version %}
37
37
  <div class="w-full flex pt-4 gap-4 justify-center">
38
+ <a id="daemon-logs-btn"
39
+ class="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-300 rounded-lg shadow border border-gray-300"
40
+ href="/logs">
41
+ Logs
42
+ </a>
38
43
  <a id="daemon-settings-btn"
39
44
  class="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-300 rounded-lg shadow border border-gray-300"
40
45
  href="/settings">
@@ -5,6 +5,7 @@
5
5
  <div class="grid gap-4">
6
6
  {% include "sections/update.html" %}
7
7
  {% include "sections/wifi.html" %}
8
+ {% include "sections/cache.html" %}
8
9
  </div>
9
10
  {% endblock %}
10
11