Eluza133 commited on
Commit
5584295
·
verified ·
1 Parent(s): c9ba6aa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +519 -304
app.py CHANGED
@@ -45,7 +45,7 @@ BASE_STYLE = '''
45
  --glass-bg: rgba(30, 30, 30, 0.7); --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
46
  --delete-color: #ff4444; --folder-color: #ffc107; --selection-color: rgba(139, 92, 246, 0.3);
47
  --note-color: #6a5acd; --share-color: #4caf50; --archive-color: #78909c;
48
- --todolist-color: #29b6f6; --shoppinglist-color: #ffa726;
49
  }
50
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
51
  * { margin: 0; padding: 0; box-sizing: border-box; }
@@ -55,14 +55,15 @@ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Robo
55
  .app-header { position: fixed; top: 0; left: 0; right: 0; background: var(--glass-bg); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border-bottom: 1px solid rgba(255,255,255,0.1); z-index: 1000; padding: 10px 20px; display: flex; justify-content: space-between; align-items: center; }
56
  .user-info { font-weight: 600; }
57
  .view-toggle { display: flex; align-items: center; gap: 5px; }
58
- .view-toggle button { background: none; border: none; color: var(--text-muted); font-size: 1.2em; padding: 5px; cursor: pointer; transition: var(--transition); }
59
- .view-toggle button:hover, .view-toggle button.active { color: var(--primary); }
60
  h2, h3, h4, h5 { color: var(--text-dark); }
61
  h2 { font-size: 1.3em; margin-bottom: 15px; margin-top: 15px; }
62
  .breadcrumbs { font-size: 1em; margin-bottom: 20px; white-space: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; }
63
  .breadcrumbs a { color: var(--accent); text-decoration: none; }
64
  .breadcrumbs span { margin: 0 5px; color: var(--text-muted); }
65
  input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px solid #333; border-radius: 12px; background: #2a2a2a; color: var(--text-dark); font-size: 1em; }
 
66
  .btn { padding: 12px 24px; background: var(--primary); color: white; border: none; border-radius: 12px; cursor: pointer; font-size: 1em; font-weight: 600; transition: var(--transition); text-decoration: none; display: inline-block; text-align: center; }
67
  .btn:hover { filter: brightness(1.2); }
68
  .btn:active { transform: scale(0.98); }
@@ -121,6 +122,7 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px
121
  #fab-option-folder i { color: var(--folder-color); }
122
  #fab-option-todolist i { color: var(--todolist-color); }
123
  #fab-option-shoppinglist i { color: var(--shoppinglist-color); }
 
124
  #create-folder-form { display: none; margin-top: 15px; }
125
  .shared-link-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #333; }
126
  .shared-link-item:last-child { border-bottom: none; }
@@ -142,6 +144,14 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px
142
  .public-list-item label { flex-grow: 1; cursor: pointer; }
143
  .public-list-item.purchased label { text-decoration: line-through; color: var(--text-muted); }
144
  .public-list-item .quantity { font-weight: bold; color: var(--secondary); background: #2a2a2a; padding: 2px 8px; border-radius: 6px; }
 
 
 
 
 
 
 
 
145
  '''
146
 
147
  PUBLIC_SHARE_PAGE_HTML = '''
@@ -292,6 +302,13 @@ body { padding-bottom: 30px; }
292
  </body></html>
293
  '''
294
 
 
 
 
 
 
 
 
295
  def find_node_by_id(filesystem, node_id):
296
  if not filesystem: return None, None
297
  if filesystem.get('id') == node_id:
@@ -392,24 +409,24 @@ def initialize_user_filesystem_tma(user_data, tma_user_id_str):
392
  }
393
  add_node(user_data['filesystem'], 'root', file_node)
394
  del user_data['files']
395
- if 'business_profiles' not in user_data:
396
- user_data['business_profiles'] = []
397
 
398
  @cache.memoize(timeout=300)
399
  def load_data():
400
  try:
401
  download_db_from_hf()
402
  with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file)
403
- if not isinstance(data, dict): data = {'users': {}, 'shared_links': {}}
404
  data.setdefault('users', {})
405
  data.setdefault('shared_links', {})
 
406
  for tma_user_id_str, user_data_item in data['users'].items():
407
  initialize_user_filesystem_tma(user_data_item, tma_user_id_str)
408
  user_data_item.setdefault('reminders', [])
 
409
  return data
410
  except Exception as e:
411
  logging.error(f"Error loading data: {e}")
412
- return {'users': {}, 'shared_links': {}}
413
 
414
  def save_data(data):
415
  with save_data_lock:
@@ -433,17 +450,17 @@ def upload_db_to_hf():
433
  def download_db_from_hf():
434
  if not HF_TOKEN_READ:
435
  if not os.path.exists(DATA_FILE):
436
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}}, f)
437
  return
438
  try:
439
  hf_hub_download(repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False)
440
  except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError):
441
  if not os.path.exists(DATA_FILE):
442
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}}, f)
443
  except Exception as e:
444
  logging.error(f"Error downloading database: {e}")
445
  if not os.path.exists(DATA_FILE):
446
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}}, f)
447
 
448
  def periodic_backup():
449
  while True:
@@ -510,6 +527,15 @@ def admin_browser_login_required(f):
510
  return f(*args, **kwargs)
511
  return decorated_function
