Aleksmorshen commited on
Commit
6f80d8b
·
verified ·
1 Parent(s): 128db4a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +784 -1054
app.py CHANGED
@@ -1,62 +1,80 @@
1
- from flask import Flask, render_template_string, request, redirect, url_for
2
- import json
 
3
  import os
4
- import logging
5
- import threading
 
 
 
6
  import time
7
  from datetime import datetime
 
8
  from huggingface_hub import HfApi, hf_hub_download
9
  from huggingface_hub.utils import RepositoryNotFoundError
10
- from werkzeug.utils import secure_filename
11
 
12
- app = Flask(__name__)
13
- DATA_FILE = 'data_luxoff.json'
 
 
 
14
 
15
- # Настройки Hugging Face
16
- REPO_ID = "flpolprojects/Clients"
17
- HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
18
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
19
 
20
- # Ссылка на логотип
21
- LOGO_URL = "https://cdn-avatars.huggingface.co/v1/production/uploads/63b6a710ccebeadccc864577/j1KeCVskZ3ZxI7oYQuu7o.png"
 
 
 
22
 
23
- # Настройка логирования
24
- logging.basicConfig(level=logging.DEBUG)
25
 
26
- def load_data():
27
- try:
28
- download_db_from_hf()
29
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
30
- data = json.load(file)
31
- logging.info("Данные успешно загружены из JSON")
32
- if not isinstance(data, dict) or 'products' not in data or 'categories' not in data:
33
- return {'products': [], 'categories': [] if not isinstance(data, list) else data}
34
- return data
35
- except FileNotFoundError:
36
- logging.warning("Локальный файл базы данных не найден после скачивания.")
37
- return {'products': [], 'categories': []}
38
- except json.JSONDecodeError:
39
- logging.error("Ошибка: Невозможно декодировать JSON файл.")
40
- return {'products': [], 'categories': []}
41
- except RepositoryNotFoundError:
42
- logging.error("Репозиторий не найден. Создание локальной базы данных.")
43
- return {'products': [], 'categories': []}
44
- except Exception as e:
45
- logging.error(f"Произошла ошибка при загрузке данных: {e}")
46
- return {'products': [], 'categories': []}
47
 
48
- def save_data(data):
 
 
 
 
 
 
 
 
49
  try:
50
- with open(DATA_FILE, 'w', encoding='utf-8') as file:
51
- json.dump(data, file, ensure_ascii=False, indent=4)
52
- logging.info("Данные успешно сохранены в JSON")
53
- upload_db_to_hf()
 
 
 
 
 
 
 
 
 
 
 
54
  except Exception as e:
55
- logging.error(f"Ошибка при сохранении данных: {e}")
56
- raise
 
 
 
 
57
 
58
  def upload_db_to_hf():
 
 
 
 
 
 
59
  try:
 
60
  api = HfApi()
61
  api.upload_file(
62
  path_or_fileobj=DATA_FILE,
@@ -64,1049 +82,761 @@ def upload_db_to_hf():
64
  repo_id=REPO_ID,
65
  repo_type="dataset",
66
  token=HF_TOKEN_WRITE,
67
- commit_message=f"Автоматическое резервное копирование базы данных {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
68
  )
69
- logging.info("Резервная копия JSON базы успешно загружена на Hugging Face.")
 
70
  except Exception as e:
71
- logging.error(f"Ошибка при загрузке резервной копии: {e}")
 
72
 
73
- def download_db_from_hf():
 
 
 
 
74
  try:
75
- hf_hub_download(
76
- repo_id=REPO_ID,
77
- filename=DATA_FILE,
78
- repo_type="dataset",
79
- token=HF_TOKEN_READ,
80
- local_dir=".",
81
- local_dir_use_symlinks=False
82
- )
83
- logging.info("JSON база успешно скачана из Hugging Face.")
84
- except RepositoryNotFoundError as e:
85
- logging.error(f"Репозиторий не найден: {e}")
86
- raise
 
 
87
  except Exception as e:
88
- logging.error(f"Ошибка при скачивании JSON базы: {e}")
89
- raise
90
 
91
- def periodic_backup():
92
- while True:
 
 
 
 
93
  upload_db_to_hf()
94
- time.sleep(15)
 
95
 
