Aleksmorshen commited on
Commit
71eba6c
·
verified ·
1 Parent(s): 047a9f2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +68 -1036
app.py CHANGED
@@ -1,1047 +1,79 @@
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
- import threading
14
- from huggingface_hub import HfApi, hf_hub_download
15
- from huggingface_hub.utils import RepositoryNotFoundError
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' # Local file for visitor data
22
-
23
- # Hugging Face Settings
24
- REPO_ID = "flpolprojects/teledata"
25
- HF_DATA_FILE_PATH = "data.json" # Path within the HF repo
26
- HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") # Token with write access
27
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # Token with read access (can be same as write)
28
-
29
  app = Flask(__name__)
30
- logging.basicConfig(level=logging.INFO)
31
- app.secret_key = os.urandom(24) # For potential future session use
32
-
33
- # --- Hugging Face & Data Handling ---
34
- _data_lock = threading.Lock()
35
- visitor_data_cache = {} # In-memory cache
36
-
37
- def download_data_from_hf():
38
- global visitor_data_cache
39
- if not HF_TOKEN_READ:
40
- logging.warning("HF_TOKEN_READ not set. Skipping Hugging Face download.")
41
- return False
42
- try:
43
- logging.info(f"Attempting to download {HF_DATA_FILE_PATH} from {REPO_ID}...")
44
- hf_hub_download(
45
- repo_id=REPO_ID,
46
- filename=HF_DATA_FILE_PATH,
47
- repo_type="dataset",
48
- token=HF_TOKEN_READ,
49
- local_dir=".",
50
- local_dir_use_symlinks=False,
51
- force_download=True, # Ensure we get the latest version
52
- etag_timeout=10 # Shorter timeout to avoid hanging
53
- )
54
- logging.info("Data file successfully downloaded from Hugging Face.")
55
- # Force reload from downloaded file
56
- with _data_lock:
57
- try:
58
- with open(DATA_FILE, 'r', encoding='utf-8') as f:
59
- visitor_data_cache = json.load(f)
60
- logging.info("Successfully loaded downloaded data into cache.")
61
- except (FileNotFoundError, json.JSONDecodeError) as e:
62
- logging.error(f"Error reading downloaded data file: {e}. Starting with empty cache.")
63
- visitor_data_cache = {}
64
- return True
65
- except RepositoryNotFoundError:
66
- logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download data.")
67
- # Don't clear local cache if repo not found, might have local data
68
- except Exception as e:
69
- logging.error(f"Error downloading data from Hugging Face: {e}")
70
- # Don't clear local cache on generic download errors
71
- return False
72
-
73
- def load_visitor_data():
74
- global visitor_data_cache
75
- with _data_lock:
76
- if not visitor_data_cache: # Only load from file if cache is empty
77
- try:
78
- with open(DATA_FILE, 'r', encoding='utf-8') as f:
79
- visitor_data_cache = json.load(f)
80
- logging.info("Visitor data loaded from local JSON.")
81
- except FileNotFoundError:
82
- logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.")
83
- visitor_data_cache = {}
84
- except json.JSONDecodeError:
85
- logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.")
86
- visitor_data_cache = {}
87
- except Exception as e:
88
- logging.error(f"Unexpected error loading visitor data: {e}")
89
- visitor_data_cache = {}
90
- return visitor_data_cache
91
-
92
- def save_visitor_data(data):
93
- with _data_lock:
94
- try:
95
- # Update cache first
96
- visitor_data_cache.update(data)
97
- # Save updated cache to file
98
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
99
- json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
100
- logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
101
- # Trigger upload after successful local save
102
- upload_data_to_hf_async() # Use async upload
103
- except Exception as e:
104
- logging.error(f"Error saving visitor data: {e}")
105
-
106
- def upload_data_to_hf():
107
- if not HF_TOKEN_WRITE:
108
- logging.warning("HF_TOKEN_WRITE not set. Skipping Hugging Face upload.")
109
- return
110
- if not os.path.exists(DATA_FILE):
111
- logging.warning(f"{DATA_FILE} does not exist. Skipping upload.")
112
- return
113
-
114
- try:
115
- api = HfApi()
116
- with _data_lock: # Ensure file isn't being written while reading for upload
117
- file_content_exists = os.path.getsize(DATA_FILE) > 0
118
- if not file_content_exists:
119
- logging.warning(f"{DATA_FILE} is empty. Skipping upload.")
120
- return
121
-
122
- logging.info(f"Attempting to upload {DATA_FILE} to {REPO_ID}/{HF_DATA_FILE_PATH}...")
123
- api.upload_file(
124
- path_or_fileobj=DATA_FILE,
125
- path_in_repo=HF_DATA_FILE_PATH,
126
- repo_id=REPO_ID,
127
- repo_type="dataset",
128
- token=HF_TOKEN_WRITE,
129
- commit_message=f"Update visitor data {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
130
- )
131
- logging.info("Visitor data successfully uploaded to Hugging Face.")
132
- except Exception as e:
133
- logging.error(f"Error uploading data to Hugging Face: {e}")
134
- # Consider adding retry logic here if needed
135
-
136
- def upload_data_to_hf_async():
137
- # Run upload in a separate thread to avoid blocking web requests
138
- upload_thread = threading.Thread(target=upload_data_to_hf, daemon=True)
139
- upload_thread.start()
140
-
141
- def periodic_backup():
142
- if not HF_TOKEN_WRITE:
143
- logging.info("Periodic backup disabled: HF_TOKEN_WRITE not set.")
144
- return
145
- while True:
146
- time.sleep(3600) # Backup every hour
147
- logging.info("Initiating periodic backup...")
148
- upload_data_to_hf()
149
-
150
- # --- Telegram Verification ---
151
- def verify_telegram_data(init_data_str):
152
- try:
153
- parsed_data = parse_qs(init_data_str)
154
- received_hash = parsed_data.pop('hash', [None])[0]
155
-
156
- if not received_hash:
157
- return None, False
158
-
159
- data_check_list = []
160
- for key, value in sorted(parsed_data.items()):
161
- data_check_list.append(f"{key}={value[0]}")
162
- data_check_string = "\n".join(data_check_list)
163
-
164
- secret_key = hmac.new("WebAppData".encode(), BOT_TOKEN.encode(), hashlib.sha256).digest()
165
- calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
166
-
167
- if calculated_hash == received_hash:
168
- auth_date = int(parsed_data.get('auth_date', [0])[0])
169
- current_time = int(time.time())
170
- if current_time - auth_date > 86400: # Allow data up to 24 hours old
171
- logging.warning(f"Telegram InitData is older than 1 hour (Auth Date: {auth_date}, Current: {current_time}).")
172
- return parsed_data, True
173
- else:
174
- logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
175
- return parsed_data, False
176
- except Exception as e:
177
- logging.error(f"Error verifying Telegram data: {e}")
178
- return None, False
179
-
180
- # --- HTML Templates ---
181
- TEMPLATE = """
182
- <!DOCTYPE html>
183
- <html lang="ru">
184
- <head>
185
- <meta charset="UTF-8">
186
- <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
187
- <title>Morshen Group</title>
188
- <script src="https://telegram.org/js/telegram-web-app.js"></script>
189
- <link rel="preconnect" href="https://fonts.googleapis.com">
190
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
191
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
192
- <style>
193
- :root {
194
- --tg-theme-bg-color: {{ theme.bg_color | default('#121212') }};
195
- --tg-theme-text-color: {{ theme.text_color | default('#ffffff') }};
196
- --tg-theme-hint-color: {{ theme.hint_color | default('#aaaaaa') }};
197
- --tg-theme-link-color: {{ theme.link_color | default('#62bcf9') }};
198
- --tg-theme-button-color: {{ theme.button_color | default('#31a5f5') }};
199
- --tg-theme-button-text-color: {{ theme.button_text_color | default('#ffffff') }};
200
- --tg-theme-secondary-bg-color: {{ theme.secondary_bg_color | default('#1e1e1e') }};
201
-
202
- --bg-gradient: linear-gradient(160deg, #1a232f 0%, #121212 100%);
203
- --card-bg: rgba(44, 44, 46, 0.8); /* Semi-transparent card */
204
- --card-bg-solid: #2c2c2e;
205
- --text-color: var(--tg-theme-text-color);
206
- --text-secondary-color: var(--tg-theme-hint-color);
207
- --accent-gradient: linear-gradient(95deg, var(--tg-theme-button-color, #007aff), #5856d6);
208
- --accent-gradient-green: linear-gradient(95deg, #34c759, #30d158);
209
- --tag-bg: rgba(255, 255, 255, 0.1);
210
- --border-radius-s: 8px;
211
- --border-radius-m: 14px; /* Increased radius */
212
- --border-radius-l: 18px; /* Increased radius */
213
- --padding-s: 10px;
214
- --padding-m: 18px; /* Increased padding */
215
- --padding-l: 28px; /* Increased padding */
216
- --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
217
- --shadow-color: rgba(0, 0, 0, 0.3);
218
- --shadow-light: 0 4px 15px var(--shadow-color);
219
- --shadow-medium: 0 6px 25px var(--shadow-color);
220
- --backdrop-blur: 10px; /* Glassmorphism effect */
221
- }
222
- * { box-sizing: border-box; margin: 0; padding: 0; }
223
- html {
224
- background-color: var(--tg-theme-bg-color);
225
- scroll-behavior: smooth;
226
- }
227
- body {
228
- font-family: var(--font-family);
229
- background: var(--bg-gradient);
230
- color: var(--text-color);
231
- padding: var(--padding-m);
232
- padding-bottom: 120px; /* More space for fixed button */
233
- overscroll-behavior-y: none;
234
- -webkit-font-smoothing: antialiased;
235
- -moz-osx-font-smoothing: grayscale;
236
- visibility: hidden; /* Hide until ready */
237
- min-height: 100vh;
238
- }
239
- .container {
240
- max-width: 650px;
241
- margin: 0 auto;
242
- display: flex;
243
- flex-direction: column;
244
- gap: var(--padding-l);
245
- }
246
- .header {
247
- display: flex;
248
- justify-content: space-between;
249
- align-items: center;
250
- margin-bottom: var(--padding-m);
251
- padding: var(--padding-s) 0;
252
- }
253
- .logo { display: flex; align-items: center; gap: var(--padding-s); }
254
- .logo img {
255
- width: 50px; /* Larger logo */
256
- height: 50px;
257
- border-radius: 50%;
258
- background-color: var(--card-bg-solid);
259
- object-fit: cover;
260
- border: 2px solid rgba(255, 255, 255, 0.15);
261
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
262
- }
263
- .logo span { font-size: 1.6em; font-weight: 700; letter-spacing: -0.5px; } /* Bold, slightly larger */
264
- .btn {
265
- display: inline-flex; align-items: center; justify-content: center;
266
- padding: 12px var(--padding-m); border-radius: var(--border-radius-m);
267
- background: var(--accent-gradient); color: var(--tg-theme-button-text-color);
268
- text-decoration: none; font-weight: 600; /* Bolder */
269
- border: none; cursor: pointer;
270
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
271
- gap: 8px; font-size: 1em;
272
- box-shadow: var(--shadow-light);
273
- letter-spacing: 0.3px;
274
- }
275
- .btn:hover {
276
- opacity: 0.9;
277
- box-shadow: var(--shadow-medium);
278
- transform: translateY(-2px);
279
- }
280
- .btn-secondary {
281
- background: var(--card-bg);
282
- color: var(--tg-theme-link-color);
283
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1), var(--shadow-light);
284
- }
285
- .btn-secondary:hover {
286
- background: rgba(44, 44, 46, 0.95);
287
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), var(--shadow-medium);
288
- }
289
- .tag {
290
- display: inline-block; background: var(--tag-bg); color: var(--text-secondary-color);
291
- padding: 6px 12px; border-radius: var(--border-radius-s); font-size: 0.85em;
292
- margin: 4px 6px 4px 0; white-space: nowrap;
293
- font-weight: 500;
294
- border: 1px solid rgba(255, 255, 255, 0.05);
295
- }
296
- .tag i { margin-right: 5px; opacity: 0.8; }
297
- .section-card {
298
- background-color: var(--card-bg);
299
- border-radius: var(--border-radius-l);
300
- padding: var(--padding-l);
301
- margin-bottom: 0; /* Removed bottom margin, gap handles spacing */
302
- box-shadow: var(--shadow-medium);
303
- border: 1px solid rgba(255, 255, 255, 0.08);
304
- backdrop-filter: blur(var(--backdrop-blur));
305
- -webkit-backdrop-filter: blur(var(--backdrop-blur));
306
- }
307
- .section-title {
308
- font-size: 2em; /* Larger titles */
309
- font-weight: 700; margin-bottom: var(--padding-s); line-height: 1.25;
310
- letter-spacing: -0.6px;
311
- }
312
- .section-subtitle {
313
- font-size: 1.2em; font-weight: 500; color: var(--text-secondary-color);
314
- margin-bottom: var(--padding-m);
315
- }
316
- .description {
317
- font-size: 1.05em; line-height: 1.6; color: var(--text-secondary-color); /* Slightly larger desc */
318
- margin-bottom: var(--padding-m);
319
- }
320
- .stats-grid {
321
- display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
322
- gap: var(--padding-m); margin-top: var(--padding-m); text-align: center;
323
- }
324
- .stat-item {
325
- background-color: rgba(255, 255, 255, 0.05);
326
- padding: var(--padding-m); border-radius: var(--border-radius-m);
327
- border: 1px solid rgba(255, 255, 255, 0.05);
328
- transition: background-color 0.2s ease;
329
- }
330
- .stat-item:hover { background-color: rgba(255, 255, 255, 0.08); }
331
- .stat-value { font-size: 1.7em; font-weight: 600; display: block; color: var(--tg-theme-link-color);}
332
- .stat-label { font-size: 0.9em; color: var(--text-secondary-color); display: block; margin-top: 4px;}
333
- .list-item {
334
- background-color: var(--card-bg-solid);
335
- padding: var(--padding-m); border-radius: var(--border-radius-m);
336
- margin-bottom: var(--padding-s); display: flex; align-items: center;
337
- gap: var(--padding-m); /* Increased gap */
338
- font-size: 1.1em; font-weight: 500;
339
- border: 1px solid rgba(255, 255, 255, 0.08);
340
- transition: background-color 0.2s ease, transform 0.2s ease;
341
- }
342
- .list-item:hover {
343
- background-color: rgba(44, 44, 46, 0.9);
344
- transform: translateX(3px);
345
- }
346
- .list-item i { font-size: 1.4em; color: var(--accent-gradient-start, #34c759); opacity: 0.9; width: 25px; text-align: center;}
347
- .footer-greeting {
348
- text-align: center; color: var(--text-secondary-color); font-size: 0.95em;
349
- margin-top: var(--padding-l); padding-bottom: var(--padding-l);
350
- }
351
- .save-card-button {
352
- position: fixed;
353
- bottom: 30px; /* Raised */
354
- left: 50%;
355
- transform: translateX(-50%);
356
- padding: 14px 28px; /* Larger padding */
357
- border-radius: 30px; /* More rounded */
358
- background: var(--accent-gradient-green);
359
- color: var(--tg-theme-button-text-color);
360
- text-decoration: none;
361
- font-weight: 600;
362
- border: none;
363
- cursor: pointer;
364
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
365
- z-index: 1000;
366
- box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.5); /* Add outer glow */
367
- font-size: 1.05em; /* Slightly larger text */
368
- display: flex;
369
- align-items: center;
370
- gap: 10px; /* Increased gap */
371
- backdrop-filter: blur(5px);
372
- -webkit-backdrop-filter: blur(5px);
373
- }
374
- .save-card-button:hover {
375
- opacity: 0.95;
376
- transform: translateX(-50%) scale(1.05); /* Slightly larger scale */
377
- box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.3);
378
- }
379
- .save-card-button i { font-size: 1.2em; }
380
-
381
- /* Modal Styles */
382
- .modal {
383
- display: none; position: fixed; z-index: 1001;
384
- left: 0; top: 0; width: 100%; height: 100%;
385
- overflow: auto; background-color: rgba(0,0,0,0.7); /* Darker backdrop */
386
- backdrop-filter: blur(8px);
387
- -webkit-backdrop-filter: blur(8px);
388
- animation: fadeIn 0.3s ease-out;
389
- }
390
- @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
391
- .modal-content {
392
- background-color: var(--card-bg-solid); color: var(--text-color);
393
- margin: 15% auto; padding: var(--padding-l);
394
- border: 1px solid rgba(255, 255, 255, 0.1);
395
- width: 88%; max-width: 480px;
396
- border-radius: var(--border-radius-l);
397
- text-align: center; position: relative;
398
- box-shadow: var(--shadow-medium);
399
- animation: scaleUp 0.3s ease-out;
400
- }
401
- @keyframes scaleUp { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
402
- .modal-close {
403
- color: var(--text-secondary-color);
404
- position: absolute; top: 15px; right: 20px;
405
- font-size: 32px; font-weight: bold; cursor: pointer;
406
- line-height: 1; transition: color 0.2s ease;
407
- }
408
- .modal-close:hover, .modal-close:focus { color: var(--text-color); }
409
- .modal-text { font-size: 1.2em; line-height: 1.6; margin-bottom: var(--padding-s); word-wrap: break-word; }
410
- .modal-text b { color: var(--tg-theme-link-color); font-weight: 600; }
411
- .modal-instruction { font-size: 1em; color: var(--text-secondary-color); margin-top: var(--padding-m); }
412
-
413
- /* Icons */
414
- .icon { display: inline-block; width: 1.2em; text-align: center; margin-right: 8px; opacity: 0.9; }
415
- .icon-save::before { content: '💾'; }
416
- .icon-web::before { content: '🌐'; }
417
- .icon-mobile::before { content: '📱'; }
418
- .icon-code::before { content: '💻'; }
419
- .icon-ai::before { content: '🧠'; }
420
- .icon-quantum::before { content: '⚛️'; }
421
- .icon-business::before { content: '💼'; }
422
- .icon-speed::before { content: '⚡️'; }
423
- .icon-complexity::before { content: '🧩'; }
424
- .icon-experience::before { content: '⏳'; }
425
- .icon-clients::before { content: '👥'; }
426
- .icon-market::before { content: '📈'; }
427
- .icon-location::before { content: '📍'; }
428
- .icon-global::before { content: '🌍'; }
429
- .icon-innovation::before { content: '💡'; }
430
- .icon-contact::before { content: '💬'; }
431
- .icon-link::before { content: '🔗'; }
432
- .icon-leader::before { content: '🏆'; }
433
- .icon-company::before { content: '🏢'; }
434
-
435
- /* Responsive adjustments */
436
- @media (max-width: 480px) {
437
- .section-title { font-size: 1.8em; }
438
- .logo span { font-size: 1.4em; }
439
- .btn { padding: 10px var(--padding-m); font-size: 0.95em; }
440
- .save-card-button { padding: 12px 24px; font-size: 1em; bottom: 20px; }
441
- .stats-grid { grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: var(--padding-s); }
442
- .stat-value { font-size: 1.5em; }
443
- .modal-content { margin: 25% auto; width: 92%; }
444
- }
445
- </style>
446
- </head>
447
- <body>
448
- <div class="container">
449
-
450
- <section class="morshen-group-intro section-card">
451
- <div class="header">
452
- <div class="logo">
453
- <img src="https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg" alt="Morshen Group Logo">
454
- <span>Morshen Group</span>
455
- </div>
456
- <a href="#" class="btn contact-link"><i class="icon icon-contact"></i>Связаться</a>
457
- </div>
458
- <div>
459
- <span class="tag"><i class="icon icon-leader"></i>Лидер инноваций 2025</span>
460
- </div>
461
- <h1 class="section-title">Международный IT холдинг</h1>
462
- <p class="description">
463
- Объединяем передовые технологические компании для создания инновационных
464
- решений мирового уровня. Мы строим будущее технологий сегодня.
465
- </p>
466
- <a href="#" class="btn contact-link" style="background: var(--accent-gradient-green); width: 100%; margin-top: var(--padding-s);">
467
- <i class="icon icon-contact"></i>Написать нам в Telegram
468
- </a>
469
- </section>
470
-
471
- <section class="ecosystem-header">
472
- <h2 class="section-title"><i class="icon icon-company"></i>Экосистема инноваций</h2>
473
- <p class="description">
474
- В состав холдинга входят компании, специализирующиеся на различных
475
- направлениях передовых технологий, создавая синергию для прорывных решений.
476
- </p>
477
- </section>
478
-
479
- <section class="section-card">
480
- <div class="logo" style="margin-bottom: var(--padding-m);">
481
- <img src="https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg" alt="Morshen Alpha Logo">
482
- <span style="font-size: 1.4em; font-weight: 600;">Morshen Alpha</span>
483
- </div>
484
- <div style="margin-bottom: var(--padding-m);">
485
- <span class="tag"><i class="icon icon-ai"></i>Искусственный интеллект</span>
486
- <span class="tag"><i class="icon icon-quantum"></i>Квантовые технологии</span>
487
- <span class="tag"><i class="icon icon-business"></i>Бизнес-решения</span>
488
- </div>
489
- <p class="description">
490
- Флагманская компания холдинга. Разрабатываем передовые бизнес-решения, проводим R&D в сфере AI
491
- и квантовых технологий. Наши инновации формируют будущее индустрии.
492
- </p>
493
- <div class="stats-grid">
494
- <div class="stat-item">
495
- <span class="stat-value"><i class="icon icon-global"></i> 3+</span>
496
- <span class="stat-label">Страны присутствия</span>
497
- </div>
498
- <div class="stat-item">
499
- <span class="stat-value"><i class="icon icon-clients"></i> 3K+</span>
500
- <span class="stat-label">Готовых клиентов</span>
501
- </div>
502
- <div class="stat-item">
503
- <span class="stat-value"><i class="icon icon-market"></i> 5+</span>
504
- <span class="stat-label">Лет на рынке</span>
505
- </div>
506
- </div>
507
- </section>
508
-
509
- <section class="section-card">
510
- <div class="logo" style="margin-bottom: var(--padding-m);">
511
- <img src="https://huggingface.co/spaces/holmgardstudio/dev/resolve/main/image.jpg" alt="Holmgard Logo" style="width: 50px; height: 50px;">
512
- <span style="font-size: 1.4em; font-weight: 600;">Holmgard Studio</span>
513
- </div>
514
- <div style="margin-bottom: var(--padding-m);">
515
- <span class="tag"><i class="icon icon-web"></i>Веб-разработка</span>
516
- <span class="tag"><i class="icon icon-mobile"></i>Мобильные приложения</span>
517
- <span class="tag"><i class="icon icon-code"></i>ПО на заказ</span>
518
- </div>
519
- <p class="description">
520
- Инновационная студия разработки, создающая высокотехнологичные веб-са��ты,
521
- мобильные приложения и ПО для бизнеса любого масштаба.
522
- Используем передовые технологии и гибкие методологии.
523
- </p>
524
- <div class="stats-grid">
525
- <div class="stat-item">
526
- <span class="stat-value"><i class="icon icon-experience"></i> 10+</span>
527
- <span class="stat-label">Лет опыта</span>
528
- </div>
529
- <div class="stat-item">
530
- <span class="stat-value"><i class="icon icon-complexity"></i> PRO</span>
531
- <span class="stat-label">Любая сложность</span>
532
- </div>
533
- <div class="stat-item">
534
- <span class="stat-value"><i class="icon icon-speed"></i> FAST</span>
535
- <span class="stat-label">Высокая скорость</span>
536
- </div>
537
- </div>
538
- <div style="display: flex; gap: var(--padding-s); margin-top: var(--padding-m); flex-wrap: wrap;">
539
- <a href="https://holmgard.ru" target="_blank" class="btn btn-secondary" style="flex-grow: 1;"><i class="icon icon-link"></i>Веб-сайт студии</a>
540
- <a href="#" class="btn contact-link" style="flex-grow: 1;"><i class="icon icon-contact"></i>Связаться</a>
541
- </div>
542
- </section>
543
-
544
- <section class="section-card">
545
- <h2 class="section-title"><i class="icon icon-global"></i>Глобальное присутствие</h2>
546
- <p class="description">Наши инновационные решения и экспертиза доступны в странах Центральной Азии и за ее пределами:</p>
547
- <div>
548
- <div class="list-item"><i class="icon icon-location"></i>Узбекистан</div>
549
- <div class="list-item"><i class="icon icon-location"></i>Казахстан</div>
550
- <div class="list-item"><i class="icon icon-location"></i>Кыргызстан</div>
551
- <div class="list-item"><i class="icon icon-innovation"></i>Расширяем горизонты...</div>
552
- </div>
553
- </section>
554
-
555
- <footer class="footer-greeting">
556
- <p id="greeting">Инициализация...</p>
557
- </footer>
558
-
559
- </div>
560
-
561
- <button class="save-card-button" id="save-card-btn">
562
- <i class="icon icon-save"></i>Сохранить визитку
563
- </button>
564
-
565
- <!-- The Modal -->
566
- <div id="saveModal" class="modal">
567
- <div class="modal-content">
568
- <span class="modal-close" id="modal-close-btn">×</span>
569
- <p class="modal-text"><b>+996 500 398 754</b></p>
570
- <p class="modal-text">Morshen Group</p>
571
- <p class="modal-text" style="font-size: 1em; color: var(--text-secondary-color);">Международный IT Холдинг</p>
572
- <p class="modal-instruction">Сделайте скриншот экрана, чтобы сохранить контакт.</p>
573
- </div>
574
- </div>
575
-
576
-
577
- <script>
578
- const tg = window.Telegram.WebApp;
579
-
580
- function applyTheme(themeParams) {
581
- const root = document.documentElement;
582
- root.style.setProperty('--tg-theme-bg-color', themeParams.bg_color || '#121212');
583
- root.style.setProperty('--tg-theme-text-color', themeParams.text_color || '#ffffff');
584
- root.style.setProperty('--tg-theme-hint-color', themeParams.hint_color || '#aaaaaa');
585
- root.style.setProperty('--tg-theme-link-color', themeParams.link_color || '#62bcf9');
586
- root.style.setProperty('--tg-theme-button-color', themeParams.button_color || '#31a5f5');
587
- root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || '#ffffff');
588
- root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || '#1e1e1e');
589
-
590
- // Optional: Convert main bg color to RGB for glow effect alpha
591
- try {
592
- const bgColor = themeParams.bg_color || '#121212';
593
- const r = parseInt(bgColor.slice(1, 3), 16);
594
- const g = parseInt(bgColor.slice(3, 5), 16);
595
- const b = parseInt(bgColor.slice(5, 7), 16);
596
- root.style.setProperty('--tg-theme-bg-color-rgb', `${r}, ${g}, ${b}`);
597
- } catch (e) {
598
- root.style.setProperty('--tg-theme-bg-color-rgb', `18, 18, 18`); // Fallback
599
- }
600
- }
601
-
602
- function setupTelegram() {
603
- if (!tg || !tg.initData) {
604
- console.error("Telegram WebApp script not loaded or initData is missing.");
605
- const greetingElement = document.getElementById('greeting');
606
- if(greetingElement) greetingElement.textContent = 'Не удалось связаться с Telegram.';
607
- // Apply default dark theme maybe? Or leave as is.
608
- document.body.style.visibility = 'visible';
609
- return;
610
- }
611
-
612
- tg.ready();
613
- tg.expand();
614
 
615
- applyTheme(tg.themeParams);
616
- tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
617
-
618
- // Send initData for verification and user logging
619
- fetch('/verify', {
620
- method: 'POST',
621
- headers: {
622
- 'Content-Type': 'application/json',
623
- 'Accept': 'application/json'
624
- },
625
- body: JSON.stringify({ initData: tg.initData }),
626
- })
627
- .then(response => {
628
- if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
629
- return response.json();
630
- })
631
- .then(data => {
632
- if (data.status === 'ok' && data.verified) {
633
- console.log('Backend verification successful.');
634
- } else {
635
- console.warn('Backend verification failed:', data.message);
636
- // Potentially show a non-blocking warning to user if needed
637
- }
638
- })
639
- .catch(error => {
640
- console.error('Error sending initData for verification:', error);
641
- // Display a more user-friendly error?
642
- });
643
-
644
-
645
- // User Greeting (using unsafe data for immediate feedback)
646
- const user = tg.initDataUnsafe?.user;
647
- const greetingElement = document.getElementById('greeting');
648
- if (user) {
649
- const name = user.first_name || user.username || 'Гость';
650
- greetingElement.textContent = `Добро пожаловать, ${name}! 👋`;
651
- } else {
652
- greetingElement.textContent = 'Добро пожаловать!';
653
- console.warn('Telegram User data not available (initDataUnsafe.user is empty).');
654
- }
655
-
656
- // Contact Links
657
- const contactButtons = document.querySelectorAll('.contact-link');
658
- contactButtons.forEach(button => {
659
- button.addEventListener('click', (e) => {
660
- e.preventDefault();
661
- tg.openTelegramLink('https://t.me/morshenkhan'); // Use actual contact username
662
- });
663
- });
664
-
665
- // Modal Setup
666
- const modal = document.getElementById("saveModal");
667
- const saveCardBtn = document.getElementById("save-card-btn");
668
- const closeBtn = document.getElementById("modal-close-btn");
669
-
670
- if (saveCardBtn && modal && closeBtn) {
671
- saveCardBtn.addEventListener('click', (e) => {
672
- e.preventDefault();
673
- modal.style.display = "block";
674
- if (tg.HapticFeedback) {
675
- tg.HapticFeedback.impactOccurred('light');
676
- }
677
- });
678
-
679
- closeBtn.addEventListener('click', () => {
680
- modal.style.display = "none";
681
- });
682
-
683
- window.addEventListener('click', (event) => {
684
- if (event.target == modal) {
685
- modal.style.display = "none";
686
- }
687
- });
688
- } else {
689
- console.error("Modal elements not found!");
690
- }
691
-
692
- document.body.style.visibility = 'visible';
693
- }
694
-
695
- if (window.Telegram && window.Telegram.WebApp) {
696
- setupTelegram();
697
- } else {
698
- console.warn("Telegram WebApp script not immediately available, waiting for window.onload");
699
- window.addEventListener('load', setupTelegram);
700
- setTimeout(() => {
701
- if (document.body.style.visibility !== 'visible') {
702
- console.error("Telegram WebApp script fallback timeout triggered.");
703
- const greetingElement = document.getElementById('greeting');
704
- if(greetingElement) greetingElement.textContent = 'Ошибка загрузки интерфейса Telegram.';
705
- document.body.style.visibility = 'visible';
706
- }
707
- }, 3500); // Slightly longer timeout
708
- }
709
-
710
- </script>
711
- </body>
712
- </html>
713
- """
714
-
715
- ADMIN_TEMPLATE = """
716
- <!DOCTYPE html>
717
- <html lang="ru">
718
- <head>
719
- <meta charset="UTF-8">
720
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
721
- <title>Admin - Посетители</title>
722
- <link rel="preconnect" href="https://fonts.googleapis.com">
723
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
724
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
725
- <style>
726
- :root {
727
- --admin-bg: #f8f9fa;
728
- --admin-text: #212529;
729
- --admin-card-bg: #ffffff;
730
- --admin-border: #dee2e6;
731
- --admin-shadow: rgba(0, 0, 0, 0.05);
732
- --admin-primary: #0d6efd;
733
- --admin-secondary: #6c757d;
734
- --admin-success: #198754;
735
- --admin-danger: #dc3545;
736
- --admin-warning: #ffc107;
737
- --border-radius: 12px;
738
- --padding: 1.5rem;
739
- --font-family: 'Inter', sans-serif;
740
- }
741
- body {
742
- font-family: var(--font-family);
743
- background-color: var(--admin-bg);
744
- color: var(--admin-text);
745
- margin: 0;
746
- padding: var(--padding);
747
- line-height: 1.6;
748
- }
749
- .container { max-width: 1140px; margin: 0 auto; }
750
- h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
751
- .user-grid {
752
- display: grid;
753
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
754
- gap: var(--padding);
755
- margin-top: var(--padding);
756
- }
757
- .user-card {
758
- background-color: var(--admin-card-bg);
759
- border-radius: var(--border-radius);
760
- padding: var(--padding);
761
- box-shadow: 0 4px 15px var(--admin-shadow);
762
- border: 1px solid var(--admin-border);
763
- display: flex;
764
- flex-direction: column;
765
- align-items: center;
766
- text-align: center;
767
- transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
768
- }
769
- .user-card:hover {
770
- transform: translateY(-5px);
771
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
772
- }
773
- .user-card img {
774
- width: 80px; height: 80px;
775
- border-radius: 50%; margin-bottom: 1rem;
776
- object-fit: cover; border: 3px solid var(--admin-border);
777
- background-color: #eee; /* Placeholder bg */
778
- }
779
- .user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
780
- .user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
781
- .user-card .details { font-size: 0.9em; color: #495057; word-break: break-word; }
782
- .user-card .detail-item { margin-bottom: 0.3rem; }
783
- .user-card .detail-item strong { color: var(--admin-text); }
784
- .user-card .timestamp { font-size: 0.8em; color: var(--admin-secondary); margin-top: 1rem; }
785
- .no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
786
- .alert {
787
- background-color: #fff3cd; border-left: 6px solid var(--admin-warning);
788
- margin-bottom: var(--padding); padding: 1rem 1.5rem;
789
- color: #664d03; border-radius: 8px; text-align: center; font-weight: 500;
790
- }
791
- .refresh-btn {
792
- display: block;
793
- margin: 1rem auto;
794
- padding: 10px 20px;
795
- font-size: 1em;
796
- font-weight: 500;
797
- color: #fff;
798
- background-color: var(--admin-primary);
799
- border: none;
800
- border-radius: 8px;
801
- cursor: pointer;
802
- transition: background-color 0.2s ease;
803
- }
804
- .refresh-btn:hover { background-color: #0b5ed7; }
805
-
806
- /* Admin Controls */
807
- .admin-controls {
808
- background: var(--admin-card-bg);
809
- padding: var(--padding);
810
- border-radius: var(--border-radius);
811
- box-shadow: 0 4px 15px var(--admin-shadow);
812
- border: 1px solid var(--admin-border);
813
- margin-bottom: var(--padding);
814
- text-align: center;
815
- }
816
- .admin-controls h2 { margin-top: 0; margin-bottom: 1rem; font-weight: 600; color: var(--admin-secondary); }
817
- .admin-controls .btn {
818
- padding: 10px 18px;
819
- font-size: 0.95em;
820
- font-weight: 500;
821
- border: none;
822
- border-radius: 8px;
823
- cursor: pointer;
824
- transition: all 0.2s ease;
825
- margin: 0.5rem;
826
- color: #fff;
827
- }
828
- .admin-controls .btn-backup { background-color: var(--admin-success); }
829
- .admin-controls .btn-backup:hover { background-color: #157347; }
830
- .admin-controls .btn-download { background-color: var(--admin-primary); }
831
- .admin-controls .btn-download:hover { background-color: #0b5ed7; }
832
- .admin-controls .status { font-size: 0.9em; margin-top: 1rem; color: var(--admin-secondary); }
833
- .admin-controls .loader {
834
- border: 4px solid #f3f3f3; border-radius: 50%; border-top: 4px solid var(--admin-primary);
835
- width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-left: 10px; vertical-align: middle;
836
- display: none; /* Hidden by default */
837
- }
838
- @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
839
- </style>
840
- </head>
841
- <body>
842
- <div class="container">
843
- <h1>Посетители Mini App</h1>
844
- <div class="alert">ВНИМАНИЕ: Этот раздел не защищен! Добавьте аутентификацию для реального использования.</div>
845
-
846
- <div class="admin-controls">
847
- <h2>Управление данными</h2>
848
- <button class="btn btn-download" onclick="triggerDownload()">Скачать данные с HF</button>
849
- <button class="btn btn-backup" onclick="triggerUpload()">Загрузить данные на HF</button>
850
- <div class="loader" id="loader"></div>
851
- <div class="status" id="status-message"></div>
852
- </div>
853
-
854
- <button class="refresh-btn" onclick="location.reload()">Обновить список</button>
855
-
856
- {% if users %}
857
- <div class="user-grid">
858
- {% for user in users|sort(attribute='visited_at', reverse=true) %}
859
- <div class="user-card">
860
- <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">
861
- <div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
862
- {% if user.username %}
863
- <div class="username"><a href="https://t.me/{{ user.username }}" target="_blank" style="color: inherit; text-decoration: none;">@{{ user.username }}</a></div>
864
- {% else %}
865
- <div class="username" style="height: 1.3em;"></div> {# Placeholder for spacing #}
866
- {% endif %}
867
- <div class="details">
868
- <div class="detail-item"><strong>ID:</strong> {{ user.id }}</div>
869
- <div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
870
- <div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
871
- <div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
872
- </div>
873
- <div class="timestamp">Визит: {{ user.visited_at_str }}</div>
874
- </div>
875
- {% endfor %}
876
- </div>
877
- {% else %}
878
- <p class="no-users">Данных о посетителях пока нет.</p>
879
- {% endif %}
880
- </div>
881
-
882
- <script>
883
- const loader = document.getElementById('loader');
884
- const statusMessage = document.getElementById('status-message');
885
-
886
- async function handleFetch(url, action) {
887
- loader.style.display = 'inline-block';
888
- statusMessage.textContent = `Выполняется ${action}...`;
889
- statusMessage.style.color = 'var(--admin-secondary)';
890
- try {
891
- const response = await fetch(url, { method: 'POST' });
892
- const data = await response.json();
893
- if (response.ok && data.status === 'ok') {
894
- statusMessage.textContent = data.message;
895
- statusMessage.style.color = 'var(--admin-success)';
896
- if (action === 'скачивание') {
897
- setTimeout(() => location.reload(), 1500); // Reload after download success
898
- }
899
- } else {
900
- throw new Error(data.message || 'Произошла ошибка');
901
- }
902
- } catch (error) {
903
- statusMessage.textContent = `Ошибка ${action}: ${error.message}`;
904
- statusMessage.style.color = 'var(--admin-danger)';
905
- console.error(`Error during ${action}:`, error);
906
- } finally {
907
- loader.style.display = 'none';
908
- }
909
- }
910
-
911
- function triggerDownload() {
912
- handleFetch('/admin/download_data', 'скачивание');
913
- }
914
-
915
- function triggerUpload() {
916
- handleFetch('/admin/upload_data', 'загрузка');
917
- }
918
- </script>
919
- </body>
920
- </html>
921
- """
922
-
923
- # --- Flask Routes ---
924
  @app.route('/')