512
 
 
 
 
 
 
 
 
 
 
513
  TMA_ENTRY_HTML = '''
514
  <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
515
  <title>Zeus Cloud TMA</title><script src="https://telegram.org/js/telegram-web-app.js"></script>
@@ -564,13 +590,11 @@ def auth_via_telegram():
564
  user_info['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
565
  user_info['filesystem'] = {"type": "folder", "id": "root", "name": "root", "children": []}
566
  user_info['reminders'] = []
567
- user_info['business_profiles'] = []
568
  data['users'][tma_user_id_str] = user_info
569
  initialize_user_filesystem_tma(data['users'][tma_user_id_str], tma_user_id_str)
570
  else:
571
  data['users'][tma_user_id_str].update(user_info)
572
- if 'business_profiles' not in data['users'][tma_user_id_str]:
573
- data['users'][tma_user_id_str]['business_profiles'] = []
574
 
575
  try: save_data(data)
576
  except Exception as e:
@@ -592,9 +616,7 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
592
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
593
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
594
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
595
- <style>''' + BASE_STYLE + '''
596
- #fab-option-business i { color: #4caf50; }
597
- </style></head><body>
598
  <div class="app-header">
599
  <div class="user-info">{{ display_name }}</div>
600
  <div class="view-toggle">
@@ -692,7 +714,7 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
692
  <div class="fab-option" id="fab-option-folder"><i class="fa-solid fa-folder-plus"></i><span>Папку</span></div>
693
  <div class="fab-option" id="fab-option-todolist" onclick="openListEditorModal(null, 'todolist')"><i class="fa-solid fa-list-check"></i><span>Список дел</span></div>
694
  <div class="fab-option" id="fab-option-shoppinglist" onclick="openListEditorModal(null, 'shoppinglist')"><i class="fa-solid fa-cart-shopping"></i><span>Покупки</span></div>
695
- <a href="{{ url_for('tma_business_profiles') }}" class="fab-option" id="fab-option-business"><i class="fa-solid fa-briefcase"></i><span>Бизнес</span></a>
696
  </div>
697
  <form id="upload-form" method="POST" enctype="multipart/form-data" action="{{ url_for('tma_dashboard') }}" style="display:none;">
698
  <input type="hidden" name="current_folder_id" value="{{ current_folder_id }}">
@@ -1300,10 +1322,8 @@ ARCHIVED_LISTS_HTML = '''
1300
  '''
1301
 
1302
  @app.route('/tma_dashboard', methods=['GET', 'POST'])
 
1303
  def tma_dashboard():
1304
- if 'telegram_user_id' not in session:
1305
- flash('Пожалуйста, авторизуйтесь через Telegram.', 'error')
1306
- return redirect(url_for('tma_entry_page'))
1307
  tma_user_id = session['telegram_user_id']
1308
  display_name = session.get('telegram_display_name', 'Пользователь')
1309
  data = load_data()
@@ -1376,11 +1396,11 @@ def tma_dashboard():
1376
 
1377
  all_folders_for_move = get_all_folders(user_data['filesystem'])
1378
 
1379
- return render_template_string(TMA_DASHBOARD_HTML_TEMPLATE, display_name=display_name, items=items_in_folder, current_folder_id=current_folder_id, current_folder=current_folder, parent_folder_id=parent_folder_id, breadcrumbs=breadcrumbs, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}", is_tma_user_admin_flag=is_admin_tma(), all_folders_for_move=all_folders_for_move)
1380
 
1381
  @app.route('/tma_archive')
 
1382
  def tma_archive_view():
1383
- if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page'))
1384
  tma_user_id = session['telegram_user_id']
1385
  display_name = session.get('telegram_display_name', 'Пользователь')
1386
  data = load_data()
@@ -1393,8 +1413,8 @@ def tma_archive_view():
1393
  return render_template_string(ARCHIVED_LISTS_HTML, display_name=display_name, items=sorted_items)
1394
 
1395
  @app.route('/create_folder_tma', methods=['POST'])
 
1396
  def create_folder_tma():
1397
- if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page'))
1398
  tma_user_id = session['telegram_user_id']
1399
  data = load_data()
1400
  user_data = data['users'].get(tma_user_id)
@@ -1462,12 +1482,12 @@ def public_download(token):
1462
  return Response("Ошибка: Путь к файлу не найден.", status=500)
1463
 
1464
  try:
1465
- hf_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(hf_path)}"
1466
  headers = {}
1467
  if HF_TOKEN_READ:
1468
  headers["Authorization"] = f"Bearer {HF_TOKEN_READ}"
1469
 
1470
- req = requests.get(hf_url, headers=headers, stream=True, allow_redirects=True)
1471
  req.raise_for_status()
1472
 
1473
  encoded_filename = quote(original_filename)
@@ -1492,8 +1512,8 @@ def public_download(token):
1492
  return Response(f'Ошибка скачивания файла: {e}', status=502)
1493
 
1494
  @app.route('/batch_download_tma')
 
1495
  def batch_download_tma():
1496
- if 'telegram_user_id' not in session: return Response("Unauthorized", 401)
1497
  file_ids_str = request.args.get('file_ids')
1498
  if not file_ids_str: return Response("No file IDs provided", 400)
1499
  file_ids = file_ids_str.split(',')
@@ -1523,8 +1543,8 @@ def batch_download_tma():
1523
  os.unlink(temp_zip_file.name)
1524
 
1525
  @app.route('/batch_delete_tma', methods=['POST'])
 
1526
  def batch_delete_tma():
1527
- if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
1528
  tma_user_id = session['telegram_user_id']
1529
  data = load_data()
1530
  user_data = data['users'].get(tma_user_id)
@@ -1562,8 +1582,8 @@ def batch_delete_tma():
1562
  return jsonify({'status': 'success', 'message': f'Удалено {success_count} элемент(ов).'})
1563
 
1564
  @app.route('/batch_move_tma', methods=['POST'])
 
1565
  def batch_move_tma():
1566
- if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
1567
  tma_user_id = session['telegram_user_id']
1568
  data = load_data()
1569
  user_data = data['users'].get(tma_user_id)
@@ -1602,8 +1622,8 @@ def batch_move_tma():
1602
  return jsonify({'status': 'success', 'message': f'Перемещено {moved_count} элемент(ов).'})
1603
 
1604
  @app.route('/batch_archive_tma', methods=['POST'])
 
1605
  def batch_archive_tma():
1606
- if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
1607
  tma_user_id = session['telegram_user_id']
1608
  data = load_data()
1609
  user_data = data['users'].get(tma_user_id)
@@ -1627,8 +1647,8 @@ def batch_archive_tma():
1627
  return jsonify({'status': 'error', 'message': 'Не найдено списков для архивации.'})
1628
 
1629
  @app.route('/batch_unarchive_tma', methods=['POST'])
 
1630
  def batch_unarchive_tma():
1631
- if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
1632
  tma_user_id = session['telegram_user_id']
1633
  data = load_data()
1634
  user_data = data['users'].get(tma_user_id)
@@ -1657,11 +1677,11 @@ def get_text_content_tma(file_id):
1657
  if not file_node or file_node.get('file_type') != 'text': return Response("Текстовый файл не найден", 404)
1658
  hf_path = file_node.get('path')
1659
  if not hf_path: return Response("Ошибка: путь к файлу отсутствует", 500)
1660
- file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(hf_path)}?download=true"
1661
  try:
1662
  req_headers = {};
1663
  if HF_TOKEN_READ: req_headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
1664
- response = requests.get(file_url, headers=req_headers)
1665
  response.raise_for_status()
1666
  if len(response.content) > 1 * 1024 * 1024: return Response("Файл слишком большой для предпросмотра.", 413)
1667
  try: text_content = response.content.decode('utf-8')
@@ -1670,16 +1690,16 @@ def get_text_content_tma(file_id):
1670
  except Exception as e: return Response(f"Ошибка загрузки: {e}", 502)
1671
 
1672
  @app.route('/get_note_tma/<note_id>')
 
1673
  def get_note_tma(note_id):
1674
- if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
1675
  note_node = get_item_node_for_user(note_id)
1676
  if not note_node or note_node.get('type') != 'note':
1677
  return jsonify({'status': 'error', 'message': 'Note not found'}), 404
1678
  return jsonify({'status': 'success', 'note': note_node})
1679
 
1680
  @app.route('/create_or_update_note_tma', methods=['POST'])
 
1681
  def create_or_update_note_tma():
1682
- if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
1683
  tma_user_id = session['telegram_user_id']
1684
  data = load_data()
1685
  user_data = data['users'].get(tma_user_id)
@@ -1717,16 +1737,16 @@ def create_or_update_note_tma():
1717
  return jsonify({'status': 'error', 'message': f'Failed to save data: {e}'}), 500
1718
 
1719
  @app.route('/get_list_tma/<list_id>')
 
1720
  def get_list_tma(list_id):
1721
- if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
1722
  list_node = get_item_node_for_user(list_id)
1723
  if not list_node or list_node.get('type') not in ['todolist', 'shoppinglist']:
1724
  return jsonify({'status': 'error', 'message': 'List not found'}), 404
1725
  return jsonify({'status': 'success', 'list': list_node})
1726
 
1727
  @app.route('/create_or_update_list_tma', methods=['POST'])
 
1728
  def create_or_update_list_tma():
1729
- if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
1730
  tma_user_id = session['telegram_user_id']
1731
  data = load_data()
1732
  user_data = data['users'].get(tma_user_id)
@@ -1768,8 +1788,8 @@ def create_or_update_list_tma():
1768
  return jsonify({'status': 'error', 'message': f'Failed to save data: {e}'}), 500
1769
 
1770
  @app.route('/get_reminders_tma')
 
1771
  def get_reminders_tma():
1772
- if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
1773
  user_data = load_data()['users'].get(session['telegram_user_id'])
1774
  if not user_data: return jsonify({'status': 'error', 'message': 'User not found'}), 404
1775
 
@@ -1777,8 +1797,8 @@ def get_reminders_tma():
1777
  return jsonify({'status': 'success', 'reminders': reminders})
1778
 
1779
  @app.route('/create_reminder_tma', methods=['POST'])
 
1780
  def create_reminder_tma():
1781
- if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
1782
  tma_user_id = session['telegram_user_id']
1783
  data = load_data()
1784
  user_data = data['users'].get(tma_user_id)
@@ -1814,8 +1834,8 @@ def create_reminder_tma():
1814
  return jsonify({'status': 'error', 'message': f'Failed to save: {e}'}), 500
1815
 
1816
  @app.route('/delete_reminder_tma/<reminder_id>', methods=['POST'])
 
1817
  def delete_reminder_tma(reminder_id):
1818
- if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401
1819
  tma_user_id = session['telegram_user_id']
1820
  data = load_data()
1821
  user_data = data['users'].get(tma_user_id)
@@ -1837,8 +1857,8 @@ def tma_logout():
1837
  return redirect(url_for('tma_entry_page'))
1838
 
1839
  @app.route('/create_public_link', methods=['POST'])
 
1840
  def create_public_link():
1841
- if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
1842
  tma_user_id = session['telegram_user_id']
1843
  data = load_data()
1844
  user_data = data['users'].get(tma_user_id)
@@ -1878,8 +1898,8 @@ def create_public_link():
1878
  return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500
1879
 
1880
  @app.route('/delete_public_link', methods=['POST'])
 
1881
  def delete_public_link():
1882
- if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
1883
  tma_user_id = session['telegram_user_id']
1884
  data = load_data()
1885
 
@@ -1905,8 +1925,8 @@ def delete_public_link():
1905
  return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500
1906
 
1907
  @app.route('/get_public_links/<item_id>')
 
1908
  def get_public_links(item_id):
1909
- if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
1910
  tma_user_id = session['telegram_user_id']
1911
  data = load_data()
1912
  user_data = data['users'].get(tma_user_id)
@@ -1978,7 +1998,7 @@ def shared_folder_view(link_id, subfolder_id=None):
1978
 
1979
  items_in_folder = sorted(folder_node.get('children', []), key=lambda x: (x['type'] != 'folder', x.get('name', x.get('original_filename', x.get('title', ''))).lower()))
1980
 
1981
- return render_template_string(PUBLIC_SHARE_PAGE_HTML, folder=folder_node, items=items_in_folder, user=user_data, link=link_data, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}")
1982
 
1983
  @app.route('/public_download/<link_id>/<item_id>')
1984
  def public_download_via_link(link_id, item_id):
@@ -2051,6 +2071,266 @@ def public_toggle_item(link_id, item_id):
2051
  else:
2052
  return jsonify({'status': 'error', 'message': 'Элемент в списке не найден.'}), 404
2053
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2054
 
2055
  ADMIN_LOGIN_HTML = '''
2056
  <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Admin Login</title>