96
- @app.route('/')
97
- def catalog():
98
- data = load_data()
99
- products = data['products']
100
- categories = data['categories']
101
-
102
- catalog_html = '''
103
- <!DOCTYPE html>
104
- <html lang="ru">
105
- <head>
106
- <meta charset="UTF-8">
107
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
108
- <title>LUXoff женская одежда оптом </title>
109
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
110
- <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
111
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
112
- <style>
113
- * {
114
- margin: 0;
115
- padding: 0;
116
- box-sizing: border-box;
117
- }
118
- body {
119
- font-family: 'Poppins', sans-serif;
120
- background: linear-gradient(135deg, #f0f2f5, #e9ecef);
121
- color: #2d3748;
122
- line-height: 1.6;
123
- transition: background 0.3s, color 0.3s;
124
- }
125
- body.dark-mode {
126
- background: linear-gradient(135deg, #1a202c, #2d3748);
127
- color: #e2e8f0;
128
- }
129
- .container {
130
- max-width: 1300px;
131
- margin: 0 auto;
132
- padding: 20px;
133
- }
134
- .header {
135
- display: flex;
136
- justify-content: space-between;
137
- align-items: center;
138
- padding: 15px 0;
139
- border-bottom: 1px solid #e2e8f0;
140
- }
141
- .header-logo {
142
- width: 60px;
143
- height: 60px;
144
- border-radius: 50%;
145
- object-fit: cover;
146
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
147
- transition: transform 0.3s ease, box-shadow 0.3s ease;
148
- }
149
- .header-logo:hover {
150
- transform: scale(1.1);
151
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
152
- }
153
- .header h1 {
154
- font-size: 1.5rem;
155
- font-weight: 600;
156
- margin-left: 15px;
157
- }
158
- .theme-toggle {
159
- background: none;
160
- border: none;
161
- font-size: 1.5rem;
162
- cursor: pointer;
163
- color: #4a5568;
164
- transition: color 0.3s ease;
165
- }
166
- .theme-toggle:hover {
167
- color: #3b82f6;
168
- }
169
- .filters-container {
170
- margin: 20px 0;
171
- display: flex;
172
- flex-wrap: wrap;
173
- gap: 10px;
174
- justify-content: center;
175
- }
176
- .search-container {
177
- margin: 20px 0;
178
- text-align: center;
179
- }
180
- #search-input {
181
- width: 90%;
182
- max-width: 600px;
183
- padding: 12px 18px;
184
- font-size: 1rem;
185
- border: 1px solid #e2e8f0;
186
- border-radius: 8px;
187
- outline: none;
188
- box-shadow: 0 2px 5px rgba(0,0,0,0.05);
189
- transition: all 0.3s ease;
190
- }
191
- #search-input:focus {
192
- border-color: #3b82f6;
193
- box-shadow: 0 4px 15px rgba(59, 130, 246, 0.2);
194
- }
195
- .category-filter {
196
- padding: 8px 16px;
197
- border: 1px solid #e2e8f0;
198
- border-radius: 8px;
199
- background-color: #fff;
200
- cursor: pointer;
201
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
202
- font-size: 0.9rem;
203
- font-weight: 400;
204
- }
205
- .category-filter.active, .category-filter:hover {
206
- background-color: #3b82f6;
207
- color: white;
208
- border-color: #3b82f6;
209
- box-shadow: 0 2px 10px rgba(59, 130, 246, 0.3);
210
- }
211
- .products-grid {
212
- display: grid;
213
- grid-template-columns: repeat(2, minmax(200px, 1fr));
214
- gap: 15px;
215
- padding: 10px;
216
- }
217
- .product {
218
- background: #fff;
219
- border-radius: 15px;
220
- padding: 15px;
221
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
222
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease;
223
- overflow: hidden;
224
- }
225
- body.dark-mode .product {
226
- background: #2d3748;
227
- color: #fff;
228
- }
229
- .product:hover {
230
- transform: translateY(-5px) scale(1.02);
231
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
232
- }
233
- .product-image {
234
- width: 100%;
235
- aspect-ratio: 1;
236
- background-color: #fff;
237
- border-radius: 10px;
238
- overflow: hidden;
239
- display: flex;
240
- justify-content: center;
241
- align-items: center;
242
- }
243
- .product-image img {
244
- max-width: 100%;
245
- max-height: 100%;
246
- object-fit: contain;
247
- transition: transform 0.3s ease;
248
- }
249
- .product-image img:hover {
250
- transform: scale(1.1);
251
- }
252
- .product h2 {
253
- font-size: 1rem;
254
- font-weight: 600;
255
- margin: 10px 0;
256
- text-align: center;
257
- white-space: nowrap;
258
- overflow: hidden;
259
- text-overflow: ellipsis;
260
- }
261
- .product-price {
262
- font-size: 1.1rem;
263
- color: #ef4444;
264
- font-weight: 700;
265
- text-align: center;
266
- margin: 5px 0;
267
- }
268
- .product-description {
269
- font-size: 0.8rem;
270
- color: #718096;
271
- text-align: center;
272
- margin-bottom: 15px;
273
- overflow: hidden;
274
- text-overflow: ellipsis;
275
- white-space: nowrap;
276
- }
277
- body.dark-mode .product-description {
278
- color: #a0aec0;
279
- }
280
- .product-button {
281
- display: block;
282
- width: 100%;
283
- padding: 8px;
284
- border: none;
285
- border-radius: 8px;
286
- background-color: #3b82f6;
287
- color: white;
288
- font-size: 0.8rem;
289
- font-weight: 500;
290
- cursor: pointer;
291
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
292
- margin: 5px 0;
293
- text-align: center;
294
- text-decoration: none;
295
- }
296
- .product-button:hover {
297
- background-color: #2563eb;
298
- box-shadow: 0 4px 15px rgba(37, 99, 235, 0.4);
299
- transform: translateY(-2px);
300
- }
301
- .add-to-cart {
302
- background-color: #10b981;
303
- }
304
- .add-to-cart:hover {
305
- background-color: #059669;
306
- box-shadow: 0 4px 15px rgba(5, 150, 105, 0.4);
307
- }
308
- #cart-button {
309
- position: fixed;
310
- bottom: 20px;
311
- right: 20px;
312
- background-color: #ef4444;
313
- color: white;
314
- border: none;
315
- border-radius: 50%;
316
- width: 50px;
317
- height: 50px;
318
- font-size: 1.2rem;
319
- cursor: pointer;
320
- display: none;
321
- box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
322
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
323
- z-index: 1000;
324
- }
325
- .modal {
326
- display: none;
327
- position: fixed;
328
- z-index: 1001;
329
- left: 0;
330
- top: 0;
331
- width: 100%;
332
- height: 100%;
333
- background-color: rgba(0,0,0,0.5);
334
- backdrop-filter: blur(5px);
335
- }
336
- .modal-content {
337
- background: #fff;
338
- margin: 5% auto;
339
- padding: 20px;
340
- border-radius: 15px;
341
- width: 90%;
342
- max-width: 700px;
343
- box-shadow: 0 10px 30px rgba(0,0,0,0.2);
344
- animation: slideIn 0.3s ease-out;
345
- }
346
- body.dark-mode .modal-content {
347
- background: #2d3748;
348
- color: #e2e8f0;
349
- }
350
- @keyframes slideIn {
351
- from { transform: translateY(-50px); opacity: 0; }
352
- to { transform: translateY(0); opacity: 1; }
353
- }
354
- .close {
355
- float: right;
356
- font-size: 1.5rem;
357
- color: #718096;
358
- cursor: pointer;
359
- transition: color 0.3s;
360
- }
361
- .close:hover {
362
- color: #2d3748;
363
- }
364
- body.dark-mode .close {
365
- color: #a0aec0;
366
- }
367
- body.dark-mode .close:hover {
368
- color: #fff;
369
- }
370
- .cart-item {
371
- display: flex;
372
- justify-content: space-between;
373
- align-items: center;
374
- padding: 15px 0;
375
- border-bottom: 1px solid #e2e8f0;
376
- }
377
- body.dark-mode .cart-item {
378
- border-bottom: 1px solid #4a5568;
379
- }
380
- .cart-item img {
381
- width: 50px;
382
- height: 50px;
383
- object-fit: contain;
384
- border-radius: 8px;
385
- margin-right: 15px;
386
- }
387
- .quantity-input, .color-select {
388
- width: 100%;
389
- max-width: 150px;
390
- padding: 8px;
391
- border: 1px solid #e2e8f0;
392
- border-radius: 8px;
393
- font-size: 1rem;
394
- margin: 5px 0;
395
- }
396
- .clear-cart {
397
- background-color: #ef4444;
398
- }
399
- .clear-cart:hover {
400
- background-color: #dc2626;
401
- box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
402
- }
403
- .order-button {
404
- background-color: #10b981;
405
- }
406
- .order-button:hover {
407
- background-color: #059669;
408
- box-shadow: 0 4px 15px rgba(5, 150, 105, 0.4);
409
- }
410
- </style>
411
- </head>
412
- <body>
413
- <div class="container">
414
  <div class="header">
415
- <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
416
- <h1>Каталог</h1>
417
- <button class="theme-toggle" onclick="toggleTheme()">
418
- <i class="fas fa-moon"></i>
419
- </button>
420
  </div>
421
- <div class="filters-container">
422
- <button class="category-filter active" data-category="all">Все категории</button>
423
- {% for category in categories %}
424
- <button class="category-filter" data-category="{{ category }}">{{ category }}</button>
425
- {% endfor %}
426
  </div>
427
- <div class="search-container">
428
- <input type="text" id="search-input" placeholder="Поиск товаров...">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  </div>
430
- <div class="products-grid" id="products-grid">
431
- {% for product in products %}
432
- <div class="product"
433
- data-name="{{ product['name']|lower }}"
434
- data-description="{{ product['description']|lower }}"
435
- data-category="{{ product.get('category', 'Без категории') }}">
436
- {% if product.get('photos') and product['photos']|length > 0 %}
437
- <div class="product-image">
438
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}"
439
- alt="{{ product['name'] }}"
440
- loading="lazy">
441
- </div>
442
- {% endif %}
443
- <h2>{{ product['name'] }}</h2>
444
- <div class="product-price">{{ product['price'] }} с</div>
445
- <p class="product-description">{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}</p>
446
- <button class="product-button" onclick="openModal({{ loop.index0 }})">Подробнее</button>
447
- <button class="product-button add-to-cart" onclick="openQuantityModal({{ loop.index0 }})">В корзину</button>
 
 
 
 
448
  </div>
449
- {% endfor %}
450
  </div>
451
- </div>
452
 
453
- <!-- Product Modal -->
454
- <div id="productModal" class="modal">
455
- <div class="modal-content">
456
- <span class="close" onclick="closeModal('productModal')">×</span>
457
- <div id="modalContent"></div>
458
  </div>
459
- </div>
460
-
461
- <!-- Quantity and Color Modal -->
462
- <div id="quantityModal" class="modal">
463
- <div class="modal-content">
464
- <span class="close" onclick="closeModal('quantityModal')">×</span>
465
- <h2>Укажите количество и цвет</h2>
466
- <input type="number" id="quantityInput" class="quantity-input" min="1" value="1">
467
- <select id="colorSelect" class="color-select"></select>
468
- <button class="product-button" onclick="confirmAddToCart()">Добавить</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  </div>
470
- </div>
471
 
472
- <!-- Cart Modal -->
473
- <div id="cartModal" class="modal">
474
- <div class="modal-content">
475
- <span class="close" onclick="closeModal('cartModal')">×</span>
476
- <h2>Корзина</h2>
477
- <div id="cartContent"></div>
478
- <div style="margin-top: 20px; text-align: right;">
479
- <strong>Итого: <span id="cartTotal">0</span> с</strong>
480
- <button class="product-button clear-cart" onclick="clearCart()">Очистить</button>
481
- <button class="product-button order-button" onclick="orderViaWhatsApp()">Заказать</button>
482
- </div>
483
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
  </div>
485
 
486
- <button id="cart-button" onclick="openCartModal()">🛒</button>
487
-
488
- <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
489
- <script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
490
- <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
491
- <script>
492
- const products = {{ products|tojson }};
493
- let selectedProductIndex = null;
494
-
495
- function toggleTheme() {
496
- document.body.classList.toggle('dark-mode');
497
- const icon = document.querySelector('.theme-toggle i');
498
- icon.classList.toggle('fa-moon');
499
- icon.classList.toggle('fa-sun');
500
- localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light');
501
- }
502
-
503
- if (localStorage.getItem('theme') === 'dark') {
504
- document.body.classList.add('dark-mode');
505
- document.querySelector('.theme-toggle i').classList.replace('fa-moon', 'fa-sun');
506
- }
507
-
508
- function openModal(index) {
509
- loadProductDetails(index);
510
- document.getElementById('productModal').style.display = "block";
511
- }
512
-
513
- function closeModal(modalId) {
514
- document.getElementById(modalId).style.display = "none";
515
- }
516
-
517
- function loadProductDetails(index) {
518
- fetch('/product/' + index)
519
- .then(response => response.text())
520
- .then(data => {
521
- document.getElementById('modalContent').innerHTML = data;
522
- initializeSwiper();
523
- })
524
- .catch(error => console.error('Ошибка:', error));
525
- }
526
-
527
- function initializeSwiper() {
528
- new Swiper('.swiper-container', {
529
- slidesPerView: 1,
530
- spaceBetween: 20,
531
- loop: true,
532
- grabCursor: true,
533
- pagination: { el: '.swiper-pagination', clickable: true },
534
- navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
535
- zoom: { maxRatio: 3 }
536
- });
537
- }
538
-
539
- function openQuantityModal(index) {
540
- selectedProductIndex = index;
541
- const product = products[index];
542
- const colorSelect = document.getElementById('colorSelect');
543
- colorSelect.innerHTML = '';
544
- if (product.colors && product.colors.length > 0) {
545
- product.colors.forEach(color => {
546
- const option = document.createElement('option');
547
- option.value = color;
548
- option.text = color;
549
- colorSelect.appendChild(option);
550
- });
551
- } else {
552
- const option = document.createElement('option');
553
- option.value = 'Нет цвета';
554
- option.text = 'Нет цвета';
555
- colorSelect.appendChild(option);
556
- }
557
- document.getElementById('quantityModal').style.display = 'block';
558
- document.getElementById('quantityInput').value = 1;
559
- }
560
-
561
- function confirmAddToCart() {
562
- if (selectedProductIndex === null) return;
563
- const quantity = parseInt(document.getElementById('quantityInput').value) || 1;
564
- const color = document.getElementById('colorSelect').value;
565
- if (quantity <= 0) {
566
- alert("Укажите количество больше 0");
567
- return;
568
- }
569
- let cart = JSON.parse(localStorage.getItem('cart') || '[]');
570
- const product = products[selectedProductIndex];
571
- const cartItemId = `${product.name}-${color}`;
572
- const existingItem = cart.find(item => item.id === cartItemId);
573
-
574
- if (existingItem) {
575
- existingItem.quantity += quantity;
576
- } else {
577
- cart.push({
578
- id: cartItemId,
579
- name: product.name,
580
- price: product.price,
581
- photo: product.photos && product.photos.length > 0 ? product.photos[0] : '',
582
- quantity: quantity,
583
- color: color
584
- });
585
- }
586
-
587
- localStorage.setItem('cart', JSON.stringify(cart));
588
- closeModal('quantityModal');
589
- updateCartButton();
590
- }
591
-
592
- function updateCartButton() {
593
- const cart = JSON.parse(localStorage.getItem('cart') || '[]');
594
- document.getElementById('cart-button').style.display = cart.length > 0 ? 'block' : 'none';
595
- }
596
-
597
- function openCartModal() {
598
- const cart = JSON.parse(localStorage.getItem('cart') || '[]');
599
- const cartContent = document.getElementById('cartContent');
600
- let total = 0;
601
-
602
- cartContent.innerHTML = cart.length === 0 ? '<p>Корзина пуста</p>' : cart.map(item => {
603
- const itemTotal = item.price * item.quantity;
604
- total += itemTotal;
605
- return `
606
- <div class="cart-item">
607
- <div style="display: flex; align-items: center;">
608
- ${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/${item.photo}" alt="${item.name}">` : ''}
609
- <div>
610
- <strong>${item.name}</strong>
611
- <p>${item.price} с × ${item.quantity} (Цвет: ${item.color})</p>
612
- </div>
613
- </div>
614
- <span>${itemTotal} с</span>
615
  </div>
616
- `;
617
- }).join('');
618
-
619
- document.getElementById('cartTotal').textContent = total;
620
- document.getElementById('cartModal').style.display = 'block';
621
- }
622
-
623
- function orderViaWhatsApp() {
624
- const cart = JSON.parse(localStorage.getItem('cart') || '[]');
625
- if (cart.length === 0) {
626
- alert("Корзина пуста!");
627
- return;
628
- }
629
- let total = 0;
630
- let orderText = "Заказ:%0A";
631
- cart.forEach((item, index) => {
632
- const itemTotal = item.price * item.quantity;
633
- total += itemTotal;
634
- orderText += `${index + 1}. ${item.name} - ${item.price} с × ${item.quantity} (Цвет: ${item.color})%0A`;
635
- });
636
- orderText += `Итого: ${total} с`;
637
- window.open(`https://api.whatsapp.com/send?phone=996556563359&text=${orderText}`, '_blank');
638
- }
639
-
640
- function clearCart() {
641
- localStorage.removeItem('cart');
642
- closeModal('cartModal');
643
- updateCartButton();
644
- }
645
-
646
- window.onclick = function(event) {
647
- if (event.target.className === 'modal') event.target.style.display = "none";
648
- }
649
-
650
- document.getElementById('search-input').addEventListener('input', filterProducts);
651
- document.querySelectorAll('.category-filter').forEach(filter => {
652
- filter.addEventListener('click', function() {
653
- document.querySelectorAll('.category-filter').forEach(f => f.classList.remove('active'));
654
- this.classList.add('active');
655
- filterProducts();
656
- });
657
- });
658
-
659
- function filterProducts() {
660
- const searchTerm = document.getElementById('search-input').value.toLowerCase();
661
- const activeCategory = document.querySelector('.category-filter.active').dataset.category;
662
- document.querySelectorAll('.product').forEach(product => {
663
- const name = product.getAttribute('data-name');
664
- const description = product.getAttribute('data-description');
665
- const category = product.getAttribute('data-category');
666
- const matchesSearch = name.includes(searchTerm) || description.includes(searchTerm);
667
- const matchesCategory = activeCategory === 'all' || category === activeCategory;
668
- product.style.display = matchesSearch && matchesCategory ? 'block' : 'none';
669
- });
670
- }
671
-
672
- updateCartButton();
673
- </script>
674
- </body>
675
- </html>
676
- '''
677
- return render_template_string(catalog_html, products=products, categories=categories, repo_id=REPO_ID)
678
-
679
- @app.route('/product/<int:index>')
680
- def product_detail(index):
681
- data = load_data()
682
- products = data['products']
683
- try:
684
- product = products[index]
685
- except IndexError:
686
- return "Продукт не найден", 404
687
- detail_html = '''
688
- <div class="container" style="padding: 20px;">
689
- <h2 style="font-size: 1.8rem; font-weight: 600; margin-bottom: 20px;">{{ product['name'] }}</h2>
690
- <div class="swiper-container" style="max-width: 400px; margin: 0 auto 20px;">
691
- <div class="swiper-wrapper">
692
- {% if product.get('photos') %}
693
- {% for photo in product['photos'] %}
694
- <div class="swiper-slide" style="background-color: #fff; display: flex; justify-content: center; align-items: center;">
695
- <div class="swiper-zoom-container">
696
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
697
- alt="{{ product['name'] }}"
698
- style="max-width: 100%; max-height: 300px; object-fit: contain;">
699
  </div>
700
- </div>
701
  {% endfor %}
702
- {% else %}
703
- <div class="swiper-slide">
704
- <img src="https://via.placeholder.com/300" alt="No Image">
705
- </div>
706
- {% endif %}
707
  </div>
708
- <div class="swiper-pagination"></div>
709
- <div class="swiper-button-next"></div>
710
- <div class="swiper-button-prev"></div>
711
- </div>
712
- <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
713
- <p><strong>Цена:</strong> {{ product['price'] }} с</p>
714
- <p><strong>Описание:</strong> {{ product['description'] }}</p>
715
- <p><strong>Доступные цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p>
716
  </div>
717
- '''
718
- return render_template_string(detail_html, product=product, repo_id=REPO_ID)
719
-
720
- @app.route('/admin', methods=['GET', 'POST'])
721
- def admin():
722
- data = load_data()
723
- products = data['products']
724
- categories = data['categories']
725
-
726
- if request.method == 'POST':
727
- action = request.form.get('action')
728
-
729
- if action == 'add_category':
730
- category_name = request.form.get('category_name')
731
- if category_name and category_name not in categories:
732
- categories.append(category_name)
733
- save_data(data)
734
- return redirect(url_for('admin'))
735
- return "Ошибка: Категория уже существует или не указано название", 400
736
-
737
- elif action == 'delete_category':
738
- category_index = int(request.form.get('category_index'))
739
- deleted_category = categories.pop(category_index)
740
- for product in products:
741
- if product.get('category') == deleted_category:
742
- product['category'] = 'Без категории'
743
- save_data(data)
744
- return redirect(url_for('admin'))
745
-
746
- elif action == 'add':
747
- name = request.form.get('name')
748
- price = request.form.get('price')
749
- description = request.form.get('description')
750
- category = request.form.get('category')
751
- photos_files = request.files.getlist('photos')
752
- colors = request.form.getlist('colors')
753
- photos_list = []
754
-
755
- if photos_files:
756
- for photo in photos_files[:10]: # Ограничение до 10 фото
757
- if photo and photo.filename:
758
- photo_filename = secure_filename(photo.filename)
759
- uploads_dir = 'uploads'
760
- os.makedirs(uploads_dir, exist_ok=True)
761
- temp_path = os.path.join(uploads_dir, photo_filename)
762
- photo.save(temp_path)
763
- api = HfApi()
764
- api.upload_file(
765
- path_or_fileobj=temp_path,
766
- path_in_repo=f"photos/{photo_filename}",
767
- repo_id=REPO_ID,
768
- repo_type="dataset",
769
- token=HF_TOKEN_WRITE,
770
- commit_message=f"Добавлено фото для товара {name}"
771
- )
772
- photos_list.append(photo_filename)
773
- if os.path.exists(temp_path):
774
- os.remove(temp_path)
775
-
776
- if not name or not price or not description:
777
- return "Ошибка: Заполните все обязательные поля", 400
778
-
779
- price = float(price.replace(',', '.'))
780
- new_product = {
781
- 'name': name,
782
- 'price': price,
783
- 'description': description,
784
- 'category': category if category in categories else 'Без категории',
785
- 'photos': photos_list,
786
- 'colors': colors if colors else []
787
- }
788
- products.append(new_product)
789
- save_data(data)
790
- return redirect(url_for('admin'))
791
-
792
- elif action == 'edit':
793
- index = int(request.form.get('index'))
794
- name = request.form.get('name')
795
- price = request.form.get('price')
796
- description = request.form.get('description')
797
- category = request.form.get('category')
798
- photos_files = request.files.getlist('photos')
799
- colors = request.form.getlist('colors')
800
-
801
- if photos_files and any(photo.filename for photo in photos_files):
802
- new_photos_list = []
803
- for photo in photos_files[:10]: # Ограничение до 10 фото
804
- if photo and photo.filename:
805
- photo_filename = secure_filename(photo.filename)
806
- uploads_dir = 'uploads'
807
- os.makedirs(uploads_dir, exist_ok=True)
808
- temp_path = os.path.join(uploads_dir, photo_filename)
809
- photo.save(temp_path)
810
- api = HfApi()
811
- api.upload_file(
812
- path_or_fileobj=temp_path,
813
- path_in_repo=f"photos/{photo_filename}",
814
- repo_id=REPO_ID,
815
- repo_type="dataset",
816
- token=HF_TOKEN_WRITE,
817
- commit_message=f"Обновлено фото для товара {name}"
818
- )
819
- new_photos_list.append(photo_filename)
820
- if os.path.exists(temp_path):
821
- os.remove(temp_path)
822
- products[index]['photos'] = new_photos_list
823
-
824
- products[index]['name'] = name
825
- products[index]['price'] = float(price.replace(',', '.'))
826
- products[index]['description'] = description
827
- products[index]['category'] = category if category in categories else 'Без категории'
828
- products[index]['colors'] = colors if colors else []
829
- save_data(data)
830
- return redirect(url_for('admin'))
831
-
832
- elif action == 'delete':
833
- index = int(request.form.get('index'))
834
- del products[index]
835
- save_data(data)
836
- return redirect(url_for('admin'))
837
-
838
- admin_html = '''
839
- <!DOCTYPE html>
840
- <html lang="ru">
841
- <head>
842
- <meta charset="UTF-8">
843
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
844
- <title>Админ-панель</title>
845
- <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
846
- <style>
847
- body {
848
- font-family: 'Poppins', sans-serif;
849
- background: linear-gradient(135deg, #f0f2f5, #e9ecef);
850
- color: #2d3748;
851
- padding: 20px;
852
- }
853
- .container {
854
- max-width: 1200px;
855
- margin: 0 auto;
856
- }
857
- .header {
858
- display: flex;
859
- align-items: center;
860
- padding: 15px 0;
861
- border-bottom: 1px solid #e2e8f0;
862
- }
863
- .header-logo {
864
- width: 60px;
865
- height: 60px;
866
- border-radius: 50%;
867
- object-fit: cover;
868
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
869
- transition: transform 0.3s ease, box-shadow 0.3s ease;
870
- margin-right: 15px;
871
- }
872
- .header-logo:hover {
873
- transform: scale(1.1);
874
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
875
- }
876
- h1, h2 {
877
- font-weight: 600;
878
- margin-bottom: 20px;
879
- }
880
- form {
881
- background: #fff;
882
- padding: 20px;
883
- border-radius: 15px;
884
- box-shadow: 0 4px 15px rgba(0,0,0,0.1);
885
- margin-bottom: 30px;
886
- }
887
- label {
888
- font-weight: 500;
889
- margin-top: 15px;
890
- display: block;
891
- }
892
- input, textarea, select {
893
- width: 100%;
894
- padding: 12px;
895
- margin-top: 5px;
896
- border: 1px solid #e2e8f0;
897
- border-radius: 8px;
898
- font-size: 1rem;
899
- transition: all 0.3s ease;
900
- }
901
- input:focus, textarea:focus, select:focus {
902
- border-color: #3b82f6;
903
- box-shadow: 0 0 5px rgba(59, 130, 246, 0.3);
904
- outline: none;
905
- }
906
- button {
907
- padding: 12px 20px;
908
- border: none;
909
- border-radius: 8px;
910
- background-color: #3b82f6;
911
- color: white;
912
- font-weight: 500;
913
- cursor: pointer;
914
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
915
- margin-top: 15px;
916
- }
917
- button:hover {
918
- background-color: #2563eb;
919
- box-shadow: 0 4px 15px rgba(37, 99, 235, 0.4);
920
- transform: translateY(-2px);
921
- }
922
- .delete-button {
923
- background-color: #ef4444;
924
- }
925
- .delete-button:hover {
926
- background-color: #dc2626;
927
- box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
928
- }
929
- .product-list, .category-list {
930
- display: grid;
931
- gap: 20px;
932
- }
933
- .product-item, .category-item {
934
- background: #fff;
935
- padding: 20px;
936
- border-radius: 15px;
937
- box-shadow: 0 4px 15px rgba(0,0,0,0.1);
938
- }
939
- .edit-form {
940
- margin-top: 15px;
941
- padding: 15px;
942
- background: #f7fafc;
943
- border-radius: 10px;
944
- }
945
- .color-input-group {
946
- display: flex;
947
- gap: 10px;
948
- margin-top: 5px;
949
- }
950
- .add-color-btn {
951
- background-color: #10b981;
952
- }
953
- .add-color-btn:hover {
954
- background-color: #059669;
955
- }
956
- </style>
957
- </head>
958
- <body>
959
- <div class="container">
960
- <div class="header">
961
- <img src="''' + LOGO_URL + '''" alt="Logo" class="header-logo">
962
- <h1>Админ-панель</h1>
963
- </div>
964
- <h1>Добавление товара</h1>
965
- <form method="POST" enctype="multipart/form-data">
966
- <input type="hidden" name="action" value="add">
967
- <label>Название товара:</label>
968
- <input type="text" name="name" required>
969
- <label>Цена:</label>
970
- <input type="number" name="price" step="0.01" required>
971
- <label>Описание:</label>
972
- <textarea name="description" rows="4" required></textarea>
973
- <label>Категория:</label>
974
- <select name="category">
975
- <option value="Без категории">Без категории</option>
976
- {% for category in categories %}
977
- <option value="{{ category }}">{{ category }}</option>
978
- {% endfor %}
979
- </select>
980
- <label>Фотографии (до 10):</label>
981
- <input type="file" name="photos" accept="image/*" multiple>
982
- <label>Цвета:</label>
983
- <div id="color-inputs">
984
- <div class="color-input-group">
985
- <input type="text" name="colors" placeholder="Например: Красный">
986
- </div>
987
- </div>
988
- <button type="button" class="add-color-btn" onclick="addColorInput()">Добавить цвет</button>
989
- <button type="submit">Добавить товар</button>
990
- </form>
991
 
992
- <h1>Управление категориями</h1>
993
- <form method="POST">
994
- <input type="hidden" name="action" value="add_category">
995
- <label>Название категории:</label>
996
- <input type="text" name="category_name" required>
997
- <button type="submit">Добавить</button>
998
- </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
999
 
1000
- <h2>Список категорий</h2>
1001
- <div class="category-list">
1002
- {% for category in categories %}
1003
- <div class="category-item">
1004
- <h3>{{ category }}</h3>
1005
- <form method="POST" style="display: inline;">
1006
- <input type="hidden" name="action" value="delete_category">
1007
- <input type="hidden" name="category_index" value="{{ loop.index0 }}">
1008
- <button type="submit" class="delete-button">Удалить</button>
1009
- </form>
1010
- </div>
1011
- {% endfor %}
1012
- </div>
1013
 
1014
- <h2>Управление базой данных</h2>
1015
- <form method="POST" action="{{ url_for('backup') }}" style="display: inline;">
1016
- <button type="submit">Создать копию</button>
1017
- </form>
1018
- <form method="GET" action="{{ url_for('download') }}" style="display: inline;">
1019
- <button type="submit">Скачать базу</button>
1020
- </form>
1021
 
1022
- <h2>Список товаров</h2>
1023
- <div class="product-list">
1024
- {% for product in products %}
1025
- <div class="product-item">
1026
- <h3>{{ product['name'] }}</h3>
1027
- <p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
1028
- <p><strong>Цена:</strong> {{ product['price'] }} с</p>
1029
- <p><strong>Описание:</strong> {{ product['description'] }}</p>
1030
- <p><strong>Цвета:</strong> {{ product.get('colors', ['Нет цветов'])|join(', ') }}</p>
1031
- {% if product.get('photos') and product['photos']|length > 0 %}
1032
- <div style="display: flex; flex-wrap: wrap; gap: 10px;">
1033
- {% for photo in product['photos'] %}
1034
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}"
1035
- alt="{{ product['name'] }}"
1036
- style="max-width: 100px; border-radius: 10px;">
1037
- {% endfor %}
1038
- </div>
1039
- {% endif %}
1040
- <details>
1041
- <summary>Редактировать</summary>
1042
- <form method="POST" enctype="multipart/form-data" class="edit-form">
1043
- <input type="hidden" name="action" value="edit">
1044
- <input type="hidden" name="index" value="{{ loop.index0 }}">
1045
- <label>Название:</label>
1046
- <input type="text" name="name" value="{{ product['name'] }}" required>
1047
- <label>Цена:</label>
1048
- <input type="number" name="price" step="0.01" value="{{ product['price'] }}" required>
1049
- <label>Описание:</label>
1050
- <textarea name="description" rows="4" required>{{ product['description'] }}</textarea>
1051
- <label>Категория:</label>
1052
- <select name="category">
1053
- <option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
1054
- {% for category in categories %}
1055
- <option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
1056
- {% endfor %}
1057
- </select>
1058
- <label>Фотографии (до 10):</label>
1059
- <input type="file" name="photos" accept="image/*" multiple>
1060
- <label>Цвета:</label>
1061
- <div id="edit-color-inputs-{{ loop.index0 }}">
1062
- {% for color in product.get('colors', []) %}
1063
- <div class="color-input-group">
1064
- <input type="text" name="colors" value="{{ color }}">
1065
- </div>
1066
- {% endfor %}
1067
- </div>
1068
- <button type="button" class="add-color-btn" onclick="addColorInput('edit-color-inputs-{{ loop.index0 }}')">Добавить цвет</button>
1069
- <button type="submit">Сохранить</button>
1070
- </form>
1071
- </details>
1072
- <form method="POST">
1073
- <input type="hidden" name="action" value="delete">
1074
- <input type="hidden" name="index" value="{{ loop.index0 }}">
1075
- <button type="submit" class="delete-button">Удалить</button>
1076
- </form>
1077
- </div>
1078
- {% endfor %}
1079
- </div>
1080
- </div>
1081
- <script>
1082
- function addColorInput(containerId = 'color-inputs') {
1083
- const container = document.getElementById(containerId);
1084
- const newInput = document.createElement('div');
1085
- newInput.className = 'color-input-group';
1086
- newInput.innerHTML = '<input type="text" name="colors" placeholder="Например: Красный">';
1087
- container.appendChild(newInput);
1088
- }
1089
- </script>
1090
- </body>
1091
- </html>
1092
- '''
1093
- return render_template_string(admin_html, products=products, categories=categories, repo_id=REPO_ID)
1094
 
