Kgshop commited on
Commit
a9eb7ec
·
verified ·
1 Parent(s): 9644a78

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +254 -171
app.py CHANGED
@@ -5,7 +5,7 @@ import os
5
  import logging
6
  import threading
7
  import time
8
- import socket
9
  from datetime import datetime
10
  from huggingface_hub import HfApi, hf_hub_download
11
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
@@ -13,14 +13,9 @@ from werkzeug.utils import secure_filename
13
  from dotenv import load_dotenv
14
  import requests
15
  import uuid
16
- import asyncio
17
- from aiohttp import TCPConnector
18
- from aiogram import Bot, Dispatcher, types
19
- from aiogram.client.session.aiohttp import AiohttpSession
20
  from aiogram.filters import CommandStart
21
- from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo
22
- from aiogram.client.bot import DefaultBotProperties
23
- from aiogram.enums import ParseMode
24
 
25
  load_dotenv()
26
 
@@ -33,9 +28,7 @@ SYNC_FILES = [DATA_FILE]
33
  REPO_ID = "Kgshop/aiocult"
34
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
35
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
36
-
37
- BOT_TOKEN = os.getenv("BOT_TOKEN", "8283649768:AAGYWatM-nUVQirgBiBwoAtWZgzfp3QnQjY")
38
- WEB_APP_URL = "https://shveiauto-wall.hf.space/"
39
 
40
  STORE_ADDRESS = " Алматы, «Байсат» 1 этаж, синий сектор 117-118 Бутик "
41
 
@@ -54,23 +47,54 @@ STATUS_MAP_RU = {
54
 
55
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
56
 
57
- connector = TCPConnector(family=socket.AF_INET, ssl=True)
58
- session = AiohttpSession(connector=connector)
59
- bot = Bot(token=BOT_TOKEN, session=session, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
60
- dp = Dispatcher()
61
- bot_loop = asyncio.new_event_loop()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
 
 
 
64
  token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
65
 
66
  files_to_download = [specific_file] if specific_file else SYNC_FILES
 
67
  all_successful = True
68
 
69
  for file_name in files_to_download:
70
  success = False
71
  for attempt in range(retries + 1):
72
  try:
73
- hf_hub_download(
 
74
  repo_id=REPO_ID,
75
  filename=file_name,
76
  repo_type="dataset",
@@ -80,38 +104,50 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
80
  force_download=True,
81
  resume_download=False
82
  )
 
83
  success = True
84
  break
85
  except RepositoryNotFoundError:
 
86
  return False
87
  except HfHubHTTPError as e:
88
  if e.response.status_code == 404:
 
89
  if attempt == 0 and not os.path.exists(file_name):
90
  try:
91
  if file_name == DATA_FILE:
92
  with open(file_name, 'w', encoding='utf-8') as f:
93
- json.dump({'products': [], 'categories': [], 'orders': {}, 'bot_users': []}, f)
94
- except Exception:
95
- pass
 
96
  success = False
97
  break
98
- except Exception:
99
- pass
 
 
 
 
100
 
101
  if attempt < retries:
102
  time.sleep(delay)
103
 
104
  if not success:
 
105
  all_successful = False
106
 
 
107
  return all_successful
108
 
109
  def upload_db_to_hf(specific_file=None):
110
  if not HF_TOKEN_WRITE:
 
111
  return
112
  try:
113
  api = HfApi()
114
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
 
115
 
116
  for file_name in files_to_upload:
117
  if os.path.exists(file_name):
@@ -124,19 +160,26 @@ def upload_db_to_hf(specific_file=None):
124
  token=HF_TOKEN_WRITE,
125
  commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
126
  )
127
- except Exception:
128
- pass
129
- except Exception:
130
- pass
 
 
 
 
131
 
132
  def periodic_backup():
133
  backup_interval = 1800
 
134
  while True:
135
  time.sleep(backup_interval)
 
136
  upload_db_to_hf()
 
137
 
138
- def load_data():
139
- default_data = {'products': [], 'categories': [], 'orders': {}, 'bot_users': []}
140
  try:
141
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
142
  data = json.load(file)
@@ -145,102 +188,89 @@ def load_data():
145
  if 'products' not in data: data['products'] = []
146
  if 'categories' not in data: data['categories'] = []
147
  if 'orders' not in data: data['orders'] = {}
148
- if 'bot_users' not in data: data['bot_users'] = []
149
  return data
150
  except (FileNotFoundError, json.JSONDecodeError):
151
- pass
152
 
153
- if download_db_from_hf(specific_file=DATA_FILE):
 
 
154
  try:
155
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
156
  data = json.load(file)
 
157
  if not isinstance(data, dict):
158
- return default_data
 
159
  if 'products' not in data: data['products'] = []
160
  if 'categories' not in data: data['categories'] = []
161
  if 'orders' not in data: data['orders'] = {}
162
- if 'bot_users' not in data: data['bot_users'] = []
163
  return data
164
- except Exception:
165
- return default_data
166
- else:
167
- if not os.path.exists(DATA_FILE):
 
 
168
  try:
169
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
170
- json.dump(default_data, f)
171
- except Exception:
172
- pass
173
- return default_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
- def save_data(data):
176
  try:
177
- if not isinstance(data, dict):
178
- return
179
  if 'products' not in data: data['products'] = []
180
  if 'categories' not in data: data['categories'] = []
181
  if 'orders' not in data: data['orders'] = {}
182
- if 'bot_users' not in data: data['bot_users'] = []
183
 
184
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
185
  json.dump(data, file, ensure_ascii=False, indent=4)
186
  upload_db_to_hf(specific_file=DATA_FILE)
187
- except Exception:
188
- pass
189
-
190
- async def send_new_product_notification(product_name, price, photo_filename=None):
191
- data = load_data()
192
- users = data.get('bot_users', [])
193
- if not users:
194
- return
195
-
196
- keyboard = InlineKeyboardMarkup(inline_keyboard=[
197
- [InlineKeyboardButton(text="Посмотреть новинку", web_app=WebAppInfo(url=WEB_APP_URL))]
198
- ])
199
- message_text = f"✨ <b>Поступление нового товара!</b> ✨\n\nВ нашем каталоге появился <b>{product_name}</b> по цене {price} {CURRENCY_CODE}."
200
 
201
- for user_id in users:
202
- try:
203
- if photo_filename:
204
- photo_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{photo_filename}"
205
- await bot.send_photo(chat_id=user_id, photo=photo_url, caption=message_text, reply_markup=keyboard)
206
- else:
207
- await bot.send_message(user_id, message_text, reply_markup=keyboard)
208
- await asyncio.sleep(0.1)
209
- except Exception:
210
- pass
211
-
212
- @dp.message(CommandStart())
213
- async def send_welcome(message: types.Message):
214
- user_id = message.from_user.id
215
- data = load_data()
216
- if user_id not in data.get('bot_users', []):
217
- data.setdefault('bot_users', []).append(user_id)
218
- save_data(data)
219
 
220
- keyboard = InlineKeyboardMarkup(inline_keyboard=[
221
- [InlineKeyboardButton(text="Перейти в каталог", web_app=WebAppInfo(url=WEB_APP_URL))]
222
- ])
223
- await message.answer("Добро пожаловать в Мобильный мир! Нажмите кнопку ниже, чтобы открыть наш каталог.", reply_markup=keyboard)
224
 
225
- async def main_bot():
226
- for i in range(20):
 
227
  try:
228
- await bot.delete_webhook(drop_pending_updates=True)
229
- logging.info("Webhook deleted successfully.")
230
- break
231
  except Exception as e:
232
- logging.warning(f"Network not ready yet (Attempt {i+1}/20): {e}")
233
- await asyncio.sleep(5)
234
-
235
- try:
236
- await dp.start_polling(bot)
237
- except Exception as e:
238
- logging.error(f"Polling failed to start: {e}")
239
-
240
- def run_bot():
241
- time.sleep(5)
242
- asyncio.set_event_loop(bot_loop)
243
- bot_loop.run_until_complete(main_bot())
244
 
245
  CATALOG_TEMPLATE = '''
246
  <!DOCTYPE html>
@@ -252,10 +282,9 @@ CATALOG_TEMPLATE = '''
252
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
253
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
254
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
255
- <script src="https://telegram.org/js/telegram-web-app.js"></script>
256
  <style>
257
  :root {
258
- --primary-color: #007AFF;
259
  --primary-dark: #0056b3;
260
  --surface-color: #FFFFFF;
261
  --surface-dark: #21252B;
@@ -280,10 +309,6 @@ CATALOG_TEMPLATE = '''
280
  .theme-toggle { background: none; border: none; font-size: 1.3rem; cursor: pointer; color: var(--primary-color); padding: 5px; }
281
  .theme-toggle:hover { opacity: 0.8; }
282
 
283
- #user-info { display: none; align-items: center; gap: 8px; font-size: 0.9rem; }
284
- #user-info img { width: 35px; height: 35px; border-radius: 50%; }
285
- #user-info span { font-weight: 500; }
286
-
287
  .store-address { padding: 12px 15px; text-align: center; background-color: var(--surface-color); border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); font-size: 0.9rem; color: #555; margin: 15px; }
288
  body.dark-mode .store-address { background-color: var(--surface-dark); color: var(--text-color-dark); box-shadow: 0 1px 3px rgba(255,255,255,0.1); }
289
 
@@ -311,7 +336,7 @@ CATALOG_TEMPLATE = '''
311
  .product h2 { font-size: 0.95rem; font-weight: 500; margin: 0 0 5px 0; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-color); }
312
  body.dark-mode .product h2 { color: var(--text-color-dark); }
313
  .product-price { font-size: 1.1rem; color: var(--primary-color); font-weight: 700; text-align: center; margin: 5px 0; }
314
- .product-description { display: none; }
315
  .product-actions { padding: 0 10px 10px 10px; display: flex; flex-direction: column; gap: 6px; }
316
 
317
  .product-button { display: block; width: 100%; padding: 8px; border: none; border-radius: 8px; background-color: var(--primary-color); color: white; font-size: 0.85rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; text-align: center; text-decoration: none; }
@@ -369,10 +394,6 @@ CATALOG_TEMPLATE = '''
369
  <div class="logo-title-container" style="display: flex; align-items: center; gap: 10px;">
370
  <img src="https://huggingface.co/spaces/Kgshop/mobilnymir/resolve/main/IMG_20251019_120056_121.jpg" alt="Мобильный мир Logo">
371
  <h1>Мобильный мир</h1>
372
- </div>
373
- <div id="user-info">
374
- <img id="user-avatar" src="" alt="avatar">
375
- <span id="user-name"></span>
376
  </div>
377
  <button class="theme-toggle" onclick="toggleTheme()" aria-label="Переключить тему">
378
  <i class="fas fa-moon"></i>
@@ -630,7 +651,7 @@ CATALOG_TEMPLATE = '''
630
  return;
631
  }