@@ -2114,6 +2394,7 @@ ADMIN_PANEL_HTML = '''
2114
  <span class="id">Created: {{ user.get('created_at', 'N/A') }}</span>
2115
  <span class="id">Items: <strong>{{ user.get('item_count', 0) }}</strong></span>
2116
  <span class="id">Reminders: <strong>{{ user.get('reminders', [])|length }}</strong></span>
 
2117
  </div>
2118
  </div>
2119
  <div class="user-actions">
@@ -2430,7 +2711,7 @@ def admin_user_files(tma_user_id_str):
2430
  current_folder_id=current_folder_id,
2431
  current_folder=current_folder,
2432
  breadcrumbs=breadcrumbs,
2433
- hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}")
2434
 
2435
  @app.route('/admhosto/user/<tma_user_id_str>/reminders')
2436
  @admin_browser_login_required
@@ -2465,11 +2746,11 @@ def admin_get_text_content(tma_user_id_str, file_id):
2465
  hf_path = file_node.get('path')
2466
  if not hf_path:
2467
  return Response("Error: file path is missing", 500)
2468
- file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(hf_path)}?download=true"
2469
  try:
2470
  req_headers = {}
2471
  if HF_TOKEN_READ: req_headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
2472
- response = requests.get(file_url, headers=req_headers)
2473
  response.raise_for_status()
2474
  if len(response.content) > 1 * 1024 * 1024:
2475
  return Response("File too large for preview.", 413)
@@ -2554,274 +2835,207 @@ def admin_delete_reminder(tma_user_id_str, reminder_id):
2554
  flash('Reminder not found.', 'error')
2555
  return redirect(url_for('admin_user_reminders', tma_user_id_str=tma_user_id_str))
2556
 
2557
- def find_business_profile_by_login(login):
2558
- data = load_data()
2559
- for user_id, user_data in data.get('users', {}).items():
2560
- for profile in user_data.get('business_profiles', []):
2561
- if profile.get('login') == login:
2562
- return profile, user_data
2563
- return None, None
2564
 
2565
- def is_business_login_unique(login, user_id, profile_id=None):
 
 
 
 
 
 
2566
  data = load_data()
2567
- for uid, udata in data.get('users', {}).items():
2568
- for profile in udata.get('business_profiles', []):
2569
- if profile.get('login') == login:
2570
- if uid == user_id and profile.get('id') == profile_id:
2571
- continue
2572
- return False
2573
- return True
2574
-
2575
- BUSINESS_PROFILES_LIST_HTML = '''
2576
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
2577
- <title>Бизнес-страницы</title>
2578
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
2579
- <script src="https://telegram.org/js/telegram-web-app.js"></script>
2580
- <style>''' + BASE_STYLE + '''
2581
- .profile-list-item { background: var(--card-bg-dark); border-radius: 12px; margin-bottom: 10px; padding: 15px; display: flex; justify-content: space-between; align-items: center; }
2582
- .profile-details { text-align: left; }
2583
- .profile-details a { color: var(--text-dark); text-decoration: none; font-weight: 600; }
2584
- .profile-details small { color: var(--text-muted); display: block; }
2585
- .profile-actions { display: flex; gap: 10px; }
2586
- </style></head><body>
2587
- <div class="app-header">
2588
- <div class="user-info">{{ display_name }}</div>
2589
- <a href="{{ url_for('tma_dashboard') }}" style="color: var(--text-muted); font-size: 1.2em; padding: 5px;"><i class="fa-solid fa-arrow-left"></i></a>
2590
- </div>
2591
- <div class="container">
2592
- <h2>Мои бизнес-страницы</h2>
2593
- {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
2594
- {% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}
2595
- {% endif %}{% endwith %}
2596
- <a href="{{ url_for('tma_create_business_profile') }}" class="btn" style="width: 100%; margin-bottom: 20px;">Создать новую страницу</a>
2597
- <div class="profile-list">
2598
- {% for profile in profiles %}
2599
- <div class="profile-list-item">
2600
- <div class="profile-details">
2601
- <a href="{{ url_for('tma_manage_business_profile', profile_id=profile.id) }}"><strong>{{ profile.org_name }}</strong></a>
2602
- <small>/biz/{{ profile.login }}</small>
2603
- </div>
2604
- <div class="profile-actions">
2605
- <a href="{{ url_for('tma_manage_business_profile', profile_id=profile.id) }}" class="btn" style="padding: 8px 12px;"><i class="fa-solid fa-store"></i></a>
2606
- </div>
2607
- </div>
2608
- {% else %}
2609
- <p>У вас еще нет бизнес-страниц.</p>
2610
- {% endfor %}
2611
- </div>
2612
- </div>
2613
- <script>
2614
- window.Telegram.WebApp.ready();
2615
- window.Telegram.WebApp.BackButton.show();
2616
- window.Telegram.WebApp.BackButton.onClick(() => { window.location.href = "{{ url_for('tma_dashboard') }}"; });
2617
- </script>
2618
- </body></html>
2619
- '''
2620
 
2621
- CREATE_EDIT_BUSINESS_PROFILE_HTML = '''
2622
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
2623
- <title>{{ 'Редактировать' if profile else 'Создать' }} бизнес-страницу</title>
2624
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
2625
- <script src="https://telegram.org/js/telegram-web-app.js"></script>
2626
- <style>''' + BASE_STYLE + '''</style></head><body>
2627
- <div class="app-header">
2628
- <div class="user-info">{{ display_name }}</div>
2629
- <a href="{{ url_for('tma_business_profiles') }}" style="color: var(--text-muted); font-size: 1.2em; padding: 5px;"><i class="fa-solid fa-arrow-left"></i></a>
2630
- </div>
2631
- <div class="container">
2632
- <h2>{{ 'Редактировать' if profile else 'Создать' }} бизнес-страницу</h2>
2633
- {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
2634
- {% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}
2635
- {% endif %}{% endwith %}
2636
- <form method="post" enctype="multipart/form-data">
2637
- <label for="org_name">Название организации</label>
2638
- <input type="text" name="org_name" value="{{ profile.org_name or '' }}" required>
2639
-
2640
- <label for="login">Логин (URL)</label>
2641
- <input type="text" name="login" value="{{ profile.login or '' }}" required pattern="[a-zA-Z0-9_.-]+" title="Только латинские буквы, цифры и символы _, -, .">
2642
-
2643
- <label for="avatar">Аватар (необязательно)</label>
2644
- <input type="file" name="avatar" accept="image/*">
2645
-
2646
- <label for="currency">Валюта</label>
2647
- <select name="currency" required>
2648
- {% set currencies = {'тенге': 'KZT', 'рубль': 'RUB', 'кыргызский сом': 'KGS', 'узбекский сум': 'UZS', 'украинская гривна': 'UAH'} %}
2649
- {% for name, code in currencies.items() %}
2650
- <option value="{{ code }}" {% if profile and profile.currency == code %}selected{% endif %}>{{ name }}</option>
2651
- {% endfor %}
2652
- </select>
2653
-
2654
- <label>
2655
- <input type="checkbox" name="show_prices" value="true" {% if profile and profile.show_prices %}checked{% endif %} style="width: auto; margin-right: 10px;">
2656
- Указывать цены
2657
- </label>
2658
-
2659
- <label for="order_destination">Куда будут приходить заказы?</label>
2660
- <select name="order_destination" required>
2661
- <option value="whatsapp" {% if profile and profile.order_destination == 'whatsapp' %}selected{% endif %}>WhatsApp</option>
2662
- <option value="telegram" {% if profile and profile.order_destination == 'telegram' %}selected{% endif %}>Telegram</option>
2663
- </select>
2664
-
2665
- <label for="contact_info">Номер телефона или username</label>
2666
- <input type="text" name="contact_info" value="{{ profile.contact_info or '' }}" placeholder="+7 (XXX) XXX-XX-XX или @username" required>
2667
 
2668
- <button type="submit" class="btn" style="width: 100%; margin-top: 20px;">Сохранить</button>
2669
- </form>
2670
- </div>
2671
- <script>
2672
- window.Telegram.WebApp.ready();
2673
- window.Telegram.WebApp.BackButton.show();
2674
- window.Telegram.WebApp.BackButton.onClick(() => { window.location.href = "{{ url_for('tma_business_profiles') }}"; });
2675
- </script>
2676
- </body></html>
2677
- '''
 
 
 
 
 
 
 
 
 
2678
 
2679
- MANAGE_BUSINESS_PROFILE_HTML = '''
2680
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
2681
- <title>Управление: {{ profile.org_name }}</title>
2682
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
2683
- <script src="https://telegram.org/js/telegram-web-app.js"></script>
2684
- <style>''' + BASE_STYLE + '''
2685
- .profile-header { text-align: center; margin-bottom: 20px; }
2686
- .profile-avatar { width: 100px; height: 100px; border-radius: 50%; object-fit: cover; margin-bottom: 10px; background: #333; }
2687
- .public-link-bar { background: var(--card-bg-dark); padding: 10px; border-radius: 12px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
2688
- .public-link-bar input { background: transparent; border: none; color: var(--text-muted); margin: 0; padding: 0; }
2689
- .product-card { background: var(--card-bg-dark); border-radius: 12px; margin-bottom: 15px; padding: 15px; text-align: left; }
2690
- .product-card img { max-width: 100%; border-radius: 8px; margin-bottom: 10px; }
2691
- .product-actions { display: flex; gap: 10px; margin-top: 15px; }
2692
- .add-product-form { background: var(--card-bg-dark); padding: 20px; border-radius: 12px; margin-top: 25px; }
2693
- </style></head><body>
2694
- <div class="app-header">
2695
- <div class="user-info">{{ display_name }}</div>
2696
- <a href="{{ url_for('tma_business_profiles') }}" style="color: var(--text-muted); font-size: 1.2em; padding: 5px;"><i class="fa-solid fa-arrow-left"></i></a>
2697
- </div>
2698
- <div class="container">
2699
- {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
2700
- {% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}
2701
- {% endif %}{% endwith %}
2702
- <div class="profile-header">
2703
- {% if profile.avatar_path %}
2704
- <img src="{{ hf_file_url_jinja(profile.avatar_path) }}" class="profile-avatar">
2705
- {% else %}
2706
- <div class="profile-avatar" style="display: inline-flex; align-items: center; justify-content: center; font-size: 3em; color: var(--primary);">{{ profile.org_name[0] }}</div>
2707
- {% endif %}
2708
- <h2>{{ profile.org_name }}</h2>
2709
- <a href="{{ url_for('tma_edit_business_profile', profile_id=profile.id) }}" class="btn" style="padding: 8px 15px; font-size: 0.9em;">Редактировать</a>
2710
- </div>
2711
-
2712
- <div class="public-link-bar">
2713
- <input type="text" value="{{ url_for('public_business_page', login=profile.login, _external=True) }}" readonly>
2714
- <button class="btn" onclick="copyToClipboard('{{ url_for('public_business_page', login=profile.login, _external=True) }}')" style="padding: 8px 12px;"><i class="fa-solid fa-copy"></i></button>
2715
- </div>
2716
 
2717
- <h3>Товары</h3>
2718
- <div class="products-list">
2719
- {% for product in profile.products %}
2720
- <div class="product-card">
2721
- {% if product.photo_path %}<img src="{{ hf_file_url_jinja(product.photo_path) }}">{% endif %}
2722
- <h4>{{ product.name }}</h4>
2723
- {% if profile.show_prices %}<p style="color: var(--secondary); font-weight: 600;">{{ "%.2f"|format(product.price|float) }} {{ profile.currency }}</p>{% endif %}
2724
- <p style="white-space: pre-wrap; color: var(--text-muted);">{{ product.description }}</p>
2725
- <div class="product-actions">
2726
- <form method="post" action="{{ url_for('tma_delete_product', profile_id=profile.id, product_id=product.id) }}" onsubmit="return confirm('Удалить этот товар?');">
2727
- <button type="submit" class="btn delete-btn" style="padding: 8px 15px;">Удалить</button>
2728
- </form>
2729
- </div>
2730
- </div>
2731
- {% else %}
2732
- <p>Вы еще не добавили ни одного товара.</p>
2733
- {% endfor %}
2734
- </div>
2735
 
2736
- <div class="add-product-form">
2737
- <h4>Добавить новый товар</h4>
2738
- <form method="post" action="{{ url_for('tma_add_product', profile_id=profile.id) }}" enctype="multipart/form-data">
2739
- <label for="product_name">Название товара</label>
2740
- <input type="text" name="product_name" required>
2741
-
2742
- <label for="product_photo">Фото товара</label>
2743
- <input type="file" name="product_photo" accept="image/*" required>
2744
-
2745
- <label for="product_description">Описание</label>
2746
- <textarea name="product_description" rows="4"></textarea>
2747
 
2748
- {% if profile.show_prices %}
2749
- <label for="product_price">Цена ({{ profile.currency }})</label>
2750
- <input type="number" name="product_price" step="0.01" min="0">
2751
- {% endif %}
2752
-
2753
- <button type="submit" class="btn" style="width: 100%; margin-top: 15px;">Добавить товар</button>
2754
- </form>
2755
- </div>
2756
- </div>
2757
- <script>
2758
- window.Telegram.WebApp.ready();
2759
- window.Telegram.WebApp.BackButton.show();
2760
- window.Telegram.WebApp.BackButton.onClick(() => { window.location.href = "{{ url_for('tma_business_profiles') }}"; });
2761
 
2762
- function copyToClipboard(text) {
2763
- navigator.clipboard.writeText(text).then(() => {
2764
- window.Telegram.WebApp.HapticFeedback.notificationOccurred('success');
2765
- window.Telegram.WebApp.showAlert('Ссылка скопирована!');
2766
- }, () => {
2767
- window.Telegram.WebApp.HapticFeedback.notificationOccurred('error');
2768
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2769
  }
2770
- </script>
2771
- </body></html>
2772
- '''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2773
 
2774
- PUBLIC_BUSINESS_PAGE_HTML = '''
2775
- <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
2776
- <title>{{ profile.org_name }}</title>
2777
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
2778
- <style>''' + BASE_STYLE + '''
2779
- body { padding-bottom: 80px; }
2780
- .public-header { padding: 20px; text-align: center; }
2781
- .public-avatar { width: 120px; height: 120px; border-radius: 50%; object-fit: cover; margin-bottom: 15px; border: 3px solid var(--card-bg-dark); }
2782
- .product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
2783
- .product-public-card { background: var(--card-bg-dark); border-radius: 16px; overflow: hidden; text-align: left; }
2784
- .product-public-card img { width: 100%; height: 200px; object-fit: cover; display: block; }
2785
- .product-public-info { padding: 15px; }
2786
- .product-public-info h4 { font-size: 1.2em; margin-bottom: 5px; }
2787
- .product-public-info .price { font-size: 1.1em; font-weight: 600; color: var(--secondary); margin-bottom: 10px; }
2788
- .product-public-info .desc { color: var(--text-muted); font-size: 0.9em; }
2789
- .order-button-container { position: fixed; bottom: 0; left: 0; right: 0; padding: 15px; background: var(--glass-bg); backdrop-filter: blur(10px); text-align: center; }
2790
- </style></head><body>
2791
- <div class="public-header">
2792
- {% if profile.avatar_path %}
2793
- <img src="{{ hf_file_url_jinja(profile.avatar_path) }}" class="public-avatar">
2794
- {% else %}
2795
- <div class="public-avatar" style="display: inline-flex; align-items: center; justify-content: center; font-size: 4em; color: var(--primary);">{{ profile.org_name[0] }}</div>
2796
- {% endif %}
2797
- <h1>{{ profile.org_name }}</h1>
2798
- </div>
2799
- <div class="container" style="padding-top: 0;">
2800
- <div class="product-grid">
2801
- {% for product in profile.products %}
2802
- <div class="product-public-card">
2803
- {% if product.photo_path %}<img src="{{ hf_file_url_jinja(product.photo_path) }}">{% endif %}
2804
- <div class="product-public-info">
2805
- <h4>{{ product.name }}</h4>
2806
- {% if profile.show_prices and product.price is not none %}
2807
- <p class="price">{{ "%.2f"|format(product.price|float) }} {{ profile.currency }}</p>
2808
- {% endif %}
2809
- <p class="desc">{{ product.description }}</p>
2810
- </div>
2811
- </div>
2812
- {% else %}
2813
- <p style="grid-column: 1 / -1; text-align: center;">Товары скоро появятся.</p>
2814
- {% endfor %}
2815
- </div>
2816
- </div>
2817
- <div class="order-button-container">
2818
- <a href="{{ order_link }}" target="_blank" class="btn" style="width: 100%; max-width: 400px; background: {{ '#25D366' if profile.order_destination == 'whatsapp' else '#0088cc' }};">
2819
- <i class="fa-brands fa-{{ 'whatsapp' if profile.order_destination == 'whatsapp' else 'telegram' }}"></i>
2820
- Заказать в {{ profile.order_destination.capitalize() }}
2821
- </a>
2822
- </div>
2823
- </body></html>
2824
- '''
2825
 
2826
  if __name__ == '__main__':
2827
  if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write) is not set. Uploads/deletions will fail.")
@@ -2834,7 +3048,7 @@ if __name__ == '__main__':
2834
  download_db_from_hf()
2835
  else:
2836
  if not os.path.exists(DATA_FILE):
2837
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}}, f)
2838
 
2839
  if HF_TOKEN_WRITE:
2840
  threading.Thread(target=periodic_backup, daemon=True).start()
@@ -2842,3 +3056,4 @@ if __name__ == '__main__':
2842
  threading.Thread(target=check_reminders, daemon=True).start()
2843
 
2844
  app.run(debug=False, host='0.0.0.0', port=7860)
 
 
45
  --glass-bg: rgba(30, 30, 30, 0.7); --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
46
  --delete-color: #ff4444; --folder-color: #ffc107; --selection-color: rgba(139, 92, 246, 0.3);
47
  --note-color: #6a5acd; --share-color: #4caf50; --archive-color: #78909c;
48
+ --todolist-color: #29b6f6; --shoppinglist-color: #ffa726; --business-color: #fca5a5;
49
  }
50
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
51
  * { margin: 0; padding: 0; box-sizing: border-box; }
 
55
  .app-header { position: fixed; top: 0; left: 0; right: 0; background: var(--glass-bg); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border-bottom: 1px solid rgba(255,255,255,0.1); z-index: 1000; padding: 10px 20px; display: flex; justify-content: space-between; align-items: center; }
56
  .user-info { font-weight: 600; }
57
  .view-toggle { display: flex; align-items: center; gap: 5px; }
58
+ .view-toggle button, .view-toggle a { background: none; border: none; color: var(--text-muted); font-size: 1.2em; padding: 5px; cursor: pointer; transition: var(--transition); text-decoration: none;}
59
+ .view-toggle button:hover, .view-toggle button.active, .view-toggle a:hover { color: var(--primary); }
60
  h2, h3, h4, h5 { color: var(--text-dark); }
61
  h2 { font-size: 1.3em; margin-bottom: 15px; margin-top: 15px; }
62
  .breadcrumbs { font-size: 1em; margin-bottom: 20px; white-space: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; }
63
  .breadcrumbs a { color: var(--accent); text-decoration: none; }
64
  .breadcrumbs span { margin: 0 5px; color: var(--text-muted); }
65
  input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px solid #333; border-radius: 12px; background: #2a2a2a; color: var(--text-dark); font-size: 1em; }
66
+ label { display: block; margin-top: 10px; font-weight: 500; font-size: 0.9em;}
67
  .btn { padding: 12px 24px; background: var(--primary); color: white; border: none; border-radius: 12px; cursor: pointer; font-size: 1em; font-weight: 600; transition: var(--transition); text-decoration: none; display: inline-block; text-align: center; }
68
  .btn:hover { filter: brightness(1.2); }
69
  .btn:active { transform: scale(0.98); }
 
122
  #fab-option-folder i { color: var(--folder-color); }
123
  #fab-option-todolist i { color: var(--todolist-color); }
124
  #fab-option-shoppinglist i { color: var(--shoppinglist-color); }
125
+ #fab-option-business i { color: var(--business-color); }
126
  #create-folder-form { display: none; margin-top: 15px; }
127
  .shared-link-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #333; }
128
  .shared-link-item:last-child { border-bottom: none; }
 
144
  .public-list-item label { flex-grow: 1; cursor: pointer; }
145
  .public-list-item.purchased label { text-decoration: line-through; color: var(--text-muted); }
146
  .public-list-item .quantity { font-weight: bold; color: var(--secondary); background: #2a2a2a; padding: 2px 8px; border-radius: 6px; }
147
+ .form-group { margin-bottom: 15px; }
148
+ .form-group label { margin-bottom: 5px; }
149
+ .form-group small { color: var(--text-muted); font-size: 0.8em; }
150
+ .page-card { background: var(--card-bg-dark); border-radius: 16px; padding: 15px; margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center; gap: 10px; }
151
+ .page-card-info { flex-grow: 1; }
152
+ .page-card-info h4 { margin: 0 0 5px 0; }
153
+ .page-card-info a { color: var(--accent); text-decoration: none; font-size: 0.9em; }
154
+ .page-card-actions { display: flex; gap: 8px; }
155
  '''
156
 
157
  PUBLIC_SHARE_PAGE_HTML = '''
 
302
  </body></html>
303
  '''
304
 
305
+ def hf_file_url(path, download=False):
306
+ if not path: return ""
307
+ base_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}"
308
+ if download:
309
+ return f"{base_url}?download=true"
310
+ return base_url
311
+
312
  def find_node_by_id(filesystem, node_id):
313
  if not filesystem: return None, None
314
  if filesystem.get('id') == node_id:
 
409
  }
410
  add_node(user_data['filesystem'], 'root', file_node)
411
  del user_data['files']
 
 
412
 
413
  @cache.memoize(timeout=300)
414
  def load_data():
415
  try:
416
  download_db_from_hf()
417
  with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file)
418
+ if not isinstance(data, dict): data = {'users': {}, 'shared_links': {}, 'business_pages': {}}
419
  data.setdefault('users', {})
420
  data.setdefault('shared_links', {})
421
+ data.setdefault('business_pages', {})
422
  for tma_user_id_str, user_data_item in data['users'].items():
423
  initialize_user_filesystem_tma(user_data_item, tma_user_id_str)
424
  user_data_item.setdefault('reminders', [])
425
+ user_data_item.setdefault('business_pages', [])
426
  return data
427
  except Exception as e:
428
  logging.error(f"Error loading data: {e}")
429
+ return {'users': {}, 'shared_links': {}, 'business_pages': {}}
430
 
431
  def save_data(data):
432
  with save_data_lock:
 
450
  def download_db_from_hf():
451
  if not HF_TOKEN_READ:
452
  if not os.path.exists(DATA_FILE):
453
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}, 'business_pages': {}}, f)
454
  return
455
  try:
456
  hf_hub_download(repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False)
457
  except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError):
