Aleksmorshen commited on
Commit
bc18334
·
verified ·
1 Parent(s): 20d8d19

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +557 -408
app.py CHANGED
@@ -10,40 +10,36 @@ 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", "flpolprojects/teledata") # 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,
@@ -51,78 +47,93 @@ def download_db_from_hf():
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,
81
  path_in_repo=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
 
@@ -132,12 +143,12 @@ def verify_telegram_data(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
 
@@ -147,16 +158,13 @@ def verify_telegram_data(init_data_str):
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}")
@@ -169,106 +177,198 @@ TEMPLATE = """
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 */
@@ -278,68 +378,102 @@ TEMPLATE = """
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>
@@ -351,56 +485,52 @@ TEMPLATE = """
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>
@@ -409,106 +539,97 @@ TEMPLATE = """
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: {
@@ -522,33 +643,35 @@ TEMPLATE = """
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");
@@ -557,17 +680,16 @@ TEMPLATE = """
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
  });
@@ -575,23 +697,26 @@ TEMPLATE = """
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>
@@ -603,130 +728,141 @@ ADMIN_TEMPLATE = """
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('/')
@@ -735,37 +871,32 @@ def index():
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'),
@@ -773,70 +904,88 @@ def verify_data():
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
 
 
 
 
 
 
 
 
 
 
 
 
10
  import time
11
  from datetime import datetime
12
  import logging
13
+ import threading
14
  from huggingface_hub import HfApi, hf_hub_download
15
+ from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
16
 
17
+ # --- Configuration ---
18
+ BOT_TOKEN = os.getenv("BOT_TOKEN", "7566834146:AAGiG4MaTZZvvbTVsqEJVG5SYK5hUlc_Ewo") # Use environment variable or default
19
  HOST = '0.0.0.0'
20
  PORT = 7860
21
  DATA_FILE = 'data.json' # File to store visited user data
22
 
23
+ # Hugging Face Hub Configuration
24
+ REPO_ID = "flpolprojects/teledata"
25
+ HF_TOKEN = os.getenv("HF_TOKEN") # Write token
26
+ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ", HF_TOKEN) # Read token (defaults to write token if not set)
27
+ BACKUP_INTERVAL = 900 # Seconds (15 minutes)
 
 
 
 
 
 
28
 
29
  app = Flask(__name__)
30
+ app.secret_key = os.urandom(24) # Needed for flash messages or sessions if used later
31
 
32
+ # Logging Setup
33
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
34
 
35
+ # --- Hugging Face Hub Functions ---
36
 
37
  def download_db_from_hf():
38
+ if not HF_TOKEN_READ:
39
+ logging.warning("HF_TOKEN_READ not set. Skipping download from Hugging Face Hub.")
40
  return False
41
  try:
42
+ logging.info(f"Attempting to download {DATA_FILE} from {REPO_ID}...")
43
  hf_hub_download(
44
  repo_id=REPO_ID,
45
  filename=DATA_FILE,
 
47
  token=HF_TOKEN_READ,
48
  local_dir=".",
49
  local_dir_use_symlinks=False,
50
+ force_download=True, # Ensure we get the latest version
51
+ resume_download=False
52
  )
53
+ logging.info(f"{DATA_FILE} successfully downloaded from Hugging Face Hub.")
54
  return True
55
  except RepositoryNotFoundError:
56
+ logging.warning(f"Repository {REPO_ID} not found on Hugging Face Hub. Will use/create local file.")
57
  return False
58
+ except HfHubHTTPError as e:
59
+ if e.response.status_code == 404:
60
+ logging.warning(f"{DATA_FILE} not found in repository {REPO_ID}. Will use/create local file.")
 
61
  else:
62
+ logging.error(f"HTTP error downloading {DATA_FILE} from Hugging Face Hub: {e}")
63
+ return False
64
+ except Exception as e:
65
+ logging.error(f"Error downloading {DATA_FILE} from Hugging Face Hub: {e}")
66
  return False
67
 
68
  def upload_db_to_hf():
69
+ if not HF_TOKEN:
70
+ logging.warning("HF_TOKEN not set. Skipping upload to Hugging Face Hub.")
71
  return False
72
  if not os.path.exists(DATA_FILE):
73
+ logging.warning(f"{DATA_FILE} not found locally. Skipping upload.")
74
  return False
75
  try:
 
76
  api = HfApi()
77
+ logging.info(f"Attempting to upload {DATA_FILE} to {REPO_ID}...")
78
  api.upload_file(
79
  path_or_fileobj=DATA_FILE,
80
  path_in_repo=DATA_FILE,
81
  repo_id=REPO_ID,
82
  repo_type="dataset",
83
+ token=HF_TOKEN,
84
+ commit_message=f"Automated user data backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
85
  )
86
+ logging.info(f"{DATA_FILE} successfully uploaded to Hugging Face Hub.")
87
  return True
88
  except Exception as e:
89
+ logging.error(f"Error uploading {DATA_FILE} to Hugging Face Hub: {e}")
90
  return False
91
 
92
+ def periodic_backup():
93
+ logging.info(f"Starting periodic backup thread. Interval: {BACKUP_INTERVAL} seconds.")
94
+ while True:
95
+ time.sleep(BACKUP_INTERVAL)
96
+ logging.info("Initiating scheduled backup...")
97
+ upload_db_to_hf()
98
+
99
  # --- Data Handling ---
100
 
101
+ def load_users():
102
+ # Attempt download first
103
  download_db_from_hf()
104
+
105
+ if not os.path.exists(DATA_FILE):
106
+ logging.warning(f"{DATA_FILE} not found. Initializing empty user data.")
107
+ return {}
108
  try:
