Shveiauto commited on
Commit
4c695d7
·
verified ·
1 Parent(s): f1118b2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +383 -393
app.py CHANGED
@@ -8,14 +8,16 @@ import threading
8
  import time
9
  from datetime import datetime
10
  from huggingface_hub import HfApi, hf_hub_download
11
- from huggingface_hub.utils import RepositoryNotFoundError
12
  from werkzeug.utils import secure_filename
13
  from dotenv import load_dotenv
 
14
 
15
  load_dotenv()
16
 
17
  app = Flask(__name__)
18
- app.secret_key = 'your_unique_secret_key_soola_cosmetics_67890'
 
19
  DATA_FILE = 'data_soola.json'
20
  USERS_FILE = 'users_soola.json'
21
 
@@ -26,118 +28,147 @@ HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
26
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
27
 
28
  STORE_ADDRESS = "Рынок Дордой, Джунхай, терминал, 38"
29
-
30
  CURRENCY_CODE = 'KGS'
31
  CURRENCY_NAME = 'Кыргызский сом (с)'
32
 
33
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  def load_data():
 
 
 
 
 
 
 
 
 
 
36
  try:
37
- download_db_from_hf()
38
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
39
  data = json.load(file)
40
- logging.info(f"Данные успешно загружены из {DATA_FILE}")
41
- if not isinstance(data, dict):
42
- logging.warning(f"{DATA_FILE} не является словарем. Инициализация пустой структурой.")
43
- return {'products': [], 'categories': []}
44
- if 'products' not in data:
45
- data['products'] = []
46
- if 'categories' not in data:
47
- data['categories'] = []
48
- return data
49
- except FileNotFoundError:
50
- logging.warning(f"Локальный файл {DATA_FILE} не найден. Попытка скачать с HF.")
51
- try:
52
- if not os.path.exists(DATA_FILE):
53
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'products': [], 'categories': []}, f)
54
- logging.info(f"Создан пустой файл {DATA_FILE}")
55
- return {'products': [], 'categories': []}
56
- else:
57
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
58
- data = json.load(file)
59
- logging.info(f"Данные успешно загружены из {DATA_FILE} после попытки скачивания.")
60
- if not isinstance(data, dict): return {'products': [], 'categories': []}
61
- if 'products' not in data: data['products'] = []
62
- if 'categories' not in data: data['categories'] = []
63
- return data
64
- except (FileNotFoundError, RepositoryNotFoundError) as e:
65
- logging.warning(f"Файл {DATA_FILE} не найден локально и ошибка при доступе к репозиторию HF ({e}). Создание пустой структуры.")
66
- if not os.path.exists(DATA_FILE):
67
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'products': [], 'categories': []}, f)
68
- return {'products': [], 'categories': []}
69
- except json.JSONDecodeError:
70
- logging.error(f"Ошибка декодирования JSON в {DATA_FILE} после попытки скачивания.")
71
- return {'products': [], 'categories': []}
72
- except Exception as e:
73
- logging.error(f"Неизвестная ошибка при загрузке данных после попытки скачивания: {e}")
74
- return {'products': [], 'categories': []}
75
  except json.JSONDecodeError:
76
- logging.error(f"Ошибка декодирования JSON в локальном {DATA_FILE}. Файл может быть поврежден. Возврат пустой структуры.")
77
- return {'products': [], 'categories': []}
 
 
78
  except Exception as e:
79
- logging.error(f"Неизвестная ошибка при загрузке данных ({DATA_FILE}): {e}", exc_info=True)
80
- return {'products': [], 'categories': []}
81
 
82
-
83
- def save_data(data):
84
  try:
85
- with open(DATA_FILE, 'w', encoding='utf-8') as file:
86
  json.dump(data, file, ensure_ascii=False, indent=4)
87
- logging.info(f"Данные успешно сохранены в {DATA_FILE}")
88
- upload_db_to_hf(specific_file=DATA_FILE)
 
 
89
  except Exception as e:
90
- logging.error(f"Ошибка при сохранении данных в {DATA_FILE}: {e}", exc_info=True)
 
 
 
 
 
 
 
 
 
 
 
91
 
92
  def load_users():
 
 
 
 
 
 
 
 
 
 
93
  try:
94
- with open(USERS_FILE, 'r', encoding='utf-8') as file:
95
  users = json.load(file)
96
- logging.info(f"Данные пользователей успешно загружены из {USERS_FILE}")
97
- return users if isinstance(users, dict) else {}
98
- except FileNotFoundError:
99
- logging.warning(f"Локальный файл {USERS_FILE} не найден. Попытка скачать с HF.")
100
- try:
101
- download_db_from_hf(specific_file=USERS_FILE)
102
- with open(USERS_FILE, 'r', encoding='utf-8') as file:
103
- users = json.load(file)
104
- logging.info(f"Данные пользователей загружены из {USERS_FILE} после скачивания.")
105
- return users if isinstance(users, dict) else {}
106
- except (FileNotFoundError, RepositoryNotFoundError):
107
- logging.warning(f"Файл {USERS_FILE} не найден локально и в репозитории HF. Создание пустого файла.")
108
- with open(USERS_FILE, 'w', encoding='utf-8') as f: json.dump({}, f)
109
- return {}
110
- except json.JSONDecodeError:
111
- logging.error(f"Ошибка декодирования JSON в {USERS_FILE} после скачивания.")
112
- return {}
113
- except Exception as e:
114
- logging.error(f"Неизвестная ошибка при загрузке пользователей после скачивания: {e}", exc_info=True)
115
- return {}
116
  except json.JSONDecodeError:
117
- logging.error(f"Ошибка декодирования JSON в локальном {USERS_FILE}. Файл может быть поврежден. Возврат пустого словаря.")
118
- return {}
119
  except Exception as e:
120
- logging.error(f"Неизвестная ошибка при загрузке пользователей ({USERS_FILE}): {e}", exc_info=True)
121
- return {}
122
 
123
  def save_users(users):
124
- try:
125
- with open(USERS_FILE, 'w', encoding='utf-8') as file:
126
- json.dump(users, file, ensure_ascii=False, indent=4)
127
- logging.info(f"Данные пользователей успешно сохранены в {USERS_FILE}")
128
  upload_db_to_hf(specific_file=USERS_FILE)
129
- except Exception as e:
130
- logging.error(f"Ошибка при сохранении данных пользователей в {USERS_FILE}: {e}", exc_info=True)
131
-
132
 
133
  def upload_db_to_hf(specific_file=None):
134
  if not HF_TOKEN_WRITE:
135
- logging.warning("Переменная окружения HF_TOKEN (для записи) не установлена. Загрузка на Hugging Face пропущена.")
136
  return
137
  try:
138
  api = HfApi()
139
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
140
- logging.info(f"Начало загрузки файлов {files_to_upload} на HF репозиторий {REPO_ID}...")
141
 
142
  for file_name in files_to_upload:
143
  if os.path.exists(file_name):
@@ -150,66 +181,29 @@ def upload_db_to_hf(specific_file=None):
150
  token=HF_TOKEN_WRITE,
151
  commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
152
  )
153
- logging.info(f"Файл {file_name} успешно загружен на Hugging Face.")
154
  except Exception as e:
155
- logging.error(f"Ошибка при загрузке файла {file_name} на Hugging Face: {e}")
156
  else:
157
- logging.warning(f"Файл {file_name} не найден локально, пропуск загрузки.")
158
- logging.info("Загрузка файлов на HF завершена.")
159
- except Exception as e:
160
- logging.error(f"Общая ошибка при инициализации или загрузке на Hugging Face: {e}", exc_info=True)
161
-
162
- def download_db_from_hf(specific_file=None):
163
- if not HF_TOKEN_READ:
164
- logging.warning("Переменная окружения HF_TOKEN_READ не установлена. Попытка скачивания с Hugging Face без токена (может не сработать для приватных репо).")
165
-
166
- files_to_download = [specific_file] if specific_file else SYNC_FILES
167
- logging.info(f"Начало скачивания файлов {files_to_download} с HF репозитория {REPO_ID}...")
168
- downloaded_files_count = 0
169
- try:
170
- for file_name in files_to_download:
171
- try:
172
- local_path = hf_hub_download(
173
- repo_id=REPO_ID,
174
- filename=file_name,
175
- repo_type="dataset",
176
- token=HF_TOKEN_READ,
177
- local_dir=".",
178
- local_dir_use_symlinks=False,
179
- force_download=True
180
- )
181
- logging.info(f"Файл {file_name} успешно скачан из Hugging Face в {local_path}.")
182
- downloaded_files_count += 1
183
- except RepositoryNotFoundError:
184
- logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face. Скачивание прервано.")
185
- break
186
- except Exception as e:
187
- if "404" in str(e) or isinstance(e, FileNotFoundError):
188
- logging.warning(f"Файл {file_name} не найден в репозитории {REPO_ID} или ошибка доступа. Пропуск скачивания этого файла.")
189
- else:
190
- logging.error(f"Ошибка при скачивании файла {file_name} с Hugging Face: {e}", exc_info=True)
191
- logging.info(f"Скачивание файлов с HF завершено. Скачано файлов: {downloaded_files_count}/{len(files_to_download)}.")
192
- except RepositoryNotFoundError:
193
- logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face. Скачивание не выполнено.")
194
  except Exception as e:
195
- logging.error(f"Общая ошибка при попытке скачивания с Hugging Face: {e}", exc_info=True)
196
-
197
 
198
  def periodic_backup():
199
- backup_interval = 1800
200
- logging.info(f"Настройка периодического резервного копирования каждые {backup_interval} секунд.")
201
  while True:
202
  time.sleep(backup_interval)
203
- logging.info("Запуск периодического резервного копирования...")
204
  upload_db_to_hf()
205
- logging.info("Периодическое резервное копирование завершено.")
206
-
207
 
208
  @app.route('/')
209
  def catalog():
210
  data = load_data()
211
  all_products = data.get('products', [])
212
- categories = data.get('categories', [])
213
  is_authenticated = 'user' in session
214
 