925
  def index():
926
- # Pass theme parameters for initial render if available (e.g., from query params or session)
927
- # For simplicity, we let the JS handle theme application after tg.ready()
928
- theme_params = {} # Or load from request if needed
929
- return render_template_string(TEMPLATE, theme=theme_params)
930
-
931
- @app.route('/verify', methods=['POST'])
932
- def verify_data():
933
- try:
934
- req_data = request.get_json()
935
- init_data_str = req_data.get('initData')
936
- if not init_data_str:
937
- return jsonify({"status": "error", "message": "Missing initData"}), 400
 
 
 
 
 
 
 
938
 
939
- user_data_parsed, is_valid = verify_telegram_data(init_data_str)
 
 
 
 
 
 
 
 
 
940
 
941
- user_info_dict = {}
942
- if user_data_parsed and 'user' in user_data_parsed:
943
- try:
944
- user_json_str = unquote(user_data_parsed['user'][0])
945
- user_info_dict = json.loads(user_json_str)
946
- except Exception as e:
947
- logging.error(f"Could not parse user JSON: {e}")
948
- user_info_dict = {}
949
 
950
- if is_valid:
951
- user_id = user_info_dict.get('id')
952
- if user_id:
953
- now = time.time()
954
- # Create data entry for the specific user
955
- user_entry = {
956
- str(user_id): { # Use string keys for JSON compatibility
957
- 'id': user_id,
958
- 'first_name': user_info_dict.get('first_name'),
959
- 'last_name': user_info_dict.get('last_name'),
960
- 'username': user_info_dict.get('username'),
961
- 'photo_url': user_info_dict.get('photo_url'),
962
- 'language_code': user_info_dict.get('language_code'),
963
- 'is_premium': user_info_dict.get('is_premium', False),
964
- 'phone_number': user_info_dict.get('phone_number'), # Note: Only available if requested via button
965
- 'visited_at': now,
966
- 'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
967
- }
 
 
 
 
 
 
 
968
  }