632
 
633
- const cartItemId = `${product.name}-${color}-${model}`;
634
  const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
635
 
636
  if (existingItemIndex > -1) {
@@ -643,7 +664,7 @@ CATALOG_TEMPLATE = '''
643
  photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
644
  quantity: quantity,
645
  color: color,
646
- model: model
647
  });
648
  }
649
 
@@ -690,7 +711,7 @@ CATALOG_TEMPLATE = '''
690
 
691
  let variantText = [];
692
  if (item.color !== 'N/A') variantText.push(`Цвет: ${item.color}`);
693
- if (item.model !== 'N/A') variantText.push(`Модель: ${item.model}`);
694
 
695
  const variantDisplay = variantText.length > 0 ? ` (${variantText.join(', ')})` : '';
696
 
@@ -787,7 +808,7 @@ CATALOG_TEMPLATE = '''
787
  if (existingNoResults) existingNoResults.remove();
788
 
789
  const allProductElements = document.querySelectorAll('.products-grid .product');
790
- if (allProductElements.length === 0 && products.length > 0) return;
791
 
792
  document.querySelectorAll('.products-grid .product').forEach(productElement => {
793
  const name = productElement.getAttribute('data-name');
@@ -865,40 +886,11 @@ CATALOG_TEMPLATE = '''
865
  notification.addEventListener('transitionend', () => notification.remove());
866
  }, duration);
867
  }
868
-
869
- function initTelegramWebApp() {
870
- if (window.Telegram && window.Telegram.WebApp) {
871
- const tg = window.Telegram.WebApp;
872
- tg.ready();
873
-
874
- if (tg.initDataUnsafe && tg.initDataUnsafe.user) {
875
- const user = tg.initDataUnsafe.user;
876
- const userInfoDiv = document.getElementById('user-info');
877
- const userAvatarImg = document.getElementById('user-avatar');
878
- const userNameSpan = document.getElementById('user-name');
879
-
880
- if (userInfoDiv && userAvatarImg && userNameSpan) {
881
- if (user.photo_url) {
882
- userAvatarImg.src = user.photo_url;
883
- } else {
884
- userAvatarImg.style.display = 'none';
885
- }
886
- userNameSpan.textContent = user.first_name + (user.last_name ? ' ' + user.last_name : '');
887
- userInfoDiv.style.display = 'flex';
888
- }
889
- } else {
890
- console.log("Telegram Web App user data not found.");
891
- }
892
- } else {
893
- console.log("Telegram Web App script not loaded or not in Telegram environment.");
894
- }
895
- }
896
 