458
  if not os.path.exists(DATA_FILE):
459
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}, 'business_pages': {}}, f)
460
  except Exception as e:
461
  logging.error(f"Error downloading database: {e}")
462
  if not os.path.exists(DATA_FILE):
463
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}, 'business_pages': {}}, f)
464
 
465
  def periodic_backup():
466
  while True:
 
527
  return f(*args, **kwargs)
528
  return decorated_function
529
 
530
+ def tma_login_required(f):
531
+ @wraps(f)
532
+ def decorated_function(*args, **kwargs):
533
+ if 'telegram_user_id' not in session:
534
+ flash('Пожалуйста, авторизуйтесь через Telegram.', 'error')
535
+ return redirect(url_for('tma_entry_page'))
536
+ return f(*args, **kwargs)
537
+ return decorated_function
538
+
539
  TMA_ENTRY_HTML = '''
540
  <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
541
  <title>Zeus Cloud TMA</title><script src="https://telegram.org/js/telegram-web-app.js"></script>
 
590
  user_info['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
591
  user_info['filesystem'] = {"type": "folder", "id": "root", "name": "root", "children": []}
592
  user_info['reminders'] = []
593
+ user_info['business_pages'] = []
594
  data['users'][tma_user_id_str] = user_info
595
  initialize_user_filesystem_tma(data['users'][tma_user_id_str], tma_user_id_str)
596
  else:
597
  data['users'][tma_user_id_str].update(user_info)
 
 
598
 
599
  try: save_data(data)
600
  except Exception as e:
 
616
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
617
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
618
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
619
+ <style>''' + BASE_STYLE + '''</style></head><body>
 
 
620
  <div class="app-header">
621
  <div class="user-info">{{ display_name }}</div>
622
  <div class="view-toggle">
 
714
  <div class="fab-option" id="fab-option-folder"><i class="fa-solid fa-folder-plus"></i><span>Папку</span></div>
715
  <div class="fab-option" id="fab-option-todolist" onclick="openListEditorModal(null, 'todolist')"><i class="fa-solid fa-list-check"></i><span>Список дел</span></div>
716
  <div class="fab-option" id="fab-option-shoppinglist" onclick="openListEditorModal(null, 'shoppinglist')"><i class="fa-solid fa-cart-shopping"></i><span>Покупки</span></div>
717
+ <div class="fab-option" id="fab-option-business" onclick="window.location.href='{{ url_for('my_business_pages') }}'"><i class="fa-solid fa-store"></i><span>Бизнес</span></div>
718
  </div>
719
  <form id="upload-form" method="POST" enctype="multipart/form-data" action="{{ url_for('tma_dashboard') }}" style="display:none;">
720
  <input type="hidden" name="current_folder_id" value="{{ current_folder_id }}">
 
1322
  '''