109
+ with open(DATA_FILE, 'r', encoding='utf-8') as f:
110
+ users_data = json.load(f)
111
+ if not isinstance(users_data, dict):
112
+ logging.warning(f"{DATA_FILE} does not contain a valid JSON dictionary. Resetting.")
113
+ return {}
114
+ logging.info(f"Loaded {len(users_data)} user records from {DATA_FILE}.")
115
+ return users_data
 
 
 
 
116
  except json.JSONDecodeError:
117
  logging.error(f"Error decoding JSON from {DATA_FILE}. Returning empty data.")
118
+ # Consider backing up the corrupted file here
119
  return {}
120
  except Exception as e:
121
+ logging.error(f"Error loading user data from {DATA_FILE}: {e}")
122
  return {}
123
 
124
+ def save_users(users_data):
125
  try:
126
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
127
+ json.dump(users_data, f, ensure_ascii=False, indent=4)
128
+ logging.info(f"Saved {len(users_data)} user records to {DATA_FILE}.")
129
+ # Attempt upload after saving locally
130
  upload_db_to_hf()
131
  except Exception as e:
132
+ logging.error(f"Error saving user data to {DATA_FILE}: {e}")
133
+
134
+
135
+ # Load initial data on startup
136
+ visited_users = load_users()
137
 
138
  # --- Telegram Verification ---
139
 
 
143
  received_hash = parsed_data.pop('hash', [None])[0]
144
 
145
  if not received_hash:
146
+ logging.warning("Verification failed: No hash found in initData.")
147
  return None, False
148
 
149
  data_check_list = []
150
  for key, value in sorted(parsed_data.items()):
151
+ # Ensure values are strings before appending
152
  data_check_list.append(f"{key}={value[0]}")
153
  data_check_string = "\n".join(data_check_list)
154
 
 
158
  if calculated_hash == received_hash:
159
  auth_date = int(parsed_data.get('auth_date', [0])[0])
160
  current_time = int(time.time())
161
+ # Allow slightly older data, adjust timeout as needed (e.g., 3600 for 1 hour)
162
+ if current_time - auth_date > 86400: # 24 hours tolerance
163
+ logging.warning(f"Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}).")
164
+ # logging.info("Telegram data verified successfully.")
 
165
  return parsed_data, True
166
  else:
167
+ logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
 
 
168
  return parsed_data, False
169
  except Exception as e:
170
  logging.error(f"Error verifying Telegram data: {e}")
 
177
  <html lang="ru">
178
  <head>
179
  <meta charset="UTF-8">
180
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
181
+ <title>Morshen Group - IT Holding</title>
182
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
183
+ <link rel="preconnect" href="https://fonts.googleapis.com">
184
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
185
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
186
  <style>