897
  document.addEventListener('DOMContentLoaded', () => {
898
  applyInitialTheme();
899
  updateCartButton();
900
  setupFilters();
901
- initTelegramWebApp();
902
 
903
  window.addEventListener('click', function(event) {
904
  if (event.target.classList.contains('modal')) {
@@ -1102,7 +1094,7 @@ ADMIN_TEMPLATE = '''
1102
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
1103
  <style>
1104
  :root {
1105
- --primary-color: #007AFF;
1106
  --primary-dark: #0056b3;
1107
  --surface-color: #FFFFFF;
1108
  --surface-dark: #21252B;
@@ -1176,10 +1168,10 @@ ADMIN_TEMPLATE = '''
1176
  .status-indicator.out-of-stock { background-color: #fed7d7; color: #c53030; }
1177
  .status-indicator.top-product { background-color: #fff3c4; color: #9c4221; margin-left: 5px;}
1178
 
1179
- .status-indicator.new { background-color: #ffe0b2; color: #e65100; }
1180
- .status-indicator.accepted { background-color: #b3e5fc; color: #0277bd; }
1181
- .status-indicator.prepared { background-color: #c6f6d5; color: #2f855a; }
1182
- .status-indicator.shipped { background-color: #a7f3d0; color: #065f46; }
1183
 
1184
  details { background-color: var(--surface-color); border: 1px solid var(--border-color); border-radius: 8px; margin-bottom: 10px; }
1185
  body.dark-mode details { background-color: var(--surface-dark); border-color: var(--border-color-dark); }
@@ -1231,6 +1223,24 @@ ADMIN_TEMPLATE = '''
1231
  {% endfor %}
1232
  {% endif %}
1233
  {% endwith %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1234
 
1235
  <div class="section">
1236
  <h2><i class="fas fa-history"></i> История заказов</h2>
@@ -1562,7 +1572,7 @@ ADMIN_TEMPLATE = '''
1562
  if (container) {
1563
  const existingInputs = container.querySelectorAll('input[name="' + fieldName + '"]');
1564
  if (existingInputs.length === 1 && existingInputs[0].value.trim() === '') {
1565
- existingInputs[0].value = '';
1566
  }
1567
 
1568
  const newInputGroup = document.createElement('div');
@@ -1632,6 +1642,7 @@ def product_detail(index):
1632
  try:
1633
  product = products_sorted[index]
1634
  except IndexError:
 
1635
  return "Товар не найден или отсутствует в наличии.", 404
1636
 
1637
  products_index = index
@@ -1649,6 +1660,7 @@ def create_order():
1649
  order_data = request.get_json()
1650
 
1651
  if not order_data or 'cart' not in order_data or not order_data['cart']:
 
1652
  return jsonify({"error": "Корзина пуста или не передана."}), 400
1653
 
1654
  cart_items = order_data['cart']
@@ -1657,6 +1669,7 @@ def create_order():
1657
  processed_cart = []
1658
  for item in cart_items:
1659
  if not all(k in item for k in ('name', 'price', 'quantity')):
 
1660
  return jsonify({"error": "Неверный формат товара в корзине."}), 400
1661
  try:
1662
  price = float(item['price'])
@@ -1678,6 +1691,7 @@ def create_order():
1678
  "photo_url": photo_url
1679
  })
1680
  except (ValueError, TypeError) as e:
 
1681
  return jsonify({"error": "Неверная цена или количество в товаре."}), 400
1682
 
1683
  order_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}"
@@ -1699,9 +1713,11 @@ def create_order():
1699
 
1700
  data['orders'][order_id] = new_order
1701
  save_data(data)
 
1702
  return jsonify({"order_id": order_id}), 201
1703
 
1704
  except Exception as e:
 
1705
  return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500
1706
 
1707
  @app.route('/order/<order_id>')
@@ -1709,6 +1725,11 @@ def view_order(order_id):
1709
  data = load_data()
1710
  order = data.get('orders', {}).get(order_id)
1711
 
 
 
 
 
 
1712
  return render_template_string(ORDER_TEMPLATE,
1713
  order=order,
1714
  status_map_ru=STATUS_MAP_RU,
@@ -1721,6 +1742,7 @@ def admin():
1721
  products = data.get('products', [])
1722
  categories = data.get('categories', [])
1723
  orders = data.get('orders', {})
 
1724
 
1725
  needs_save = False
1726
  for product in products:
@@ -1728,18 +1750,36 @@ def admin():
1728
  product['id'] = str(uuid.uuid4())
1729
  needs_save = True
1730
  if needs_save:
 
1731
  data['products'] = products
1732
  save_data(data)
1733
 
1734
-
1735
  if 'orders' not in data or not isinstance(data.get('orders'), dict):
1736
  data['orders'] = {}
1737
 
1738
  if request.method == 'POST':
1739
  action = request.form.get('action')
 
1740
 
1741
  try:
1742
- if action == 'update_order_status':
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1743
  order_id = request.form.get('order_id')
1744
  new_status = request.form.get('new_status')
1745
 
@@ -1748,9 +1788,12 @@ def admin():
1748
  valid_statuses = list(STATUS_MAP_RU.keys())
1749
 
1750
  if new_status in valid_statuses:
 
 
1751
  order['status'] = new_status
1752
  data['orders'][order_id] = order
1753
  save_data(data)
 
1754
  flash(f"Статус заказа №{order_id} успешно изменен на '{STATUS_MAP_RU.get(new_status)}'.", 'success')
1755
  else:
1756
  flash(f"Неверный статус: {new_status}", 'error')
@@ -1765,10 +1808,13 @@ def admin():
1765
  categories.append(category_name)
1766
  data['categories'] = categories
1767
  save_data(data)
 
1768
  flash(f"Категория '{category_name}' успешно добавлена.", 'success')
1769
  elif not category_name:
 
1770
  flash("Название категории не может быть пустым.", 'error')
1771
  else:
 
1772
  flash(f"Категория '{category_name}' уже существует.", 'error')
1773
 
1774
  elif action == 'delete_category':
@@ -1783,8 +1829,10 @@ def admin():
1783
  data['categories'] = categories
1784
  data['products'] = products
1785
  save_data(data)
 
1786
  flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
1787
  else:
 
1788
  flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
1789
 
1790
  elif action == 'add_product':
@@ -1794,7 +1842,7 @@ def admin():
1794
  category = request.form.get('category')
1795
  photos_files = request.files.getlist('photos')
1796
  colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1797
- models = [m.strip() for m in request.form.getlist('models') if m.strip()]
1798
  in_stock = 'in_stock' in request.form
1799
  is_top = 'is_top' in request.form
1800
 
@@ -1818,12 +1866,14 @@ def admin():
1818
  uploaded_count = 0
1819
  for photo in photos_files:
1820
  if uploaded_count >= photo_limit:
 
1821
  flash(f"Загружено только первые {photo_limit} фото.", "warning")
1822
  break
1823
  if photo and photo.filename:
1824
  try:
1825
  ext = os.path.splitext(photo.filename)[1].lower()
1826
  if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
 
1827
  flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
1828
  continue
1829
 
@@ -1831,6 +1881,7 @@ def admin():
1831
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1832
  temp_path = os.path.join(uploads_dir, photo_filename)
1833
  photo.save(temp_path)
 
1834
  api.upload_file(
1835
  path_or_fileobj=temp_path,
1836
  path_in_repo=f"photos/{photo_filename}",
@@ -1840,18 +1891,22 @@ def admin():
1840
  commit_message=f"Add photo for product {name}"
1841
  )
1842
  photos_list.append(photo_filename)
 
1843
  os.remove(temp_path)
1844
  uploaded_count += 1
1845
- except Exception:
 
1846
  flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
1847
  if os.path.exists(temp_path):
1848
  try: os.remove(temp_path)
1849
  except OSError: pass
 
 
1850
  try:
1851
  if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
1852
  os.rmdir(uploads_dir)
1853
- except OSError:
1854
- pass
1855
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
1856
  flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
1857
 
@@ -1860,19 +1915,14 @@ def admin():
1860
  'id': str(uuid.uuid4()),
1861
  'name': name, 'price': price, 'description': description,
1862
  'category': category if category in categories else 'Без категории',
1863
- 'photos': photos_list, 'colors': colors, 'models': models,
1864
  'in_stock': in_stock, 'is_top': is_top
1865
  }
1866
  products.append(new_product)
1867
  data['products'] = products
1868
  save_data(data)
 
1869
  flash(f"Товар '{name}' успешно добавлен.", 'success')
1870
-
1871
- first_photo_filename = photos_list[0] if photos_list else None
1872
- if bot_loop and bot_loop.is_running():
1873
- coro = send_new_product_notification(name, price, first_photo_filename)
1874
- asyncio.run_coroutine_threadsafe(coro, bot_loop)
1875
-
1876
 
1877
  elif action == 'edit_product':
1878
  product_id = request.form.get('product_id')
@@ -1888,6 +1938,7 @@ def admin():
1888
 
1889
  if product_index == -1:
1890
  flash(f"Ошибка редактирования: товар с ID '{product_id}' не найден.", 'error')
 
1891
  return redirect(url_for('admin'))
1892
 
1893
  product_to_edit = products[product_index]
@@ -1899,7 +1950,7 @@ def admin():
1899
  category = request.form.get('category')
1900
  product_to_edit['category'] = category if category in categories else 'Без категории'
1901
  product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1902
- product_to_edit['models'] = [m.strip() for m in request.form.getlist('models') if m.strip()]
1903
  product_to_edit['in_stock'] = 'in_stock' in request.form
1904
  product_to_edit['is_top'] = 'is_top' in request.form
1905
 
@@ -1908,6 +1959,7 @@ def admin():
1908
  if price < 0: price = 0
1909
  product_to_edit['price'] = price
1910
  except ValueError:
 
1911
  flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
1912
 
1913
  photos_files = request.files.getlist('photos')
@@ -1918,14 +1970,17 @@ def admin():
1918
  new_photos_list = []
1919
  photo_limit = 10
1920
  uploaded_count = 0
 
1921
  for photo in photos_files:
1922
  if uploaded_count >= photo_limit:
 
1923
  flash(f"Загружено только первые {photo_limit} фото.", "warning")
1924
  break
1925
  if photo and photo.filename:
1926
  try:
1927
  ext = os.path.splitext(photo.filename)[1].lower()
1928
  if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
 
1929
  flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
1930
  continue
1931
 
@@ -1933,13 +1988,16 @@ def admin():
1933
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1934
  temp_path = os.path.join(uploads_dir, photo_filename)
1935
  photo.save(temp_path)
 
1936
  api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}",
1937
  repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
1938
  commit_message=f"Update photo for product {product_to_edit['name']}")
1939
  new_photos_list.append(photo_filename)
 
1940
  os.remove(temp_path)
1941
  uploaded_count += 1
1942
- except Exception:
 
1943
  flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error')
1944
  if os.path.exists(temp_path):
1945
  try: os.remove(temp_path)
@@ -1947,12 +2005,14 @@ def admin():
1947
  try:
1948
  if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
1949
  os.rmdir(uploads_dir)
1950
- except OSError:
1951
- pass
1952
 
1953
  if new_photos_list:
 
1954
  old_photos = product_to_edit.get('photos', [])
1955
  if old_photos:
 
1956
  try:
1957
  api.delete_files(
1958
  repo_id=REPO_ID,
@@ -1961,7 +2021,9 @@ def admin():
1961
  token=HF_TOKEN_WRITE,
1962
  commit_message=f"Delete old photos for product {product_to_edit['name']}"
1963
  )
1964
- except Exception:
 
 
1965
  flash("Не удалось удалить старые фотографии с сервера. Новые фото загружены.", "warning")
1966
  product_to_edit['photos'] = new_photos_list
1967
  flash("Фотографии товара успешно обновлены.", "success")
@@ -1973,6 +2035,7 @@ def admin():
1973
  products[product_index] = product_to_edit
1974
  data['products'] = products
1975
  save_data(data)
 
1976
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
1977
 
1978
  elif action == 'delete_product':
@@ -1993,6 +2056,7 @@ def admin():
1993
 
1994
  photos_to_delete = product_to_delete.get('photos', [])
1995
  if photos_to_delete and HF_TOKEN_WRITE:
 
1996
  try:
1997
  api = HfApi()
1998
  api.delete_files(
@@ -2002,23 +2066,30 @@ def admin():
2002
  token=HF_TOKEN_WRITE,
2003
  commit_message=f"Delete photos for deleted product {product_name}"
2004
  )
2005
- except Exception:
 
 
2006
  flash(f"Не удалось удалить фото для товара '{product_name}' с сервера. Товар удален локально.", "warning")
2007
  elif photos_to_delete and not HF_TOKEN_WRITE:
 
2008
  flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
2009
 
2010
  data['products'] = products
2011
  save_data(data)
 
2012
  flash(f"Товар '{product_name}' удален.", 'success')
2013
  else:
2014
  flash(f"Ошибка удаления: неверный ID товара '{product_id}'.", 'error')
 
2015
 
2016
  else:
 
2017
  flash(f"Неизвестное действие: {action}", 'warning')
2018
 
2019
  return redirect(url_for('admin'))
2020
 
2021
- except Exception:
 
2022
  flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
2023
  return redirect(url_for('admin'))
2024
 
@@ -2027,6 +2098,7 @@ def admin():
2027
  display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
2028
  display_categories = sorted(current_data.get('categories', []))
2029
  display_orders = list(current_data.get('orders', {}).values())
 
2030
 
2031
  return render_template_string(
2032
  ADMIN_TEMPLATE,
@@ -2037,19 +2109,30 @@ def admin():
2037
  repo_id=REPO_ID,
2038
  store_address=STORE_ADDRESS,
2039
  currency_code=CURRENCY_CODE,
2040
- currency_name=CURRENCY_NAME
 
2041
  )
2042
 
2043
  if __name__ == '__main__':
 
2044
  download_db_from_hf()
2045
  load_data()
 
2046
 
2047
  if HF_TOKEN_WRITE:
2048
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
2049
  backup_thread.start()
 
 
 
2050
 
2051
- bot_thread = threading.Thread(target=run_bot, daemon=True)
2052
- bot_thread.start()
 
 
 
 
2053
 
2054
  port = int(os.environ.get('PORT', 7860))
 
2055
  app.run(debug=False, host='0.0.0.0', port=port)
 
5
  import logging
6
  import threading
7
  import time
8
+ import asyncio
9
  from datetime import datetime
10
  from huggingface_hub import HfApi, hf_hub_download
11
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
 
13
  from dotenv import load_dotenv
14
  import requests
15
  import uuid
16
+ from aiogram import Bot, Dispatcher, types, F
 
 
 
17
  from aiogram.filters import CommandStart
18
+ from aiogram.types import Message
 
 
19
 
20
  load_dotenv()
21
 
 
28
  REPO_ID = "Kgshop/aiocult"
29
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
30
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
31
+ TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
 
 
32
 
33
  STORE_ADDRESS = " Алматы, «Байсат» 1 этаж, синий сектор 117-118 Бутик "
34
 
 
47
 
48
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
49
 
50
+ data_lock = threading.Lock()
51
+
52
+ bot = None
53
+ dp = None
54
+
55
+ if TELEGRAM_BOT_TOKEN:
56
+ bot = Bot(token=TELEGRAM_BOT_TOKEN)
57
+ dp = Dispatcher()
58
+
59
+ @dp.message(CommandStart())
60
+ async def command_start_handler(message: Message):
61
+ user_id = message.from_user.id
62
+ username = message.from_user.username
63
+ first_name = message.from_user.first_name
64
+
65
+ with data_lock:
66
+ data = load_data_internal()
67
+ if 'telegram_users' not in data:
68
+ data['telegram_users'] = {}
69
+
70
+ str_id = str(user_id)
71
+ if str_id not in data['telegram_users']:
72
+ data['telegram_users'][str_id] = {
73
+ 'username': username,
74
+ 'first_name': first_name,
75
+ 'joined_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
76
+ }
77
+ save_data_internal(data)
78
+ logging.info(f"New Telegram user registered: {user_id}")
79
+
80
+ await message.answer(f"Добро пожаловать в Мобильный мир, {first_name}! Здесь вы будете получать обновления о новинках.")
81
 
82
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
83
+ if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
84
+ logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
85
+
86
  token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
87
 
88
  files_to_download = [specific_file] if specific_file else SYNC_FILES
89
+ logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
90
  all_successful = True
91
 
92
  for file_name in files_to_download:
93
  success = False
94
  for attempt in range(retries + 1):
95
  try:
96
+ logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...")
97
+ local_path = hf_hub_download(
98
  repo_id=REPO_ID,
99
  filename=file_name,
100
  repo_type="dataset",
 
104
  force_download=True,
105
  resume_download=False
106
  )
107
+ logging.info(f"Successfully downloaded {file_name} to {local_path}.")
108
  success = True
109
  break
110
  except RepositoryNotFoundError:
111
+ logging.error(f"Repository {REPO_ID} not found. Download cancelled for all files.")
112
  return False
113
  except HfHubHTTPError as e:
114
  if e.response.status_code == 404:
115
+ logging.warning(f"File {file_name} not found in repo {REPO_ID} (404). Skipping this file.")
116
  if attempt == 0 and not os.path.exists(file_name):
117
  try:
118
  if file_name == DATA_FILE:
119
  with open(file_name, 'w', encoding='utf-8') as f:
120
+ json.dump({'products': [], 'categories': [], 'orders': {}, 'telegram_users': {}}, f)
121
+ logging.info(f"Created empty local file {file_name} because it was not found on HF.")
122
+ except Exception as create_e:
123
+ logging.error(f"Failed to create empty local file {file_name}: {create_e}")
124
  success = False
125
  break
126
+ else:
127
+ logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
128
+ except requests.exceptions.RequestException as e:
129
+ logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
130
+ except Exception as e:
131
+ logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
132
 
133
  if attempt < retries:
134
  time.sleep(delay)
135
 
136
  if not success:
137
+ logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
138
  all_successful = False
139
 
140
+ logging.info(f"Download process finished. Overall success: {all_successful}")
141
  return all_successful
142
 
143
  def upload_db_to_hf(specific_file=None):
144
  if not HF_TOKEN_WRITE:
145
+ logging.warning("HF_TOKEN (for writing) not set. Skipping upload to Hugging Face.")
146
  return
147
  try:
148
  api = HfApi()
149
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
150
+ logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...")
151
 
152
  for file_name in files_to_upload:
153
  if os.path.exists(file_name):
 
160
  token=HF_TOKEN_WRITE,
161
  commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
162
  )
163
+ logging.info(f"File {file_name} successfully uploaded to Hugging Face.")
164
+ except Exception as e:
165
+ logging.error(f"Error uploading file {file_name} to Hugging Face: {e}")
166
+ else:
167
+ logging.warning(f"File {file_name} not found locally, skipping upload.")
168
+ logging.info("Finished uploading files to HF.")
169
+ except Exception as e:
170
+ logging.error(f"General error during Hugging Face upload initialization or process: {e}", exc_info=True)
171
 
172
  def periodic_backup():
173
  backup_interval = 1800
174
+ logging.info(f"Setting up periodic backup every {backup_interval} seconds.")
175
  while True:
176
  time.sleep(backup_interval)
177
+ logging.info("Starting periodic backup...")
178
  upload_db_to_hf()
179
+ logging.info("Periodic backup finished.")
180
 
181
+ def load_data_internal():
182
+ default_data = {'products': [], 'categories': [], 'orders': {}, 'telegram_users': {}}
183
  try:
184
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
185
  data = json.load(file)
 
188
  if 'products' not in data: data['products'] = []
189
  if 'categories' not in data: data['categories'] = []
190
  if 'orders' not in data: data['orders'] = {}
191
+ if 'telegram_users' not in data: data['telegram_users'] = {}
192
  return data
193
  except (FileNotFoundError, json.JSONDecodeError):
194
+ return default_data
195
 
196
+ def load_data():
197
+ with data_lock:
198
+ default_data = {'products': [], 'categories': [], 'orders': {}, 'telegram_users': {}}
199
  try:
200
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
201
  data = json.load(file)
202
+ logging.info(f"Local data loaded successfully from {DATA_FILE}")
203
  if not isinstance(data, dict):
204
+ logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.")
205
+ raise FileNotFoundError
206
  if 'products' not in data: data['products'] = []
207
  if 'categories' not in data: data['categories'] = []
208
  if 'orders' not in data: data['orders'] = {}
209
+ if 'telegram_users' not in data: data['telegram_users'] = {}
210
  return data
211
+ except FileNotFoundError:
212
+ logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
213
+ except json.JSONDecodeError:
214
+ logging.error(f"Error decoding JSON in local {DATA_FILE}. File might be corrupt. Attempting download.")
215
+
216
+ if download_db_from_hf(specific_file=DATA_FILE):
217
  try:
218
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
219
+ data = json.load(file)
220
+ logging.info(f"Data loaded successfully from {DATA_FILE} after download.")
221
+ if not isinstance(data, dict):
222
+ return default_data
223
+ if 'products' not in data: data['products'] = []
224
+ if 'categories' not in data: data['categories'] = []
225
+ if 'orders' not in data: data['orders'] = {}
226
+ if 'telegram_users' not in data: data['telegram_users'] = {}
227
+ return data
228
+ except Exception as e:
229
+ logging.error(f"Unknown error loading downloaded {DATA_FILE}: {e}. Using default.", exc_info=True)
230
+ return default_data
231
+ else:
232
+ logging.error(f"Failed to download {DATA_FILE} from HF after retries. Using empty default data structure.")
233
+ if not os.path.exists(DATA_FILE):
234
+ try:
235
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
236
+ json.dump(default_data, f)
237
+ logging.info(f"Created empty local file {DATA_FILE} after failed download.")
238
+ except Exception as create_e:
239
+ logging.error(f"Failed to create empty local file {DATA_FILE}: {create_e}")
240
+ return default_data
241
 
242
+ def save_data_internal(data):
243
  try:
 
 
244
  if 'products' not in data: data['products'] = []
245
  if 'categories' not in data: data['categories'] = []
246
  if 'orders' not in data: data['orders'] = {}
247
+ if 'telegram_users' not in data: data['telegram_users'] = {}
248
 
249
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
250
  json.dump(data, file, ensure_ascii=False, indent=4)
251
  upload_db_to_hf(specific_file=DATA_FILE)
252
+ except Exception as e:
253
+ logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
 
 
 
 
 
 
 
 
 
 
 
254
 
255
+ def save_data(data):
256
+ with data_lock:
257
+ save_data_internal(data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
+ def start_bot_polling():
260
+ if bot and dp:
261
+ logging.info("Starting Aiogram bot polling...")
262
+ asyncio.run(dp.start_polling(bot))
263
 
264
+ async def broadcast_message(user_ids, text):
265
+ count = 0
266
+ for user_id in user_ids:
267
  try:
268
+ await bot.send_message(chat_id=user_id, text=text)
269
+ count += 1
270
+ await asyncio.sleep(0.05)
271
  except Exception as e:
272
+ logging.error(f"Failed to send message to {user_id}: {e}")
273
+ return count
 
 
 
 
 
 
 
 
 
 
274
 
275
  CATALOG_TEMPLATE = '''
276
  <!DOCTYPE html>
 
282
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
283
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
284
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
 
285
  <style>
286
  :root {
287
+ --primary-color: #007AFF;
288
  --primary-dark: #0056b3;
289
  --surface-color: #FFFFFF;
290
  --surface-dark: #21252B;
 
309
  .theme-toggle { background: none; border: none; font-size: 1.3rem; cursor: pointer; color: var(--primary-color); padding: 5px; }
310
  .theme-toggle:hover { opacity: 0.8; }
311
 
 
 
 
 
312
  .store-address { padding: 12px 15px; text-align: center; background-color: var(--surface-color); border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); font-size: 0.9rem; color: #555; margin: 15px; }
313
  body.dark-mode .store-address { background-color: var(--surface-dark); color: var(--text-color-dark); box-shadow: 0 1px 3px rgba(255,255,255,0.1); }
314
 
 
336
  .product h2 { font-size: 0.95rem; font-weight: 500; margin: 0 0 5px 0; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-color); }
337
  body.dark-mode .product h2 { color: var(--text-color-dark); }
338
  .product-price { font-size: 1.1rem; color: var(--primary-color); font-weight: 700; text-align: center; margin: 5px 0; }
339
+ .product-description { display: none; }
340
  .product-actions { padding: 0 10px 10px 10px; display: flex; flex-direction: column; gap: 6px; }
341
 
342
  .product-button { display: block; width: 100%; padding: 8px; border: none; border-radius: 8px; background-color: var(--primary-color); color: white; font-size: 0.85rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; text-align: center; text-decoration: none; }
 
394
  <div class="logo-title-container" style="display: flex; align-items: center; gap: 10px;">
395
  <img src="https://huggingface.co/spaces/Kgshop/mobilnymir/resolve/main/IMG_20251019_120056_121.jpg" alt="Мобильный мир Logo">
396
  <h1>Мобильный мир</h1>
 
 
 
 
397
  </div>
398
  <button class="theme-toggle" onclick="toggleTheme()" aria-label="Переключить тему">
399
  <i class="fas fa-moon"></i>
 
651
  return;
652
  }
653
 
654
+ const cartItemId = `${product.name}-${color}-${model}`;
655
  const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
656
 
657
  if (existingItemIndex > -1) {
 
664
  photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
665
  quantity: quantity,
666
  color: color,
667
+ model: model
668
  });
669
  }
670
 
 
711
 
712
  let variantText = [];
713
  if (item.color !== 'N/A') variantText.push(`Цвет: ${item.color}`);
714
+ if (item.model !== 'N/A') variantText.push(`Модель: ${item.model}`);
715
 
716
  const variantDisplay = variantText.length > 0 ? ` (${variantText.join(', ')})` : '';
717
 
 
808
  if (existingNoResults) existingNoResults.remove();
809
 
810
  const allProductElements = document.querySelectorAll('.products-grid .product');
811
+ if (allProductElements.length === 0 && products.length > 0) return;
812
 
813
  document.querySelectorAll('.products-grid .product').forEach(productElement => {
814
  const name = productElement.getAttribute('data-name');
 
886
  notification.addEventListener('transitionend', () => notification.remove());
887
  }, duration);
888
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
889
 
890
  document.addEventListener('DOMContentLoaded', () => {
891
  applyInitialTheme();
892
  updateCartButton();
893
  setupFilters();
 
894
 
895
  window.addEventListener('click', function(event) {
896
  if (event.target.classList.contains('modal')) {
 
1094
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
1095
  <style>
1096
  :root {
1097
+ --primary-color: #007AFF;
1098
  --primary-dark: #0056b3;
1099
  --surface-color: #FFFFFF;
1100
  --surface-dark: #21252B;
 
1168
  .status-indicator.out-of-stock { background-color: #fed7d7; color: #c53030; }
1169
  .status-indicator.top-product { background-color: #fff3c4; color: #9c4221; margin-left: 5px;}
1170
 
1171
+ .status-indicator.new { background-color: #ffe0b2; color: #e65100; }
1172
+ .status-indicator.accepted { background-color: #b3e5fc; color: #0277bd; }
1173
+ .status-indicator.prepared { background-color: #c6f6d5; color: #2f855a; }
1174
+ .status-indicator.shipped { background-color: #a7f3d0; color: #065f46; }
1175
 
1176
  details { background-color: var(--surface-color); border: 1px solid var(--border-color); border-radius: 8px; margin-bottom: 10px; }
1177
  body.dark-mode details { background-color: var(--surface-dark); border-color: var(--border-color-dark); }
 
1223
  {% endfor %}
1224
  {% endif %}
1225
  {% endwith %}
1226
+
1227
+ <div class="section">
1228
+ <h2><i class="fab fa-telegram-plane"></i> Рассылка Telegram</h2>
1229
+ <details>
1230
+ <summary>Отправить сообщение всем пользователям бота</summary>
1231
+ <div class="form-content" style="padding: 15px;">
1232
+ <form method="POST">
1233
+ <input type="hidden" name="action" value="send_telegram_broadcast">
1234
+ <label for="broadcast_message">Текст сообщения:</label>
1235
+ <textarea id="broadcast_message" name="message" rows="4" required></textarea>
1236
+ <p style="font-size: 0.85rem; color: #666; margin-top: 5px;">
1237
+ Сообщение будет отправлено {{ telegram_users_count }} пользователям.
1238
+ </p>
1239
+ <button type="submit" class="button"><i class="fas fa-paper-plane"></i> Отправить</button>
1240
+ </form>
1241
+ </div>
1242
+ </details>
1243
+ </div>
1244
 
1245
  <div class="section">
1246
  <h2><i class="fas fa-history"></i> История заказов</h2>
 
1572
  if (container) {
1573
  const existingInputs = container.querySelectorAll('input[name="' + fieldName + '"]');
1574
  if (existingInputs.length === 1 && existingInputs[0].value.trim() === '') {
1575
+ existingInputs[0].value = '';
1576
  }
1577
 
1578
  const newInputGroup = document.createElement('div');
 
1642
  try:
1643
  product = products_sorted[index]
1644
  except IndexError:
1645
+ logging.warning(f"Attempted access to non-existent or out-of-stock product with index {index}")
1646
  return "Товар не найден или отсутствует в наличии.", 404
1647
 
1648
  products_index = index
 
1660
  order_data = request.get_json()
1661
 
1662
  if not order_data or 'cart' not in order_data or not order_data['cart']:
1663
+ logging.warning("Create order request missing cart data or cart is empty.")
1664
  return jsonify({"error": "Корзина пуста или не передана."}), 400
1665
 
1666
  cart_items = order_data['cart']
 
1669
  processed_cart = []
1670
  for item in cart_items:
1671
  if not all(k in item for k in ('name', 'price', 'quantity')):
1672
+ logging.error(f"Invalid cart item structure received: {item}")
1673
  return jsonify({"error": "Неверный формат товара в корзине."}), 400
1674
  try:
1675
  price = float(item['price'])
 
1691
  "photo_url": photo_url
1692
  })
1693
  except (ValueError, TypeError) as e:
1694
+ logging.error(f"Invalid price/quantity in cart item: {item}. Error: {e}")
1695
  return jsonify({"error": "Неверная цена или количество в товаре."}), 400
1696
 
1697
  order_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}"
 
1713
 
1714
  data['orders'][order_id] = new_order
1715
  save_data(data)
1716
+ logging.info(f"Order {order_id} created successfully (anonymously).")
1717
  return jsonify({"order_id": order_id}), 201
1718
 
1719
  except Exception as e:
1720
+ logging.error(f"Failed to save order {order_id}: {e}", exc_info=True)
1721
  return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500
1722
 
1723
  @app.route('/order/<order_id>')
 
1725
  data = load_data()
1726
  order = data.get('orders', {}).get(order_id)
1727
 
1728
+ if order:
1729
+ logging.info(f"Displaying order {order_id}")
1730
+ else:
1731
+ logging.warning(f"Order {order_id} not found.")
1732
+
1733
  return render_template_string(ORDER_TEMPLATE,
1734
  order=order,
1735
  status_map_ru=STATUS_MAP_RU,
 
1742
  products = data.get('products', [])
1743
  categories = data.get('categories', [])
1744
  orders = data.get('orders', {})
1745
+ telegram_users = data.get('telegram_users', {})
1746
 
1747
  needs_save = False
1748
  for product in products:
 
1750
  product['id'] = str(uuid.uuid4())
1751
  needs_save = True
1752
  if needs_save:
1753
+ logging.info("Assigning unique IDs to products that were missing them.")
1754
  data['products'] = products
1755
  save_data(data)
1756
 
 
1757
  if 'orders' not in data or not isinstance(data.get('orders'), dict):
1758
  data['orders'] = {}
1759
 
1760
  if request.method == 'POST':
1761
  action = request.form.get('action')
1762
+ logging.info(f"Admin action received: {action}")
1763
 
1764
  try:
1765
+ if action == 'send_telegram_broadcast':
1766
+ message_text = request.form.get('message', '').strip()
1767
+ if message_text:
1768
+ if telegram_users:
1769
+ user_ids = list(telegram_users.keys())
1770
+
1771
+ loop = asyncio.new_event_loop()
1772
+ asyncio.set_event_loop(loop)
1773
+ sent_count = loop.run_until_complete(broadcast_message(user_ids, message_text))
1774
+ loop.close()
1775
+
1776
+ flash(f"Сообщение отправлено {sent_count} из {len(user_ids)} пользователей.", 'success')
1777
+ else:
1778
+ flash("Нет зарегистрированных пользователей Telegram для рассылки.", 'warning')
1779
+ else:
1780
+ flash("Текст сообщения не может быть пустым.", 'error')
1781
+
1782
+ elif action == 'update_order_status':
1783
  order_id = request.form.get('order_id')
1784
  new_status = request.form.get('new_status')
1785
 
 
1788
  valid_statuses = list(STATUS_MAP_RU.keys())
1789
 
1790
  if new_status in valid_statuses:
1791
+ old_status_key = order.get('status', 'new')
1792
+
1793
  order['status'] = new_status
1794
  data['orders'][order_id] = order
1795
  save_data(data)
1796
+ logging.info(f"Order {order_id} status updated from {old_status_key} to {new_status}")
1797
  flash(f"Статус заказа №{order_id} успешно изменен на '{STATUS_MAP_RU.get(new_status)}'.", 'success')
1798
  else:
1799
  flash(f"Неверный статус: {new_status}", 'error')
 
1808
  categories.append(category_name)
1809
  data['categories'] = categories
1810
  save_data(data)
1811
+ logging.info(f"Category '{category_name}' added.")
1812
  flash(f"Категория '{category_name}' успешно добавлена.", 'success')
1813
  elif not category_name:
1814
+ logging.warning("Attempted to add empty category.")
1815
  flash("Название категории не может быть пустым.", 'error')
1816
  else:
1817
+ logging.warning(f"Category '{category_name}' already exists.")
1818
  flash(f"Категория '{category_name}' уже существует.", 'error')
1819
 
1820
  elif action == 'delete_category':
 
1829
  data['categories'] = categories
1830
  data['products'] = products
1831
  save_data(data)
1832
+ logging.info(f"Category '{category_to_delete}' deleted. Updated products: {updated_count}.")
1833
  flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
1834
  else:
1835
+ logging.warning(f"Attempted to delete non-existent or empty category: {category_to_delete}")
1836
  flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
1837
 
1838
  elif action == 'add_product':
 
1842
  category = request.form.get('category')
1843
  photos_files = request.files.getlist('photos')
1844
  colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1845
+ models = [m.strip() for m in request.form.getlist('models') if m.strip()]
1846
  in_stock = 'in_stock' in request.form
1847
  is_top = 'is_top' in request.form
1848
 
 
1866
  uploaded_count = 0
1867
  for photo in photos_files:
1868
  if uploaded_count >= photo_limit:
1869
+ logging.warning(f"Photo limit ({photo_limit}) reached, ignoring remaining photos.")
1870
  flash(f"Загружено только первые {photo_limit} фото.", "warning")
1871
  break
1872
  if photo and photo.filename:
1873
  try:
1874
  ext = os.path.splitext(photo.filename)[1].lower()
1875
  if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
1876
+ logging.warning(f"Skipping non-image file upload: {photo.filename}")
1877
  flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
1878
  continue
1879
 
 
1881
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1882
  temp_path = os.path.join(uploads_dir, photo_filename)
1883
  photo.save(temp_path)
1884
+ logging.info(f"Uploading photo {photo_filename} to HF for product {name}...")
1885
  api.upload_file(
1886
  path_or_fileobj=temp_path,
1887
  path_in_repo=f"photos/{photo_filename}",
 
1891
  commit_message=f"Add photo for product {name}"
1892
  )
1893
  photos_list.append(photo_filename)
1894
+ logging.info(f"Photo {photo_filename} uploaded successfully.")
1895
  os.remove(temp_path)
1896
  uploaded_count += 1
1897
+ except Exception as e:
1898
+ logging.error(f"Error uploading photo {photo.filename} to HF: {e}", exc_info=True)
1899
  flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
1900
  if os.path.exists(temp_path):
1901
  try: os.remove(temp_path)
1902
  except OSError: pass
1903
+ elif photo and not photo.filename:
1904
+ logging.warning("Received an empty photo file object when adding product.")
1905
  try:
1906
  if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
1907
  os.rmdir(uploads_dir)
1908
+ except OSError as e:
1909
+ logging.warning(f"Could not remove temporary upload directory {uploads_dir}: {e}")
1910
  elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
1911
  flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
1912
 
 
1915
  'id': str(uuid.uuid4()),
1916
  'name': name, 'price': price, 'description': description,
1917
  'category': category if category in categories else 'Без категории',
1918
+ 'photos': photos_list, 'colors': colors, 'models': models,
1919
  'in_stock': in_stock, 'is_top': is_top
1920
  }
1921
  products.append(new_product)
1922
  data['products'] = products
1923
  save_data(data)
1924
+ logging.info(f"Product '{name}' added.")
1925
  flash(f"Товар '{name}' успешно добавлен.", 'success')
 
 
 
 
 
 
1926
 
1927
  elif action == 'edit_product':
1928
  product_id = request.form.get('product_id')
 
1938
 
1939
  if product_index == -1:
1940
  flash(f"Ошибка редактирования: товар с ID '{product_id}' не найден.", 'error')
1941
+ logging.error(f"Invalid product ID '{product_id}' for editing.")
1942
  return redirect(url_for('admin'))
1943
 
1944
  product_to_edit = products[product_index]
 
1950
  category = request.form.get('category')
1951
  product_to_edit['category'] = category if category in categories else 'Без категории'
1952
  product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1953
+ product_to_edit['models'] = [m.strip() for m in request.form.getlist('models') if m.strip()]
1954
  product_to_edit['in_stock'] = 'in_stock' in request.form
1955
  product_to_edit['is_top'] = 'is_top' in request.form
1956
 
 
1959
  if price < 0: price = 0
1960
  product_to_edit['price'] = price
1961
  except ValueError:
1962
+ logging.warning(f"Invalid price format '{price_str}' during edit of product {original_name}. Price not changed.")
1963
  flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
1964
 
1965
  photos_files = request.files.getlist('photos')
 
1970
  new_photos_list = []
1971
  photo_limit = 10
1972
  uploaded_count = 0
1973
+ logging.info(f"Uploading new photos for product {product_to_edit['name']}...")
1974
  for photo in photos_files:
1975
  if uploaded_count >= photo_limit:
1976
+ logging.warning(f"Photo limit ({photo_limit}) reached, ignoring remaining photos.")
1977
  flash(f"Загружено только первые {photo_limit} фото.", "warning")
1978
  break
1979
  if photo and photo.filename:
1980
  try:
1981
  ext = os.path.splitext(photo.filename)[1].lower()
1982
  if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
1983
+ logging.warning(f"Skipping non-image file upload during edit: {photo.filename}")
1984
  flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
1985
  continue
1986
 
 
1988
  photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1989
  temp_path = os.path.join(uploads_dir, photo_filename)
1990
  photo.save(temp_path)
1991
+ logging.info(f"Uploading new photo {photo_filename} to HF...")
1992
  api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}",
1993
  repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
1994
  commit_message=f"Update photo for product {product_to_edit['name']}")
1995
  new_photos_list.append(photo_filename)
1996
+ logging.info(f"New photo {photo_filename} uploaded successfully.")
1997
  os.remove(temp_path)
1998
  uploaded_count += 1
1999
+ except Exception as e:
2000
+ logging.error(f"Error uploading new photo {photo.filename}: {e}", exc_info=True)
2001
  flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error')
2002
  if os.path.exists(temp_path):
2003
  try: os.remove(temp_path)
 
2005
  try:
2006
  if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
2007
  os.rmdir(uploads_dir)
2008
+ except OSError as e:
2009
+ logging.warning(f"Could not remove temporary upload directory {uploads_dir}: {e}")
2010
 
2011
  if new_photos_list:
2012
+ logging.info(f"New photo list for product {product_to_edit['name']} generated.")
2013
  old_photos = product_to_edit.get('photos', [])
2014
  if old_photos:
2015
+ logging.info(f"Attempting to delete old photos: {old_photos}")
2016
  try:
2017
  api.delete_files(
2018
  repo_id=REPO_ID,
 
2021
  token=HF_TOKEN_WRITE,
2022
  commit_message=f"Delete old photos for product {product_to_edit['name']}"
2023
  )
2024
+ logging.info(f"Old photos for product {product_to_edit['name']} deleted from HF.")
2025
+ except Exception as e:
2026
+ logging.error(f"Error deleting old photos {old_photos} from HF: {e}", exc_info=True)
2027
  flash("Не удалось удалить старые фотографии с сервера. Новые фото загружены.", "warning")
2028
  product_to_edit['photos'] = new_photos_list
2029
  flash("Фотографии товара успешно обновлены.", "success")
 
2035
  products[product_index] = product_to_edit
2036
  data['products'] = products
2037
  save_data(data)
2038
+ logging.info(f"Product '{original_name}' (ID {product_id}) updated to '{product_to_edit['name']}'.")
2039
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
2040
 
2041
  elif action == 'delete_product':
 
2056
 
2057
  photos_to_delete = product_to_delete.get('photos', [])
2058
  if photos_to_delete and HF_TOKEN_WRITE:
2059
+ logging.info(f"Attempting to delete photos for product '{product_name}' from HF: {photos_to_delete}")
2060
  try:
2061
  api = HfApi()
2062
  api.delete_files(
 
2066
  token=HF_TOKEN_WRITE,
2067
  commit_message=f"Delete photos for deleted product {product_name}"
2068
  )
2069
+ logging.info(f"Photos for product '{product_name}' deleted from HF.")
2070
+ except Exception as e:
2071
+ logging.error(f"Error deleting photos {photos_to_delete} for product '{product_name}' from HF: {e}", exc_info=True)
2072
  flash(f"Не удалось удалить фото для товара '{product_name}' с сервера. Товар удален локально.", "warning")
2073
  elif photos_to_delete and not HF_TOKEN_WRITE:
2074
+ logging.warning(f"HF_TOKEN (write) not set. Cannot delete photos {photos_to_delete} for deleted product '{product_name}'.")
2075
  flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
2076
 
2077
  data['products'] = products
2078
  save_data(data)
2079
+ logging.info(f"Product '{product_name}' (ID {product_id}) deleted.")
2080
  flash(f"Товар '{product_name}' удален.", 'success')
2081
  else:
2082
  flash(f"Ошибка удаления: неверный ID товара '{product_id}'.", 'error')
2083
+ logging.error(f"Invalid product ID '{product_id}' for deletion.")
2084
 
2085
  else:
2086
+ logging.warning(f"Received unknown admin action: {action}")
2087
  flash(f"Неизвестное действие: {action}", 'warning')
2088
 
2089
  return redirect(url_for('admin'))
2090
 
2091
+ except Exception as e:
2092
+ logging.error(f"Error processing admin action '{action}': {e}", exc_info=True)
2093
  flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
2094
  return redirect(url_for('admin'))
2095
 
 
2098
  display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
2099
  display_categories = sorted(current_data.get('categories', []))
2100
  display_orders = list(current_data.get('orders', {}).values())
2101
+ telegram_users_count = len(current_data.get('telegram_users', {}))
2102
 
2103
  return render_template_string(
2104
  ADMIN_TEMPLATE,
 
2109
  repo_id=REPO_ID,
2110
  store_address=STORE_ADDRESS,
2111
  currency_code=CURRENCY_CODE,
2112
+ currency_name=CURRENCY_NAME,
2113
+ telegram_users_count=telegram_users_count
2114
  )
2115
 
2116
  if __name__ == '__main__':
2117
+ logging.info("Application starting up. Performing initial data load/download...")
2118
  download_db_from_hf()
2119
  load_data()
2120
+ logging.info("Initial data load complete.")
2121
 
2122
  if HF_TOKEN_WRITE:
2123
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
2124
  backup_thread.start()
2125
+ logging.info("Periodic backup thread started.")
2126
+ else:
2127
+ logging.warning("Periodic backup will NOT run (HF_TOKEN for writing not set).")
2128
 
2129
+ if TELEGRAM_BOT_TOKEN:
2130
+ bot_thread = threading.Thread(target=start_bot_polling, daemon=True)
2131
+ bot_thread.start()
2132
+ logging.info("Telegram bot polling thread started.")
2133
+ else:
2134
+ logging.warning("TELEGRAM_BOT_TOKEN not set. Telegram bot features disabled.")
2135
 
2136
  port = int(os.environ.get('PORT', 7860))
2137
+ logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
2138
  app.run(debug=False, host='0.0.0.0', port=port)