969
- # Save/update this specific user's data
970
- save_visitor_data(user_entry)
971
- return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
972
- else:
973
- logging.warning(f"Verification failed for user: {user_info_dict.get('id')}")
974
- return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
975
-
976
- except Exception as e:
977
- logging.exception("Error in /verify endpoint")
978
- return jsonify({"status": "error", "message": "Internal server error"}), 500
979
-
980
- @app.route('/admin')
981
- def admin_panel():
982
- # WARNING: This route is unprotected! Add proper authentication/authorization.
983
- current_data = load_visitor_data() # Load from cache/file
984
- users_list = list(current_data.values())
985
- return render_template_string(ADMIN_TEMPLATE, users=users_list)
986
-
987
- @app.route('/admin/download_data', methods=['POST'])
988
- def admin_trigger_download():
989
- # WARNING: Unprotected endpoint
990
- success = download_data_from_hf()
991
- if success:
992
- return jsonify({"status": "ok", "message": "Скачивание данных с Hugging Face завершено. Страница будет обновлена."})
993
- else:
994
- return jsonify({"status": "error", "message": "Ошибка скачивания данных с Hugging Face. Проверьте логи."}), 500
995
-
996
- @app.route('/admin/upload_data', methods=['POST'])
997
- def admin_trigger_upload():
998
- # WARNING: Unprotected endpoint
999
- if not HF_TOKEN_WRITE:
1000
- return jsonify({"status": "error", "message": "HF_TOKEN_WRITE не настроен на сервере."}), 400
1001
- upload_data_to_hf_async() # Trigger async upload
1002
- return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
1003
 
 
 
 
 
 
 
 
 