1095
  @app.route('/backup', methods=['POST'])
1096
  def backup():
1097
- upload_db_to_hf()
1098
- return "Резервная копия создана.", 200
1099
-
1100
- @app.route('/download', methods=['GET'])
 
 
 
 
1101
  def download():
1102
- download_db_from_hf()
1103
- return "База данных скачана.", 200
 
 
 
 
 
 
 
 
1104
 
1105
  if __name__ == '__main__':
1106
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1107
- backup_thread.start()
1108
- try:
1109
- load_data()
1110
- except Exception as e:
1111
- logging.error(f"Не удалось загрузить базу данных: {e}")
1112
- app.run(debug=True, host='0.0.0.0', port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
  import os
5
+ from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for
6
+ import hmac
7
+ import hashlib
8
+ import json
9
+ from urllib.parse import unquote, parse_qs, quote
10
  import time
11
  from datetime import datetime
12
+ import logging
13
  from huggingface_hub import HfApi, hf_hub_download
14
  from huggingface_hub.utils import RepositoryNotFoundError
 
15
 
16
+ # Configuration
17
+ BOT_TOKEN = os.getenv("BOT_TOKEN", "7566834146:AAGiG4MaTZZvvbTVsqEJVG5SYK5hUlc_Ewo") # Use env var or default
18
+ HOST = '0.0.0.0'
19
+ PORT = 7860
20
+ DATA_FILE = 'data.json' # File to store visited user data
21
 
22
+ # Hugging Face Settings
23
+ REPO_ID = os.getenv("HF_REPO_ID", "Morshen/TeleUserData") # Your HF repo ID (e.g., YourUsername/YourDatasetName)
24
+ HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") # HF Write Token from env
25
+ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ", HF_TOKEN_WRITE) # HF Read Token from env (can be same as write)
26
 
27
+ # Check if essential HF config is present
28
+ if not HF_TOKEN_WRITE:
29
+ print("WARNING: HF_TOKEN_WRITE environment variable not set. Hugging Face uploads will fail.")
30
+ if not REPO_ID:
31
+ print("WARNING: HF_REPO_ID environment variable not set. Using default 'Morshen/TeleUserData'.")
32
 
 
 
33
 
34
+ app = Flask(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
+ # Setup logging
37
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
38
+
39
+ # --- Hugging Face Sync Functions ---
40
+
41
+ def download_db_from_hf():
42
+ if not HF_TOKEN_READ or not REPO_ID:
43
+ logging.warning("Skipping Hugging Face download: Read token or Repo ID missing.")
44
+ return False
45
  try:
46
+ logging.info(f"Attempting to download {DATA_FILE} from Hugging Face repo {REPO_ID}")
47
+ hf_hub_download(
48
+ repo_id=REPO_ID,
49
+ filename=DATA_FILE,
50
+ repo_type="dataset",
51
+ token=HF_TOKEN_READ,
52
+ local_dir=".",
53
+ local_dir_use_symlinks=False,
54
+ force_download=True # Ensure latest version
55
+ )
56
+ logging.info(f"{DATA_FILE} successfully downloaded from Hugging Face.")
57
+ return True
58
+ except RepositoryNotFoundError:
59
+ logging.warning(f"Hugging Face repository {REPO_ID} not found. Will create local {DATA_FILE} if needed.")
60
+ return False
61
  except Exception as e:
62
+ # Specifically check for 404 which might mean the *file* doesn't exist yet
63
+ if "404 Client Error" in str(e):
64
+ logging.warning(f"{DATA_FILE} not found in Hugging Face repository {REPO_ID}. Will create local file if needed.")
65
+ else:
66
+ logging.error(f"Error downloading {DATA_FILE} from Hugging Face: {e}")
67
+ return False
68
 
69
  def upload_db_to_hf():
70
+ if not HF_TOKEN_WRITE or not REPO_ID:
71
+ logging.warning("Skipping Hugging Face upload: Write token or Repo ID missing.")
72
+ return False
73
+ if not os.path.exists(DATA_FILE):
74
+ logging.warning(f"Skipping Hugging Face upload: Local file {DATA_FILE} does not exist.")
75
+ return False
76
  try:
77
+ logging.info(f"Attempting to upload {DATA_FILE} to Hugging Face repo {REPO_ID}")
78
  api = HfApi()
79
  api.upload_file(
80
  path_or_fileobj=DATA_FILE,
 
82
  repo_id=REPO_ID,
83
  repo_type="dataset",
84
  token=HF_TOKEN_WRITE,
85
+ commit_message=f"Update user data {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
86
  )
87
+ logging.info(f"{DATA_FILE} successfully uploaded to Hugging Face.")
88
+ return True
89
  except Exception as e:
90
+ logging.error(f"Error uploading {DATA_FILE} to Hugging Face: {e}")
91
+ return False
92
 
93
+ # --- Data Handling ---
94
+
95
+ def load_data():
96
+ # Try to download the latest version first
97
+ download_db_from_hf()
98
  try:
99
+ if os.path.exists(DATA_FILE):
100
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
101
+ data = json.load(file)
102
+ if not isinstance(data, dict): # Ensure it's a dictionary
103
+ logging.warning(f"{DATA_FILE} does not contain a valid dictionary. Initializing empty data.")
104
+ return {}
105
+ logging.info(f"Data successfully loaded from local {DATA_FILE}")
106
+ return data
107
+ else:
108
+ logging.info(f"Local file {DATA_FILE} not found. Initializing empty data.")
109
+ return {} # Return empty dict if file doesn't exist
110
+ except json.JSONDecodeError:
111
+ logging.error(f"Error decoding JSON from {DATA_FILE}. Returning empty data.")
112
+ return {}
113
  except Exception as e:
114
+ logging.error(f"Error loading data from {DATA_FILE}: {e}")
115
+ return {}
116
 
117
+ def save_data(data):
118
+ try:
119
+ with open(DATA_FILE, 'w', encoding='utf-8') as file:
120
+ json.dump(data, file, ensure_ascii=False, indent=4)
121
+ logging.info(f"Data successfully saved to local {DATA_FILE}")
122
+ # Attempt to upload after saving locally
123
  upload_db_to_hf()
124
+ except Exception as e:
125
+ logging.error(f"Error saving data to {DATA_FILE}: {e}")
126
 
127
+ # --- Telegram Verification ---
128
+
129
+ def verify_telegram_data(init_data_str):
130
+ try:
131
+ parsed_data = parse_qs(init_data_str)
132
+ received_hash = parsed_data.pop('hash', [None])[0]
133
+
134
+ if not received_hash:
135
+ logging.warning("Verification failed: Hash missing in initData.")
136
+ return None, False
137
+
138
+ data_check_list = []
139
+ for key, value in sorted(parsed_data.items()):
140
+ # Values from parse_qs are lists, take the first element
141
+ data_check_list.append(f"{key}={value[0]}")
142
+ data_check_string = "\n".join(data_check_list)
143
+
144
+ secret_key = hmac.new("WebAppData".encode(), BOT_TOKEN.encode(), hashlib.sha256).digest()
145
+ calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
146
+
147
+ if calculated_hash == received_hash:
148
+ auth_date = int(parsed_data.get('auth_date', [0])[0])
149
+ current_time = int(time.time())
150
+ # Allow slightly older data, maybe network latency. Adjust 3600 (1 hour) as needed.
151
+ if current_time - auth_date > 3600:
152
+ logging.warning(f"Telegram InitData is older than 1 hour (Auth Date: {auth_date}, Current: {current_time}).")
153
+ # Log successful verification without printing sensitive parts
154
+ logging.info("Telegram data verified successfully.")
155
+ return parsed_data, True
156
+ else:
157
+ logging.warning(f"Data verification failed. Calculated hash mismatch.")
158
+ # Avoid logging hashes directly in production for security
159
+ # logging.debug(f"Calculated: {calculated_hash}, Received: {received_hash}")
160
+ return parsed_data, False
161
+ except Exception as e:
162
+ logging.error(f"Error verifying Telegram data: {e}")
163
+ return None, False
164
+
165
+ # --- Templates ---
166
+
167
+ TEMPLATE = """
168
+ <!DOCTYPE html>
169
+ <html lang="ru">
170
+ <head>
171
+ <meta charset="UTF-8">
172
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no">
173
+ <title>Morshen Group</title>
174
+ <script src="https://telegram.org/js/telegram-web-app.js"></script>
175
+ <style>
176
+ :root {
177
+ --bg-color: #1c1c1e;
178
+ --card-bg: #2c2c2e;
179
+ --text-color: #ffffff;
180
+ --text-secondary-color: #a0a0a5;
181
+ --accent-gradient: linear-gradient(90deg, #007aff, #5856d6);
182
+ --accent-gradient-green: linear-gradient(90deg, #34c759, #30d158);
183
+ --tag-bg: rgba(255, 255, 255, 0.1);
184
+ --border-radius-s: 8px;
185
+ --border-radius-m: 12px;
186
+ --border-radius-l: 16px;
187
+ --padding-s: 8px;
188
+ --padding-m: 16px;
189
+ --padding-l: 24px;
190
+ --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
191
+ }
192
+ * { box-sizing: border-box; margin: 0; padding: 0; }
193
+ html { background-color: var(--bg-color); }
194
+ body {
195
+ font-family: var(--font-family);
196
+ background: linear-gradient(180deg, #1a2a3a 0%, #101820 100%);
197
+ color: var(--text-color);
198
+ padding: var(--padding-m);
199
+ padding-bottom: 100px;
200
+ overscroll-behavior-y: none;
201
+ -webkit-font-smoothing: antialiased;
202
+ -moz-osx-font-smoothing: grayscale;
203
+ visibility: hidden; /* Hide until ready */
204
+ }
205
+ .container { max-width: 600px; margin: 0 auto; display: flex; flex-direction: column; gap: var(--padding-l); }
206
+ .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--padding-m); }
207
+ .logo { display: flex; align-items: center; gap: var(--padding-s); }
208
+ .logo img, .logo-icon {
209
+ width: 40px;
210
+ height: 40px;
211
+ border-radius: 50%;
212
+ background-color: var(--card-bg);
213
+ object-fit: cover;
214
+ border: 1px solid rgba(255, 255, 255, 0.1);
215
+ }
216
+ .logo-icon { display: inline-flex; align-items: center; justify-content: center; font-weight: bold; color: var(--text-secondary-color); }
217
+ .logo span { font-size: 1.4em; font-weight: 600; }
218
+ .btn {
219
+ display: inline-flex; align-items: center; justify-content: center;
220
+ padding: 10px var(--padding-m); border-radius: var(--border-radius-m);
221
+ background: var(--accent-gradient); color: var(--text-color);
222
+ text-decoration: none; font-weight: 500; border: none; cursor: pointer;
223
+ transition: opacity 0.2s ease; gap: 6px; font-size: 0.95em;
224
+ }
225
+ .btn:hover { opacity: 0.9; }
226
+ .btn-secondary { background: var(--card-bg); color: var(--accent-gradient-start, #007aff); }
227
+ .btn-secondary:hover { background: rgba(44, 44, 46, 0.8); }
228
+ .tag {
229
+ display: inline-block; background: var(--tag-bg); color: var(--text-secondary-color);
230
+ padding: 4px 10px; border-radius: var(--border-radius-s); font-size: 0.8em;
231
+ margin: 4px 4px 4px 0; white-space: nowrap;
232
+ }
233
+ .tag i { margin-right: 4px; opacity: 0.7; }
234
+ .section-card {
235
+ background-color: var(--card-bg); border-radius: var(--border-radius-l);
236
+ padding: var(--padding-m); margin-bottom: var(--padding-l);
237
+ }
238
+ .section-title { font-size: 1.8em; font-weight: 700; margin-bottom: var(--padding-s); line-height: 1.2; }
239
+ .section-subtitle { font-size: 1.1em; font-weight: 500; color: var(--text-secondary-color); margin-bottom: var(--padding-m); }
240
+ .description { font-size: 1em; line-height: 1.5; color: var(--text-secondary-color); margin-bottom: var(--padding-m); }
241
+ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: var(--padding-s); margin-top: var(--padding-m); text-align: center; }
242
+ .stat-item { background-color: rgba(255, 255, 255, 0.05); padding: var(--padding-s); border-radius: var(--border-radius-m); }
243
+ .stat-value { font-size: 1.5em; font-weight: 600; display: block; }
244
+ .stat-label { font-size: 0.85em; color: var(--text-secondary-color); display: block; }
245
+ .list-item { background-color: var(--card-bg); padding: var(--padding-m); border-radius: var(--border-radius-m); margin-bottom: var(--padding-s); display: flex; align-items: center; gap: var(--padding-s); font-size: 1.1em; font-weight: 500; }
246
+ .list-item i { font-size: 1.2em; color: var(--accent-gradient-start, #34c759); opacity: 0.8; }
247
+ .centered-cta { text-align: center; margin-top: var(--padding-l); }
248
+ .footer-greeting { text-align: center; color: var(--text-secondary-color); font-size: 0.9em; margin-top: var(--padding-l); }
249
+ .save-card-button {
250
+ position: fixed;
251
+ bottom: 20px;
252
+ left: 50%;
253
+ transform: translateX(-50%);
254
+ padding: 12px 24px;
255
+ border-radius: 25px;
256
+ background: var(--accent-gradient-green);
257
+ color: var(--text-color);
258
+ text-decoration: none;
259
+ font-weight: 600;
260
+ border: none;
261
+ cursor: pointer;
262
+ transition: opacity 0.2s ease, transform 0.2s ease;
263
+ z-index: 1000;
264
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
265
+ font-size: 1em;
266
+ display: flex;
267
+ align-items: center;
268
+ gap: 8px;
269
+ }
270
+ .save-card-button:hover { opacity: 0.9; transform: translateX(-50%) scale(1.03); }
271
+
272
+ .modal {
273
+ display: none; /* Hidden by default */
274
+ position: fixed; /* Stay in place */
275
+ z-index: 1001; /* Sit on top */
276
+ left: 0;
277
+ top: 0;
278
+ width: 100%; /* Full width */
279
+ height: 100%; /* Full height */
280
+ overflow: auto; /* Enable scroll if needed */
281
+ background-color: rgba(0,0,0,0.6); /* Black w/ opacity */
282
+ padding-top: 60px; /* Location of the box */
283
+ }
284
+ .modal-content {
285
+ background-color: var(--card-bg, #2c2c2e);
286
+ color: var(--text-color, #ffffff);
287
+ margin: 5% auto; /* 5% from the top and centered */
288
+ padding: var(--padding-l, 24px);
289
+ border: 1px solid rgba(255, 255, 255, 0.1);
290
+ width: 85%; /* Could be more or less, depending on screen size */
291
+ max-width: 450px;
292
+ border-radius: var(--border-radius-l, 16px);
293
+ text-align: center;
294
+ position: relative;
295
+ box-shadow: 0 5px 20px rgba(0,0,0,0.3);
296
+ }
297
+ .modal-close {
298
+ color: var(--text-secondary-color, #aaa);
299
+ position: absolute;
300
+ top: 10px;
301
+ right: 15px;
302
+ font-size: 28px;
303
+ font-weight: bold;
304
+ cursor: pointer;
305
+ line-height: 1;
306
+ }
307
+ .modal-close:hover,
308
+ .modal-close:focus {
309
+ color: var(--text-color, #fff);
310
+ text-decoration: none;
311
+ }
312
+ .modal-text {
313
+ font-size: 1.1em;
314
+ line-height: 1.6;
315
+ margin-bottom: var(--padding-m, 16px);
316
+ word-wrap: break-word;
317
+ }
318
+ .modal-instruction {
319
+ font-size: 0.9em;
320
+ color: var(--text-secondary-color, #a0a0a5);
321
+ margin-top: var(--padding-s, 8px);
322
+ }
323
+
324
+ .icon-save::before { content: '💾'; margin-right: 5px; }
325
+ .icon-web::before { content: '🌐'; margin-right: 5px; }
326
+ .icon-mobile::before { content: '📱'; margin-right: 5px; }
327
+ .icon-code::before { content: '💻'; margin-right: 5px; }
328
+ .icon-ai::before { content: '🧠'; margin-right: 5px; }
329
+ .icon-quantum::before { content: '⚛️'; margin-right: 5px; }
330
+ .icon-business::before { content: '💼'; margin-right: 5px; }
331
+ .icon-speed::before { content: '⚡️'; margin-right: 5px; }
332
+ .icon-complexity::before { content: '🧩'; margin-right: 5px; }
333
+ .icon-experience::before { content: '⏳'; margin-right: 5px; }
334
+ .icon-clients::before { content: '👥'; margin-right: 5px; }
335
+ .icon-market::before { content: '📈'; margin-right: 5px; }
336
+ .icon-location::before { content: '📍'; margin-right: 5px; }
337
+ .icon-global::before { content: '🌍'; margin-right: 5px; }
338
+ .icon-innovation::before { content: '💡'; margin-right: 5px; }
339
+ .icon-contact::before { content: '💬'; margin-right: 5px; }
340
+ .icon-link::before { content: '🔗'; margin-right: 5px; }
341
+ .icon-leader::before { content: '🏆'; margin-right: 5px; }
342
+ .icon-company::before { content: '🏢'; margin-right: 5px; }
343
+ </style>
344
+ </head>
345
+ <body>
346
+ <div class="container">
347
+
348
+ <section class="morshen-group-intro">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  <div class="header">
350
+ <div class="logo">
351
+ <img src="https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg" alt="Morshen Group Logo">
352
+ <span>Morshen Group</span>
353
+ </div>
354
+ <a href="#" class="btn contact-link"><i class="icon-contact"></i>Связаться</a>
355
  </div>
356
+ <div>
357
+ <span class="tag"><i class="icon-leader"></i>Лидер инноваций 2025</span>
 
 
 
358
  </div>
359
+ <h1 class="section-title">Международный IT холдинг</h1>
360
+ <p class="description">
361
+ Объединяем передовые технологические компании для создания инновационных
362
+ решений мирового уровня.
363
+ </p>
364
+ <a href="#" class="btn contact-link" style="background: var(--accent-gradient-green); width: 100%; margin-top: var(--padding-s);">
365
+ <i class="icon-contact"></i>Связаться с нами
366
+ </a>
367
+ </section>
368
+
369
+ <section class="ecosystem-header">
370
+ <span class="tag"><i class="icon-company"></i>Наши компании</span>
371
+ <h2 class="section-title">Экосистема инноваций</h2>
372
+ <p class="description">
373
+ В состав холдинга входят компании, специализирующиеся на различных
374
+ направлениях передовых технологий.
375
+ </p>
376
+ </section>
377
+
378
+ <section class="section-card">
379
+ <div class="logo">
380
+ <img src="https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg" alt="Morshen Alpha Logo">
381
+ <span style="font-size: 1.3em; font-weight: 600;">Morshen Alpha</span>
382
  </div>
383
+ <div style="margin: var(--padding-m) 0;">
384
+ <span class="tag"><i class="icon-ai"></i>Искусственный интеллект</span>
385
+ <span class="tag"><i class="icon-quantum"></i>Квантовые технологии</span>
386
+ <span class="tag"><i class="icon-business"></i>Бизнес-решения</span>
387
+ </div>
388
+ <p class="description">
389
+ Флагманская компания холдинга, специализирующаяся на разработке передовых
390
+ бизнес-решений, исследованиях и разработках в сфере искусственного интеллекта
391
+ и квантовых технологий. Наши инновации формируют будущее технологической индустрии.
392
+ </p>
393
+ <div class="stats-grid">
394
+ <div class="stat-item">
395
+ <span class="stat-value"><i class="icon-global"></i> 3</span>
396
+ <span class="stat-label">Страны присутст��ия</span>
397
+ </div>
398
+ <div class="stat-item">
399
+ <span class="stat-value"><i class="icon-clients"></i> 3000+</span>
400
+ <span class="stat-label">Готовых клиентов</span>
401
+ </div>
402
+ <div class="stat-item">
403
+ <span class="stat-value"><i class="icon-market"></i> 5</span>
404
+ <span class="stat-label">Лет на рынке</span>
405
  </div>
 
406
  </div>
407
+ </section>
408
 
409
+ <section class="section-card">
410
+ <div class="logo">
411
+ <img src="https://huggingface.co/spaces/holmgardstudio/dev/resolve/main/image.jpg" alt="Holmgard Logo" style="width: 50px; height: 50px;">
412
+ <span style="font-size: 1.3em; font-weight: 600;">Holmgard</span>
 
413
  </div>
414
+ <div style="margin: var(--padding-m) 0;">
415
+ <span class="tag"><i class="icon-web"></i>Веб-разработка</span>
416
+ <span class="tag"><i class="icon-mobile"></i>Мобильные приложения</span>
417
+ <span class="tag"><i class="icon-code"></i>Программное обеспечение</span>
418
+ </div>
419
+ <p class="description">
420
+ Инновационная студия разработки, создающая высокотехнологичные решения
421
+ для бизнеса любого масштаба. Специализируется на разработке сайтов,
422
+ мобильных приложений и программного обеспечения с использованием
423
+ передовых технологий и методологий.
424
+ </p>
425
+ <div class="stats-grid">
426
+ <div class="stat-item">
427
+ <span class="stat-value"><i class="icon-experience"></i> 10+</span>
428
+ <span class="stat-label">Лет опыта</span>
429
+ </div>
430
+ <div class="stat-item">
431
+ <span class="stat-value"><i class="icon-complexity"></i> ПО</span>
432
+ <span class="stat-label">Любой сложности</span>
433
+ </div>
434
+ <div class="stat-item">
435
+ <span class="stat-value"><i class="icon-speed"></i></span>
436
+ <span class="stat-label">Высокая скорость</span>
437
+ </div>
438
+ </div>
439
+ <div style="display: flex; gap: var(--padding-s); margin-top: var(--padding-m); flex-wrap: wrap;">
440
+ <a href="https://holmgard.ru" target="_blank" class="btn btn-secondary" style="flex-grow: 1;"><i class="icon-link"></i>Перейти на сайт</a>
441
+ <a href="#" class="btn contact-link" style="flex-grow: 1;"><i class="icon-contact"></i>Связаться</a>
442
+ </div>
443
+ </section>
444
+
445
+ <section>
446
+ <span class="tag"><i class="icon-global"></i>Глобальное присутствие</span>
447
+ <h2 class="section-title">Международное присутствие</h2>
448
+ <p class="description">Наши инновационные решения используются в странах:</p>
449
+ <div>
450
+ <div class="list-item"><i class="icon-location"></i>Узбекистан</div>
451
+ <div class="list-item"><i class="icon-location"></i>Казахстан</div>
452
+ <div class="list-item"><i class="icon-location"></i>Кыргызстан</div>
453
  </div>
454
+ </section>
455
 
456
+ <footer class="footer-greeting">
457
+ <p id="greeting">Загрузка данных пользователя...</p>
458
+ </footer>
459
+
460
+ </div>
461
+
462
+ <button class="save-card-button" id="save-card-btn">
463
+ <i class="icon-save"></i>Сохранить визитку
464
+ </button>
465
+
466
+ <div id="saveModal" class="modal">
467
+ <div class="modal-content">
468
+ <span class="modal-close" id="modal-close-btn">×</span>
469
+ <p class="modal-text"><b>+996500398754</b></p>
470
+ <p class="modal-text">Morshen Group, IT компания</p>
471
+ <p class="modal-instruction">Сделайте скриншот экрана, чтобы сохранить контакт.</p>
472
+ </div>
473
+ </div>
474
+
475
+ <script>
476
+ const tg = window.Telegram.WebApp;
477
+
478
+ function setupTelegram() {
479
+ if (!tg || !tg.initData) {
480
+ console.error("Telegram WebApp script not loaded or initData is missing.");
481
+ const greetingElement = document.getElementById('greeting');
482
+ if(greetingElement) greetingElement.textContent = 'Не удалось загрузить данные Telegram.';
483
+ document.body.style.visibility = 'visible';
484
+ return;
485
+ }
486
+
487
+ tg.ready();
488
+ tg.expand();
489
+
490
+ const themeParams = tg.themeParams;
491
+ document.documentElement.style.setProperty('--bg-color', themeParams.bg_color || '#1c1c1e');
492
+ document.documentElement.style.setProperty('--text-color', themeParams.text_color || '#ffffff');
493
+ document.documentElement.style.setProperty('--hint-color', themeParams.hint_color || '#a0a0a5');
494
+ document.documentElement.style.setProperty('--link-color', themeParams.link_color || '#007aff');
495
+ document.documentElement.style.setProperty('--button-color', themeParams.button_color || '#007aff');
496
+ document.documentElement.style.setProperty('--button-text-color', themeParams.button_text_color || '#ffffff');
497
+ document.documentElement.style.setProperty('--card-bg', themeParams.secondary_bg_color || '#2c2c2e');
498
+ if (themeParams.button_color) {
499
+ document.documentElement.style.setProperty('--accent-gradient-start', themeParams.button_color);
500
+ // Adjust button gradients if needed based on theme
501
+ document.querySelectorAll('.btn').forEach(btn => {
502
+ if (!btn.classList.contains('btn-secondary') && !btn.style.background.includes('green')) { // Avoid overriding special buttons
503
+ btn.style.background = themeParams.button_color;
504
+ }
505
+ });
506
+ document.querySelectorAll('.save-card-button').forEach(btn => {
507
+ // Optional: theme the save button too, or keep it green
508
+ // btn.style.background = themeParams.button_color;
509
+ });
510
+ }
511
+
512
+ fetch('/verify', {
513
+ method: 'POST',
514
+ headers: {
515
+ 'Content-Type': 'application/json',
516
+ },
517
+ body: JSON.stringify({ initData: tg.initData }),
518
+ })
519
+ .then(response => response.json())
520
+ .then(data => {
521
+ if (data.status === 'ok' && data.verified) {
522
+ console.log('Backend verification successful.');
523
+ } else {
524
+ console.warn('Backend verification failed:', data.message);
525
+ // Optionally inform user verification failed if critical
526
+ // tg.showAlert('Не удалось проверить ваши данные.');
527
+ }
528
+ })
529
+ .catch(error => {
530
+ console.error('Error sending initData for verification:', error);
531
+ // tg.showAlert('Произошла ошибка при связи с сервером.');
532
+ });
533
+
534
+ const user = tg.initDataUnsafe?.user;
535
+ const greetingElement = document.getElementById('greeting');
536
+ if (user) {
537
+ const name = user.first_name || user.username || 'User';
538
+ greetingElement.textContent = `Добро пожаловать, ${name}! 👋`;
539
+ } else {
540
+ greetingElement.textContent = 'Добро пожаловать!';
541
+ console.warn('Telegram User data not available (initDataUnsafe.user is empty).');
542
+ }
543
+
544
+ const contactButtons = document.querySelectorAll('.contact-link');
545
+ contactButtons.forEach(button => {
546
+ button.addEventListener('click', (e) => {
547
+ e.preventDefault();
548
+ tg.openTelegramLink('https://t.me/morshenkhan');
549
+ });
550
+ });
551
+
552
+ const modal = document.getElementById("saveModal");
553
+ const saveCardBtn = document.getElementById("save-card-btn");
554
+ const closeBtn = document.getElementById("modal-close-btn");
555
+
556
+ if (saveCardBtn && modal && closeBtn) {
557
+ saveCardBtn.addEventListener('click', (e) => {
558
+ e.preventDefault();
559
+ modal.style.display = "block";
560
+ if (tg.HapticFeedback) {
561
+ tg.HapticFeedback.impactOccurred('light');
562
+ }
563
+ });
564
+
565
+ closeBtn.addEventListener('click', () => {
566
+ modal.style.display = "none";
567
+ });
568
+
569
+ window.addEventListener('click', (event) => {
570
+ if (event.target == modal) {
571
+ modal.style.display = "none";
572
+ }
573
+ });
574
+ } else {
575
+ console.error("Modal elements not found!");
576
+ }
577
+
578
+ document.body.style.visibility = 'visible';
579
+ }
580
+
581
+ if (window.Telegram && window.Telegram.WebApp) {
582
+ setupTelegram();
583
+ } else {
584
+ window.addEventListener('load', setupTelegram);
585
+ setTimeout(() => {
586
+ if (document.body.style.visibility !== 'visible') {
587
+ console.error("Telegram WebApp script fallback timeout triggered.");
588
+ const greetingElement = document.getElementById('greeting');
589
+ if(greetingElement) greetingElement.textContent = 'Ошибка загрузки Telegram.';
590
+ document.body.style.visibility = 'visible';
591
+ }
592
+ }, 3000);
593
+ }
594
+
595
+ </script>
596
+ </body>
597
+ </html>
598
+ """
599
+
600
+ ADMIN_TEMPLATE = """
601
+ <!DOCTYPE html>
602
+ <html lang="ru">
603
+ <head>
604
+ <meta charset="UTF-8">
605
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
606
+ <title>Admin - Visited Users</title>
607
+ <style>
608
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: #f0f2f5; color: #1c1c1e; margin: 0; padding: 20px; }
609
+ h1, h2 { text-align: center; color: #333; font-weight: 600; }
610
+ .container { max-width: 1200px; margin: 20px auto; background-color: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
611
+ .user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; margin-top: 20px; }
612
+ .user-card { background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 15px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); display: flex; flex-direction: column; align-items: center; text-align: center; transition: transform 0.2s ease, box-shadow 0.2s ease; }
613
+ .user-card:hover { transform: translateY(-3px); box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
614
+ .user-card img { width: 70px; height: 70px; border-radius: 50%; margin-bottom: 12px; object-fit: cover; border: 2px solid #ced4da; background-color: #e9ecef; }
615
+ .user-card .name { font-weight: 600; font-size: 1.1em; margin-bottom: 4px; color: #212529; }
616
+ .user-card .username { color: #007bff; margin-bottom: 8px; font-size: 0.95em; word-break: break-all; }
617
+ .user-card .details { font-size: 0.9em; color: #495057; line-height: 1.4; }
618
+ .user-card .timestamp { font-size: 0.8em; color: #6c757d; margin-top: 12px; }
619
+ .no-users { text-align: center; color: #6c757d; margin-top: 30px; font-size: 1.1em; }
620
+ .alert { background-color: #fff3cd; border-left: 6px solid #ffc107; margin-bottom: 20px; padding: 12px 18px; color: #856404; border-radius: 4px; text-align: center; font-weight: 500;}
621
+ .action-buttons { margin-top: 30px; text-align: center; padding-bottom: 10px;}
622
+ .action-buttons button {
623
+ padding: 10px 18px; border: none; border-radius: 8px;
624
+ background-color: #007bff; color: white;
625
+ font-weight: 500; cursor: pointer; margin: 0 10px;
626
+ transition: background-color 0.2s ease, box-shadow 0.2s ease;
627
+ font-size: 0.95em;
628
+ }
629
+ .action-buttons button:hover { background-color: #0056b3; box-shadow: 0 2px 8px rgba(0, 123, 255, 0.4); }
630
+ .action-buttons .backup-btn { background-color: #28a745; }
631
+ .action-buttons .backup-btn:hover { background-color: #218838; box-shadow: 0 2px 8px rgba(40, 167, 69, 0.4); }
632
+ .action-buttons .download-btn { background-color: #17a2b8; }
633
+ .action-buttons .download-btn:hover { background-color: #138496; box-shadow: 0 2px 8px rgba(23, 162, 184, 0.4); }
634
+
635
+ /* Spinner */
636
+ .loader {
637
+ border: 4px solid #f3f3f3; border-radius: 50%; border-top: 4px solid #3498db;
638
+ width: 20px; height: 20px; animation: spin 1s linear infinite;
639
+ display: none; /* Hidden by default */ margin: 5px auto 0;
640
+ }
641
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
642
+ .button-loading .loader { display: inline-block; vertical-align: middle; margin-left: 8px; margin-top: -2px; }
643
+ button:disabled { opacity: 0.7; cursor: not-allowed; }
644
+ </style>
645
+ </head>
646
+ <body>
647
+ <div class="container">
648
+ <h1>Admin Panel</h1>
649
+ <div class="alert">WARNING: This panel is not password protected. Implement authentication for production use.</div>
650
+
651
+ <div class="action-buttons">
652
+ <form id="backup-form" method="POST" action="{{ url_for('backup') }}" style="display: inline;">
653
+ <button type="submit" class="backup-btn" id="backup-btn">Backup to HF <span class="loader"></span></button>
654
+ </form>
655
+ <form id="download-form" method="POST" action="{{ url_for('download') }}" style="display: inline;">
656
+ <button type="submit" class="download-btn" id="download-btn">Download from HF <span class="loader"></span></button>
657
+ </form>
658
  </div>
659
 
660
+ <h2>Visited Users</h2>
661
+ {% if users %}
662
+ <div class="user-grid">
663
+ {% for user in users|sort(attribute='visited_at', reverse=true) %}
664
+ <div class="user-card">
665
+ <img src="{{ user.photo_url if user.photo_url else 'data:image/svg+xml;charset=UTF-8,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 100 100%27%3e%3crect width=%27100%27 height=%27100%27 fill=%27%23e9ecef%27/%3e%3ctext x=%2750%25%27 y=%2755%25%27 dominant-baseline=%27middle%27 text-anchor=%27middle%27 font-size=%2745%27 font-family=%27sans-serif%27 fill=%27%23adb5bd%27%3e?%3c/text%3e%3c/svg%3e' }}" alt="User Avatar">
666
+ <div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
667
+ {% if user.username %}
668
+ <div class="username"><a href="https://t.me/{{ user.username }}" target="_blank" title="Open profile">@{{ user.username }}</a></div>
669
+ {% else %}
670
+ <div class="username">- no username -</div>
671
+ {% endif %}
672
+ <div class="details">
673
+ ID: {{ user.id }} <br>
674
+ Lang: {{ user.language_code or 'N/A' }} <br>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
675
  </div>
676
+ <div class="timestamp">Last Visit: {{ user.visited_at_str }}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  </div>
 
678
  {% endfor %}
 
 
 
 
 
679
  </div>
680
+ {% else %}
681
+ <p class="no-users">No user visit data recorded yet.</p>
682
+ {% endif %}
 
 
 
 
 
683
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
684
 
685
+ <script>
686
+ function handleFormSubmit(formId, buttonId) {
687
+ const form = document.getElementById(formId);
688
+ const button = document.getElementById(buttonId);
689
+ const loader = button.querySelector('.loader');
690
+
691
+ form.addEventListener('submit', function(event) {
692
+ event.preventDefault(); // Prevent default form submission
693
+
694
+ button.disabled = true;
695
+ loader.style.display = 'inline-block';
696
+ button.classList.add('button-loading');
697
+
698
+ fetch(form.action, {
699
+ method: form.method,
700
+ // No body needed for these actions unless sending data
701
+ })
702
+ .then(response => response.json()) // Expect JSON response
703
+ .then(data => {
704
+ alert(data.message || 'Operation completed.'); // Show feedback
705
+ if(formId === 'download-form' && data.success) {
706
+ // Reload page after successful download to show updated data
707
+ window.location.reload();
708
+ }
709
+ })
710
+ .catch(error => {
711
+ console.error('Error:', error);
712
+ alert('An error occurred.');
713
+ })
714
+ .finally(() => {
715
+ // Re-enable button and hide loader regardless of success/failure
716
+ button.disabled = false;
717
+ loader.style.display = 'none';
718
+ button.classList.remove('button-loading');
719
+ });
720
+ });
721
+ }
722
 
723
+ handleFormSubmit('backup-form', 'backup-btn');
724
+ handleFormSubmit('download-form', 'download-btn');
725
+ </script>
726
+ </body>
727
+ </html>
728
+ """
 
 
 
 
 
 
 
729
 
730
+ # --- Flask Routes ---
 
 
 
 
 
 
731
 
732
+ @app.route('/')
733
+ def index():
734
+ return render_template_string(TEMPLATE)
735
+
736
+ @app.route('/verify', methods=['POST'])
737
+ def verify_data():
738
+ start_time = time.time()
739
+ try:
740
+ data = request.get_json()
741
+ init_data_str = data.get('initData')
742
+ if not init_data_str:
743
+ logging.warning("Received request to /verify with missing initData.")
744
+ return jsonify({"status": "error", "message": "Missing initData"}), 400
745
+
746
+ # We parse the data *before* verification to potentially log user ID even on failure
747
+ user_info_dict = {}
748
+ parsed_data, is_valid = verify_telegram_data(init_data_str)
749
+
750
+ if parsed_data and 'user' in parsed_data:
751
+ try:
752
+ # Make sure to decode the URL-encoded JSON string
753
+ user_json_str = unquote(parsed_data['user'][0])
754
+ user_info_dict = json.loads(user_json_str)
755
+ except Exception as e:
756
+ logging.error(f"Could not parse user JSON from initData: {e}")
757
+ user_info_dict = {} # Ensure it's a dict even on error
758
+
759
+ user_id = user_info_dict.get('id')
760
+
761
+ if is_valid:
762
+ if user_id:
763
+ # Load current data, update, and save
764
+ all_user_data = load_data()
765
+ now = time.time()
766
+ # Ensure ID is stored as string key for JSON compatibility
767
+ user_id_str = str(user_id)
768
+ all_user_data[user_id_str] = {
769
+ 'id': user_id,
770
+ 'first_name': user_info_dict.get('first_name'),
771
+ 'last_name': user_info_dict.get('last_name'),
772
+ 'username': user_info_dict.get('username'),
773
+ 'photo_url': user_info_dict.get('photo_url'),
774
+ 'language_code': user_info_dict.get('language_code'),
775
+ 'visited_at': now,
776
+ 'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S UTC') # Use UTC
777
+ }
778
+ save_data(all_user_data) # Save triggers HF upload
779
+ logging.info(f"Verified and recorded visit for user ID: {user_id}. Processing time: {time.time() - start_time:.4f}s")
780
+ else:
781
+ logging.warning("Verification successful, but user ID missing in parsed data.")
782
+
783
+ return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
784
+ else:
785
+ logging.warning(f"Verification failed for user ID: {user_id if user_id else 'Unknown'}. Processing time: {time.time() - start_time:.4f}s")
786
+ return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
787
+
788
+ except Exception as e:
789
+ logging.error(f"Error in /verify endpoint: {e}", exc_info=True)
790
+ return jsonify({"status": "error", "message": "Internal server error"}), 500
791
+
792
+ @app.route('/admin')
793
+ def admin_panel():
794
+ # WARNING: This route is unprotected! Add proper authentication/authorization for production.
795
+ user_data = load_data()
796
+ users_list = list(user_data.values())
797
+ return render_template_string(ADMIN_TEMPLATE, users=users_list)
 
 
 
 
 
 
798
 
799
  @app.route('/backup', methods=['POST'])
800
  def backup():
801
+ # WARNING: Protect this route in production
802
+ success = upload_db_to_hf()
803
+ if success:
804
+ return jsonify({"success": True, "message": "Backup to Hugging Face initiated successfully."}), 200
805
+ else:
806
+ return jsonify({"success": False, "message": "Backup to Hugging Face failed. Check logs."}), 500
807
+
808
+ @app.route('/download', methods=['POST'])
809
  def download():
810
+ # WARNING: Protect this route in production
811
+ success = download_db_from_hf()
812
+ if success:
813
+ return jsonify({"success": True, "message": f"{DATA_FILE} downloaded successfully from Hugging Face. Refresh page to see changes."}), 200
814
+ else:
815
+ # It might fail because the repo/file doesn't exist, which isn't necessarily a server error
816
+ return jsonify({"success": False, "message": f"Failed to download {DATA_FILE} from Hugging Face. It might not exist yet. Check logs."}), 404
817
+
818
+
819
+ # --- Main Execution ---
820
 
821
  if __name__ == '__main__':
822
+ # Initial load attempt on startup
823
+ logging.info("Application starting. Attempting initial data load...")
824
+ load_data()
825
+
826
+ print(f"\n--- SECURITY WARNING ---")
827
+ print(f"The /admin, /backup, /download routes are NOT password protected.")
828
+ print(f"Anyone knowing the URL can access visitor data and trigger backups/downloads.")
829
+ print(f"Implement proper authentication before deploying to production.")
830
+ print(f"------------------------")
831
+ print(f"Starting Flask server on http://{HOST}:{PORT}")
832
+ print(f"User data will be stored in: {DATA_FILE}")
833
+ if REPO_ID and HF_TOKEN_WRITE and HF_TOKEN_READ:
834
+ print(f"Hugging Face sync enabled for repo: {REPO_ID}")
835
+ else:
836
+ print(f"Hugging Face sync DISABLED (missing REPO_ID, HF_TOKEN_WRITE, or HF_TOKEN_READ)")
837
+ print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
838
+
839
+ # Use a production server like Gunicorn or Waitress instead of app.run() for deployment
840
+ # Example using waitress: waitress-serve --host=0.0.0.0 --port=7860 app:app
841
+ # For development:
842
+ app.run(host=HOST, port=PORT, debug=False) # Keep debug=False unless troubleshooting