1323
 
1324
  @app.route('/tma_dashboard', methods=['GET', 'POST'])
1325
+ @tma_login_required
1326
  def tma_dashboard():
 
 
 
1327
  tma_user_id = session['telegram_user_id']
1328
  display_name = session.get('telegram_display_name', 'Пользователь')
1329
  data = load_data()
 
1396
 
1397
  all_folders_for_move = get_all_folders(user_data['filesystem'])
1398
 
1399
+ return render_template_string(TMA_DASHBOARD_HTML_TEMPLATE, display_name=display_name, items=items_in_folder, current_folder_id=current_folder_id, current_folder=current_folder, parent_folder_id=parent_folder_id, breadcrumbs=breadcrumbs, hf_file_url_jinja=hf_file_url, is_tma_user_admin_flag=is_admin_tma(), all_folders_for_move=all_folders_for_move)
1400
 
1401
  @app.route('/tma_archive')
1402
+ @tma_login_required
1403
  def tma_archive_view():
 
1404
  tma_user_id = session['telegram_user_id']
1405
  display_name = session.get('telegram_display_name', 'Пользователь')
1406
  data = load_data()
 
1413
  return render_template_string(ARCHIVED_LISTS_HTML, display_name=display_name, items=sorted_items)
1414
 
1415
  @app.route('/create_folder_tma', methods=['POST'])
1416
+ @tma_login_required
1417
  def create_folder_tma():
 
1418
  tma_user_id = session['telegram_user_id']
1419
  data = load_data()
1420
  user_data = data['users'].get(tma_user_id)
 
1482
  return Response("Ошибка: Путь к файлу не найден.", status=500)
1483
 
1484
  try:
1485
+ hf_url_stream = hf_file_url(hf_path)
1486
  headers = {}
1487
  if HF_TOKEN_READ:
1488
  headers["Authorization"] = f"Bearer {HF_TOKEN_READ}"
1489
 
1490
+ req = requests.get(hf_url_stream, headers=headers, stream=True, allow_redirects=True)
1491
  req.raise_for_status()
1492
 
1493
  encoded_filename = quote(original_filename)
 
1512
  return Response(f'Ошибка скачивания файла: {e}', status=502)
1513
 
1514
  @app.route('/batch_download_tma')
1515
+ @tma_login_required
1516
  def batch_download_tma():
 
1517
  file_ids_str = request.args.get('file_ids')
1518
  if not file_ids_str: return Response("No file IDs provided", 400)
1519
  file_ids = file_ids_str.split(',')
 
1543
  os.unlink(temp_zip_file.name)
1544
 
1545
  @app.route('/batch_delete_tma', methods=['POST'])
1546
+ @tma_login_required
1547
  def batch_delete_tma():
 
1548
  tma_user_id = session['telegram_user_id']
1549
  data = load_data()
1550
  user_data = data['users'].get(tma_user_id)
 
1582
  return jsonify({'status': 'success', 'message': f'Удалено {success_count} элемент(ов).'})
1583
 
1584
  @app.route('/batch_move_tma', methods=['POST'])
1585
+ @tma_login_required
1586
  def batch_move_tma():
 
1587
  tma_user_id = session['telegram_user_id']
1588
  data = load_data()
1589
  user_data = data['users'].get(tma_user_id)
 
1622
  return jsonify({'status': 'success', 'message': f'Перемещено {moved_count} элемент(ов).'})
1623
 
1624
  @app.route('/batch_archive_tma', methods=['POST'])
1625
+ @tma_login_required
1626
  def batch_archive_tma():
 
1627
  tma_user_id = session['telegram_user_id']
1628
  data = load_data()
1629
  user_data = data['users'].get(tma_user_id)
 
1647
  return jsonify({'status': 'error', 'message': 'Не найдено списков для архивации.'})
1648
 
1649
  @app.route('/batch_unarchive_tma', methods=['POST'])
1650
+ @tma_login_required
1651
  def batch_unarchive_tma():
 
1652
  tma_user_id = session['telegram_user_id']
1653
  data = load_data()
1654
  user_data = data['users'].get(tma_user_id)
 
1677
  if not file_node or file_node.get('file_type') != 'text': return Response("Текстовый файл не найден", 404)
1678
  hf_path = file_node.get('path')
1679
  if not hf_path: return Response("Ошибка: путь к файлу отсутствует", 500)
1680
+ file_url_path = hf_file_url(hf_path, download=True)
1681
  try:
1682
  req_headers = {};
1683
  if HF_TOKEN_READ: req_headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
1684
+ response = requests.get(file_url_path, headers=req_headers)
1685
  response.raise_for_status()
1686
  if len(response.content) > 1 * 1024 * 1024: return Response("Файл слишком большой для предпросмотра.", 413)
1687
  try: text_content = response.content.decode('utf-8')
 
1690
  except Exception as e: return Response(f"Ошибка загрузки: {e}", 502)
1691
 
1692
  @app.route('/get_note_tma/<note_id>')
1693
+ @tma_login_required
1694
  def get_note_tma(note_id):
 
1695
  note_node = get_item_node_for_user(note_id)
1696
  if not note_node or note_node.get('type') != 'note':
1697
  return jsonify({'status': 'error', 'message': 'Note not found'}), 404
1698
  return jsonify({'status': 'success', 'note': note_node})
1699
 
1700
  @app.route('/create_or_update_note_tma', methods=['POST'])
1701
+ @tma_login_required
1702
  def create_or_update_note_tma():
 
1703
  tma_user_id = session['telegram_user_id']
1704
  data = load_data()
1705
  user_data = data['users'].get(tma_user_id)
 
1737
  return jsonify({'status': 'error', 'message': f'Failed to save data: {e}'}), 500
1738
 
1739
  @app.route('/get_list_tma/<list_id>')
1740
+ @tma_login_required
1741
  def get_list_tma(list_id):
 
1742
  list_node = get_item_node_for_user(list_id)
1743
  if not list_node or list_node.get('type') not in ['todolist', 'shoppinglist']:
1744
  return jsonify({'status': 'error', 'message': 'List not found'}), 404
1745
  return jsonify({'status': 'success', 'list': list_node})
1746
 
1747
  @app.route('/create_or_update_list_tma', methods=['POST'])
1748
+ @tma_login_required
1749
  def create_or_update_list_tma():
 
1750
  tma_user_id = session['telegram_user_id']