1004
 
1005
- # --- App Initialization ---
1006
  if __name__ == '__main__':
1007
- print("---")
1008
- print("--- MORSHEN GROUP MINI APP SERVER ---")
1009
- print("---")
1010
- print(f"Flask server starting on http://{HOST}:{PORT}")
1011
- print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
1012
- print(f"Visitor data file: {DATA_FILE}")
1013
- print(f"Hugging Face Repo: {REPO_ID}")
1014
- print(f"HF Data Path: {HF_DATA_FILE_PATH}")
1015
- if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
1016
- print("---")
1017
- print("--- WARNING: HUGGING FACE TOKEN(S) NOT SET ---")
1018
- print("--- Backup/restore functionality will be limited. Set HF_TOKEN_READ and HF_TOKEN_WRITE environment variables.")
1019
- print("---")
1020
- else:
1021
- print("--- Hugging Face tokens found.")
1022
- # Initial attempt to download data on startup
1023
- print("--- Attempting initial data download from Hugging Face...")
1024
- download_data_from_hf()
1025
-
1026
- # Load initial data from local file (might have been updated by download)
1027
- load_visitor_data()
1028
-
1029
- print("---")
1030
- print("--- SECURITY WARNING ---")
1031
- print("--- The /admin route and its sub-routes are NOT protected.")
1032
- print("--- Implement proper authentication before deploying.")
1033
- print("---")
1034
-
1035
- # Start periodic backup thread if write token is available
1036
- if HF_TOKEN_WRITE:
1037
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1038
- backup_thread.start()
1039
- print("--- Periodic backup thread started (every hour).")
1040
- else:
1041
- print("--- Periodic backup disabled (HF_TOKEN_WRITE missing).")
1042
-
1043
- print("--- Server Ready ---")
1044
- # Use a production server like Waitress or Gunicorn instead of app.run() for deployment
1045
- # from waitress import serve
1046
- # serve(app, host=HOST, port=PORT)
1047
- app.run(host=HOST, port=PORT, debug=False) # debug=False for production recommended
 