215
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
@@ -245,7 +239,6 @@ def catalog():
245
  body.dark-mode .theme-toggle:hover { color: #55a683; }
246
  .store-address { padding: 15px; text-align: center; background-color: #ffffff; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); font-size: 1rem; color: #44524c; }
247
  body.dark-mode .store-address { background-color: #253f37; color: #b0c8c1; }
248
-
249
  .filters-container { margin: 20px 0; display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
250
  .search-container { margin: 20px 0; text-align: center; }
251
  #search-input { width: 90%; max-width: 600px; padding: 12px 18px; font-size: 1rem; border: 1px solid #d1e7dd; border-radius: 25px; outline: none; box-shadow: 0 2px 5px rgba(0,0,0,0.05); transition: all 0.3s ease; }
@@ -256,9 +249,11 @@ def catalog():
256
  body.dark-mode .category-filter { background-color: #253f37; border-color: #2c4a41; color: #97b7ae; }
257
  .category-filter.active, .category-filter:hover { background-color: #1C6758; color: white; border-color: #1C6758; box-shadow: 0 2px 10px rgba(28, 103, 88, 0.3); }
258
  body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover { background-color: #3D8361; border-color: #3D8361; color: #1a2b26; box-shadow: 0 2px 10px rgba(61, 131, 97, 0.4); }
259
-
260
  .products-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; padding: 10px; }
261
- .product { background: #fff; border-radius: 15px; padding: 0; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; height: 100%; border: 1px solid #e1f0e9;}
 
 
 
262
  body.dark-mode .product { background: #253f37; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); border-color: #2c4a41; }
263
  .product:hover { transform: translateY(-5px) scale(1.02); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); }
264
  body.dark-mode .product:hover { box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); }
@@ -276,13 +271,11 @@ def catalog():
276
  .product-button { display: block; width: 100%; padding: 10px; border: none; border-radius: 8px; background-color: #1C6758; color: white; font-size: 0.9rem; font-weight: 500; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); text-align: center; text-decoration: none; }
277
  .product-button:hover { background-color: #164B41; box-shadow: 0 4px 15px rgba(22, 75, 65, 0.4); transform: translateY(-2px); }
278
  .product-button i { margin-right: 5px; }
279
-
280
  .add-to-cart { background-color: #38a169; }
281
  .add-to-cart:hover { background-color: #2f855a; box-shadow: 0 4px 15px rgba(47, 133, 90, 0.4); }
282
  #cart-button { position: fixed; bottom: 25px; right: 25px; background-color: #1C6758; color: white; border: none; border-radius: 50%; width: 55px; height: 55px; font-size: 1.5rem; cursor: pointer; display: none; align-items: center; justify-content: center; box-shadow: 0 4px 15px rgba(28, 103, 88, 0.4); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 1000; }
283
  #cart-button .fa-shopping-cart { margin-right: 0; }
284
  #cart-button span { position: absolute; top: -5px; right: -5px; background-color: #38a169; color: white; border-radius: 50%; padding: 2px 6px; font-size: 0.7rem; font-weight: bold; }
285
-
286
  .modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(5px); overflow-y: auto; }
287
  .modal-content { background: #f8fcfb; margin: 5% auto; padding: 25px; border-radius: 15px; width: 90%; max-width: 700px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); animation: slideIn 0.3s ease-out; position: relative; }
288
  body.dark-mode .modal-content { background: #253f37; color: #c8d8d3; }
@@ -315,13 +308,11 @@ def catalog():
315
  .clear-cart:hover { background-color: #5e6e68; box-shadow: 0 4px 15px rgba(94, 110, 104, 0.4); }
316
  .order-button { background-color: #38a169; }
317
  .order-button:hover { background-color: #2f855a; box-shadow: 0 4px 15px rgba(47, 133, 90, 0.4); }
318
-
319
  .notification { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background-color: #38a169; color: white; padding: 10px 20px; border-radius: 20px; box-shadow: 0 4px 10px rgba(0,0,0,0.2); z-index: 1002; opacity: 0; transition: opacity 0.5s ease; font-size: 0.9rem;}
320
  .notification.show { opacity: 1;}
321
  .no-results-message { grid-column: 1 / -1; text-align: center; padding: 40px; font-size: 1.1rem; color: #5e6e68; }
322
  body.dark-mode .no-results-message { color: #8aa39a; }
323
  .top-product-indicator { position: absolute; top: 8px; right: 8px; background-color: rgba(255, 215, 0, 0.8); color: #333; padding: 2px 6px; font-size: 0.7rem; border-radius: 4px; font-weight: bold; z-index: 10; backdrop-filter: blur(2px); }
324
- .product { position: relative; }
325
 
326
  </style>
327
  </head>
@@ -358,6 +349,7 @@ def catalog():
358
  <div class="products-grid" id="products-grid">
359
  {% for product in products %}
360
  <div class="product"
 
361
  data-name="{{ product['name']|lower }}"
362
  data-description="{{ product.get('description', '')|lower }}"
363
  data-category="{{ product.get('category', 'Без категории') }}">
@@ -445,7 +437,7 @@ def catalog():
445
 
446
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
447
  <script>
448
- const products = {{ products|tojson }};
449
  const repoId = '{{ repo_id }}';
450
  const currencyCode = '{{ currency_code }}';
451
  const isAuthenticated = {{ is_authenticated|tojson }};
@@ -468,6 +460,10 @@ def catalog():
468
  document.body.classList.add('dark-mode');
469
  const icon = document.querySelector('.theme-toggle i');
470
  if (icon) icon.classList.replace('fa-moon', 'fa-sun');
 
 
 
 
471
  }
472
  }
473
 
@@ -569,13 +565,13 @@ def catalog():
569
  const colorLabel = document.querySelector('label[for="colorSelect"]');
570
  colorSelect.innerHTML = '';
571
 
572
- const validColors = product.colors ? product.colors.filter(c => c && c.trim() !== "") : [];
573
 
574
  if (validColors.length > 0) {
575
  validColors.forEach(color => {
576
  const option = document.createElement('option');
577
- option.value = color.trim();
578
- option.text = color.trim();
579
  colorSelect.appendChild(option);
580
  });
581
  colorSelect.style.display = 'block';
@@ -594,7 +590,11 @@ def catalog():
594
  }
595
 
596
  function confirmAddToCart() {
597
- if (selectedProductIndex === null) return;
 
 
 
 
598
 
599
  const quantityInput = document.getElementById('quantityInput');
600
  const quantity = parseInt(quantityInput.value);
@@ -664,7 +664,7 @@ def catalog():
664
  cartTotalElement.textContent = '0.00';
665
  } else {
666
  cartContent.innerHTML = cart.map(item => {
667
- const itemTotal = item.price * item.quantity;
668
  total += itemTotal;
669
  const photoUrl = item.photo
670
  ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}`
@@ -676,7 +676,7 @@ def catalog():
676
  <img src="${photoUrl}" alt="${item.name}">
677
  <div class="cart-item-details">
678
  <strong>${item.name}${colorText}</strong>
679
- <p class="cart-item-price">${item.price.toFixed(2)} ${currencyCode} × ${item.quantity}</p>
680
  </div>
681
  <span class="cart-item-total">${itemTotal.toFixed(2)} ${currencyCode}</span>
682
  <button class="cart-item-remove" onclick="removeFromCart('${item.id}')" title="Удалить товар">×</button>
@@ -714,40 +714,40 @@ def catalog():
714
  return;
715
  }
716
  let total = 0;
717
- let orderText = "🛍️ *Новый Заказ от Soola Cosmetics* 🛍️";
718
- orderText += "----------------------------------------";
719
- orderText += "*Детали заказа:*%0A";
720
- orderText += "---------------------------------------- ";
721
  cart.forEach((item, index) => {
722
- const itemTotal = item.price * item.quantity;
723
  total += itemTotal;
724
  const colorText = item.color !== 'N/A' ? ` (${item.color})` : '';
725
- orderText += `${index + 1}. *${item.name}*${colorText} `;
726
- orderText += ` Кол-во: ${item.quantity} `;
727
- orderText += ` Цена: ${item.price.toFixed(2)} ${currencyCode} `;
728
- orderText += ` *Сумма: ${itemTotal.toFixed(2)} ${currencyCode} `;
729
  });
730
- orderText += "----------------------------------------";
731
- orderText += `*ИТОГО: ${total.toFixed(2)} ${currencyCode} `;
732
- orderText += "---------------------------------------- ";
733
 
734
  if (userInfo && userInfo.login) {
735
- orderText += "*Данные клиента: ";
736
- orderText += `Имя: ${userInfo.first_name || ''} ${userInfo.last_name || ''} `;
737
- orderText += `Логин: ${userInfo.login} `;
738
  if (userInfo.phone) {
739
- orderText += `Телефон: ${userInfo.phone}%0A`;
740
  }
741
- orderText += `Страна: ${userInfo.country || 'Не указана'} `;
742
- orderText += `Город: ${userInfo.city || 'Не указан'} `;
743
  } else {
744
- orderText += "*Клиент не авторизован ";
745
  }
746
- orderText += "---------------------------------------- ";
747
 
748
  const now = new Date();
749
  const dateTimeString = now.toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' });
750
- orderText += `Дата заказа: ${dateTimeString} `;
751
  orderText += `_Сформировано автоматически_`;
752
 
753
  const whatsappNumber = "996997703090";
@@ -766,9 +766,9 @@ def catalog():
766
  if (existingNoResults) existingNoResults.remove();
767
 
768
  document.querySelectorAll('.products-grid .product').forEach(productElement => {
769
- const name = productElement.getAttribute('data-name');
770
- const description = productElement.getAttribute('data-description');
771
- const category = productElement.getAttribute('data-category');
772
 
773
  const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm);
774
  const matchesCategory = activeCategory === 'all' || category === activeCategory;
@@ -781,17 +781,21 @@ def catalog():
781
  }
782
  });
783
 
784
- if (visibleProducts === 0 && products.length > 0) {
 
785
  const p = document.createElement('p');
786
  p.className = 'no-results-message';
787
  p.textContent = 'По вашему запросу товары не найдены.';
788
  grid.appendChild(p);
789
- } else if (products.length === 0 && !grid.querySelector('.no-results-message')) {
790
  const p = document.createElement('p');
791
  p.className = 'no-results-message';
792
  p.textContent = 'Товары пока не добавлены.';
793
  grid.appendChild(p);
794
  }
 
 
 
795
  }
796
 
797
  function setupFilters() {
@@ -807,12 +811,15 @@ def catalog():
807
  filterProducts();
808
  });
809
  });
810
- filterProducts(); // Initial filter on load
811
  }
812
 
813
  function showNotification(message, duration = 3000) {
814
  const placeholder = document.getElementById('notification-placeholder');
815
- if (!placeholder) return;
 
 
 
816
 
817
  const notification = document.createElement('div');
818
  notification.className = 'notification';
@@ -829,7 +836,7 @@ def catalog():
829
 
830
  document.addEventListener('DOMContentLoaded', () => {
831
  applyInitialTheme();
832
- attemptAutoLogin();
833
  updateCartButton();
834
  setupFilters();
835
 
@@ -846,6 +853,11 @@ def catalog():
846
  });
847
  }
848
  });
 
 
 
 
 
849
  });
850
 
851
  </script>
@@ -868,16 +880,17 @@ def catalog():
868
  def product_detail(index):
869
  data = load_data()
870
  all_products = data.get('products', [])
 
871
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
872
  products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
873
 
874
  is_authenticated = 'user' in session
875
  try:
 
 
876
  product = products_sorted[index]
877
- if not product.get('in_stock', True):
878
- raise IndexError("Товар не в наличии")
879
- except IndexError:
880
- logging.warning(f"Попытка доступа к несуществующему или отсутствующему продукту с индексом {index}")
881
  return "Товар не найден или отсутствует в наличии.", 404
882
 
883
  detail_html = '''
@@ -917,8 +930,9 @@ def product_detail(index):
917
  {% endif %}
918
  <p><strong>Описание:</strong><br> {{ product.get('description', 'Описание отсутствует.')|replace('\\n', '<br>')|safe }}</p>
919
  {% set colors = product.get('colors', []) %}
920
- {% if colors and colors|select('ne', '')|list|length > 0 %}
921
- <p><strong>Доступные цвета/варианты:</strong> {{ colors|select('ne', '')|join(', ') }}</p>
 
922
  {% endif %}
923
  </div>
924
  </div>
@@ -993,54 +1007,17 @@ def login():
993
  'city': user_info.get('city', ''),
994
  'phone': user_info.get('phone', '')
995
  }
996
- logging.info(f"Пользователь {login} успешно вошел в систему.")
997
- login_response_html = f'''
998
- <!DOCTYPE html><html><head><title>Перенаправление...</title></head><body>
999
- <script>
1000
- try {{ localStorage.setItem('soolaUser', '{login}'); }} catch (e) {{ console.error("Ошибка сохранения в localStorage:", e); }}
1001
- window.location.href = "{url_for('catalog')}";
1002
- </script>
1003
- <p>Вход выполнен успешно. Перенаправление в <a href="{url_for('catalog')}">каталог</a>...</p>
1004
- </body></html>
1005
- '''
1006
- return login_response_html
1007
  else:
1008
- logging.warning(f"Неудачная попытка входа для пользователя {login}.")
1009
  error_message = "Неверный логин или пароль."
1010
  return render_template_string(LOGIN_TEMPLATE, error=error_message), 401
1011
 
1012
  return render_template_string(LOGIN_TEMPLATE, error=None)
1013
 
1014
-
1015
- @app.route('/auto_login', methods=['POST'])
1016
- def auto_login():
1017
- data = request.get_json()
1018
- if not data or 'login' not in data:
1019
- logging.warning("Запрос auto_login без данных или логина.")
1020
- return "Неверный запрос", 400
1021
-
1022
- login = data.get('login')
1023
- if not login:
1024
- logging.warning("Попытка auto_login с пустым логином.")
1025
- return "Логин не предоставлен", 400
1026
-
1027
- users = load_users()
1028
- if login in users:
1029
- user_info = users[login]
1030
- session['user'] = login
1031
- session['user_info'] = {
1032
- 'login': login,
1033
- 'first_name': user_info.get('first_name', ''),
1034
- 'last_name': user_info.get('last_name', ''),
1035
- 'country': user_info.get('country', ''),
1036
- 'city': user_info.get('city', ''),
1037
- 'phone': user_info.get('phone', '')
1038
- }
1039
- logging.info(f"Автоматический вход для пользователя {login} выполнен.")
1040
- return "OK", 200
1041
- else:
1042
- logging.warning(f"Неудачная попытка автоматического входа для несуществующего пользователя {login}.")
1043
- return "Ошибка авто-входа", 400
1044
 
1045
  @app.route('/logout')
1046
  def logout():
@@ -1048,17 +1025,10 @@ def logout():
1048
  session.pop('user', None)
1049
  session.pop('user_info', None)
1050
  if logged_out_user:
1051
- logging.info(f"Пользователь {logged_out_user} вышел из системы.")
1052
- logout_response_html = '''
1053
- <!DOCTYPE html><html><head><title>Выход...</title></head><body>
1054
- <script>
1055
- try { localStorage.removeItem('soolaUser'); } catch (e) { console.error("Ошибка удаления из localStorage:", e); }
1056
- window.location.href = "/";
1057
- </script>
1058
- <p>Выход выполнен. Перенаправление на <a href="/">главную страницу</a>...</p>
1059
- </body></html>
1060
- '''
1061
- return logout_response_html
1062
 
1063
  ADMIN_TEMPLATE = '''
1064
  <!DOCTYPE html>
@@ -1097,7 +1067,7 @@ ADMIN_TEMPLATE = '''
1097
  .add-button:hover { background-color: #2f855a; }
1098
  .item-list { display: grid; gap: 20px; }
1099
  .item { background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.07); border: 1px solid #e1f0e9; }
1100
- .item p { margin: 5px 0; font-size: 0.9rem; color: #44524c; }
1101
  .item strong { color: #2d332f; }
1102
  .item .description { font-size: 0.85rem; color: #5e6e68; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
1103
  .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
@@ -1154,7 +1124,7 @@ ADMIN_TEMPLATE = '''
1154
  <button type="submit" class="button" title="Загрузить локальные файлы на Hugging Face"><i class="fas fa-upload"></i> Загрузить БД</button>
1155
  </form>
1156
  <form method="POST" action="{{ url_for('force_download') }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите принудительно скачать данные с сервера? Это перезапишет ваши локальные файлы.');">
1157
- <button type="submit" class="button download-hf-button" title="Скачать файлы (перезапишет локальные)"><i class="fas fa-download"></i> Скачать БД</button>
1158
  </form>
1159
  </div>
1160
  <p style="font-size: 0.85rem; color: #5e6e68;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
@@ -1284,11 +1254,11 @@ ADMIN_TEMPLATE = '''
1284
  <button type="button" class="button add-color-btn" style="margin-top: 5px;" onclick="addColorInput('add-color-inputs')"><i class="fas fa-palette"></i> Добавить поле для цвета/варианта</button>
1285
  <br>
1286
  <div style="margin-top: 15px;">
1287
- <input type="checkbox" id="add_in_stock" name="in_stock" checked>
1288
  <label for="add_in_stock" class="inline-label">В наличии</label>
1289
  </div>
1290
  <div style="margin-top: 5px;">
1291
- <input type="checkbox" id="add_is_top" name="is_top">
1292
  <label for="add_is_top" class="inline-label">Топ товар (показывать наверху)</label>
1293
  </div>
1294
  <br>
@@ -1304,7 +1274,7 @@ ADMIN_TEMPLATE = '''
1304
  <div class="item">
1305
  <div style="display: flex; gap: 15px; align-items: flex-start;">
1306
  <div class="photo-preview" style="flex-shrink: 0;">
1307
- {% if product.get('photos') %}
1308
  <a href="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}" target="_blank" title="Посмотреть первое фото">
1309
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}" alt="Фото">
1310
  </a>
@@ -1328,7 +1298,8 @@ ADMIN_TEMPLATE = '''
1328
  <p><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p>
1329
  <p class="description" title="{{ product.get('description', '') }}"><strong>Описание:</strong> {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}</p>
1330
  {% set colors = product.get('colors', []) %}
1331
- <p><strong>Цвета/Вар-ты:</strong> {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}</p>
 
1332
  {% if product.get('photos') and product['photos']|length > 1 %}
1333
  <p style="font-size: 0.8rem; color: #5e6e68;">(Всего фото: {{ product['photos']|length }})</p>
1334
  {% endif %}
@@ -1374,15 +1345,13 @@ ADMIN_TEMPLATE = '''
1374
  {% endif %}
1375
  <label>Цвета/Варианты:</label>
1376
  <div id="edit-color-inputs-{{ loop.index0 }}">
1377
- {% set current_colors = product.get('colors', []) %}
1378
- {% if current_colors and current_colors|select('ne', '')|list|length > 0 %}
1379
  {% for color in current_colors %}
1380
- {% if color.strip() %}
1381
  <div class="color-input-group">
1382
  <input type="text" name="colors" value="{{ color }}">
1383
  <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1384
  </div>
1385
- {% endif %}
1386
  {% endfor %}
1387
  {% else %}
1388
  <div class="color-input-group">
@@ -1394,11 +1363,11 @@ ADMIN_TEMPLATE = '''
1394
  <button type="button" class="button add-color-btn" style="margin-top: 5px;" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')"><i class="fas fa-palette"></i> Добавить поле для цвета</button>
1395
  <br>
1396
  <div style="margin-top: 15px;">
1397
- <input type="checkbox" id="edit_in_stock_{{ loop.index0 }}" name="in_stock" {% if product.get('in_stock', True) %}checked{% endif %}>
1398
  <label for="edit_in_stock_{{ loop.index0 }}" class="inline-label">В наличии</label>
1399
  </div>
1400
  <div style="margin-top: 5px;">
1401
- <input type="checkbox" id="edit_is_top_{{ loop.index0 }}" name="is_top" {% if product.get('is_top', False) %}checked{% endif %}>
1402
  <label for="edit_is_top_{{ loop.index0 }}" class="inline-label">Топ товар</label>
1403
  </div>
1404
  <br>
@@ -1444,21 +1413,19 @@ ADMIN_TEMPLATE = '''
1444
  const group = button.closest('.color-input-group');
1445
  if (group) {
1446
  const container = group.parentNode;
1447
- // Only remove if it's not the last one (or handle adding a placeholder if it is)
1448
- // For simplicity, let's allow removing all. Add logic if needed later.
1449
  group.remove();
1450
- // Optional: If container is now empty, add a placeholder input back
1451
  if (container && container.children.length === 0) {
1452
  const placeholderGroup = document.createElement('div');
1453
  placeholderGroup.className = 'color-input-group';
1454
  placeholderGroup.innerHTML = `
1455
- <input type="text" name="colors" placeholder="Например: Цвет">
1456
  <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1457
  `;
1458
  container.appendChild(placeholderGroup);
1459
  }
1460
  } else {
1461
- console.warn("Не удалось найти родительский .color-input-group для кнопки удаления");
1462
  }
1463
  }
1464
  </script>
@@ -1468,10 +1435,16 @@ ADMIN_TEMPLATE = '''
1468
 
1469
  @app.route('/admin', methods=['GET', 'POST'])
1470
  def admin():
 
 
 
 
 
1471
  data = load_data()
1472
- products = data.get('products', [])
1473
- categories = data.get('categories', [])
1474
  users = load_users()
 
 
 
1475
 
1476
  if request.method == 'POST':
1477
  action = request.form.get('action')
@@ -1480,36 +1453,37 @@ def admin():
1480
  try:
1481
  if action == 'add_category':
1482
  category_name = request.form.get('category_name', '').strip()
1483
- if category_name and category_name not in categories:
1484
- categories.append(category_name)
1485
- categories.sort()
1486
  save_data(data)
1487
- logging.info(f"Категория '{category_name}' добавлена.")
1488
  flash(f"Категория '{category_name}' успешно добавлена.", 'success')
1489
  elif not category_name:
1490
- logging.warning("Попытка добавить пустую категорию.")
1491
  flash("Название категории не может быть пустым.", 'error')
1492
  else:
1493
- logging.warning(f"Категория '{category_name}' уже существует.")
1494
  flash(f"Категория '{category_name}' уже существует.", 'error')
1495
 
1496
  elif action == 'delete_category':
1497
  category_to_delete = request.form.get('category_name')
1498
- if category_to_delete and category_to_delete in categories:
1499
- categories.remove(category_to_delete)
 
1500
  updated_count = 0
1501
- for product in products:
 
1502
  if product.get('category') == category_to_delete:
1503
  product['category'] = 'Без категории'
1504
  updated_count += 1
1505
  save_data(data)
1506
- logging.info(f"Категория '{category_to_delete}' удалена. Обновлено товаров: {updated_count}.")
1507
  flash(f"Категория '{category_to_delete}' удалена.", 'success')
1508
  else:
1509
- logging.warning(f"Попытка удалить несуществующую или пустую категорию: {category_to_delete}")
1510
  flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
1511
 
1512
-
1513
  elif action == 'add_product':
1514
  name = request.form.get('name', '').strip()
1515
  price_str = request.form.get('price', '').replace(',', '.')
@@ -1517,9 +1491,8 @@ def admin():
1517
  category = request.form.get('category')
1518
  photos_files = request.files.getlist('photos')
1519
  colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1520
- in_stock = 'in_stock' in request.form
1521
- is_top = 'is_top' in request.form
1522
-
1523
 
1524
  if not name or not price_str:
1525
  flash("Название и цена товара обязательны.", 'error')
@@ -1527,64 +1500,64 @@ def admin():
1527
 
1528
  try:
1529
  price = round(float(price_str), 2)
1530
- if price < 0: price = 0
1531
  except ValueError:
1532
- flash("Неверный формат цены.", 'error')
1533
  return redirect(url_for('admin'))
1534
 
1535
  photos_list = []
1536
- if photos_files and HF_TOKEN_WRITE:
1537
  uploads_dir = 'uploads_temp'
1538
  os.makedirs(uploads_dir, exist_ok=True)
1539
  api = HfApi()
1540
  photo_limit = 10
1541
  uploaded_count = 0
1542
  for photo in photos_files:
1543
- if uploaded_count >= photo_limit:
1544
- logging.warning(f"Достигнут лимит фото ({photo_limit}), остальные фото проигнорированы.")
 
1545
  flash(f"Загружено только первые {photo_limit} фото.", "warning")
1546
  break
1547
- if photo and photo.filename:
1548
- try:
1549
- ext = os.path.splitext(photo.filename)[1]
1550
- photo_filename = secure_filename(f"{name.replace(' ','_')}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}")
1551
- temp_path = os.path.join(uploads_dir, photo_filename)
1552
- photo.save(temp_path)
1553
- logging.info(f"Загрузка фото {photo_filename} на HF для товара {name}...")
1554
- api.upload_file(
1555
- path_or_fileobj=temp_path,
1556
- path_in_repo=f"photos/{photo_filename}",
1557
- repo_id=REPO_ID,
1558
- repo_type="dataset",
1559
- token=HF_TOKEN_WRITE,
1560
- commit_message=f"Add photo for product {name}"
1561
- )
1562
- photos_list.append(photo_filename)
1563
- logging.info(f"Фото {photo_filename} успешно загружено.")
1564
- os.remove(temp_path)
1565
- uploaded_count += 1
1566
- except Exception as e:
1567
- logging.error(f"Ошибка загрузки фото {photo.filename} на HF: {e}", exc_info=True)
1568
- flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
1569
- elif photo and not photo.filename:
1570
- logging.warning("Получен пустой объект файла фото при добавлении товара.")
1571
- try:
1572
- if not os.listdir(uploads_dir):
1573
- os.rmdir(uploads_dir)
 
1574
  except OSError as e:
1575
- logging.warning(f"Не удалось удалить временную папку {uploads_dir}: {e}")
1576
-
1577
 
1578
  new_product = {
1579
  'name': name, 'price': price, 'description': description,
1580
- 'category': category if category in categories else 'Без категории',
1581
  'photos': photos_list, 'colors': colors,
1582
  'in_stock': in_stock, 'is_top': is_top
1583
  }
1584
- products.append(new_product)
1585
-
1586
  save_data(data)
1587
- logging.info(f"Товар '{name}' добавлен.")
1588
  flash(f"Товар '{name}' успешно добавлен.", 'success')
1589
 
1590
  elif action == 'edit_product':
@@ -1595,100 +1568,107 @@ def admin():
1595
 
1596
  try:
1597
  index = int(index_str)
1598
-
1599
- # We need to find the *original* index in the unsorted/unfiltered list
1600
- original_product_list = data.get('products', [])
1601
- if not (0 <= index < len(original_product_list)): raise IndexError("Индекс вне диапазона")
1602
- product_to_edit = original_product_list[index]
1603
  original_name = product_to_edit.get('name', 'N/A')
1604
-
1605
  except (ValueError, IndexError):
1606
  flash(f"Ошибка редактирования: неверный индекс товара '{index_str}'.", 'error')
1607
  return redirect(url_for('admin'))
1608
 
1609
- product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
1610
- price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.')
1611
- product_to_edit['description'] = request.form.get('description', product_to_edit['description']).strip()
 
1612
  category = request.form.get('category')
1613
- product_to_edit['category'] = category if category in categories else 'Без категории'
1614
  product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1615
- product_to_edit['in_stock'] = 'in_stock' in request.form
1616
- product_to_edit['is_top'] = 'is_top' in request.form
1617
-
1618
 
1619
  try:
1620
  price = round(float(price_str), 2)
1621
- if price < 0: price = 0
1622
  product_to_edit['price'] = price
1623
  except ValueError:
1624
- logging.warning(f"Неверный формат цены '{price_str}' при редактировании товара {original_name}. Цена не изменена.")
1625
- flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
1626
 
1627
  photos_files = request.files.getlist('photos')
1628
- if photos_files and any(f.filename for f in photos_files) and HF_TOKEN_WRITE:
 
1629
  uploads_dir = 'uploads_temp'
1630
  os.makedirs(uploads_dir, exist_ok=True)
1631
  api = HfApi()
1632
  new_photos_list = []
1633
  photo_limit = 10
1634
  uploaded_count = 0
1635
- logging.info(f"Загрузка новых фото для товара {product_to_edit['name']}...")
1636
  for photo in photos_files:
1637
- if uploaded_count >= photo_limit:
1638
- logging.warning(f"Достигнут лимит фото ({photo_limit}), остальные фото проигнорированы.")
 
1639
  flash(f"Загружено только первые {photo_limit} фото.", "warning")
1640
  break
1641
- if photo and photo.filename:
1642
- try:
1643
- ext = os.path.splitext(photo.filename)[1]
1644
- photo_filename = secure_filename(f"{product_to_edit['name'].replace(' ','_')}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}")
1645
- temp_path = os.path.join(uploads_dir, photo_filename)
1646
- photo.save(temp_path)
1647
- logging.info(f"Загрузка нового фото {photo_filename} на HF...")
1648
- api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}",
1649
- repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
1650
- commit_message=f"Update photo for product {product_to_edit['name']}")
1651
- new_photos_list.append(photo_filename)
1652
- logging.info(f"Новое фото {photo_filename} успешно загружено.")
1653
- os.remove(temp_path)
1654
- uploaded_count += 1
1655
- except Exception as e:
1656
- logging.error(f"Ошибка загрузки нового фото {photo.filename}: {e}", exc_info=True)
1657
- flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error')
1658
- try:
1659
- if not os.listdir(uploads_dir):
 
 
 
 
1660
  os.rmdir(uploads_dir)
1661
  except OSError as e:
1662
- logging.warning(f"Не удалось удалить временную папку {uploads_dir}: {e}")
1663
 
 
1664
  if new_photos_list:
1665
- logging.info(f"Список фото для товара {product_to_edit['name']} обновлен.")
1666
  old_photos = product_to_edit.get('photos', [])
1667
  if old_photos:
1668
- logging.info(f"Попытка удаления старых фото: {old_photos}")
1669
  try:
 
1670
  api.delete_files(
1671
  repo_id=REPO_ID,
1672
- paths_in_repo=[f"photos/{p}" for p in old_photos],
1673
  repo_type="dataset",
1674
  token=HF_TOKEN_WRITE,
1675
- commit_message=f"Delete old photos for product {product_to_edit['name']}"
 
1676
  )
1677
- logging.info(f"Старые фото для товара {product_to_edit['name']} удалены с HF.")
1678
  except Exception as e:
1679
- logging.error(f"Ошибка при удалении старых фото {old_photos} с HF: {e}", exc_info=True)
1680
  flash("Не удалось удалить старые фотографии с сервера.", "warning")
1681
  product_to_edit['photos'] = new_photos_list
1682
  flash("Фотографии товара успешно обновлены.", "success")
1683
- elif uploaded_count == 0 and any(f.filename for f in photos_files):
1684
- flash("Не удалось загрузить новые фотографии.", "error")
1685
-
 
1686
 
 
1687
  save_data(data)
1688
- logging.info(f"Товар '{original_name}' (индекс {index}) обновлен на '{product_to_edit['name']}'.")
1689
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
1690
 
1691
-
1692
  elif action == 'delete_product':
1693
  index_str = request.form.get('index')
1694
  if index_str is None:
@@ -1696,38 +1676,42 @@ def admin():
1696
  return redirect(url_for('admin'))
1697
  try:
1698
  index = int(index_str)
1699
- original_product_list = data.get('products', [])
1700
- if not (0 <= index < len(original_product_list)): raise IndexError("Индекс вне диапазона")
1701
- deleted_product = original_product_list.pop(index)
 
1702
  product_name = deleted_product.get('name', 'N/A')
1703
 
1704
  photos_to_delete = deleted_product.get('photos', [])
1705
  if photos_to_delete and HF_TOKEN_WRITE:
1706
- logging.info(f"Попытка удаления фото товара '{product_name}' с HF: {photos_to_delete}")
1707
  try:
1708
  api = HfApi()
1709
  api.delete_files(
1710
  repo_id=REPO_ID,
1711
- paths_in_repo=[f"photos/{p}" for p in photos_to_delete],
1712
  repo_type="dataset",
1713
  token=HF_TOKEN_WRITE,
1714
- commit_message=f"Delete photos for deleted product {product_name}"
 
1715
  )
1716
- logging.info(f"Фото товара '{product_name}' удалены с HF.")
1717
  except Exception as e:
1718
- logging.error(f"Ошибка при удалении фото {photos_to_delete} для товара '{product_name}' с HF: {e}", exc_info=True)
1719
  flash(f"Не удалось удалить фото для товара '{product_name}' с сервера.", "warning")
1720
 
1721
  save_data(data)
1722
- logging.info(f"Товар '{product_name}' (индекс {index}) удален.")
1723
  flash(f"Товар '{product_name}' удален.", 'success')
1724
  except (ValueError, IndexError):
1725
  flash(f"Ошибка удаления: неверный индекс товара '{index_str}'.", 'error')
1726
-
 
 
1727
 
1728
  elif action == 'add_user':
1729
  login = request.form.get('login', '').strip()
1730
- password = request.form.get('password', '').strip()
1731
  first_name = request.form.get('first_name', '').strip()
1732
  last_name = request.form.get('last_name', '').strip()
1733
  phone = request.form.get('phone', '').strip()
@@ -1742,13 +1726,13 @@ def admin():
1742
  return redirect(url_for('admin'))
1743
 
1744
  users[login] = {
1745
- 'password': password,
1746
  'first_name': first_name, 'last_name': last_name,
1747
  'phone': phone,
1748
  'country': country, 'city': city
1749
  }
1750
  save_users(users)
1751
- logging.info(f"Пользователь '{login}' добавлен.")
1752
  flash(f"Пользователь '{login}' успешно добавлен.", 'success')
1753
 
1754
  elif action == 'delete_user':
@@ -1756,38 +1740,33 @@ def admin():
1756
  if login_to_delete and login_to_delete in users:
1757
  del users[login_to_delete]
1758
  save_users(users)
1759
- logging.info(f"Пользователь '{login_to_delete}' удален.")
1760
  flash(f"Пользователь '{login_to_delete}' удален.", 'success')
1761
  else:
1762
- logging.warning(f"Попытка удалить несуществующего или пустого пользователя: {login_to_delete}")
1763
  flash(f"Не удалось удалить пользователя '{login_to_delete}'.", 'error')
1764
 
1765
  else:
1766
- logging.warning(f"Получено неизвестное действие в админ-панели: {action}")
1767
  flash(f"Неизвестное действие: {action}", 'warning')
1768
 
 
1769
  return redirect(url_for('admin'))
1770
 
1771
  except Exception as e:
1772
- logging.error(f"Ошибка при обработке действия '{action}' в админ-панели: {e}", exc_info=True)
1773
  flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
 
1774
  return redirect(url_for('admin'))
1775
 
1776
- # Pass the original, unsorted product list to the admin template with original indices
1777
- original_products_with_indices = list(enumerate(data.get('products', [])))
1778
- # Sort the indexed list for display purposes if needed, but keep original index
1779
- display_products = sorted(original_products_with_indices, key=lambda item: item[1].get('name', '').lower())
1780
- # Reconstruct list of products in display order for the template
1781
- # Need to pass the original index within the product data or handle it carefully
1782
- # Let's pass the original product list directly for simplicity in the template loops
1783
- original_product_list = data.get('products', [])
1784
-
1785
- categories.sort()
1786
  sorted_users = dict(sorted(users.items()))
1787
 
1788
  return render_template_string(
1789
  ADMIN_TEMPLATE,
1790
- products=original_product_list, # Pass the original list to preserve indices
1791
  categories=categories,
1792
  users=sorted_users,
1793
  repo_id=REPO_ID,
@@ -1796,39 +1775,50 @@ def admin():
1796
 
1797
  @app.route('/force_upload', methods=['POST'])
1798
  def force_upload():
1799
- logging.info("Запущена принудительная загрузка данных на Hugging Face...")
 
1800
  try:
1801
  upload_db_to_hf()
1802
  flash("Данные успешно загружены на Hugging Face.", 'success')
1803
  except Exception as e:
1804
- logging.error(f"Ошибка при принудительной загрузке: {e}", exc_info=True)
1805
  flash(f"Ошибка при загрузке на Hugging Face: {e}", 'error')
1806
  return redirect(url_for('admin'))
1807
 
1808
  @app.route('/force_download', methods=['POST'])
1809
  def force_download():
1810
- logging.info("Запущено принудительное скачивание данных с Hugging Face...")
 
1811
  try:
1812
- download_db_from_hf()
1813
- flash("Данные успешно скачаны с Hugging Face. Локальные файлы обновлены.", 'success')
 
 
 
 
1814
  except Exception as e:
1815
- logging.error(f"Ошибка при принудительном скачивании: {e}", exc_info=True)
1816
  flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error')
1817
  return redirect(url_for('admin'))
1818
 
1819
 
1820
  if __name__ == '__main__':
 
1821
  load_data()
1822
  load_users()
1823
 
 
1824
  if HF_TOKEN_WRITE:
1825
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1826
  backup_thread.start()
1827
- logging.info("Поток периодического резервного копирования запущен.")
1828
  else:
1829
- logging.warning("Периодическое резервное копирование НЕ будет запущено (HF_TOKEN или HF_TOKEN_WRITE не установлена).")
1830
 
 
1831
  port = int(os.environ.get('PORT', 7860))
1832
- logging.info(f"Запуск Flask приложения на хосте 0.0.0.0 и порту {port}")
 
1833
  app.run(debug=False, host='0.0.0.0', port=port)
1834
 
 
 
8
  import time
9
  from datetime import datetime
10
  from huggingface_hub import HfApi, hf_hub_download
11
+ from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
12
  from werkzeug.utils import secure_filename
13
  from dotenv import load_dotenv
14
+ import shutil # Added for atomic saves
15
 
16
  load_dotenv()
17
 
18
  app = Flask(__name__)
19
+ # IMPORTANT: Replace this with a strong, unique secret key, preferably stored securely (e.g., environment variable)
20
+ app.secret_key = os.getenv('FLASK_SECRET_KEY', 'replace_this_with_a_real_secret_key_soola_67890')
21
  DATA_FILE = 'data_soola.json'
22
  USERS_FILE = 'users_soola.json'
23
 
 
28
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
29
 
30
  STORE_ADDRESS = "Рынок Дордой, Джунхай, терминал, 38"
 
31
  CURRENCY_CODE = 'KGS'
32
  CURRENCY_NAME = 'Кыргызский сом (с)'
33
 
34
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
35
 
36
+ def download_db_from_hf(specific_file=None):
37
+ if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
38
+ logging.warning("Neither HF_TOKEN_READ nor HF_TOKEN_WRITE is set. Attempting download without token (may fail for private repos).")
39
+ token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE # Use write token if read token isn't available
40
+
41
+ files_to_download = [specific_file] if specific_file else SYNC_FILES
42
+ logging.info(f"Attempting to download files {files_to_download} from HF repo {REPO_ID}...")
43
+ downloaded_files_count = 0
44
+ all_successful = True
45
+ try:
46
+ for file_name in files_to_download:
47
+ try:
48
+ local_path = hf_hub_download(
49
+ repo_id=REPO_ID,
50
+ filename=file_name,
51
+ repo_type="dataset",
52
+ token=token_to_use,
53
+ local_dir=".",
54
+ local_dir_use_symlinks=False,
55
+ force_download=True,
56
+ cache_dir=None # Avoid caching issues when forcing download
57
+ )
58
+ logging.info(f"File {file_name} successfully downloaded from Hugging Face to {local_path}.")
59
+ downloaded_files_count += 1
60
+ except RepositoryNotFoundError:
61
+ logging.error(f"Repository {REPO_ID} not found on Hugging Face. Download aborted.")
62
+ all_successful = False
63
+ break
64
+ except HfHubHTTPError as e:
65
+ if e.response.status_code == 404:
66
+ logging.warning(f"File {file_name} not found in repository {REPO_ID}. Skipping download for this file.")
67
+ else:
68
+ logging.error(f"HTTP error downloading {file_name} from Hugging Face: {e}", exc_info=True)
69
+ all_successful = False
70
+ except Exception as e:
71
+ logging.error(f"Error downloading file {file_name} from Hugging Face: {e}", exc_info=True)
72
+ all_successful = False
73
+ logging.info(f"HF download process finished. Downloaded {downloaded_files_count}/{len(files_to_download)} files.")
74
+ except Exception as e:
75
+ logging.error(f"General error during Hugging Face download attempt: {e}", exc_info=True)
76
+ all_successful = False
77
+ return all_successful
78
+
79
  def load_data():
80
+ file_path = DATA_FILE
81
+ default_structure = {'products': [], 'categories': []}
82
+ download_success = download_db_from_hf(specific_file=file_path)
83
+
84
+ if not os.path.exists(file_path):
85
+ logging.warning(f"Local file {file_path} not found, even after download attempt. Initializing with empty structure.")
86
+ with open(file_path, 'w', encoding='utf-8') as f:
87
+ json.dump(default_structure, f)
88
+ return default_structure
89
+
90
  try:
91
+ with open(file_path, 'r', encoding='utf-8') as file:
 
92
  data = json.load(file)
93
+ logging.info(f"Data successfully loaded from {file_path}")
94
+
95
+ if not isinstance(data, dict):
96
+ logging.warning(f"{file_path} content is not a dictionary. Resetting to default structure.")
97
+ return default_structure
98
+ if 'products' not in data or not isinstance(data['products'], list):
99
+ logging.warning(f"'products' key missing or not a list in {file_path}. Initializing.")
100
+ data['products'] = []
101
+ if 'categories' not in data or not isinstance(data['categories'], list):
102
+ logging.warning(f"'categories' key missing or not a list in {file_path}. Initializing.")
103
+ data['categories'] = []
104
+ return data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  except json.JSONDecodeError:
106
+ logging.error(f"JSON decode error in {file_path}. File might be corrupted. Returning default structure.", exc_info=True)
107
+ # Optionally, try to re-download on decode error? Or just return default.
108
+ # For now, returning default to prevent cascading errors.
109
+ return default_structure
110
  except Exception as e:
111
+ logging.error(f"Unknown error loading data from {file_path}: {e}", exc_info=True)
112
+ return default_structure
113
 
114
+ def save_data_atomic(data, file_path):
115
+ temp_file_path = file_path + '.tmp'
116
  try:
117
+ with open(temp_file_path, 'w', encoding='utf-8') as file:
118
  json.dump(data, file, ensure_ascii=False, indent=4)
119
+ # Atomically replace the old file with the new one
120
+ shutil.move(temp_file_path, file_path)
121
+ logging.info(f"Data successfully saved atomically to {file_path}")
122
+ return True
123
  except Exception as e:
124
+ logging.error(f"Error during atomic save to {file_path}: {e}", exc_info=True)
125
+ # Clean up temp file if it exists
126
+ if os.path.exists(temp_file_path):
127
+ try:
128
+ os.remove(temp_file_path)
129
+ except OSError as rm_err:
130
+ logging.error(f"Error removing temporary file {temp_file_path}: {rm_err}")
131
+ return False
132
+
133
+ def save_data(data):
134
+ if save_data_atomic(data, DATA_FILE):
135
+ upload_db_to_hf(specific_file=DATA_FILE)
136
 
137
  def load_users():
138
+ file_path = USERS_FILE
139
+ default_structure = {}
140
+ download_success = download_db_from_hf(specific_file=file_path)
141
+
142
+ if not os.path.exists(file_path):
143
+ logging.warning(f"Local file {file_path} not found, even after download attempt. Initializing with empty structure.")
144
+ with open(file_path, 'w', encoding='utf-8') as f:
145
+ json.dump(default_structure, f)
146
+ return default_structure
147
+
148
  try:
149
+ with open(file_path, 'r', encoding='utf-8') as file:
150
  users = json.load(file)
151
+ logging.info(f"User data successfully loaded from {file_path}")
152
+ return users if isinstance(users, dict) else default_structure
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  except json.JSONDecodeError:
154
+ logging.error(f"JSON decode error in {file_path}. File might be corrupted. Returning default structure.", exc_info=True)
155
+ return default_structure
156
  except Exception as e:
157
+ logging.error(f"Unknown error loading users from {file_path}: {e}", exc_info=True)
158
+ return default_structure
159
 
160
  def save_users(users):
161
+ if save_data_atomic(users, USERS_FILE):
 
 
 
162
  upload_db_to_hf(specific_file=USERS_FILE)
 
 
 
163
 
164
  def upload_db_to_hf(specific_file=None):
165
  if not HF_TOKEN_WRITE:
166
+ logging.warning("HF_TOKEN (for writing) not set. Skipping upload to Hugging Face.")
167
  return
168
  try:
169
  api = HfApi()
170
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
171
+ logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...")
172
 
173
  for file_name in files_to_upload:
174
  if os.path.exists(file_name):
 
181
  token=HF_TOKEN_WRITE,
182
  commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
183
  )
184
+ logging.info(f"File {file_name} successfully uploaded to Hugging Face.")
185
  except Exception as e:
186
+ logging.error(f"Error uploading file {file_name} to Hugging Face: {e}")
187
  else:
188
+ logging.warning(f"File {file_name} not found locally, skipping upload.")
189
+ logging.info("HF upload process finished.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  except Exception as e:
191
+ logging.error(f"General error during Hugging Face upload initialization or process: {e}", exc_info=True)
 
192
 
193
  def periodic_backup():
194
+ backup_interval = 1800 # 30 minutes
195
+ logging.info(f"Setting up periodic backup every {backup_interval} seconds.")
196
  while True:
197
  time.sleep(backup_interval)
198
+ logging.info("Starting periodic backup...")
199
  upload_db_to_hf()
200
+ logging.info("Periodic backup finished.")
 
201
 
202
  @app.route('/')
203
  def catalog():
204
  data = load_data()
205
  all_products = data.get('products', [])
206
+ categories = sorted(data.get('categories', []))
207
  is_authenticated = 'user' in session
208
 
209
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
 
239
  body.dark-mode .theme-toggle:hover { color: #55a683; }
240
  .store-address { padding: 15px; text-align: center; background-color: #ffffff; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); font-size: 1rem; color: #44524c; }
241
  body.dark-mode .store-address { background-color: #253f37; color: #b0c8c1; }
 
242
  .filters-container { margin: 20px 0; display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
243
  .search-container { margin: 20px 0; text-align: center; }
244
  #search-input { width: 90%; max-width: 600px; padding: 12px 18px; font-size: 1rem; border: 1px solid #d1e7dd; border-radius: 25px; outline: none; box-shadow: 0 2px 5px rgba(0,0,0,0.05); transition: all 0.3s ease; }
 
249
  body.dark-mode .category-filter { background-color: #253f37; border-color: #2c4a41; color: #97b7ae; }
250
  .category-filter.active, .category-filter:hover { background-color: #1C6758; color: white; border-color: #1C6758; box-shadow: 0 2px 10px rgba(28, 103, 88, 0.3); }
251
  body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover { background-color: #3D8361; border-color: #3D8361; color: #1a2b26; box-shadow: 0 2px 10px rgba(61, 131, 97, 0.4); }
 
252
  .products-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; padding: 10px; }
253
+ @media (min-width: 600px) { .products-grid { grid-template-columns: repeat(3, 1fr); } }
254
+ @media (min-width: 900px) { .products-grid { grid-template-columns: repeat(4, 1fr); } }
255
+ @media (min-width: 1200px) { .products-grid { grid-template-columns: repeat(5, 1fr); } }
256
+ .product { background: #fff; border-radius: 15px; padding: 0; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; height: 100%; border: 1px solid #e1f0e9; position: relative;}
257
  body.dark-mode .product { background: #253f37; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); border-color: #2c4a41; }
258
  .product:hover { transform: translateY(-5px) scale(1.02); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); }
259
  body.dark-mode .product:hover { box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); }
 
271
  .product-button { display: block; width: 100%; padding: 10px; border: none; border-radius: 8px; background-color: #1C6758; color: white; font-size: 0.9rem; font-weight: 500; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); text-align: center; text-decoration: none; }
272
  .product-button:hover { background-color: #164B41; box-shadow: 0 4px 15px rgba(22, 75, 65, 0.4); transform: translateY(-2px); }
273
  .product-button i { margin-right: 5px; }
 
274
  .add-to-cart { background-color: #38a169; }
275
  .add-to-cart:hover { background-color: #2f855a; box-shadow: 0 4px 15px rgba(47, 133, 90, 0.4); }
276
  #cart-button { position: fixed; bottom: 25px; right: 25px; background-color: #1C6758; color: white; border: none; border-radius: 50%; width: 55px; height: 55px; font-size: 1.5rem; cursor: pointer; display: none; align-items: center; justify-content: center; box-shadow: 0 4px 15px rgba(28, 103, 88, 0.4); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 1000; }
277
  #cart-button .fa-shopping-cart { margin-right: 0; }
278
  #cart-button span { position: absolute; top: -5px; right: -5px; background-color: #38a169; color: white; border-radius: 50%; padding: 2px 6px; font-size: 0.7rem; font-weight: bold; }
 
279
  .modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(5px); overflow-y: auto; }
280
  .modal-content { background: #f8fcfb; margin: 5% auto; padding: 25px; border-radius: 15px; width: 90%; max-width: 700px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); animation: slideIn 0.3s ease-out; position: relative; }
281
  body.dark-mode .modal-content { background: #253f37; color: #c8d8d3; }
 
308
  .clear-cart:hover { background-color: #5e6e68; box-shadow: 0 4px 15px rgba(94, 110, 104, 0.4); }
309
  .order-button { background-color: #38a169; }
310
  .order-button:hover { background-color: #2f855a; box-shadow: 0 4px 15px rgba(47, 133, 90, 0.4); }
 
311
  .notification { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background-color: #38a169; color: white; padding: 10px 20px; border-radius: 20px; box-shadow: 0 4px 10px rgba(0,0,0,0.2); z-index: 1002; opacity: 0; transition: opacity 0.5s ease; font-size: 0.9rem;}
312
  .notification.show { opacity: 1;}
313
  .no-results-message { grid-column: 1 / -1; text-align: center; padding: 40px; font-size: 1.1rem; color: #5e6e68; }
314
  body.dark-mode .no-results-message { color: #8aa39a; }
315
  .top-product-indicator { position: absolute; top: 8px; right: 8px; background-color: rgba(255, 215, 0, 0.8); color: #333; padding: 2px 6px; font-size: 0.7rem; border-radius: 4px; font-weight: bold; z-index: 10; backdrop-filter: blur(2px); }
 
316
 
317
  </style>
318
  </head>
 
349
  <div class="products-grid" id="products-grid">
350
  {% for product in products %}
351
  <div class="product"
352
+ data-id="{{ loop.index0 }}"
353
  data-name="{{ product['name']|lower }}"
354
  data-description="{{ product.get('description', '')|lower }}"
355
  data-category="{{ product.get('category', 'Без категории') }}">
 
437
 
438
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
439
  <script>
440
+ let products = {{ products|tojson }};
441
  const repoId = '{{ repo_id }}';
442
  const currencyCode = '{{ currency_code }}';
443
  const isAuthenticated = {{ is_authenticated|tojson }};
 
460
  document.body.classList.add('dark-mode');
461
  const icon = document.querySelector('.theme-toggle i');
462
  if (icon) icon.classList.replace('fa-moon', 'fa-sun');
463
+ } else {
464
+ document.body.classList.remove('dark-mode');
465
+ const icon = document.querySelector('.theme-toggle i');
466
+ if (icon) icon.classList.replace('fa-sun', 'fa-moon');
467
  }
468
  }
469
 
 
565
  const colorLabel = document.querySelector('label[for="colorSelect"]');
566
  colorSelect.innerHTML = '';
567
 
568
+ const validColors = product.colors ? product.colors.filter(c => c && String(c).trim() !== "") : [];
569
 
570
  if (validColors.length > 0) {
571
  validColors.forEach(color => {
572
  const option = document.createElement('option');
573
+ option.value = String(color).trim();
574
+ option.text = String(color).trim();
575
  colorSelect.appendChild(option);
576
  });
577
  colorSelect.style.display = 'block';
 
590
  }
591
 
592
  function confirmAddToCart() {
593
+ if (selectedProductIndex === null || selectedProductIndex >= products.length) {
594
+ console.error("Invalid selectedProductIndex:", selectedProductIndex);
595
+ alert("Ошибка выбора товара.");
596
+ return;
597
+ }
598
 
599
  const quantityInput = document.getElementById('quantityInput');
600
  const quantity = parseInt(quantityInput.value);
 
664
  cartTotalElement.textContent = '0.00';
665
  } else {
666
  cartContent.innerHTML = cart.map(item => {
667
+ const itemTotal = (item.price || 0) * (item.quantity || 0);
668
  total += itemTotal;
669
  const photoUrl = item.photo
670
  ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${item.photo}`
 
676
  <img src="${photoUrl}" alt="${item.name}">
677
  <div class="cart-item-details">
678
  <strong>${item.name}${colorText}</strong>
679
+ <p class="cart-item-price">${(item.price || 0).toFixed(2)} ${currencyCode} × ${item.quantity || 0}</p>
680
  </div>
681
  <span class="cart-item-total">${itemTotal.toFixed(2)} ${currencyCode}</span>
682
  <button class="cart-item-remove" onclick="removeFromCart('${item.id}')" title="Удалить товар">×</button>
 
714
  return;
715
  }
716
  let total = 0;
717
+ let orderText = "🛍️ *Новый Заказ от Soola Cosmetics* 🛍️\n";
718
+ orderText += "----------------------------------------\n";
719
+ orderText += "*Детали заказа:*\n";
720
+ orderText += "----------------------------------------\n";
721
  cart.forEach((item, index) => {
722
+ const itemTotal = (item.price || 0) * (item.quantity || 0);
723
  total += itemTotal;
724
  const colorText = item.color !== 'N/A' ? ` (${item.color})` : '';
725
+ orderText += `${index + 1}. *${item.name}*${colorText}\n`;
726
+ orderText += ` Кол-во: ${item.quantity || 0}\n`;
727
+ orderText += ` Цена: ${(item.price || 0).toFixed(2)} ${currencyCode}\n`;
728
+ orderText += ` *Сумма: ${itemTotal.toFixed(2)} ${currencyCode}*\n\n`;
729
  });
730
+ orderText += "----------------------------------------\n";
731
+ orderText += `*ИТОГО: ${total.toFixed(2)} ${currencyCode}*\n`;
732
+ orderText += "----------------------------------------\n\n";
733
 
734
  if (userInfo && userInfo.login) {
735
+ orderText += "*Данные клиента:*\n";
736
+ orderText += `Имя: ${userInfo.first_name || ''} ${userInfo.last_name || ''}\n`;
737
+ orderText += `Логин: ${userInfo.login}\n`;
738
  if (userInfo.phone) {
739
+ orderText += `Телефон: ${userInfo.phone}\n`;
740
  }
741
+ orderText += `Страна: ${userInfo.country || 'Не указана'}\n`;
742
+ orderText += `Город: ${userInfo.city || 'Не указан'}\n`;
743
  } else {
744
+ orderText += "*Клиент не авторизован*\n";
745
  }
746
+ orderText += "----------------------------------------\n\n";
747
 
748
  const now = new Date();
749
  const dateTimeString = now.toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' });
750
+ orderText += `Дата заказа: ${dateTimeString}\n`;
751
  orderText += `_Сформировано автоматически_`;
752
 
753
  const whatsappNumber = "996997703090";
 
766
  if (existingNoResults) existingNoResults.remove();
767
 
768
  document.querySelectorAll('.products-grid .product').forEach(productElement => {
769
+ const name = productElement.getAttribute('data-name') || '';
770
+ const description = productElement.getAttribute('data-description') || '';
771
+ const category = productElement.getAttribute('data-category') || '';
772
 
773
  const matchesSearch = !searchTerm || name.includes(searchTerm) || description.includes(searchTerm);
774
  const matchesCategory = activeCategory === 'all' || category === activeCategory;
 
781
  }
782
  });
783
 
784
+ const hasProductsInitially = products && products.length > 0;
785
+ if (visibleProducts === 0 && hasProductsInitially) {
786
  const p = document.createElement('p');
787
  p.className = 'no-results-message';
788
  p.textContent = 'По вашему запросу товары не найдены.';
789
  grid.appendChild(p);
790
+ } else if (!hasProductsInitially && !grid.querySelector('.no-results-message')) {
791
  const p = document.createElement('p');
792
  p.className = 'no-results-message';
793
  p.textContent = 'Товары пока не добавлены.';
794
  grid.appendChild(p);
795
  }
796
+ // Refresh products array from DOM in case it changed (e.g., admin edits) - less efficient but safer
797
+ // Or better: ensure products array is updated via admin actions or page reload
798
+ // For now, relying on initial load and page reloads after admin actions.
799
  }
800
 
801
  function setupFilters() {
 
811
  filterProducts();
812
  });
813
  });
814
+ filterProducts();
815
  }
816
 
817
  function showNotification(message, duration = 3000) {
818
  const placeholder = document.getElementById('notification-placeholder');
819
+ if (!placeholder) {
820
+ console.warn("Notification placeholder not found");
821
+ return;
822
+ }
823
 
824
  const notification = document.createElement('div');
825
  notification.className = 'notification';
 
836
 
837
  document.addEventListener('DOMContentLoaded', () => {
838
  applyInitialTheme();
839
+ // attemptAutoLogin(); // Removed as it causes reload issues, rely on session
840
  updateCartButton();
841
  setupFilters();
842
 
 
853
  });
854
  }
855
  });
856
+
857
+ // Re-assign products if needed, especially if modified elsewhere
858
+ // products = {{ products|tojson }}; // Re-fetch potentially updated list
859
+ // Re-initialize filter if needed after potential updates
860
+ filterProducts();
861
  });
862
 
863
  </script>
 
880
  def product_detail(index):
881
  data = load_data()
882
  all_products = data.get('products', [])
883
+ # Filter and sort again here to ensure consistency with the catalog view
884
  products_in_stock = [p for p in all_products if p.get('in_stock', True)]
885
  products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
886
 
887
  is_authenticated = 'user' in session
888
  try:
889
+ if not (0 <= index < len(products_sorted)):
890
+ raise IndexError("Index out of bounds for available products.")
891
  product = products_sorted[index]
892
+ except IndexError as e:
893
+ logging.warning(f"Product detail access error: {e} (Index: {index})")
 
 
894
  return "Товар не найден или отсутствует в наличии.", 404
895
 
896
  detail_html = '''
 
930
  {% endif %}
931
  <p><strong>Описание:</strong><br> {{ product.get('description', 'Описание отсутствует.')|replace('\\n', '<br>')|safe }}</p>
932
  {% set colors = product.get('colors', []) %}
933
+ {% set valid_colors = colors|select('string')|select('ne', '')|list %}
934
+ {% if valid_colors|length > 0 %}
935
+ <p><strong>Доступные цвета/варианты:</strong> {{ valid_colors|join(', ') }}</p>
936
  {% endif %}
937
  </div>
938
  </div>
 
1007
  'city': user_info.get('city', ''),
1008
  'phone': user_info.get('phone', '')
1009
  }
1010
+ logging.info(f"User {login} logged in successfully.")
1011
+ # No need for localStorage auto-login here, session handles it
1012
+ return redirect(url_for('catalog'))
 
 
 
 
 
 
 
 
1013
  else:
1014
+ logging.warning(f"Failed login attempt for user {login}.")
1015
  error_message = "Неверный логин или пароль."
1016
  return render_template_string(LOGIN_TEMPLATE, error=error_message), 401
1017
 
1018
  return render_template_string(LOGIN_TEMPLATE, error=None)
1019
 
1020
+ # Removed auto_login route as it caused issues and session handles persistence
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1021
 
1022
  @app.route('/logout')
1023
  def logout():
 
1025
  session.pop('user', None)
1026
  session.pop('user_info', None)
1027
  if logged_out_user:
1028
+ logging.info(f"User {logged_out_user} logged out.")
1029
+ # No need for localStorage interaction here
1030
+ return redirect(url_for('catalog'))
1031
+
 
 
 
 
 
 
 
1032
 
1033
  ADMIN_TEMPLATE = '''
1034
  <!DOCTYPE html>
 
1067
  .add-button:hover { background-color: #2f855a; }
1068
  .item-list { display: grid; gap: 20px; }
1069
  .item { background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.07); border: 1px solid #e1f0e9; }
1070
+ .item p { margin: 5px 0; font-size: 0.9rem; color: #44524c; word-break: break-word; }
1071
  .item strong { color: #2d332f; }
1072
  .item .description { font-size: 0.85rem; color: #5e6e68; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
1073
  .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
 
1124
  <button type="submit" class="button" title="Загрузить локальные файлы на Hugging Face"><i class="fas fa-upload"></i> Загрузить БД</button>
1125
  </form>
1126
  <form method="POST" action="{{ url_for('force_download') }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите принудительно скачать данные с сервера? Это перезапишет ваши локальные файлы.');">
1127
+ <button type="submit" class="button download-hf-button" title="Скачать файлы (перезапишет локальные)"><i class="fas fa-download"></i> Скачать БД</button>
1128
  </form>
1129
  </div>
1130
  <p style="font-size: 0.85rem; color: #5e6e68;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
 
1254
  <button type="button" class="button add-color-btn" style="margin-top: 5px;" onclick="addColorInput('add-color-inputs')"><i class="fas fa-palette"></i> Добавить поле для цвета/варианта</button>
1255
  <br>
1256
  <div style="margin-top: 15px;">
1257
+ <input type="checkbox" id="add_in_stock" name="in_stock" value="true" checked>
1258
  <label for="add_in_stock" class="inline-label">В наличии</label>
1259
  </div>
1260
  <div style="margin-top: 5px;">
1261
+ <input type="checkbox" id="add_is_top" name="is_top" value="true">
1262
  <label for="add_is_top" class="inline-label">Топ товар (показывать наверху)</label>
1263
  </div>
1264
  <br>
 
1274
  <div class="item">
1275
  <div style="display: flex; gap: 15px; align-items: flex-start;">
1276
  <div class="photo-preview" style="flex-shrink: 0;">
1277
+ {% if product.get('photos') and product['photos']|length > 0 %}
1278
  <a href="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}" target="_blank" title="Посмотреть первое фото">
1279
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}" alt="Фото">
1280
  </a>
 
1298
  <p><strong>Цена:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p>
1299
  <p class="description" title="{{ product.get('description', '') }}"><strong>Описание:</strong> {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}</p>
1300
  {% set colors = product.get('colors', []) %}
1301
+ {% set valid_colors = colors|select('string')|select('ne', '')|list %}
1302
+ <p><strong>Цвета/Вар-ты:</strong> {{ valid_colors|join(', ') if valid_colors|length > 0 else 'Нет' }}</p>
1303
  {% if product.get('photos') and product['photos']|length > 1 %}
1304
  <p style="font-size: 0.8rem; color: #5e6e68;">(Всего фото: {{ product['photos']|length }})</p>
1305
  {% endif %}
 
1345
  {% endif %}
1346
  <label>Цвета/Варианты:</label>
1347
  <div id="edit-color-inputs-{{ loop.index0 }}">
1348
+ {% set current_colors = product.get('colors', [])|select('string')|select('ne', '')|list %}
1349
+ {% if current_colors|length > 0 %}
1350
  {% for color in current_colors %}
 
1351
  <div class="color-input-group">
1352
  <input type="text" name="colors" value="{{ color }}">
1353
  <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1354
  </div>
 
1355
  {% endfor %}
1356
  {% else %}
1357
  <div class="color-input-group">
 
1363
  <button type="button" class="button add-color-btn" style="margin-top: 5px;" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')"><i class="fas fa-palette"></i> Добавить поле для цвета</button>
1364
  <br>
1365
  <div style="margin-top: 15px;">
1366
+ <input type="checkbox" id="edit_in_stock_{{ loop.index0 }}" name="in_stock" value="true" {% if product.get('in_stock', True) %}checked{% endif %}>
1367
  <label for="edit_in_stock_{{ loop.index0 }}" class="inline-label">В наличии</label>
1368
  </div>
1369
  <div style="margin-top: 5px;">
1370
+ <input type="checkbox" id="edit_is_top_{{ loop.index0 }}" name="is_top" value="true" {% if product.get('is_top', False) %}checked{% endif %}>
1371
  <label for="edit_is_top_{{ loop.index0 }}" class="inline-label">Топ товар</label>
1372
  </div>
1373
  <br>
 
1413
  const group = button.closest('.color-input-group');
1414
  if (group) {
1415
  const container = group.parentNode;
 
 
1416
  group.remove();
1417
+ // If container is now empty, add a placeholder input back
1418
  if (container && container.children.length === 0) {
1419
  const placeholderGroup = document.createElement('div');
1420
  placeholderGroup.className = 'color-input-group';
1421
  placeholderGroup.innerHTML = `
1422
+ <input type="text" name="colors" placeholder="Цвет/вариант">
1423
  <button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
1424
  `;
1425
  container.appendChild(placeholderGroup);
1426
  }
1427
  } else {
1428
+ console.warn("Could not find parent .color-input-group for remove button");
1429
  }
1430
  }
1431
  </script>
 
1435
 
1436
  @app.route('/admin', methods=['GET', 'POST'])
1437
  def admin():
1438
+ # Ensure only logged-in users (or specific admin users if implemented) can access
1439
+ # if 'user' not in session: # Basic check, enhance if roles needed
1440
+ # flash("Доступ запрещен. Пожалуйста, войдите.", "error")
1441
+ # return redirect(url_for('login'))
1442
+
1443
  data = load_data()
 
 
1444
  users = load_users()
1445
+ # Keep original product list with original indices for editing/deleting
1446
+ original_product_list = data.get('products', [])
1447
+ categories = sorted(data.get('categories', [])) # Keep categories sorted for display
1448
 
1449
  if request.method == 'POST':
1450
  action = request.form.get('action')
 
1453
  try:
1454
  if action == 'add_category':
1455
  category_name = request.form.get('category_name', '').strip()
1456
+ if category_name and category_name not in data.get('categories', []):
1457
+ data.setdefault('categories', []).append(category_name)
1458
+ # No need to sort here, will be sorted on next load/display
1459
  save_data(data)
1460
+ logging.info(f"Category '{category_name}' added.")
1461
  flash(f"Категория '{category_name}' успешно добавлена.", 'success')
1462
  elif not category_name:
1463
+ logging.warning("Attempt to add empty category.")
1464
  flash("Название категории не может быть пустым.", 'error')
1465
  else:
1466
+ logging.warning(f"Category '{category_name}' already exists.")
1467
  flash(f"Категория '{category_name}' уже существует.", 'error')
1468
 
1469
  elif action == 'delete_category':
1470
  category_to_delete = request.form.get('category_name')
1471
+ current_categories = data.get('categories', [])
1472
+ if category_to_delete and category_to_delete in current_categories:
1473
+ current_categories.remove(category_to_delete)
1474
  updated_count = 0
1475
+ current_products = data.get('products', [])
1476
+ for product in current_products:
1477
  if product.get('category') == category_to_delete:
1478
  product['category'] = 'Без категории'
1479
  updated_count += 1
1480
  save_data(data)
1481
+ logging.info(f"Category '{category_to_delete}' deleted. Updated products: {updated_count}.")
1482
  flash(f"Категория '{category_to_delete}' удалена.", 'success')
1483
  else:
1484
+ logging.warning(f"Attempt to delete non-existent or empty category: {category_to_delete}")
1485
  flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
1486
 
 
1487
  elif action == 'add_product':
1488
  name = request.form.get('name', '').strip()
1489
  price_str = request.form.get('price', '').replace(',', '.')
 
1491
  category = request.form.get('category')
1492
  photos_files = request.files.getlist('photos')
1493
  colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1494
+ in_stock = request.form.get('in_stock') == 'true' # Checkbox value
1495
+ is_top = request.form.get('is_top') == 'true' # Checkbox value
 
1496
 
1497
  if not name or not price_str:
1498
  flash("Название и цена товара обязательны.", 'error')
 
1500
 
1501
  try:
1502
  price = round(float(price_str), 2)
1503
+ if price < 0: raise ValueError("Price cannot be negative")
1504
  except ValueError:
1505
+ flash("Неверный формат цены или отрицательное значение.", 'error')
1506
  return redirect(url_for('admin'))
1507
 
1508
  photos_list = []
1509
+ if photos_files and any(f.filename for f in photos_files) and HF_TOKEN_WRITE:
1510
  uploads_dir = 'uploads_temp'
1511
  os.makedirs(uploads_dir, exist_ok=True)
1512
  api = HfApi()
1513
  photo_limit = 10
1514
  uploaded_count = 0
1515
  for photo in photos_files:
1516
+ if not photo or not photo.filename: continue
1517
+ if uploaded_count >= photo_limit:
1518
+ logging.warning(f"Photo limit ({photo_limit}) reached, ignoring further files.")
1519
  flash(f"Загружено только первые {photo_limit} фото.", "warning")
1520
  break
1521
+ try:
1522
+ ext = os.path.splitext(photo.filename)[1].lower()
1523
+ if ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp']:
1524
+ logging.warning(f"Skipping non-image file: {photo.filename}")
1525
+ continue
1526
+ safe_base = secure_filename(name.replace(' ','_') or 'product')
1527
+ photo_filename = f"{safe_base}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1528
+ temp_path = os.path.join(uploads_dir, photo_filename)
1529
+ photo.save(temp_path)
1530
+ logging.info(f"Uploading photo {photo_filename} to HF for product {name}...")
1531
+ api.upload_file(
1532
+ path_or_fileobj=temp_path,
1533
+ path_in_repo=f"photos/{photo_filename}",
1534
+ repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
1535
+ commit_message=f"Add photo for product {name}"
1536
+ )
1537
+ photos_list.append(photo_filename)
1538
+ logging.info(f"Photo {photo_filename} uploaded successfully.")
1539
+ os.remove(temp_path)
1540
+ uploaded_count += 1
1541
+ except Exception as e:
1542
+ logging.error(f"Error uploading photo {photo.filename} to HF: {e}", exc_info=True)
1543
+ flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
1544
+ # Optionally remove temp file if upload failed
1545
+ if os.path.exists(temp_path): os.remove(temp_path)
1546
+ try: # Cleanup temp dir
1547
+ if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
1548
+ os.rmdir(uploads_dir)
1549
  except OSError as e:
1550
+ logging.warning(f"Could not remove temp upload dir {uploads_dir}: {e}")
 
1551
 
1552
  new_product = {
1553
  'name': name, 'price': price, 'description': description,
1554
+ 'category': category if category in data.get('categories', []) else 'Без категории',
1555
  'photos': photos_list, 'colors': colors,
1556
  'in_stock': in_stock, 'is_top': is_top
1557
  }
1558
+ data.setdefault('products', []).append(new_product)
 
1559
  save_data(data)
1560
+ logging.info(f"Product '{name}' added.")
1561
  flash(f"Товар '{name}' успешно добавлен.", 'success')
1562
 
1563
  elif action == 'edit_product':
 
1568
 
1569
  try:
1570
  index = int(index_str)
1571
+ # Use the original list loaded at the start of the request
1572
+ if not (0 <= index < len(original_product_list)): raise IndexError("Index out of bounds")
1573
+ product_to_edit = original_product_list[index] # Get ref to the dict in the list
 
 
1574
  original_name = product_to_edit.get('name', 'N/A')
 
1575
  except (ValueError, IndexError):
1576
  flash(f"Ошибка редактирования: неверный индекс товара '{index_str}'.", 'error')
1577
  return redirect(url_for('admin'))
1578
 
1579
+ # Update fields in the dictionary directly
1580
+ product_to_edit['name'] = request.form.get('name', product_to_edit.get('name', '')).strip()
1581
+ price_str = request.form.get('price', str(product_to_edit.get('price', 0))).replace(',', '.')
1582
+ product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
1583
  category = request.form.get('category')
1584
+ product_to_edit['category'] = category if category in data.get('categories', []) else 'Без категории'
1585
  product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
1586
+ product_to_edit['in_stock'] = request.form.get('in_stock') == 'true'
1587
+ product_to_edit['is_top'] = request.form.get('is_top') == 'true'
 
1588
 
1589
  try:
1590
  price = round(float(price_str), 2)
1591
+ if price < 0: raise ValueError("Price cannot be negative")
1592
  product_to_edit['price'] = price
1593
  except ValueError:
1594
+ logging.warning(f"Invalid price format '{price_str}' during edit for {original_name}. Price not changed.")
1595
+ flash(f"Неверный формат цены для товара '{product_to_edit['name']}'. Цена не изменена.", 'warning')
1596
 
1597
  photos_files = request.files.getlist('photos')
1598
+ # Check if any *new* files were actually selected
1599
+ if photos_files and any(f and f.filename for f in photos_files) and HF_TOKEN_WRITE:
1600
  uploads_dir = 'uploads_temp'
1601
  os.makedirs(uploads_dir, exist_ok=True)
1602
  api = HfApi()
1603
  new_photos_list = []
1604
  photo_limit = 10
1605
  uploaded_count = 0
1606
+ logging.info(f"Uploading new photos for product {product_to_edit['name']}...")
1607
  for photo in photos_files:
1608
+ if not photo or not photo.filename: continue
1609
+ if uploaded_count >= photo_limit:
1610
+ logging.warning(f"Photo limit ({photo_limit}) reached, ignoring further files.")
1611
  flash(f"Загружено только первые {photo_limit} фото.", "warning")
1612
  break
1613
+ try:
1614
+ ext = os.path.splitext(photo.filename)[1].lower()
1615
+ if ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp']:
1616
+ logging.warning(f"Skipping non-image file: {photo.filename}")
1617
+ continue
1618
+ safe_base = secure_filename(product_to_edit['name'].replace(' ','_') or 'product')
1619
+ photo_filename = f"{safe_base}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
1620
+ temp_path = os.path.join(uploads_dir, photo_filename)
1621
+ photo.save(temp_path)
1622
+ logging.info(f"Uploading new photo {photo_filename} to HF...")
1623
+ api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}",
1624
+ repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
1625
+ commit_message=f"Update photo for product {product_to_edit['name']}")
1626
+ new_photos_list.append(photo_filename)
1627
+ logging.info(f"New photo {photo_filename} uploaded successfully.")
1628
+ os.remove(temp_path)
1629
+ uploaded_count += 1
1630
+ except Exception as e:
1631
+ logging.error(f"Error uploading new photo {photo.filename}: {e}", exc_info=True)
1632
+ flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error')
1633
+ if os.path.exists(temp_path): os.remove(temp_path)
1634
+ try: # Cleanup temp dir
1635
+ if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
1636
  os.rmdir(uploads_dir)
1637
  except OSError as e:
1638
+ logging.warning(f"Could not remove temp upload dir {uploads_dir}: {e}")
1639
 
1640
+ # Only replace photos if new ones were successfully uploaded
1641
  if new_photos_list:
1642
+ logging.info(f"Replacing photos for product {product_to_edit['name']}.")
1643
  old_photos = product_to_edit.get('photos', [])
1644
  if old_photos:
1645
+ logging.info(f"Attempting to delete old photos from HF: {old_photos}")
1646
  try:
1647
+ # Use ignore_patterns for broader matching if needed, but paths_in_repo is safer
1648
  api.delete_files(
1649
  repo_id=REPO_ID,
1650
+ paths_in_repo=[f"photos/{p}" for p in old_photos if p], # Ensure no empty strings
1651
  repo_type="dataset",
1652
  token=HF_TOKEN_WRITE,
1653
+ commit_message=f"Delete old photos for product {product_to_edit['name']}",
1654
+ missing_ok=True # Don't fail if a photo was already deleted somehow
1655
  )
1656
+ logging.info(f"Old photos deletion command sent for {product_to_edit['name']}.")
1657
  except Exception as e:
1658
+ logging.error(f"Error deleting old photos {old_photos} from HF: {e}", exc_info=True)
1659
  flash("Не удалось удалить старые фотографии с сервера.", "warning")
1660
  product_to_edit['photos'] = new_photos_list
1661
  flash("Фотографии товара успешно обновлены.", "success")
1662
+ elif uploaded_count == 0 and any(f and f.filename for f in photos_files):
1663
+ # Files were selected, but none uploaded (e.g., all invalid format or upload errors)
1664
+ flash("Не удалось загрузить ни одну из выбранн��х новых фотографий.", "error")
1665
+ # If no new files were selected, photos remain unchanged.
1666
 
1667
+ # Now save the entire modified data structure
1668
  save_data(data)
1669
+ logging.info(f"Product '{original_name}' (index {index}) updated to '{product_to_edit['name']}'.")
1670
  flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
1671
 
 
1672
  elif action == 'delete_product':
1673
  index_str = request.form.get('index')
1674
  if index_str is None:
 
1676
  return redirect(url_for('admin'))
1677
  try:
1678
  index = int(index_str)
1679
+ # Use the original list loaded at the start of the request
1680
+ if not (0 <= index < len(original_product_list)): raise IndexError("Index out of bounds")
1681
+ # Remove from the main data structure
1682
+ deleted_product = data.get('products', []).pop(index)
1683
  product_name = deleted_product.get('name', 'N/A')
1684
 
1685
  photos_to_delete = deleted_product.get('photos', [])
1686
  if photos_to_delete and HF_TOKEN_WRITE:
1687
+ logging.info(f"Attempting to delete photos for deleted product '{product_name}' from HF: {photos_to_delete}")
1688
  try:
1689
  api = HfApi()
1690
  api.delete_files(
1691
  repo_id=REPO_ID,
1692
+ paths_in_repo=[f"photos/{p}" for p in photos_to_delete if p],
1693
  repo_type="dataset",
1694
  token=HF_TOKEN_WRITE,
1695
+ commit_message=f"Delete photos for deleted product {product_name}",
1696
+ missing_ok=True
1697
  )
1698
+ logging.info(f"Photos deletion command sent for product '{product_name}'.")
1699
  except Exception as e:
1700
+ logging.error(f"Error deleting photos {photos_to_delete} for product '{product_name}' from HF: {e}", exc_info=True)
1701
  flash(f"Не удалось удалить фото для товара '{product_name}' с сервера.", "warning")
1702
 
1703
  save_data(data)
1704
+ logging.info(f"Product '{product_name}' (original index {index}) deleted.")
1705
  flash(f"Товар '{product_name}' удален.", 'success')
1706
  except (ValueError, IndexError):
1707
  flash(f"Ошибка удаления: неверный индекс товара '{index_str}'.", 'error')
1708
+ except Exception as e:
1709
+ logging.error(f"Error during product deletion: {e}", exc_info=True)
1710
+ flash(f"Произошла ошибка при удалении товара.", 'error')
1711
 
1712
  elif action == 'add_user':
1713
  login = request.form.get('login', '').strip()
1714
+ password = request.form.get('password') # Keep password as is, no stripping
1715
  first_name = request.form.get('first_name', '').strip()
1716
  last_name = request.form.get('last_name', '').strip()
1717
  phone = request.form.get('phone', '').strip()
 
1726
  return redirect(url_for('admin'))
1727
 
1728
  users[login] = {
1729
+ 'password': password, # Store password as provided
1730
  'first_name': first_name, 'last_name': last_name,
1731
  'phone': phone,
1732
  'country': country, 'city': city
1733
  }
1734
  save_users(users)
1735
+ logging.info(f"User '{login}' added.")
1736
  flash(f"Пользователь '{login}' успешно добавлен.", 'success')
1737
 
1738
  elif action == 'delete_user':
 
1740
  if login_to_delete and login_to_delete in users:
1741
  del users[login_to_delete]
1742
  save_users(users)
1743
+ logging.info(f"User '{login_to_delete}' deleted.")
1744
  flash(f"Пользователь '{login_to_delete}' удален.", 'success')
1745
  else:
1746
+ logging.warning(f"Attempt to delete non-existent or empty user: {login_to_delete}")
1747
  flash(f"Не удалось удалить пользователя '{login_to_delete}'.", 'error')
1748
 
1749
  else:
1750
+ logging.warning(f"Received unknown admin action: {action}")
1751
  flash(f"Неизвестное действие: {action}", 'warning')
1752
 
1753
+ # Redirect after POST to prevent form resubmission on refresh
1754
  return redirect(url_for('admin'))
1755
 
1756
  except Exception as e:
1757
+ logging.error(f"Error processing admin action '{action}': {e}", exc_info=True)
1758
  flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
1759
+ # Redirect even on error to avoid broken state
1760
  return redirect(url_for('admin'))
1761
 
1762
+ # GET request: Render the template
1763
+ # Pass the original list to preserve indices for edit/delete forms
1764
+ # Pass sorted categories and users for display
 
 
 
 
 
 
 
1765
  sorted_users = dict(sorted(users.items()))
1766
 
1767
  return render_template_string(
1768
  ADMIN_TEMPLATE,
1769
+ products=original_product_list,
1770
  categories=categories,
1771
  users=sorted_users,
1772
  repo_id=REPO_ID,
 
1775
 
1776
  @app.route('/force_upload', methods=['POST'])
1777
  def force_upload():
1778
+ # Add access control if needed
1779
+ logging.info("Forcing upload to Hugging Face...")
1780
  try:
1781
  upload_db_to_hf()
1782
  flash("Данные успешно загружены на Hugging Face.", 'success')
1783
  except Exception as e:
1784
+ logging.error(f"Error during forced upload: {e}", exc_info=True)
1785
  flash(f"Ошибка при загрузке на Hugging Face: {e}", 'error')
1786
  return redirect(url_for('admin'))
1787
 
1788
  @app.route('/force_download', methods=['POST'])
1789
  def force_download():
1790
+ # Add access control if needed
1791
+ logging.info("Forcing download from Hugging Face...")
1792
  try:
1793
+ if download_db_from_hf():
1794
+ flash("Данные успешно скачаны с Hugging Face. Локальные файлы обновлены.", 'success')
1795
+ # Reload data might be needed if the app holds state, but here it reloads on next request
1796
+ else:
1797
+ flash("Скачивание данных с Hugging Face завершилось с ошибками. Проверьте логи.", 'warning')
1798
+
1799
  except Exception as e:
1800
+ logging.error(f"Error during forced download: {e}", exc_info=True)
1801
  flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error')
1802
  return redirect(url_for('admin'))
1803
 
1804
 
1805
  if __name__ == '__main__':
1806
+ # Initial load on startup
1807
  load_data()
1808
  load_users()
1809
 
1810
+ # Start backup thread only if write token exists
1811
  if HF_TOKEN_WRITE:
1812
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1813
  backup_thread.start()
1814
+ logging.info("Periodic backup thread started.")
1815
  else:
1816
+ logging.warning("HF_TOKEN not set, periodic backup thread will NOT run.")
1817
 
1818
+ # Run the Flask app
1819
  port = int(os.environ.get('PORT', 7860))
1820
+ logging.info(f"Starting Flask app on host 0.0.0.0 port {port}")
1821
+ # Use Waitress or Gunicorn in production instead of development server
1822
  app.run(debug=False, host='0.0.0.0', port=port)
1823
 
1824
+