1751
  data = load_data()
1752
  user_data = data['users'].get(tma_user_id)
 
1788
  return jsonify({'status': 'error', 'message': f'Failed to save data: {e}'}), 500
1789
 
1790
  @app.route('/get_reminders_tma')
1791
+ @tma_login_required
1792
  def get_reminders_tma():
 
1793
  user_data = load_data()['users'].get(session['telegram_user_id'])
1794
  if not user_data: return jsonify({'status': 'error', 'message': 'User not found'}), 404
1795
 
 
1797
  return jsonify({'status': 'success', 'reminders': reminders})
1798
 
1799
  @app.route('/create_reminder_tma', methods=['POST'])
1800
+ @tma_login_required
1801
  def create_reminder_tma():
 
1802
  tma_user_id = session['telegram_user_id']
1803
  data = load_data()
1804
  user_data = data['users'].get(tma_user_id)
 
1834
  return jsonify({'status': 'error', 'message': f'Failed to save: {e}'}), 500
1835
 
1836
  @app.route('/delete_reminder_tma/<reminder_id>', methods=['POST'])
1837
+ @tma_login_required
1838
  def delete_reminder_tma(reminder_id):
 
1839
  tma_user_id = session['telegram_user_id']
1840
  data = load_data()
1841
  user_data = data['users'].get(tma_user_id)
 
1857
  return redirect(url_for('tma_entry_page'))
1858
 
1859
  @app.route('/create_public_link', methods=['POST'])
1860
+ @tma_login_required
1861
  def create_public_link():
 
1862
  tma_user_id = session['telegram_user_id']
1863
  data = load_data()
1864
  user_data = data['users'].get(tma_user_id)
 
1898
  return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500
1899
 
1900
  @app.route('/delete_public_link', methods=['POST'])
1901
+ @tma_login_required
1902
  def delete_public_link():
 
1903
  tma_user_id = session['telegram_user_id']
1904
  data = load_data()
1905
 
 
1925
  return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500
1926
 
1927
  @app.route('/get_public_links/<item_id>')
1928
+ @tma_login_required
1929
  def get_public_links(item_id):
 
1930
  tma_user_id = session['telegram_user_id']
1931
  data = load_data()
1932
  user_data = data['users'].get(tma_user_id)
 
1998
 
1999
  items_in_folder = sorted(folder_node.get('children', []), key=lambda x: (x['type'] != 'folder', x.get('name', x.get('original_filename', x.get('title', ''))).lower()))
2000
 
2001
+ return render_template_string(PUBLIC_SHARE_PAGE_HTML, folder=folder_node, items=items_in_folder, user=user_data, link=link_data, hf_file_url_jinja=hf_file_url)
2002
 
2003
  @app.route('/public_download/<link_id>/<item_id>')
2004
  def public_download_via_link(link_id, item_id):
 
2071
  else:
2072
  return jsonify({'status': 'error', 'message': 'Элемент в списке не найден.'}), 404
2073
 