1
+ from flask import Flask, Response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  app = Flask(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  @app.route('/')
5
  def index():
6
+ html_content = '''
7
+ <!DOCTYPE html>
8
+ <html>
9
+ <head>
10
+ <title>TON Connect</title>
11
+ <meta charset="UTF-8">
12
+ <script src="https://unpkg.com/@tonconnect/sdk@latest"></script>
13
+ <style>
14
+ body {
15
+ font-family: Arial, sans-serif;
16
+ background: #0f0f0f;
17
+ color: #fff;
18
+ display: flex;
19
+ flex-direction: column;
20
+ align-items: center;
21
+ justify-content: center;
22
+ height: 100vh;
23
+ margin: 0;
24
+ }
25
 
26
+ .button {
27
+ background: #00aaff;
28
+ border: none;
29
+ color: white;
30
+ padding: 15px 30px;
31
+ font-size: 16px;
32
+ cursor: pointer;
33
+ border-radius: 10px;
34
+ transition: background 0.3s ease;
35
+ }
36
 
37
+ .button:hover {
38
+ background: #0077cc;
39
+ }
 
 
 
 
 
40
 
41
+ #status {
42
+ margin-top: 20px;
43
+ font-size: 14px;
44
+ color: #aaa;
45
+ }
46
+ </style>
47
+ </head>
48
+ <body>
49
+ <h1>Подключение к TON Connect</h1>
50
+ <button class="button" id="connect-button">Подключиться</button>
51
+ <div id="status">Не подключено</div>
52
+
53
+ <script>
54
+ const tonConnect = new TON_CONNECT_UI.TonConnectUI({
55
+ manifestUrl: 'https://huggingface.co/spaces/Aleksmorshen/MorshenGroup/resolve/main/tonconnect-manifest.json',
56
+ buttonRootId: 'connect-button'
57
+ });
58
+
59
+ const statusDiv = document.getElementById('status');
60
+
61
+ tonConnect.onStatusChange(wallet => {
62
+ if (wallet) {
63
+ statusDiv.innerText = `Кошелек подключен: ${wallet.account.address}`;
64
+ } else {
65
+ statusDiv.innerText = 'Не подключено';
66
  }
67
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
+ document.getElementById('connect-button').addEventListener('click', () => {
70
+ tonConnect.connectWallet();
71
+ });
72
+ </script>
73
+ </body>
74
+ </html>
75
+ '''
76
+ return Response(html_content, mimetype='text/html')
77
 
 
78
  if __name__ == '__main__':
79
+ app.run(host='0.0.0.0', port=7860)