187
  :root {
188
+ --tg-theme-bg-color: var(--tg-bg-color, #181a1b);
189
+ --tg-theme-text-color: var(--tg-text-color, #ffffff);
190
+ --tg-theme-hint-color: var(--tg-hint-color, #aaaaaa);
191
+ --tg-theme-link-color: var(--tg-link-color, #8774e1);
192
+ --tg-theme-button-color: var(--tg-button-color, #8774e1);
193
+ --tg-theme-button-text-color: var(--tg-button-text-color, #ffffff);
194
+ --tg-theme-secondary-bg-color: var(--tg-secondary-bg-color, #222425);
195
+
196
+ --bg-color: var(--tg-theme-bg-color);
197
+ --card-bg: var(--tg-theme-secondary-bg-color);
198
+ --text-color: var(--tg-theme-text-color);
199
+ --text-secondary-color: var(--tg-theme-hint-color);
200
+ --accent-color: var(--tg-theme-button-color);
201
+ --accent-text-color: var(--tg-theme-button-text-color);
202
+ --link-color: var(--tg-theme-link-color);
203
+ --green-accent: #34c759;
204
+ --red-accent: #ff3b30;
205
+
206
  --border-radius-s: 8px;
207
  --border-radius-m: 12px;
208
  --border-radius-l: 16px;
209
+ --padding-s: 10px;
210
+ --padding-m: 20px;
211
+ --padding-l: 30px;
212
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
213
+
214
+ --shadow-color: rgba(0, 0, 0, 0.2);
215
+ --card-shadow: 0 4px 15px var(--shadow-color);
216
+ --button-shadow: 0 3px 8px var(--shadow-color);
217
  }
218
+
219
  * { box-sizing: border-box; margin: 0; padding: 0; }
220
+
221
+ html {
222
+ background-color: var(--bg-color);
223
+ color: var(--text-color);
224
  font-family: var(--font-family);
225
+ scroll-behavior: smooth;
226
+ }
227
+
228
+ body {
229
+ background: linear-gradient(180deg, color-mix(in srgb, var(--bg-color) 80%, black) 0%, var(--bg-color) 100%);
230
  color: var(--text-color);
231
  padding: var(--padding-m);
232
+ padding-bottom: 120px; /* Space for fixed button */
233
  overscroll-behavior-y: none;
234
  -webkit-font-smoothing: antialiased;
235
  -moz-osx-font-smoothing: grayscale;
236
+ line-height: 1.6;
237
  visibility: hidden; /* Hide until ready */
238
  }
239
+
240
+ .container {
241
+ max-width: 650px;
242
+ margin: 0 auto;
243
+ display: flex;
244
+ flex-direction: column;
245
+ gap: var(--padding-l);
246
+ }
247
+
248
+ /* Header & Logo */
249
+ .header {
250
+ display: flex;
251
+ justify-content: space-between;
252
+ align-items: center;
253
+ margin-bottom: var(--padding-s);
254
+ }
255
  .logo { display: flex; align-items: center; gap: var(--padding-s); }
256
  .logo img, .logo-icon {
257
+ width: 48px;
258
+ height: 48px;
259
  border-radius: 50%;
260
  background-color: var(--card-bg);
261
  object-fit: cover;
262
+ border: 2px solid rgba(255, 255, 255, 0.1);
263
+ box-shadow: 0 2px 5px var(--shadow-color);
264
  }
265
+ .logo span { font-size: 1.6em; font-weight: 700; }
266
+
267
+ /* Buttons */
268
  .btn {
269
  display: inline-flex; align-items: center; justify-content: center;
270
+ padding: 12px var(--padding-m); border-radius: var(--border-radius-m);
271
+ background: var(--accent-color); color: var(--accent-text-color);
272
+ text-decoration: none; font-weight: 600; border: none; cursor: pointer;
273
+ transition: all 0.25s ease-out; gap: 8px; font-size: 1em;
274
+ box-shadow: var(--button-shadow);
275
+ }
276
+ .btn:hover {
277
+ opacity: 0.9;
278
+ transform: translateY(-2px);
279
+ box-shadow: 0 5px 12px var(--shadow-color);
280
+ }
281
+ .btn-secondary {
282
+ background: var(--card-bg);
283
+ color: var(--accent-color);
284
+ border: 1px solid color-mix(in srgb, var(--accent-color) 50%, transparent);
285
+ }
286
+ .btn-secondary:hover {
287
+ background: color-mix(in srgb, var(--card-bg) 90%, white);
288
+ }
289
+ .btn-green {
290
+ background: var(--green-accent); color: white;
291
+ }
292
+ .btn-green:hover {
293
+ background: color-mix(in srgb, var(--green-accent) 90%, black);
294
+ }
295
+
296
+ /* Tags */
297
+ .tag-container { margin: var(--padding-m) 0; display: flex; flex-wrap: wrap; gap: 8px; }
298
  .tag {
299
+ display: inline-flex; align-items: center; gap: 5px;
300
+ background: color-mix(in srgb, var(--card-bg) 70%, var(--accent-color) 10%);
301
+ color: var(--text-secondary-color);
302
+ padding: 6px 12px; border-radius: var(--border-radius-s); font-size: 0.85em; font-weight: 500;
303
+ border: 1px solid rgba(255, 255, 255, 0.05);
304
  }
305
+ .tag i { opacity: 0.8; }
306
+
307
+ /* Cards */
308
  .section-card {
309
+ background-color: var(--card-bg);
310
+ border-radius: var(--border-radius-l);
311
+ padding: var(--padding-m);
312
+ margin-bottom: 0; /* Removed default bottom margin */
313
+ box-shadow: var(--card-shadow);
314
+ border: 1px solid rgba(255, 255, 255, 0.05);
315
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
316
+ }
317
+ .section-card:hover {
318
+ transform: translateY(-3px);
319
+ box-shadow: 0 8px 25px var(--shadow-color);
320
+ }
321
+
322
+ /* Typography */
323
+ .section-title { font-size: 2em; font-weight: 800; margin-bottom: var(--padding-s); line-height: 1.2; }
324
+ .section-subtitle { font-size: 1.2em; font-weight: 500; color: var(--text-secondary-color); margin-bottom: var(--padding-m); }
325
+ .description { font-size: 1em; line-height: 1.7; color: var(--text-secondary-color); margin-bottom: var(--padding-m); }
326
+
327
+ /* Stats Grid */
328
+ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: var(--padding-s); margin-top: var(--padding-m); text-align: center; }
329
+ .stat-item { background-color: rgba(255, 255, 255, 0.05); padding: var(--padding-s) var(--padding-m); border-radius: var(--border-radius-m); }
330
+ .stat-value { font-size: 1.7em; font-weight: 700; display: block; }
331
+ .stat-label { font-size: 0.8em; color: var(--text-secondary-color); display: block; text-transform: uppercase; letter-spacing: 0.5px; }
332
+
333
+ /* List Items */
334
+ .list-container { display: flex; flex-direction: column; gap: var(--padding-s); margin-top: var(--padding-s); }
335
+ .list-item { background-color: color-mix(in srgb, var(--card-bg) 80%, black); padding: var(--padding-m); border-radius: var(--border-radius-m); display: flex; align-items: center; gap: var(--padding-m); font-size: 1.1em; font-weight: 500; }
336
+ .list-item i { font-size: 1.4em; color: var(--accent-color); opacity: 0.9; width: 25px; text-align: center; }
337
+
338
+ /* Footer */
339
  .footer-greeting { text-align: center; color: var(--text-secondary-color); font-size: 0.9em; margin-top: var(--padding-l); }
340
+
341
+ /* Fixed Button */
342
  .save-card-button {
343
  position: fixed;
344
+ bottom: 25px;
345
  left: 50%;
346
  transform: translateX(-50%);
347
+ padding: 14px 28px;
348
+ border-radius: 30px;
349
+ background: var(--green-accent);
350
+ color: white;
351
  text-decoration: none;
352
+ font-weight: 700;
353
  border: none;
354
  cursor: pointer;
355
+ transition: all 0.3s ease;
356
  z-index: 1000;
357
+ box-shadow: 0 6px 20px rgba(52, 199, 89, 0.4);
358
+ font-size: 1.1em;
359
  display: flex;
360
  align-items: center;
361
+ gap: 10px;
362
+ white-space: nowrap;
363
  }
364
+ .save-card-button:hover {
365
+ opacity: 0.9;
366
+ transform: translateX(-50%) scale(1.05);
367
+ box-shadow: 0 8px 25px rgba(52, 199, 89, 0.5);
368
+ }
369
+ .save-card-button i { font-size: 1.2em; }
370
 
371
+ /* Modal Styles */
372
  .modal {
373
  display: none; /* Hidden by default */
374
  position: fixed; /* Stay in place */
 
378
  width: 100%; /* Full width */
379
  height: 100%; /* Full height */
380
  overflow: auto; /* Enable scroll if needed */
381
+ background-color: rgba(0,0,0,0.7); /* Black w/ opacity */
382
+ backdrop-filter: blur(5px);
383
+ -webkit-backdrop-filter: blur(5px);
384
+ padding-top: 10vh; /* Location of the box */
385
+ animation: fadeIn 0.3s ease-out;
386
  }
387
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
388
  .modal-content {
389
  background-color: var(--card-bg, #2c2c2e);
390
  color: var(--text-color, #ffffff);
391
  margin: 5% auto; /* 5% from the top and centered */
392
+ padding: var(--padding-l, 30px);
393
  border: 1px solid rgba(255, 255, 255, 0.1);
394
+ width: 90%; /* Could be more or less, depending on screen size */
395
+ max-width: 480px;
396
  border-radius: var(--border-radius-l, 16px);
397
  text-align: center;
398
  position: relative;
399
+ box-shadow: 0 10px 30px rgba(0,0,0,0.4);
400
+ animation: slideIn 0.4s ease-out;
401
  }
402
+ @keyframes slideIn { from { transform: translateY(-30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
403
  .modal-close {
404
  color: var(--text-secondary-color, #aaa);
405
  position: absolute;
406
+ top: 15px;
407
+ right: 20px;
408
+ font-size: 32px;
409
  font-weight: bold;
410
  cursor: pointer;
411
  line-height: 1;
412
+ transition: color 0.2s ease;
413
  }
414
  .modal-close:hover,
415
  .modal-close:focus {
416
  color: var(--text-color, #fff);
417
  text-decoration: none;
418
  }
419
+ .modal-title {
420
+ font-size: 1.5em;
421
+ font-weight: 700;
422
+ margin-bottom: var(--padding-s);
423
+ }
424
  .modal-text {
425
+ font-size: 1.2em;
426
  line-height: 1.6;
427
+ margin-bottom: var(--padding-s);
428
  word-wrap: break-word;
429
+ font-weight: 500;
430
+ }
431
+ .modal-text strong {
432
+ font-weight: 700;
433
+ color: var(--accent-color);
434
  }
435
  .modal-instruction {
436
+ font-size: 1em;
437
  color: var(--text-secondary-color, #a0a0a5);
438
+ margin-top: var(--padding-m);
439
+ font-style: italic;
440
+ }
441
+
442
+ /* Icons */
443
+ .icon { display: inline-block; font-style: normal; margin-right: 8px; }
444
+ .icon-save::before { content: '💾'; }
445
+ .icon-web::before { content: '🌐'; }
446
+ .icon-mobile::before { content: '📱'; }
447
+ .icon-code::before { content: '💻'; }
448
+ .icon-ai::before { content: '🧠'; }
449
+ .icon-quantum::before { content: '⚛️'; }
450
+ .icon-business::before { content: '💼'; }
451
+ .icon-speed::before { content: '⚡️'; }
452
+ .icon-complexity::before { content: '🧩'; }
453
+ .icon-experience::before { content: ''; }
454
+ .icon-clients::before { content: '👥'; }
455
+ .icon-market::before { content: '📈'; }
456
+ .icon-location::before { content: '📍'; }
457
+ .icon-global::before { content: '🌍'; }
458
+ .icon-innovation::before { content: '💡'; }
459
+ .icon-contact::before { content: '💬'; }
460
+ .icon-link::before { content: '🔗'; }
461
+ .icon-leader::before { content: '🏆'; }
462
+ .icon-company::before { content: '🏢'; }
463
+
464
+ /* Responsive */
465
+ @media (max-width: 600px) {
466
+ body { padding: var(--padding-s); padding-bottom: 100px; }
467
+ .container { gap: var(--padding-m); }
468
+ .section-title { font-size: 1.8em; }
469
+ .section-subtitle { font-size: 1.1em; }
470
+ .btn { padding: 10px var(--padding-m); font-size: 0.95em; }
471
+ .save-card-button { padding: 12px 24px; font-size: 1em; bottom: 20px; }
472
+ .modal-content { width: 95%; padding: var(--padding-m); }
473
+ .modal-title { font-size: 1.3em; }
474
+ .modal-text { font-size: 1.1em; }
475
+ .modal-instruction { font-size: 0.9em; }
476
+ }
477
  </style>
478
  </head>
479
  <body>
 
485
  <img src="https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg" alt="Morshen Group Logo">
486
  <span>Morshen Group</span>
487
  </div>
488
+ <a href="#" class="btn btn-secondary contact-link"><i class="icon icon-contact"></i>Связаться</a>
489
  </div>
490
+ <div class="tag-container">
491
+ <span class="tag"><i class="icon icon-leader"></i>Лидер инноваций 2025</span>
492
+ <span class="tag"><i class="icon icon-global"></i>Международный Холдинг</span>
493
  </div>
494
+ <h1 class="section-title">Создаем будущее IT сегодня</h1>
495
  <p class="description">
496
+ Мы — международный IT холдинг, объединяющий передовые технологические компании для создания прорывных решений мирового уровня в сферах AI, квантовых вычислений и разработки ПО.
 
497
  </p>
498
+ <a href="#" class="btn btn-green contact-link" style="width: 100%; margin-top: var(--padding-s);">
499
+ <i class="icon icon-contact"></i>Обсудить ваш проект
500
  </a>
501
  </section>
502
 
503
  <section class="ecosystem-header">
504
+ <h2 class="section-title">Экосистема <span style="color: var(--accent-color);">Инноваций</span></h2>
 
505
  <p class="description">
506
+ В состав холдинга входят специализированные компании, каждая из которых является экспертом в своей области передовых технологий.
 
507
  </p>
508
  </section>
509
 
510
  <section class="section-card">
511
  <div class="logo">
512
  <img src="https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg" alt="Morshen Alpha Logo">
513
+ <span style="font-size: 1.5em; font-weight: 600;">Morshen Alpha</span>
514
  </div>
515
+ <div class="tag-container">
516
+ <span class="tag"><i class="icon icon-ai"></i>Искусственный интеллект</span>
517
+ <span class="tag"><i class="icon icon-quantum"></i>Квантовые технологии</span>
518
+ <span class="tag"><i class="icon icon-business"></i>Стратегические решения</span>
519
  </div>
520
  <p class="description">
521
+ Флагман холдинга. Занимаемся R&D в области AI и квантовых технологий, разрабатываем передовые бизнес-решения, формирующие будущее индустрии.
 
 
522
  </p>
523
  <div class="stats-grid">
524
  <div class="stat-item">
525
+ <span class="stat-value"><i class="icon icon-global"></i> 3+</span>
526
+ <span class="stat-label">Страны</span>
527
  </div>
528
  <div class="stat-item">
529
+ <span class="stat-value"><i class="icon icon-clients"></i> 3K+</span>
530
+ <span class="stat-label">Клиенты</span>
531
  </div>
532
  <div class="stat-item">
533
+ <span class="stat-value"><i class="icon icon-market"></i> 5+</span>
534
  <span class="stat-label">Лет на рынке</span>
535
  </div>
536
  </div>
 
539
  <section class="section-card">
540
  <div class="logo">
541
  <img src="https://huggingface.co/spaces/holmgardstudio/dev/resolve/main/image.jpg" alt="Holmgard Logo" style="width: 50px; height: 50px;">
542
+ <span style="font-size: 1.5em; font-weight: 600;">Holmgard Studio</span>
543
  </div>
544
+ <div class="tag-container">
545
+ <span class="tag"><i class="icon icon-web"></i>Веб-разработка</span>
546
+ <span class="tag"><i class="icon icon-mobile"></i>Мобильные приложения</span>
547
+ <span class="tag"><i class="icon icon-code"></i>Энтерпрайз ПО</span>
548
  </div>
549
  <p class="description">
550
+ Студия разработки полного цикла. Создаем высокотехнологичные веб-сайты, мобильные приложения и кастомное ПО для бизнеса любого масштаба, используя современные стеки и методологии.
 
 
 
551
  </p>
552
  <div class="stats-grid">
553
  <div class="stat-item">
554
+ <span class="stat-value"><i class="icon icon-experience"></i> 10+</span>
555
  <span class="stat-label">Лет опыта</span>
556
  </div>
557
  <div class="stat-item">
558
+ <span class="stat-value"><i class="icon icon-complexity"></i> Highload</span>
559
+ <span class="stat-label">Сложные проекты</span>
560
  </div>
561
  <div class="stat-item">
562
+ <span class="stat-value"><i class="icon icon-speed"></i> Agile</span>
563
+ <span class="stat-label">Быстрый запуск</span>
564
  </div>
565
  </div>
566
  <div style="display: flex; gap: var(--padding-s); margin-top: var(--padding-m); flex-wrap: wrap;">
567
+ <a href="https://holmgard.ru" target="_blank" class="btn btn-secondary" style="flex-grow: 1;"><i class="icon icon-link"></i>На сайт</a>
568
+ <a href="#" class="btn contact-link" style="flex-grow: 1;"><i class="icon icon-contact"></i>Связаться</a>
569
  </div>
570
  </section>
571
 
572
  <section>
573
+ <h2 class="section-title"><i class="icon icon-global"></i> Глобальное Присутствие</h2>
574
+ <p class="description">Наши решения и команды работают в ключевых регионах Центральной Азии:</p>
575
+ <div class="list-container">
576
+ <div class="list-item"><i class="icon icon-location"></i>Узбекистан</div>
577
+ <div class="list-item"><i class="icon icon-location"></i>Казахстан</div>
578
+ <div class="list-item"><i class="icon icon-location"></i>Кыргызстан</div>
 
579
  </div>
580
  </section>
581
 
582
  <footer class="footer-greeting">
583
+ <p id="greeting">Загрузка данных...</p>
584
  </footer>
585
 
586
  </div>
587
 
588
  <button class="save-card-button" id="save-card-btn">
589
+ <i class="icon icon-save"></i>Сохранить визитку
590
  </button>
591
 
592
+ <!-- The Modal -->
593
  <div id="saveModal" class="modal">
594
  <div class="modal-content">
595
  <span class="modal-close" id="modal-close-btn">×</span>
596
+ <h3 class="modal-title">Контактная информация</h3>
597
+ <p class="modal-text"><strong>+996 500 398 754</strong></p>
598
+ <p class="modal-text">Morshen Group, IT Holding</p>
599
+ <p class="modal-instruction">Сделайте скриншот, чтобы сохранить контакт.</p>
600
  </div>
601
  </div>
602
 
603
+
604
  <script>
605
  const tg = window.Telegram.WebApp;
606
 
607
+ function applyTheme(themeParams) {
608
+ document.documentElement.style.setProperty('--tg-bg-color', themeParams.bg_color || '#181a1b');
609
+ document.documentElement.style.setProperty('--tg-text-color', themeParams.text_color || '#ffffff');
610
+ document.documentElement.style.setProperty('--tg-hint-color', themeParams.hint_color || '#aaaaaa');
611
+ document.documentElement.style.setProperty('--tg-link-color', themeParams.link_color || '#8774e1');
612
+ document.documentElement.style.setProperty('--tg-button-color', themeParams.button_color || '#8774e1');
613
+ document.documentElement.style.setProperty('--tg-button-text-color', themeParams.button_text_color || '#ffffff');
614
+ document.documentElement.style.setProperty('--tg-secondary-bg-color', themeParams.secondary_bg_color || '#222425');
615
+ console.log("Theme applied:", themeParams);
616
+ }
617
+
618
  function setupTelegram() {
619
  if (!tg || !tg.initData) {
620
  console.error("Telegram WebApp script not loaded or initData is missing.");
621
  const greetingElement = document.getElementById('greeting');
622
+ if(greetingElement) greetingElement.textContent = 'Ошибка загрузки Telegram.';
623
+ document.body.style.visibility = 'visible'; // Show body anyway
624
  return;
625
  }
626
 
627
  tg.ready();
628
  tg.expand();
629
+ applyTheme(tg.themeParams);
630
+ tg.onEvent('themeChanged', () => applyTheme(tg.themeParams)); // Listen for theme changes
631
 
632
+ // Send initData for verification and user logging
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
633
  fetch('/verify', {
634
  method: 'POST',
635
  headers: {
 
643
  console.log('Backend verification successful.');
644
  } else {
645
  console.warn('Backend verification failed:', data.message);
646
+ // Optionally show a non-intrusive warning
 
647
  }
648
  })
649
  .catch(error => {
650
  console.error('Error sending initData for verification:', error);
 
651
  });
652
 
653
+ // User Greeting (using unsafe data for immediate feedback)
654
  const user = tg.initDataUnsafe?.user;
655
  const greetingElement = document.getElementById('greeting');
656
  if (user) {
657
+ const name = user.first_name || user.username || 'Гость';
658
+ greetingElement.textContent = `Приветствуем, ${name}! 👋`;
659
  } else {
660
  greetingElement.textContent = 'Добро пожаловать!';
661
+ console.warn('Telegram User data (initDataUnsafe.user) not available.');
662
  }
663
 
664
+ // Contact Links
665
  const contactButtons = document.querySelectorAll('.contact-link');
666
  contactButtons.forEach(button => {
667
  button.addEventListener('click', (e) => {
668
  e.preventDefault();
669
+ tg.openTelegramLink('https://t.me/morshenkhan'); // Replace with actual contact username
670
+ if (tg.HapticFeedback) tg.HapticFeedback.impactOccurred('light');
671
  });
672
  });
673
 
674
+ // Modal Setup
675
  const modal = document.getElementById("saveModal");
676
  const saveCardBtn = document.getElementById("save-card-btn");
677
  const closeBtn = document.getElementById("modal-close-btn");
 
680
  saveCardBtn.addEventListener('click', (e) => {
681
  e.preventDefault();
682
  modal.style.display = "block";
683
+ if (tg.HapticFeedback) tg.HapticFeedback.notificationOccurred('success');
 
 
684
  });
685
 
686
  closeBtn.addEventListener('click', () => {
687
  modal.style.display = "none";
688
  });
689
 
690
+ // Close modal if clicked outside the content
691
+ modal.addEventListener('click', (event) => { // Listen on modal overlay itself
692
+ if (event.target === modal) {
693
  modal.style.display = "none";
694
  }
695
  });
 
697
  console.error("Modal elements not found!");
698
  }
699
 
700
+ document.body.style.visibility = 'visible'; // Make body visible now
701
+ console.log("Telegram Mini App setup complete.");
702
  }
703
 
704
+ // Initialize Telegram WebApp
705
  if (window.Telegram && window.Telegram.WebApp) {
706
  setupTelegram();
707
  } else {
708
+ console.warn("Telegram WebApp script not immediately available. Waiting for load event.");
709
  window.addEventListener('load', setupTelegram);
710
+ // Further fallback timeout
711
  setTimeout(() => {
712
  if (document.body.style.visibility !== 'visible') {
713
+ console.error("Telegram WebApp script loading fallback timeout triggered.");
714
  const greetingElement = document.getElementById('greeting');
715
+ if(greetingElement) greetingElement.textContent = 'Ошибка загрузки интерфейса.';
716
+ document.body.style.visibility = 'visible'; // Force display anyway
717
  }
718
+ }, 4000); // Increased timeout
719
  }
 
720
  </script>
721
  </body>
722
  </html>
 
728
  <head>
729
  <meta charset="UTF-8">
730
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
731
+ <title>Admin - Посетители</title>
732
+ <link rel="preconnect" href="https://fonts.googleapis.com">
733
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
734
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
735
  <style>
736
+ :root {
737
+ --admin-bg-color: #1f2937; /* Dark Gray */
738
+ --admin-card-bg: #374151; /* Medium Gray */
739
+ --admin-text-color: #f3f4f6; /* Light Gray */
740
+ --admin-text-secondary-color: #9ca3af; /* Gray */
741
+ --admin-accent-color: #60a5fa; /* Blue */
742
+ --admin-border-color: #4b5563; /* Darker Gray */
743
+ --admin-shadow-color: rgba(0, 0, 0, 0.3);
744
+ --border-radius: 12px;
745
+ --padding: 20px;
746
+ }
747
+ body {
748
+ font-family: 'Inter', sans-serif;
749
+ background-color: var(--admin-bg-color);
750
+ color: var(--admin-text-color);
751
+ margin: 0;
752
+ padding: var(--padding);
753
+ line-height: 1.6;
754
+ }
755
+ .container { max-width: 1200px; margin: 0 auto; }
756
+ h1 { text-align: center; color: var(--admin-accent-color); font-weight: 700; margin-bottom: 30px; }
757
+ .controls { display: flex; justify-content: center; gap: 15px; margin-bottom: 30px; flex-wrap: wrap;}
758
+ .control-btn {
759
+ padding: 10px 20px;
760
+ border: none;
761
+ border-radius: 8px;
762
+ background-color: var(--admin-accent-color);
763
+ color: #fff;
764
+ font-weight: 600;
765
+ cursor: pointer;
766
+ transition: all 0.2s ease;
767
+ box-shadow: 0 2px 5px var(--admin-shadow-color);
768
+ }
769
+ .control-btn:hover {
770
+ background-color: #3b82f6; /* Darker Blue */
771
+ transform: translateY(-1px);
772
+ box-shadow: 0 4px 10px var(--admin-shadow-color);
773
+ }
774
+ .control-btn.download { background-color: #34d399; } /* Green */
775
+ .control-btn.download:hover { background-color: #059669; }
776
+ .user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--padding); }
777
+ .user-card {
778
+ background-color: var(--admin-card-bg);
779
+ border-radius: var(--border-radius);
780
+ padding: var(--padding);
781
+ box-shadow: 0 4px 15px var(--admin-shadow-color);
782
+ display: flex;
783
+ flex-direction: column;
784
+ align-items: center;
785
+ text-align: center;
786
+ border: 1px solid var(--admin-border-color);
787
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
788
+ }
789
+ .user-card:hover {
790
+ transform: translateY(-4px);
791
+ box-shadow: 0 8px 25px var(--admin-shadow-color);
792
+ }
793
+ .user-card img {
794
+ width: 90px;
795
+ height: 90px;
796
+ border-radius: 50%;
797
+ margin-bottom: 15px;
798
+ object-fit: cover;
799
+ border: 3px solid var(--admin-border-color);
800
+ background-color: var(--admin-bg-color); /* Placeholder bg */
801
+ }
802
+ .user-card .name { font-weight: 700; font-size: 1.2em; margin-bottom: 5px; color: var(--admin-text-color); }
803
+ .user-card .username { color: var(--admin-accent-color); margin-bottom: 10px; font-size: 0.95em; font-weight: 500; }
804
+ .user-card .details { font-size: 0.9em; color: var(--admin-text-secondary-color); word-break: break-all; line-height: 1.5; }
805
+ .user-card .timestamp { font-size: 0.8em; color: var(--admin-text-secondary-color); margin-top: 15px; font-style: italic; }
806
+ .no-users { text-align: center; color: var(--admin-text-secondary-color); margin-top: 40px; font-size: 1.1em; }
807
+ .alert {
808
+ background-color: #f87171; /* Red */
809
+ color: #fff;
810
+ border-left: 6px solid #dc2626; /* Darker Red */
811
+ margin-bottom: 25px;
812
+ padding: 15px 20px;
813
+ border-radius: 8px;
814
+ text-align: center;
815
+ font-weight: 600;
816
+ box-shadow: 0 2px 5px var(--admin-shadow-color);
817
+ }
818
+ a { color: var(--admin-accent-color); text-decoration: none; }
819
+ a:hover { text-decoration: underline; }
820
  </style>
821
  </head>
822
  <body>
823
  <div class="container">
824
+ <h1>Панель Администратора - Посетители</h1>
825
+ <div class="alert">ВНИМАНИЕ: Этот раздел не защищен! Добавьте аутентификацию для реального использования.</div>
826
 
827
+ <div class="controls">
828
+ <form method="POST" action="{{ url_for('backup_route') }}" style="display: inline;">
829
+ <button type="submit" class="control-btn">Создать Резервную Копию</button>
830
  </form>
831
+ <form method="GET" action="{{ url_for('download_route') }}" style="display: inline;">
832
+ <button type="submit" class="control-btn download">Скачать Базу Данных</button>
833
  </form>
834
+ <button class="control-btn" onclick="window.location.reload();">Обновить Список</button>
835
  </div>
836
 
 
837
  {% if users %}
838
  <div class="user-grid">
839
  {% for user in users|sort(attribute='visited_at', reverse=true) %}
840
  <div class="user-card">
841
+ <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%234b5563%27/%3e%3ctext x=%2750%25%27 y=%2750%25%27 dominant-baseline=%27middle%27 text-anchor=%27middle%27 font-size=%2745%27 font-family=%27sans-serif%27 fill=%27%239ca3af%27%3e?%3c/text%3e%3c/svg%3e' }}" alt="User Avatar" loading="lazy">
842
  <div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
843
  {% if user.username %}
844
+ <div class="username"><a href="https://t.me/{{ user.username }}" target="_blank">@{{ user.username }}</a></div>
845
  {% else %}
846
+ <div class="username">Нет username</div>
847
  {% endif %}
848
  <div class="details">
849
  ID: {{ user.id }} <br>
850
+ Язык: {{ user.language_code or 'N/A' }} <br>
851
+ Телефон: <span style="color: var(--admin-text-secondary-color); font-style: italic;">Недоступен</span>
852
  </div>
853
+ <div class="timestamp">Визит: {{ user.visited_at_str }}</div>
854
  </div>
855
  {% endfor %}
856
  </div>
857
  {% else %}
858
+ <p class="no-users">Пока нет данных о посетителях.</p>
859
  {% endif %}
860
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
861
  </body>
862
  </html>
863
  """
864
 
865
+
866
  # --- Flask Routes ---
867
 
868
  @app.route('/')
 
871
 
872
  @app.route('/verify', methods=['POST'])
873
  def verify_data():
874
+ global visited_users
875
  try:
876
  data = request.get_json()
877
  init_data_str = data.get('initData')
878
  if not init_data_str:
879
+ logging.warning("Verification request missing initData.")
880
  return jsonify({"status": "error", "message": "Missing initData"}), 400
881
 
882
+ user_data_parsed, is_valid = verify_telegram_data(init_data_str)
 
 
883
 
884
+ user_info_dict = {}
885
+ if user_data_parsed and 'user' in user_data_parsed:
886
  try:
887
+ # Decode JSON string within the 'user' field
888
+ user_json_str = unquote(user_data_parsed['user'][0])
889
  user_info_dict = json.loads(user_json_str)
890
+ except (KeyError, IndexError, json.JSONDecodeError, TypeError) as e:
891
+ logging.error(f"Could not parse user JSON from initData: {e} - Data: {user_data_parsed.get('user')}")
892
  user_info_dict = {} # Ensure it's a dict even on error
893
 
 
 
894
  if is_valid:
895
+ user_id = user_info_dict.get('id')
896
  if user_id:
897
+ user_id_str = str(user_id) # Use string keys for JSON consistency
 
898
  now = time.time()
899
+ update_data = {
 
 
900
  'id': user_id,
901
  'first_name': user_info_dict.get('first_name'),
902
  'last_name': user_info_dict.get('last_name'),
 
904
  'photo_url': user_info_dict.get('photo_url'),
905
  'language_code': user_info_dict.get('language_code'),
906
  'visited_at': now,
907
+ 'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S UTC') # Explicit UTC
908
  }
909
+ # Update the global dictionary and save
910
+ visited_users[user_id_str] = update_data
911
+ save_users(visited_users) # Save after modification
912
+ logging.info(f"User visit recorded/updated for ID: {user_id_str}")
913
 
914
  return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
915
  else:
916
+ logging.warning(f"Verification failed for user ID: {user_info_dict.get('id', 'Unknown')}")
917
  return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
918
 
919
  except Exception as e:
920
+ logging.exception("Critical error in /verify endpoint") # Log full traceback
921
  return jsonify({"status": "error", "message": "Internal server error"}), 500
922
 
923
  @app.route('/admin')
924
  def admin_panel():
925
  # WARNING: This route is unprotected! Add proper authentication/authorization for production.
926
+ # Load fresh data for admin view, though 'visited_users' global should be up-to-date
927
+ current_users = load_users()
928
+ users_list = list(current_users.values())
929
+ logging.info(f"Admin panel accessed. Displaying {len(users_list)} users.")
930
  return render_template_string(ADMIN_TEMPLATE, users=users_list)
931
 
932
  @app.route('/backup', methods=['POST'])
933
+ def backup_route():
934
+ # Manual backup trigger
935
+ # WARNING: Unprotected route
936
+ logging.info("Manual backup requested via /backup route.")
937
+ if upload_db_to_hf():
938
+ # Optionally add a success message (e.g., using flash)
939
+ pass
940
  else:
941
+ # Optionally add an error message
942
+ pass
943
+ return redirect(url_for('admin_panel')) # Redirect back to admin
944
+
945
+ @app.route('/download', methods=['GET'])
946
+ def download_route():
947
+ # Manual download trigger
948
+ # WARNING: Unprotected route
949
+ global visited_users
950
+ logging.info("Manual download requested via /download route.")
951
+ if download_db_from_hf():
952
+ visited_users = load_users() # Reload data after download
953
+ # Optionally add a success message
954
  else:
955
+ # Optionally add an error message
956
+ pass
957
+ return redirect(url_for('admin_panel')) # Redirect back to admin
958
 
959
 
960
  # --- Main Execution ---
961
 
962
  if __name__ == '__main__':
963
+ # Initial check for HF tokens
964
+ if not HF_TOKEN:
965
+ logging.warning("!!! HF_TOKEN environment variable is not set. Uploads to Hugging Face Hub will be disabled.")
966
+ if not HF_TOKEN_READ:
967
+ logging.warning("!!! HF_TOKEN_READ environment variable is not set. Downloads from Hugging Face Hub will be disabled (falling back to local file).")
968
+
969
+ # Start the periodic backup thread
970
+ if HF_TOKEN: # Only start if upload is possible
971
+ backup_thread = threading.Thread(target=periodic_backup, daemon=True)
972
+ backup_thread.start()
 
 
 
973
  else:
974
+ logging.warning("Periodic backup thread not started because HF_TOKEN is not set.")
975
+
976
+
977
+ logging.warning("--- SECURITY WARNING ---")
978
+ logging.warning("The /admin, /backup, /download routes are NOT protected by authentication.")
979
+ logging.warning("Anyone knowing the URL can access visitor data and trigger actions.")
980
+ logging.warning("Implement proper security (e.g., password protection, IP restriction) before deploying.")
981
+ logging.warning("------------------------")
982
+ logging.info(f"Starting Flask server on http://{HOST}:{PORT}")
983
+ logging.info(f"Ensure this address is accessible and configured in BotFather for your Mini App.")
984
+ logging.info(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
985
+ logging.info(f"User data file: {DATA_FILE}")
986
+ logging.info(f"Hugging Face Repo: {REPO_ID}")
987
+
988
+ # Use Waitress or Gunicorn for production instead of app.run()
989
+ # from waitress import serve
990
+ # serve(app, host=HOST, port=PORT)
991
+ app.run(host=HOST, port=PORT, debug=False) # debug=False for production