2074
+ # --- BUSINESS PAGES START ---
2075
+
2076
+ USER_BUSINESS_PAGES_LIST_HTML = '''
2077
+ <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
2078
+ <title>Мои бизнес-страницы</title>
2079
+ <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
2080
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
2081
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
2082
+ <script src="https://telegram.org/js/telegram-web-app.js"></script>
2083
+ <style>''' + BASE_STYLE + '''</style></head><body>
2084
+ <div class="app-header">
2085
+ <div class="user-info">{{ display_name }}</div>
2086
+ <div class="view-toggle"><a href="{{ url_for('tma_dashboard') }}"><i class="fa-solid fa-arrow-left"></i></a></div>
2087
+ </div>
2088
+ <div class="container">
2089
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
2090
+ <h2>Мои бизнес-страницы</h2>
2091
+ <a href="{{ url_for('create_business_page') }}" class="btn" style="background:var(--accent);"><i class="fa-solid fa-plus"></i> Создать</a>
2092
+ </div>
2093
+ {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
2094
+ {% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}
2095
+ {% endif %}{% endwith %}
2096
+
2097
+ {% for page in pages %}
2098
+ <div class="page-card">
2099
+ <div class="page-card-info">
2100
+ <h4>{{ page.org_name }}</h4>
2101
+ <a href="{{ url_for('public_business_page', login=page.login, _external=True) }}" target="_blank">/b/{{ page.login }}</a>
2102
+ </div>
2103
+ <div class="page-card-actions">
2104
+ <a href="{{ url_for('manage_business_page', login=page.login) }}" class="btn" title="Управлять"><i class="fa-solid fa-store"></i></a>
2105
+ <a href="{{ url_for('edit_business_page', login=page.login) }}" class="btn folder-btn" title="Настройки"><i class="fa-solid fa-gear"></i></a>
2106
+ </div>
2107
+ </div>
2108
+ {% else %}
2109
+ <p>У вас еще нет бизнес-страниц. Нажмите "Создать", чтобы начать.</p>
2110
+ {% endfor %}
2111
+ </div>
2112
+ <script>
2113
+ window.Telegram.WebApp.ready();
2114
+ window.Telegram.WebApp.expand();
2115
+ let backButton = window.Telegram.WebApp.BackButton;
2116
+ backButton.show();
2117
+ backButton.onClick(() => { window.location.href = `{{ url_for('tma_dashboard') }}`; });
2118
+ </script>
2119
+ </body></html>
2120
+ '''
2121
+
2122
+ CREATE_EDIT_BUSINESS_PAGE_HTML = '''
2123
+ <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
2124
+ <title>{{ 'Редактирова��ь' if page else 'Создать' }} бизнес-страницу</title>
2125
+ <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
2126
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
2127
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
2128
+ <script src="https://telegram.org/js/telegram-web-app.js"></script>
2129
+ <style>''' + BASE_STYLE + '''</style></head><body>
2130
+ <div class="app-header">
2131
+ <div class="user-info">{{ display_name }}</div>
2132
+ <div class="view-toggle"><a href="{{ url_for('my_business_pages') }}"><i class="fa-solid fa-arrow-left"></i></a></div>
2133
+ </div>
2134
+ <div class="container">
2135
+ <h2>{{ 'Редактировать' if page else 'Создать' }} бизнес-страницу</h2>
2136
+ {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
2137
+ {% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}
2138
+ {% endif %}{% endwith %}
2139
+
2140
+ <form method="POST" enctype="multipart/form-data">
2141
+ <div class="form-group">
2142
+ <label for="org_name">Название организации</label>
2143
+ <input type="text" id="org_name" name="org_name" value="{{ page.org_name or '' }}" required>
2144
+ </div>
2145
+ <div class="form-group">
2146
+ <label for="login">Логин (URL)</label>
2147
+ <input type="text" id="login" name="login" value="{{ page.login or '' }}" pattern="[a-zA-Z0-9_\\-]+" {{ 'readonly' if page }} required>
2148
+ <small>Только латинские буквы, цифры, дефис и подчеркивание. Это будет часть URL вашей страницы. Изменить будет нельзя.</small>
2149
+ </div>
2150
+ <div class="form-group">
2151
+ <label for="avatar">Аватар (необязательно)</label>
2152
+ <input type="file" id="avatar" name="avatar" accept="image/*">
2153
+ {% if page and page.avatar_path %}
2154
+ <small>Текущий аватар:</small>
2155
+ <img src="{{ hf_file_url_jinja(page.avatar_path) }}" style="width: 80px; height: 80px; border-radius: 50%; margin-top: 10px;">
2156
+ {% endif %}
2157
+ </div>
2158
+ <div class="form-group">
2159
+ <label for="currency">Валюта</label>
2160
+ <select id="currency" name="currency" required>
2161
+ <option value="KZT" {% if page and page.currency == 'KZT' %}selected{% endif %}>Тенге (KZT)</option>
2162
+ <option value="RUB" {% if page and page.currency == 'RUB' %}selected{% endif %}>Рубль (RUB)</option>
2163
+ <option value="KGS" {% if page and page.currency == 'KGS' %}selected{% endif %}>Кыргызский сом (KGS)</option>
2164
+ <option value="UZS" {% if page and page.currency == 'UZS' %}selected{% endif %}>Узбекский сум (UZS)</option>
2165
+ <option value="UAH" {% if page and page.currency == 'UAH' %}selected{% endif %}>Украинская гривна (UAH)</option>
2166
+ </select>
2167
+ </div>
2168
+ <div class="form-group">
2169
+ <label>Указывать цены?</label>
2170
+ <input type="checkbox" id="show_prices" name="show_prices" value="true" {% if page and page.show_prices %}checked{% endif %} style="width:auto;height:auto;">
2171
+ </div>
2172
+ <div class="form-group">
2173
+ <label for="order_destination">Куда будут приходить заказы?</label>
2174
+ <select id="order_destination" name="order_destination" required>
2175
+ <option value="whatsapp" {% if page and page.order_destination == 'whatsapp' %}selected{% endif %}>WhatsApp</option>
2176
+ <option value="telegram" {% if page and page.order_destination == 'telegram' %}selected{% endif %}>Telegram</option>
2177
+ </select>
2178
+ </div>
2179
+ <div class="form-group">
2180
+ <label for="contact_phone">Номер телефона или username</label>
2181
+ <input type="text" id="contact_phone" name="contact_phone" value="{{ page.contact_phone or '' }}" required>
2182
+ <small>Для WhatsApp укажите полный номер с кодом страны (например, 77001234567). Для Telegram - username без @.</small>
2183
+ </div>
2184
+ <button type="submit" class="btn" style="width: 100%; margin-top: 15px;">{{ 'Сохранить' if page else 'Создать' }}</button>
2185
+ </form>
2186
+ {% if page %}
2187
+ <form action="{{ url_for('delete_business_page', login=page.login) }}" method="POST" onsubmit="return confirm('Вы уверены, что хотите удалить эту страницу? Это действие необратимо.');">
2188
+ <button type="submit" class="btn delete-btn" style="width: 100%; margin-top: 10px;">Удалить страницу</button>
2189
+ </form>
2190
+ {% endif %}
2191
+ </div>
2192
+ <script>
2193
+ window.Telegram.WebApp.ready();
2194
+ window.Telegram.WebApp.expand();
2195
+ let backButton = window.Telegram.WebApp.BackButton;
2196
+ backButton.show();
2197
+ backButton.onClick(() => { window.location.href = `{{ url_for('my_business_pages') }}`; });
2198
+ </script>
2199
+ </body></html>
2200
+ '''
2201
+
2202
+ MANAGE_PRODUCTS_HTML = '''
2203
+ <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
2204
+ <title>Управление: {{ page.org_name }}</title>
2205
+ <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
2206
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
2207
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
2208
+ <script src="https://telegram.org/js/telegram-web-app.js"></script>
2209
+ <style>''' + BASE_STYLE + '''
2210
+ .product-card {
2211
+ display: flex; gap: 15px; background: var(--card-bg-dark); padding: 10px; border-radius: 12px; margin-bottom: 10px; align-items: center;
2212
+ }
2213
+ .product-img { width: 80px; height: 80px; object-fit: cover; border-radius: 8px; flex-shrink: 0;}
2214
+ .product-info { flex-grow: 1; }
2215
+ .product-info h5 { margin: 0; }
2216
+ .product-price { font-weight: bold; color: var(--secondary); }
2217
+ .add-product-form { background: var(--card-bg-dark); padding: 20px; border-radius: 16px; margin-top: 25px; }
2218
+ </style></head><body>
2219
+ <div class="app-header">
2220
+ <div class="user-info">{{ page.org_name }}</div>
2221
+ <div class="view-toggle"><a href="{{ url_for('my_business_pages') }}"><i class="fa-solid fa-arrow-left"></i></a></div>
2222
+ </div>
2223
+ <div class="container">
2224
+ <h2>Товары</h2>
2225
+ {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
2226
+ {% for category, message in messages %}<div class="flash {{ category }}">{{ message }}</div>{% endfor %}
2227
+ {% endif %}
2228
+
2229
+ <div>
2230
+ {% for product in page.products %}
2231
+ <div class="product-card">
2232
+ <img src="{{ hf_file_url_jinja(product.photo_path) }}" class="product-img">
2233
+ <div class="product-info">
2234
+ <h5>{{ product.name }}</h5>
2235
+ <p style="font-size:0.8em; color:var(--text-muted);">{{ product.description | truncate(80) }}</p>
2236
+ {% if page.show_prices %}<span class="product-price">{{ product.price }} {{ page.currency }}</span>{% endif %}
2237
+ </div>
2238
+ <form action="{{ url_for('delete_product', login=page.login, product_id=product.id) }}" method="POST" onsubmit="return confirm('Удалить товар?');">
2239
+ <button type="submit" class="btn delete-btn" style="padding: 8px 12px;"><i class="fa fa-trash"></i></button>
2240
+ </form>
2241
+ </div>
2242
+ {% else %}
2243
+ <p>У вас еще нет товаров.</p>
2244
+ {% endfor %}
2245
+ </div>
2246
+
2247
+ <div class="add-product-form">
2248
+ <h4>Добавить новый товар</h4>
2249
+ <form method="POST" action="{{ url_for('add_product', login=page.login) }}" enctype="multipart/form-data">
2250
+ <div class="form-group">
2251
+ <label for="name">Название товара</label>
2252
+ <input type="text" name="name" required>
2253
+ </div>
2254
+ <div class="form-group">
2255
+ <label for="description">Описание</label>
2256
+ <textarea name="description" rows="3"></textarea>
2257
+ </div>
2258
+ {% if page.show_prices %}
2259
+ <div class="form-group">
2260
+ <label for="price">Цена</label>
2261
+ <input type="number" step="any" name="price" required>
2262
+ </div>
2263
+ {% endif %}
2264
+ <div class="form-group">
2265
+ <label for="photo">Фото товара</label>
2266
+ <input type="file" name="photo" accept="image/*" required>
2267
+ </div>
2268
+ <button type="submit" class="btn" style="width:100%;">Добавить</button>
2269
+ </form>
2270
+ </div>
2271
+ </div>
2272
+ <script>
2273
+ window.Telegram.WebApp.ready();
2274
+ window.Telegram.WebApp.expand();
2275
+ let backButton = window.Telegram.WebApp.BackButton;
2276
+ backButton.show();
2277
+ backButton.onClick(() => { window.location.href = `{{ url_for('my_business_pages') }}`; });
2278
+ </script>
2279
+ </body></html>
2280
+ '''
2281
+
2282
+ PUBLIC_BUSINESS_PAGE_HTML = '''
2283
+ <!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
2284
+ <title>{{ page.org_name }}</title>
2285
+ <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
2286
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
2287
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
2288
+ <style>''' + BASE_STYLE + '''
2289
+ body { padding-bottom: 90px; }
2290
+ .page-header { text-align: center; padding: 20px; }
2291
+ .page-avatar { width: 100px; height: 100px; border-radius: 50%; object-fit: cover; margin: 0 auto 15px auto; border: 3px solid var(--card-bg-dark); }
2292
+ .product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
2293
+ .product-item { background: var(--card-bg-dark); border-radius: 16px; overflow: hidden; text-align: left; }
2294
+ .product-item-img { width: 100%; height: 200px; object-fit: cover; }
2295
+ .product-item-content { padding: 15px; }
2296
+ .product-item-content h3 { margin: 0 0 5px 0; font-size: 1.1em; }
2297
+ .product-item-content p { font-size: 0.9em; color: var(--text-muted); margin-bottom: 10px; }
2298
+ .product-item-price { font-weight: 600; font-size: 1.2em; color: var(--secondary); }
2299
+ .contact-fab { position: fixed; bottom: 20px; right: 20px; z-index: 1000; }
2300
+ .contact-fab .btn { background: #25D366; width: 60px; height: 60px; border-radius: 50%; font-size: 1.8em; padding: 0; display: flex; align-items: center; justify-content: center;}
2301
+ .contact-fab .btn.telegram { background: #0088cc; }
2302
+ </style></head><body>
2303
+ <div class="page-header">
2304
+ {% if page.avatar_path %}
2305
+ <img src="{{ hf_file_url_jinja(page.avatar_path) }}" class="page-avatar">
2306
+ {% endif %}
2307
+ <h1>{{ page.org_name }}</h1>
2308
+ </div>
2309
+ <div class="container" style="padding-top: 0;">
2310
+ <div class="product-grid">
2311
+ {% for product in page.products %}
2312
+ <div class="product-item">
2313
+ <img src="{{ hf_file_url_jinja(product.photo_path) }}" class="product-item-img">
2314
+ <div class="product-item-content">
2315
+ <h3>{{ product.name }}</h3>
2316
+ <p>{{ product.description }}</p>
2317
+ {% if page.show_prices %}
2318
+ <div class="product-item-price">{{ product.price }} {{ page.currency }}</div>
2319
+ {% endif %}
2320
+ </div>
2321
+ </div>
2322
+ {% else %}
2323
+ <p>Товары скоро появятся.</p>
2324
+ {% endfor %}
2325
+ </div>
2326
+ </div>
2327
+ <div class="contact-fab">
2328
+ <a href="{{ contact_url }}" class="btn {{ 'telegram' if page.order_destination == 'telegram' else '' }}" target="_blank">
2329
+ <i class="fab {{ 'fa-telegram' if page.order_destination == 'telegram' else 'fa-whatsapp' }}"></i>
2330
+ </a>
2331
+ </div>
2332
+ </body></html>
2333
+ '''
2334
 
2335
  ADMIN_LOGIN_HTML = '''
2336
  <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Admin Login</title>
 
2394
  <span class="id">Created: {{ user.get('created_at', 'N/A') }}</span>
2395
  <span class="id">Items: <strong>{{ user.get('item_count', 0) }}</strong></span>
2396
  <span class="id">Reminders: <strong>{{ user.get('reminders', [])|length }}</strong></span>
2397
+ <span class="id">Business Pages: <strong>{{ user.get('business_pages', [])|length }}</strong></span>
2398
  </div>
2399
  </div>
2400
  <div class="user-actions">
 
2711
  current_folder_id=current_folder_id,
2712
  current_folder=current_folder,
2713
  breadcrumbs=breadcrumbs,
2714
+ hf_file_url_jinja=hf_file_url)
2715
 
2716
  @app.route('/admhosto/user/<tma_user_id_str>/reminders')
2717
  @admin_browser_login_required
 
2746
  hf_path = file_node.get('path')
2747
  if not hf_path:
2748
  return Response("Error: file path is missing", 500)
2749
+ file_url_path = hf_file_url(hf_path, download=True)
2750
  try:
2751
  req_headers = {}
2752
  if HF_TOKEN_READ: req_headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
2753
+ response = requests.get(file_url_path, headers=req_headers)
2754
  response.raise_for_status()
2755
  if len(response.content) > 1 * 1024 * 1024:
2756
  return Response("File too large for preview.", 413)
 
2835
  flash('Reminder not found.', 'error')
2836
  return redirect(url_for('admin_user_reminders', tma_user_id_str=tma_user_id_str))
2837
 
 
 
 
 
 
 
 
2838
 
2839
+ # --- BUSINESS PAGES ROUTES ---
2840
+
2841
+ @app.route('/tma/my_business')
2842
+ @tma_login_required
2843
+ def my_business_pages():
2844
+ tma_user_id = session['telegram_user_id']
2845
+ display_name = session.get('telegram_display_name', 'Пользователь')
2846
  data = load_data()
2847
+ user_data = data['users'][tma_user_id]
2848
+ user_pages_logins = user_data.get('business_pages', [])
2849
+ user_pages_details = [data['business_pages'][login] for login in user_pages_logins if login in data['business_pages']]
2850
+ return render_template_string(USER_BUSINESS_PAGES_LIST_HTML, display_name=display_name, pages=user_pages_details)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2851
 
2852
+ @app.route('/tma/business/new', methods=['GET', 'POST'])
2853
+ @tma_login_required
2854
+ def create_business_page():
2855
+ if request.method == 'POST':
2856
+ tma_user_id = session['telegram_user_id']
2857
+ data = load_data()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2858
 
2859
+ login = request.form.get('login', '').lower().strip()
2860
+ if not re.match("^[a-zA-Z0-9_-]+$", login):
2861
+ flash('Логин содержит недопустимые символы.', 'error')
2862
+ return redirect(url_for('create_business_page'))
2863
+ if login in data['business_pages']:
2864
+ flash('Этот логин уже занят.', 'error')
2865
+ return redirect(url_for('create_business_page'))
2866
+
2867
+ page_data = {
2868
+ 'owner_id': tma_user_id,
2869
+ 'login': login,
2870
+ 'org_name': request.form.get('org_name'),
2871
+ 'currency': request.form.get('currency'),
2872
+ 'show_prices': 'show_prices' in request.form,
2873
+ 'order_destination': request.form.get('order_destination'),
2874
+ 'contact_phone': request.form.get('contact_phone').strip(),
2875
+ 'products': [],
2876
+ 'avatar_path': None
2877
+ }
2878
 
2879
+ avatar = request.files.get('avatar')
2880
+ if avatar and avatar.filename:
2881
+ if not HF_TOKEN_WRITE:
2882
+ flash('Загрузка аватара невозможна: токен не настроен.', 'error')
2883
+ return redirect(url_for('create_business_page'))
2884
+ try:
2885
+ api = HfApi()
2886
+ filename = secure_filename(avatar.filename)
2887
+ hf_path = f"business_avatars/{login}/{filename}"
2888
+ api.upload_file(path_or_fileobj=avatar.stream, path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
2889
+ page_data['avatar_path'] = hf_path
2890
+ except Exception as e:
2891
+ flash(f'Ошибка загрузки аватара: {e}', 'error')
2892
+ return redirect(url_for('create_business_page'))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2893
 
2894
+ data['business_pages'][login] = page_data
2895
+ data['users'][tma_user_id].setdefault('business_pages', []).append(login)
2896
+ save_data(data)
2897
+ flash('Бизнес-страница успешно создана!', 'success')
2898
+ return redirect(url_for('my_business_pages'))
 
 
 
 
 
 
 
 
 
 
 
 
 
2899
 
2900
+ return render_template_string(CREATE_EDIT_BUSINESS_PAGE_HTML, display_name=session.get('telegram_display_name'), page=None, hf_file_url_jinja=hf_file_url)
 
 
 
 
 
 
 
 
 
 
2901
 
2902
+ @app.route('/tma/business/edit/<login>', methods=['GET', 'POST'])
2903
+ @tma_login_required
2904
+ def edit_business_page(login):
2905
+ tma_user_id = session['telegram_user_id']
2906
+ data = load_data()
2907
+ page = data.get('business_pages', {}).get(login)
2908
+ if not page or page['owner_id'] != tma_user_id:
2909
+ flash('Страница не найдена или у вас нет доступа.', 'error')
2910
+ return redirect(url_for('my_business_pages'))
 
 
 
 
2911
 
2912
+ if request.method == 'POST':
2913
+ page['org_name'] = request.form.get('org_name')
2914
+ page['currency'] = request.form.get('currency')
2915
+ page['show_prices'] = 'show_prices' in request.form
2916
+ page['order_destination'] = request.form.get('order_destination')
2917
+ page['contact_phone'] = request.form.get('contact_phone').strip()
2918
+
2919
+ avatar = request.files.get('avatar')
2920
+ if avatar and avatar.filename:
2921
+ if not HF_TOKEN_WRITE:
2922
+ flash('Загрузка аватара невозможна: токен не настроен.', 'error')
2923
+ else:
2924
+ try:
2925
+ api = HfApi()
2926
+ filename = secure_filename(avatar.filename)
2927
+ hf_path = f"business_avatars/{login}/{filename}"
2928
+ api.upload_file(path_or_fileobj=avatar.stream, path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
2929
+ page['avatar_path'] = hf_path
2930
+ except Exception as e:
2931
+ flash(f'Ошибка загрузки аватара: {e}', 'error')
2932
+ save_data(data)
2933
+ flash('Настройки страницы обновлены.', 'success')
2934
+ return redirect(url_for('my_business_pages'))
2935
+
2936
+ return render_template_string(CREATE_EDIT_BUSINESS_PAGE_HTML, display_name=session.get('telegram_display_name'), page=page, hf_file_url_jinja=hf_file_url)
2937
+
2938
+ @app.route('/tma/business/delete/<login>', methods=['POST'])
2939
+ @tma_login_required
2940
+ def delete_business_page(login):
2941
+ tma_user_id = session['telegram_user_id']
2942
+ data = load_data()
2943
+ page = data.get('business_pages', {}).get(login)
2944
+ if not page or page['owner_id'] != tma_user_id:
2945
+ flash('Страница не найдена или у вас нет доступа.', 'error')
2946
+ return redirect(url_for('my_business_pages'))
2947
+
2948
+ del data['business_pages'][login]
2949
+ if login in data['users'][tma_user_id].get('business_pages', []):
2950
+ data['users'][tma_user_id]['business_pages'].remove(login)
2951
+
2952
+ save_data(data)
2953
+ flash('Страница удалена.', 'success')
2954
+ return redirect(url_for('my_business_pages'))
2955
+
2956
+ @app.route('/tma/business/manage/<login>')
2957
+ @tma_login_required
2958
+ def manage_business_page(login):
2959
+ tma_user_id = session['telegram_user_id']
2960
+ data = load_data()
2961
+ page = data.get('business_pages', {}).get(login)
2962
+ if not page or page['owner_id'] != tma_user_id:
2963
+ flash('Страница не найдена или у вас нет доступа.', 'error')
2964
+ return redirect(url_for('my_business_pages'))
2965
+
2966
+ return render_template_string(MANAGE_PRODUCTS_HTML, page=page, hf_file_url_jinja=hf_file_url)
2967
+
2968
+ @app.route('/tma/business/manage/<login>/add_product', methods=['POST'])
2969
+ @tma_login_required
2970
+ def add_product(login):
2971
+ tma_user_id = session['telegram_user_id']
2972
+ data = load_data()
2973
+ page = data.get('business_pages', {}).get(login)
2974
+ if not page or page['owner_id'] != tma_user_id:
2975
+ flash('Страница не найдена или у вас нет доступа.', 'error')
2976
+ return redirect(url_for('my_business_pages'))
2977
+
2978
+ photo = request.files.get('photo')
2979
+ if not photo or not photo.filename:
2980
+ flash('Фото товара обязательно.', 'error')
2981
+ return redirect(url_for('manage_business_page', login=login))
2982
+
2983
+ product_id = uuid.uuid4().hex
2984
+ if not HF_TOKEN_WRITE:
2985
+ flash('Загрузка фото невозможна: токен не настроен.', 'error')
2986
+ return redirect(url_for('manage_business_page', login=login))
2987
+ try:
2988
+ api = HfApi()
2989
+ _, ext = os.path.splitext(secure_filename(photo.filename))
2990
+ hf_path = f"business_products/{login}/{product_id}{ext}"
2991
+ api.upload_file(path_or_fileobj=photo.stream, path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
2992
+ except Exception as e:
2993
+ flash(f'Ошибка загрузки фото: {e}', 'error')
2994
+ return redirect(url_for('manage_business_page', login=login))
2995
+
2996
+ new_product = {
2997
+ 'id': product_id,
2998
+ 'name': request.form.get('name'),
2999
+ 'description': request.form.get('description', ''),
3000
+ 'price': request.form.get('price', '0'),
3001
+ 'photo_path': hf_path
3002
  }
3003
+ page.setdefault('products', []).append(new_product)
3004
+ save_data(data)
3005
+ flash('Товар добавлен.', 'success')
3006
+ return redirect(url_for('manage_business_page', login=login))
3007
+
3008
+ @app.route('/tma/business/manage/<login>/delete_product/<product_id>', methods=['POST'])
3009
+ @tma_login_required
3010
+ def delete_product(login, product_id):
3011
+ tma_user_id = session['telegram_user_id']
3012
+ data = load_data()
3013
+ page = data.get('business_pages', {}).get(login)
3014
+ if not page or page['owner_id'] != tma_user_id:
3015
+ flash('Страница не найдена или у вас нет доступа.', 'error')
3016
+ return redirect(url_for('my_business_pages'))
3017
+
3018
+ page['products'] = [p for p in page.get('products', []) if p.get('id') != product_id]
3019
+ save_data(data)
3020
+ flash('Товар удален.', 'success')
3021
+ return redirect(url_for('manage_business_page', login=login))
3022
+
3023
+ @app.route('/b/<login>')
3024
+ def public_business_page(login):
3025
+ data = load_data()
3026
+ page = data.get('business_pages', {}).get(login)
3027
+ if not page:
3028
+ return "Страница не найдена.", 404
3029
 
3030
+ contact_url = '#'
3031
+ if page.get('order_destination') == 'whatsapp':
3032
+ phone = ''.join(filter(str.isdigit, page.get('contact_phone', '')))
3033
+ contact_url = f"https://wa.me/{phone}"
3034
+ elif page.get('order_destination') == 'telegram':
3035
+ username = page.get('contact_phone', '').replace('@', '')
3036
+ contact_url = f"https://t.me/{username}"
3037
+
3038
+ return render_template_string(PUBLIC_BUSINESS_PAGE_HTML, page=page, contact_url=contact_url, hf_file_url_jinja=hf_file_url)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3039
 
3040
  if __name__ == '__main__':
3041
  if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write) is not set. Uploads/deletions will fail.")
 
3048
  download_db_from_hf()
3049
  else:
3050
  if not os.path.exists(DATA_FILE):
3051
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}, 'business_pages': {}}, f)
3052
 
3053
  if HF_TOKEN_WRITE:
3054
  threading.Thread(target=periodic_backup, daemon=True).start()
 
3056
  threading.Thread(target=check_reminders, daemon=True).start()
3057
 
3058
  app.run(debug=False, host='0.0.0.0', port=7860)
3059
+