Aleksmorshen commited on
Commit
f3013bf
·
verified ·
1 Parent(s): 98eb53a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +396 -416
app.py CHANGED
@@ -1,13 +1,125 @@
1
  import os
2
  import json
3
  import uuid
 
4
  from datetime import datetime
5
- from flask import Flask, Response, request, jsonify
 
 
 
 
6
  from werkzeug.security import generate_password_hash, check_password_hash
 
 
 
 
 
 
7
 
8
  app = Flask(__name__)
9
 
10
  DB_FILE = 'db.json'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  def init_db():
13
  if not os.path.exists(DB_FILE):
@@ -17,6 +129,7 @@ def init_db():
17
  "chatrooms": {},
18
  "messages": {}
19
  }, f, indent=4)
 
20
 
21
  def read_db():
22
  with open(DB_FILE, 'r') as f:
@@ -25,6 +138,7 @@ def read_db():
25
  def write_db(data):
26
  with open(DB_FILE, 'w') as f:
27
  json.dump(data, f, indent=4)
 
28
 
29
  @app.route('/')
30
  def index():
@@ -57,406 +171,90 @@ def index():
57
  --font-family: 'Inter', sans-serif;
58
  }
59
 
60
- * {
61
- box-sizing: border-box;
62
- margin: 0;
63
- padding: 0;
64
- -webkit-tap-highlight-color: transparent;
65
- }
66
-
67
- html {
68
- font-size: 16px;
69
- }
70
-
71
- body {
72
- font-family: var(--font-family);
73
- background-color: var(--bg-primary);
74
- color: var(--text-primary);
75
- overflow: hidden;
76
- height: 100vh;
77
- width: 100vw;
78
- display: flex;
79
- align-items: center;
80
- justify-content: center;
81
- }
82
-
83
- .main-container {
84
- width: 100%;
85
- height: 100%;
86
- display: flex;
87
- flex-direction: column;
88
- transition: opacity 0.3s ease;
89
- }
90
-
91
- #login-view {
92
- display: flex;
93
- flex-direction: column;
94
- align-items: center;
95
- justify-content: center;
96
- text-align: center;
97
- width: 100%;
98
- height: 100%;
99
- background: radial-gradient(circle, #1a2a3a 0%, var(--bg-primary) 70%);
100
- }
101
- #login-view img {
102
- width: 100px;
103
- height: 100px;
104
- margin-bottom: 24px;
105
- filter: drop-shadow(0 0 15px rgba(0, 136, 204, 0.5));
106
- }
107
- #login-view h1 {
108
- font-size: 2.8rem;
109
- font-weight: 700;
110
- background: linear-gradient(45deg, var(--accent-blue-light), var(--accent-blue));
111
- -webkit-background-clip: text;
112
- -webkit-text-fill-color: transparent;
113
- margin-bottom: 12px;
114
- }
115
- #login-view p {
116
- font-size: 1.1rem;
117
- color: var(--text-secondary);
118
- margin-bottom: 40px;
119
- }
120
-
121
- #app-view {
122
- display: none;
123
- width: 100%;
124
- height: 100%;
125
- }
126
-
127
- #chatroom-list-view {
128
- display: flex;
129
- flex-direction: column;
130
- height: 100%;
131
- width: 100%;
132
- background-color: var(--bg-secondary);
133
- }
134
-
135
- .list-header {
136
- padding: 16px;
137
- border-bottom: 1px solid var(--border-color);
138
- flex-shrink: 0;
139
- }
140
- .list-header-top {
141
- display: flex;
142
- justify-content: space-between;
143
- align-items: center;
144
- margin-bottom: 16px;
145
- }
146
- .list-header-top h2 {
147
- font-size: 1.5rem;
148
- font-weight: 600;
149
- }
150
-
151
- .user-profile {
152
- padding: 12px;
153
- background-color: var(--bg-tertiary);
154
- border-radius: 8px;
155
- margin-bottom: 16px;
156
- }
157
- #user-wallet, #user-nickname {
158
- font-size: 0.9rem;
159
- color: var(--text-secondary);
160
- margin-bottom: 8px;
161
- word-break: break-all;
162
- }
163
-
164
  .username-form { display: flex; gap: 8px; }
165
- .username-input {
166
- flex-grow: 1;
167
- background-color: var(--bg-primary);
168
- border: 1px solid var(--border-color);
169
- color: var(--text-primary);
170
- border-radius: 6px;
171
- padding: 8px 12px;
172
- font-size: 0.9rem;
173
- }
174
  .username-input:focus { outline: none; border-color: var(--accent-blue); }
175
-
176
- .action-btn {
177
- background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
178
- color: white;
179
- border: none;
180
- padding: 10px 16px;
181
- border-radius: 6px;
182
- cursor: pointer;
183
- font-weight: 500;
184
- transition: transform 0.2s ease, box-shadow 0.2s ease;
185
- display: flex;
186
- align-items: center;
187
- justify-content: center;
188
- gap: 8px;
189
- }
190
- .action-btn:hover {
191
- transform: translateY(-2px);
192
- box-shadow: 0 4px 15px rgba(0, 136, 204, 0.3);
193
- }
194
- .action-btn.small {
195
- padding: 8px 12px;
196
- font-size: 0.9rem;
197
- }
198
-
199
- #chatroom-list {
200
- flex-grow: 1;
201
- overflow-y: auto;
202
- }
203
- .chatroom-item {
204
- display: flex;
205
- align-items: center;
206
- gap: 12px;
207
- padding: 12px 16px;
208
- cursor: pointer;
209
- border-bottom: 1px solid var(--border-color);
210
- transition: background-color 0.2s ease;
211
- }
212
  .chatroom-item:hover { background-color: var(--bg-hover); }
213
-
214
- .avatar {
215
- width: 40px;
216
- height: 40px;
217
- border-radius: 50%;
218
- display: flex;
219
- align-items: center;
220
- justify-content: center;
221
- font-weight: 600;
222
- color: white;
223
- flex-shrink: 0;
224
- }
225
- .chatroom-info {
226
- flex-grow: 1;
227
- overflow: hidden;
228
- }
229
- .chatroom-name {
230
- font-weight: 500;
231
- white-space: nowrap;
232
- overflow: hidden;
233
- text-overflow: ellipsis;
234
- }
235
- .lock-icon {
236
- width: 16px;
237
- height: 16px;
238
- fill: var(--text-secondary);
239
- flex-shrink: 0;
240
- }
241
-
242
- #chat-window-view {
243
- display: none;
244
- flex-direction: column;
245
- height: 100%;
246
- width: 100%;
247
- background-color: var(--bg-primary);
248
- }
249
-
250
- .chat-header {
251
- display: flex;
252
- align-items: center;
253
- gap: 12px;
254
- padding: 12px 16px;
255
- background-color: var(--bg-secondary);
256
- border-bottom: 1px solid var(--border-color);
257
- flex-shrink: 0;
258
- }
259
- .back-btn {
260
- background: none;
261
- border: none;
262
- cursor: pointer;
263
- display: none;
264
- }
265
- .back-btn svg {
266
- width: 24px;
267
- height: 24px;
268
- fill: var(--text-primary);
269
- }
270
- #chat-header-title {
271
- font-size: 1.2rem;
272
- font-weight: 600;
273
- }
274
-
275
- #messages-container {
276
- flex-grow: 1;
277
- padding: 16px;
278
- overflow-y: auto;
279
- display: flex;
280
- flex-direction: column;
281
- gap: 12px;
282
- }
283
-
284
- .message {
285
- display: flex;
286
- gap: 10px;
287
- max-width: 80%;
288
- }
289
- .message .avatar {
290
- width: 36px;
291
- height: 36px;
292
- align-self: flex-end;
293
- }
294
- .message-content {
295
- display: flex;
296
- flex-direction: column;
297
- gap: 4px;
298
- }
299
- .message-sender {
300
- font-size: 0.8rem;
301
- font-weight: 500;
302
- color: var(--text-secondary);
303
- word-break: break-all;
304
- cursor: pointer;
305
- }
306
- .message-bubble {
307
- padding: 10px 14px;
308
- border-radius: 18px;
309
- line-height: 1.4;
310
- word-wrap: break-word;
311
- }
312
-
313
  .message.sent { align-self: flex-end; flex-direction: row-reverse; }
314
  .message.sent .message-sender { text-align: right; color: var(--accent-blue-light); }
315
- .message.sent .message-bubble {
316
- background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
317
- color: white;
318
- border-bottom-right-radius: 4px;
319
- }
320
-
321
  .message.received { align-self: flex-start; }
322
- .message.received .message-bubble {
323
- background-color: var(--bg-tertiary);
324
- border-bottom-left-radius: 4px;
325
- }
326
-
327
- .chat-placeholder {
328
- display: flex;
329
- flex-direction: column;
330
- align-items: center;
331
- justify-content: center;
332
- height: 100%;
333
- text-align: center;
334
- color: var(--text-secondary);
335
- padding: 20px;
336
- }
337
  .chat-placeholder img { width: 80px; margin-bottom: 20px; opacity: 0.5; }
338
-
339
- .message-form {
340
- display: flex;
341
- padding: 16px;
342
- gap: 12px;
343
- background-color: var(--bg-secondary);
344
- border-top: 1px solid var(--border-color);
345
- flex-shrink: 0;
346
- }
347
- #message-input {
348
- flex-grow: 1;
349
- padding: 12px 16px;
350
- border: 1px solid var(--border-color);
351
- background-color: var(--bg-tertiary);
352
- color: var(--text-primary);
353
- border-radius: 22px;
354
- outline: none;
355
- font-size: 1rem;
356
- transition: border-color 0.2s;
357
- }
358
  #message-input:focus { border-color: var(--accent-blue); }
359
-
360
- .send-btn {
361
- width: 44px;
362
- height: 44px;
363
- border-radius: 50%;
364
- flex-shrink: 0;
365
- padding: 0;
366
- }
367
- .send-btn svg { width: 20px; height: 20px; fill: white; }
368
-
369
- .modal-overlay {
370
- position: fixed;
371
- top: 0;
372
- left: 0;
373
- width: 100%;
374
- height: 100%;
375
- background-color: rgba(0,0,0,0.7);
376
- display: none;
377
- align-items: center;
378
- justify-content: center;
379
- z-index: 1000;
380
- -webkit-backdrop-filter: blur(5px);
381
- backdrop-filter: blur(5px);
382
- }
383
- .modal-content {
384
- background-color: var(--bg-secondary);
385
- padding: 24px;
386
- border-radius: 12px;
387
- width: 90%;
388
- max-width: 400px;
389
- border: 1px solid var(--border-color);
390
- box-shadow: 0 10px 30px rgba(0,0,0,0.5);
391
- }
392
  .modal-content h3 { margin-bottom: 20px; font-weight: 600; font-size: 1.3rem; }
393
  .modal-content label { display: block; margin-bottom: 8px; font-size: 0.9rem; color: var(--text-secondary); }
394
- .modal-content input {
395
- width: 100%;
396
- padding: 12px;
397
- margin-bottom: 16px;
398
- background-color: var(--bg-tertiary);
399
- border: 1px solid var(--border-color);
400
- color: white;
401
- border-radius: 6px;
402
- font-size: 1rem;
403
- }
404
  .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
405
- .modal-btn {
406
- padding: 10px 20px;
407
- border-radius: 6px;
408
- border: none;
409
- cursor: pointer;
410
- font-weight: 500;
411
- }
412
  .secondary-btn { background-color: var(--bg-hover); color: white; }
413
-
414
- #status-bar {
415
- position: fixed;
416
- bottom: 20px;
417
- left: 50%;
418
- transform: translateX(-50%);
419
- background-color: var(--bg-tertiary);
420
- color: white;
421
- padding: 12px 20px;
422
- border-radius: 8px;
423
- font-size: 0.9rem;
424
- opacity: 0;
425
- visibility: hidden;
426
- transition: opacity 0.3s, visibility 0.3s;
427
- z-index: 2000;
428
- box-shadow: 0 5px 15px rgba(0,0,0,0.3);
429
- }
430
  #status-bar.success { background-color: var(--success-color); }
431
  #status-bar.error { background-color: var(--error-color); }
432
  #status-bar.visible { opacity: 1; visibility: visible; }
433
-
434
  @media (min-width: 768px) {
435
- .main-container {
436
- max-width: 1100px;
437
- max-height: 800px;
438
- border-radius: 12px;
439
- overflow: hidden;
440
- box-shadow: 0 10px 40px rgba(0,0,0,0.3);
441
- border: 1px solid var(--border-color);
442
- }
443
- #app-view {
444
- flex-direction: row;
445
- }
446
- #chatroom-list-view {
447
- width: 320px;
448
- flex-shrink: 0;
449
- border-right: 1px solid var(--border-color);
450
- display: flex !important;
451
- }
452
- #chat-window-view {
453
- width: auto;
454
- flex-grow: 1;
455
- display: flex !important;
456
- }
457
- #chat-window-view.hidden-on-desktop {
458
- display: flex !important;
459
- }
460
  .back-btn { display: none !important; }
461
  }
462
  </style>
@@ -516,10 +314,21 @@ def index():
516
  </div>
517
  <div id="messages-container"></div>
518
  <form id="message-form" class="message-form">
519
- <input type="text" id="message-input" placeholder="Сообщение..." autocomplete="off">
520
- <button type="submit" class="action-btn send-btn" id="send-btn">
521
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
522
- </button>
 
 
 
 
 
 
 
 
 
 
 
523
  </form>
524
  </div>
525
  </div>
@@ -595,6 +404,7 @@ def index():
595
  let chatroomsData = {};
596
  let html5QrCode = null;
597
  let profileQrCode = null;
 
598
 
599
  const loginView = document.getElementById('login-view');
600
  const appView = document.getElementById('app-view');
@@ -604,6 +414,12 @@ def index():
604
  const activeChat = document.getElementById('active-chat');
605
  const profileModal = document.getElementById('profile-modal');
606
  const scannerModal = document.getElementById('scanner-modal');
 
 
 
 
 
 
607
 
608
  const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
609
 
@@ -752,12 +568,42 @@ def index():
752
  senderDiv.textContent = msg.display_name;
753
  senderDiv.onclick = () => showProfile(msg.sender_address);
754
 
755
- const bubbleDiv = document.createElement('div');
756
- bubbleDiv.className = 'message-bubble';
757
- bubbleDiv.textContent = msg.text;
758
-
759
  contentDiv.appendChild(senderDiv);
760
- contentDiv.appendChild(bubbleDiv);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
761
 
762
  msgDiv.appendChild(contentDiv);
763
  msgDiv.insertBefore(avatar, contentDiv);
@@ -791,6 +637,16 @@ def index():
791
  chatWindowView.style.display = 'none';
792
  }
793
  };
 
 
 
 
 
 
 
 
 
 
794
 
795
  const selectChatroom = (roomId, isPrivate) => {
796
  const roomData = chatroomsData[roomId];
@@ -799,6 +655,7 @@ def index():
799
  const proceedToRoom = () => {
800
  if (messagePollingInterval) clearInterval(messagePollingInterval);
801
  activeChatroomId = roomId;
 
802
 
803
  document.getElementById('chat-header-title').textContent = roomData.name;
804
  const headerAvatar = document.getElementById('chat-header-avatar');
@@ -846,32 +703,106 @@ def index():
846
  }
847
  };
848
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
849
  document.getElementById('message-form').addEventListener('submit', async (e) => {
850
  e.preventDefault();
851
  const input = document.getElementById('message-input');
852
  const sendBtn = document.getElementById('send-btn');
853
  const text = input.value.trim();
854
- if (text && activeChatroomId) {
855
- input.value = '';
856
- input.disabled = true;
857
- sendBtn.disabled = true;
858
- try {
859
- await apiCall('/api/send_message', {
860
- method: 'POST',
861
- headers: { 'Content-Type': 'application/json' },
862
- body: JSON.stringify({
863
- chatroom_id: activeChatroomId,
864
- sender_address: currentUser.address,
865
- text: text
866
- })
867
- });
868
- await fetchMessages(activeChatroomId);
869
- document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
870
- } finally {
871
- input.disabled = false;
872
- sendBtn.disabled = false;
873
- input.focus();
874
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
875
  }
876
  });
877
 
@@ -1043,6 +974,37 @@ def index():
1043
  '''
1044
  return Response(html_content, mimetype='text/html')
1045
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1046
 
1047
  @app.route('/api/user_data', methods=['POST'])
1048
  def get_user_data():
@@ -1146,8 +1108,11 @@ def send_message():
1146
  chatroom_id = data.get('chatroom_id')
1147
  sender_address = data.get('sender_address')
1148
  text = data.get('text')
 
 
 
1149
 
1150
- if not all([chatroom_id, sender_address, text]):
1151
  return jsonify({'error': 'Missing data'}), 400
1152
 
1153
  db = read_db()
@@ -1158,7 +1123,10 @@ def send_message():
1158
  'id': str(uuid.uuid4()),
1159
  'sender_address': sender_address,
1160
  'text': text,
1161
- 'timestamp': datetime.utcnow().isoformat() + "Z"
 
 
 
1162
  }
1163
 
1164
  if len(db['messages'][chatroom_id]) >= 100:
@@ -1170,5 +1138,17 @@ def send_message():
1170
 
1171
 
1172
  if __name__ == '__main__':
 
 
 
1173
  init_db()
 
 
 
 
 
 
 
 
 
1174
  app.run(host='0.0.0.0', port=7860)
 
1
  import os
2
  import json
3
  import uuid
4
+ import mimetypes
5
  from datetime import datetime
6
+ import threading
7
+ import time
8
+ import logging
9
+
10
+ from flask import Flask, Response, request, jsonify, send_from_directory
11
  from werkzeug.security import generate_password_hash, check_password_hash
12
+ from werkzeug.utils import secure_filename
13
+ from dotenv import load_dotenv
14
+ from huggingface_hub import HfApi, hf_hub_download
15
+ from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
16
+
17
+ load_dotenv()
18
 
19
  app = Flask(__name__)
20
 
21
  DB_FILE = 'db.json'
22
+ UPLOAD_FOLDER = 'uploads'
23
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
24
+ app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
25
+
26
+ REPO_ID = "Kgshop/virton"
27
+ HF_TOKEN = os.getenv("HF_TOKEN")
28
+
29
+ SYNC_FILES_AND_DIRS = [DB_FILE, 'files']
30
+
31
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
32
+
33
+ def download_from_hf(retries=3, delay=5):
34
+ if not HF_TOKEN:
35
+ logging.warning("HF_TOKEN not set. Download might fail for private repos.")
36
+ return False
37
+
38
+ api = HfApi()
39
+ try:
40
+ all_files = [f.path for f in api.list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN)]
41
+ except (RepositoryNotFoundError, HfHubHTTPError) as e:
42
+ logging.error(f"Could not list files in repo {REPO_ID}. Error: {e}")
43
+ return False
44
+
45
+ # Download db.json first
46
+ if DB_FILE in all_files:
47
+ logging.info(f"Attempting to download {DB_FILE} from {REPO_ID}...")
48
+ try:
49
+ hf_hub_download(
50
+ repo_id=REPO_ID,
51
+ filename=DB_FILE,
52
+ repo_type="dataset",
53
+ token=HF_TOKEN,
54
+ local_dir=".",
55
+ local_dir_use_symlinks=False,
56
+ force_download=True
57
+ )
58
+ logging.info(f"Successfully downloaded {DB_FILE}.")
59
+ except Exception as e:
60
+ logging.error(f"Failed to download {DB_FILE}: {e}")
61
+ return False
62
+ else:
63
+ logging.warning(f"{DB_FILE} not found on repo, will use/create local version.")
64
+
65
+ # Download files from the 'files' directory
66
+ files_dir_path = os.path.join(app.config['UPLOAD_FOLDER'])
67
+ os.makedirs(files_dir_path, exist_ok=True)
68
+ repo_files_dir = "files/"
69
+
70
+ for file_path in all_files:
71
+ if file_path.startswith(repo_files_dir):
72
+ local_file_path = os.path.join(files_dir_path, os.path.basename(file_path))
73
+ if not os.path.exists(local_file_path):
74
+ logging.info(f"Downloading missing file {file_path}...")
75
+ try:
76
+ hf_hub_download(
77
+ repo_id=REPO_ID,
78
+ filename=file_path,
79
+ repo_type="dataset",
80
+ token=HF_TOKEN,
81
+ local_dir=files_dir_path,
82
+ local_dir_use_symlinks=False
83
+ )
84
+ # hf_hub_download saves with full path, we need to rename it
85
+ downloaded_full_path = os.path.join(files_dir_path, file_path)
86
+ if os.path.exists(downloaded_full_path):
87
+ os.rename(downloaded_full_path, local_file_path)
88
+ # Clean up empty directories if any
89
+ if os.path.exists(os.path.join(files_dir_path, 'files')):
90
+ os.rmdir(os.path.join(files_dir_path, 'files'))
91
+ except Exception as e:
92
+ logging.error(f"Error downloading repo file {file_path}: {e}")
93
+
94
+ logging.info("Hugging Face download sync finished.")
95
+ return True
96
+
97
+ def upload_to_hf(path_or_fileobj, path_in_repo):
98
+ if not HF_TOKEN:
99
+ logging.warning("HF_TOKEN not set. Skipping upload to Hugging Face.")
100
+ return
101
+ try:
102
+ api = HfApi()
103
+ api.upload_file(
104
+ path_or_fileobj=path_or_fileobj,
105
+ path_in_repo=path_in_repo,
106
+ repo_id=REPO_ID,
107
+ repo_type="dataset",
108
+ token=HF_TOKEN,
109
+ commit_message=f"Sync {os.path.basename(path_in_repo)} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
110
+ )
111
+ logging.info(f"File {path_in_repo} successfully uploaded to Hugging Face.")
112
+ except Exception as e:
113
+ logging.error(f"Error uploading file {path_in_repo} to Hugging Face: {e}")
114
+
115
+ def periodic_backup():
116
+ backup_interval = 1800
117
+ logging.info(f"Setting up periodic backup every {backup_interval} seconds.")
118
+ while True:
119
+ time.sleep(backup_interval)
120
+ logging.info("Starting periodic backup of DB_FILE...")
121
+ upload_to_hf(path_or_fileobj=DB_FILE, path_in_repo=DB_FILE)
122
+ logging.info("Periodic backup finished.")
123
 
124
  def init_db():
125
  if not os.path.exists(DB_FILE):
 
129
  "chatrooms": {},
130
  "messages": {}
131
  }, f, indent=4)
132
+ upload_to_hf(DB_FILE, DB_FILE)
133
 
134
  def read_db():
135
  with open(DB_FILE, 'r') as f:
 
138
  def write_db(data):
139
  with open(DB_FILE, 'w') as f:
140
  json.dump(data, f, indent=4)
141
+ upload_to_hf(DB_FILE, DB_FILE)
142
 
143
  @app.route('/')
144
  def index():
 
171
  --font-family: 'Inter', sans-serif;
172
  }
173
 
174
+ * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
175
+ html { font-size: 16px; }
176
+ body { font-family: var(--font-family); background-color: var(--bg-primary); color: var(--text-primary); overflow: hidden; height: 100vh; width: 100vw; display: flex; align-items: center; justify-content: center; }
177
+ .main-container { width: 100%; height: 100%; display: flex; flex-direction: column; transition: opacity 0.3s ease; }
178
+ #login-view { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; width: 100%; height: 100%; background: radial-gradient(circle, #1a2a3a 0%, var(--bg-primary) 70%); }
179
+ #login-view img { width: 100px; height: 100px; margin-bottom: 24px; filter: drop-shadow(0 0 15px rgba(0, 136, 204, 0.5)); }
180
+ #login-view h1 { font-size: 2.8rem; font-weight: 700; background: linear-gradient(45deg, var(--accent-blue-light), var(--accent-blue)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 12px; }
181
+ #login-view p { font-size: 1.1rem; color: var(--text-secondary); margin-bottom: 40px; }
182
+ #app-view { display: none; width: 100%; height: 100%; }
183
+ #chatroom-list-view { display: flex; flex-direction: column; height: 100%; width: 100%; background-color: var(--bg-secondary); }
184
+ .list-header { padding: 16px; border-bottom: 1px solid var(--border-color); flex-shrink: 0; }
185
+ .list-header-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
186
+ .list-header-top h2 { font-size: 1.5rem; font-weight: 600; }
187
+ .user-profile { padding: 12px; background-color: var(--bg-tertiary); border-radius: 8px; margin-bottom: 16px; }
188
+ #user-wallet, #user-nickname { font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 8px; word-break: break-all; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  .username-form { display: flex; gap: 8px; }
190
+ .username-input { flex-grow: 1; background-color: var(--bg-primary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 6px; padding: 8px 12px; font-size: 0.9rem; }
 
 
 
 
 
 
 
 
191
  .username-input:focus { outline: none; border-color: var(--accent-blue); }
192
+ .action-btn { background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light)); color: white; border: none; padding: 10px 16px; border-radius: 6px; cursor: pointer; font-weight: 500; transition: transform 0.2s ease, box-shadow 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 8px; }
193
+ .action-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(0, 136, 204, 0.3); }
194
+ .action-btn.small { padding: 8px 12px; font-size: 0.9rem; }
195
+ #chatroom-list { flex-grow: 1; overflow-y: auto; }
196
+ .chatroom-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; cursor: pointer; border-bottom: 1px solid var(--border-color); transition: background-color 0.2s ease; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  .chatroom-item:hover { background-color: var(--bg-hover); }
198
+ .avatar { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 600; color: white; flex-shrink: 0; }
199
+ .chatroom-info { flex-grow: 1; overflow: hidden; }
200
+ .chatroom-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
201
+ .lock-icon { width: 16px; height: 16px; fill: var(--text-secondary); flex-shrink: 0; }
202
+ #chat-window-view { display: none; flex-direction: column; height: 100%; width: 100%; background-color: var(--bg-primary); }
203
+ .chat-header { display: flex; align-items: center; gap: 12px; padding: 12px 16px; background-color: var(--bg-secondary); border-bottom: 1px solid var(--border-color); flex-shrink: 0; }
204
+ .back-btn { background: none; border: none; cursor: pointer; display: none; }
205
+ .back-btn svg { width: 24px; height: 24px; fill: var(--text-primary); }
206
+ #chat-header-title { font-size: 1.2rem; font-weight: 600; }
207
+ #messages-container { flex-grow: 1; padding: 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; }
208
+ .message { display: flex; gap: 10px; max-width: 80%; }
209
+ .message .avatar { width: 36px; height: 36px; align-self: flex-end; }
210
+ .message-content { display: flex; flex-direction: column; gap: 4px; }
211
+ .message-sender { font-size: 0.8rem; font-weight: 500; color: var(--text-secondary); word-break: break-all; cursor: pointer; }
212
+ .message-bubble { padding: 10px 14px; border-radius: 18px; line-height: 1.4; word-wrap: break-word; }
213
+ .message-bubble.has-text { margin-bottom: 8px; }
214
+ .message-attachment { padding: 10px; background-color: rgba(0,0,0,0.2); border-radius: 12px; margin-top: 8px; }
215
+ .message-attachment-image { max-width: 100%; border-radius: 8px; display: block; cursor: pointer; }
216
+ .message-attachment-file { display: flex; align-items: center; gap: 10px; text-decoration: none; color: var(--text-primary); }
217
+ .message-attachment-file svg { width: 24px; height: 24px; fill: var(--text-primary); flex-shrink: 0; }
218
+ .message-attachment-file-info { overflow: hidden; }
219
+ .message-attachment-file-name { font-weight: 500; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  .message.sent { align-self: flex-end; flex-direction: row-reverse; }
221
  .message.sent .message-sender { text-align: right; color: var(--accent-blue-light); }
222
+ .message.sent .message-bubble { background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light)); color: white; border-bottom-right-radius: 4px; }
 
 
 
 
 
223
  .message.received { align-self: flex-start; }
224
+ .message.received .message-bubble { background-color: var(--bg-tertiary); border-bottom-left-radius: 4px; }
225
+ .chat-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; text-align: center; color: var(--text-secondary); padding: 20px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  .chat-placeholder img { width: 80px; margin-bottom: 20px; opacity: 0.5; }
227
+ .message-form { display: flex; padding: 16px; gap: 12px; background-color: var(--bg-secondary); border-top: 1px solid var(--border-color); flex-shrink: 0; flex-direction: column; }
228
+ .message-input-area { display: flex; gap: 12px; align-items: center; }
229
+ #message-input { flex-grow: 1; padding: 12px 16px; border: 1px solid var(--border-color); background-color: var(--bg-tertiary); color: var(--text-primary); border-radius: 22px; outline: none; font-size: 1rem; transition: border-color 0.2s; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  #message-input:focus { border-color: var(--accent-blue); }
231
+ .icon-btn { width: 44px; height: 44px; border-radius: 50%; flex-shrink: 0; padding: 0; background-color: var(--bg-tertiary); border: 1px solid var(--border-color); cursor: pointer; display: flex; align-items: center; justify-content: center; }
232
+ .icon-btn svg { width: 20px; height: 20px; fill: var(--text-secondary); }
233
+ .send-btn { background: var(--accent-blue); border: none; }
234
+ .send-btn svg { fill: white; }
235
+ .file-preview { display: none; align-items: center; gap: 10px; padding: 8px; background-color: var(--bg-tertiary); border-radius: 8px; margin-bottom: 12px; }
236
+ .file-preview-name { flex-grow: 1; font-size: 0.9rem; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
237
+ .file-preview-remove { background: none; border: none; color: var(--error-color); cursor: pointer; font-size: 1rem; }
238
+ #file-upload-progress { display: none; width: 100%; height: 4px; background-color: var(--border-color); border-radius: 2px; overflow: hidden; margin-bottom: 8px; }
239
+ #file-upload-progress-bar { width: 0; height: 100%; background-color: var(--accent-blue); transition: width 0.2s; }
240
+ .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); display: none; align-items: center; justify-content: center; z-index: 1000; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px); }
241
+ .modal-content { background-color: var(--bg-secondary); padding: 24px; border-radius: 12px; width: 90%; max-width: 400px; border: 1px solid var(--border-color); box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  .modal-content h3 { margin-bottom: 20px; font-weight: 600; font-size: 1.3rem; }
243
  .modal-content label { display: block; margin-bottom: 8px; font-size: 0.9rem; color: var(--text-secondary); }
244
+ .modal-content input { width: 100%; padding: 12px; margin-bottom: 16px; background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: white; border-radius: 6px; font-size: 1rem; }
 
 
 
 
 
 
 
 
 
245
  .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
246
+ .modal-btn { padding: 10px 20px; border-radius: 6px; border: none; cursor: pointer; font-weight: 500; }
 
 
 
 
 
 
247
  .secondary-btn { background-color: var(--bg-hover); color: white; }
248
+ #status-bar { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background-color: var(--bg-tertiary); color: white; padding: 12px 20px; border-radius: 8px; font-size: 0.9rem; opacity: 0; visibility: hidden; transition: opacity 0.3s, visibility 0.3s; z-index: 2000; box-shadow: 0 5px 15px rgba(0,0,0,0.3); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  #status-bar.success { background-color: var(--success-color); }
250
  #status-bar.error { background-color: var(--error-color); }
251
  #status-bar.visible { opacity: 1; visibility: visible; }
 
252
  @media (min-width: 768px) {
253
+ .main-container { max-width: 1100px; max-height: 800px; border-radius: 12px; overflow: hidden; box-shadow: 0 10px 40px rgba(0,0,0,0.3); border: 1px solid var(--border-color); }
254
+ #app-view { flex-direction: row; }
255
+ #chatroom-list-view { width: 320px; flex-shrink: 0; border-right: 1px solid var(--border-color); display: flex !important; }
256
+ #chat-window-view { width: auto; flex-grow: 1; display: flex !important; }
257
+ #chat-window-view.hidden-on-desktop { display: flex !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  .back-btn { display: none !important; }
259
  }
260
  </style>
 
314
  </div>
315
  <div id="messages-container"></div>
316
  <form id="message-form" class="message-form">
317
+ <div id="file-upload-progress"><div id="file-upload-progress-bar"></div></div>
318
+ <div class="file-preview" id="file-preview">
319
+ <span class="file-preview-name" id="file-preview-name"></span>
320
+ <button type="button" class="file-preview-remove" id="file-preview-remove">×</button>
321
+ </div>
322
+ <div class="message-input-area">
323
+ <input type="file" id="file-input" style="display: none;">
324
+ <button type="button" class="icon-btn" id="attach-btn" title="Прикрепить файл">
325
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z"/></svg>
326
+ </button>
327
+ <input type="text" id="message-input" placeholder="Сообщение..." autocomplete="off">
328
+ <button type="submit" class="action-btn send-btn" id="send-btn">
329
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
330
+ </button>
331
+ </div>
332
  </form>
333
  </div>
334
  </div>
 
404
  let chatroomsData = {};
405
  let html5QrCode = null;
406
  let profileQrCode = null;
407
+ let selectedFile = null;
408
 
409
  const loginView = document.getElementById('login-view');
410
  const appView = document.getElementById('app-view');
 
414
  const activeChat = document.getElementById('active-chat');
415
  const profileModal = document.getElementById('profile-modal');
416
  const scannerModal = document.getElementById('scanner-modal');
417
+ const fileInput = document.getElementById('file-input');
418
+ const filePreview = document.getElementById('file-preview');
419
+ const filePreviewName = document.getElementById('file-preview-name');
420
+ const uploadProgress = document.getElementById('file-upload-progress');
421
+ const uploadProgressBar = document.getElementById('file-upload-progress-bar');
422
+
423
 
424
  const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
425
 
 
568
  senderDiv.textContent = msg.display_name;
569
  senderDiv.onclick = () => showProfile(msg.sender_address);
570
 
 
 
 
 
571
  contentDiv.appendChild(senderDiv);
572
+
573
+ if (msg.text && msg.text.trim() !== "") {
574
+ const bubbleDiv = document.createElement('div');
575
+ bubbleDiv.className = 'message-bubble';
576
+ bubbleDiv.textContent = msg.text;
577
+ contentDiv.appendChild(bubbleDiv);
578
+ }
579
+
580
+ if (msg.file_url) {
581
+ const attachmentDiv = document.createElement('div');
582
+ attachmentDiv.className = 'message-attachment';
583
+
584
+ if (msg.file_type && msg.file_type.startsWith('image/')) {
585
+ const img = document.createElement('img');
586
+ img.src = msg.file_url;
587
+ img.className = 'message-attachment-image';
588
+ img.alt = msg.original_filename;
589
+ img.onclick = () => window.open(msg.file_url, '_blank');
590
+ attachmentDiv.appendChild(img);
591
+ } else {
592
+ const fileLink = document.createElement('a');
593
+ fileLink.href = msg.file_url;
594
+ fileLink.target = '_blank';
595
+ fileLink.download = msg.original_filename;
596
+ fileLink.className = 'message-attachment-file';
597
+
598
+ const fileIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/></svg>`;
599
+ const fileInfo = `<div class="message-attachment-file-info"><div class="message-attachment-file-name">${msg.original_filename}</div></div>`;
600
+
601
+ fileLink.innerHTML = fileIcon + fileInfo;
602
+ attachmentDiv.appendChild(fileLink);
603
+ }
604
+ contentDiv.appendChild(attachmentDiv);
605
+ }
606
+
607
 
608
  msgDiv.appendChild(contentDiv);
609
  msgDiv.insertBefore(avatar, contentDiv);
 
637
  chatWindowView.style.display = 'none';
638
  }
639
  };
640
+
641
+ const resetMessageForm = () => {
642
+ document.getElementById('message-input').value = '';
643
+ fileInput.value = '';
644
+ selectedFile = null;
645
+ filePreview.style.display = 'none';
646
+ filePreviewName.textContent = '';
647
+ uploadProgress.style.display = 'none';
648
+ uploadProgressBar.style.width = '0%';
649
+ };
650
 
651
  const selectChatroom = (roomId, isPrivate) => {
652
  const roomData = chatroomsData[roomId];
 
655
  const proceedToRoom = () => {
656
  if (messagePollingInterval) clearInterval(messagePollingInterval);
657
  activeChatroomId = roomId;
658
+ resetMessageForm();
659
 
660
  document.getElementById('chat-header-title').textContent = roomData.name;
661
  const headerAvatar = document.getElementById('chat-header-avatar');
 
703
  }
704
  };
705
 
706
+ document.getElementById('attach-btn').addEventListener('click', () => fileInput.click());
707
+
708
+ fileInput.addEventListener('change', () => {
709
+ if (fileInput.files.length > 0) {
710
+ selectedFile = fileInput.files[0];
711
+ filePreviewName.textContent = selectedFile.name;
712
+ filePreview.style.display = 'flex';
713
+ }
714
+ });
715
+
716
+ document.getElementById('file-preview-remove').addEventListener('click', () => {
717
+ fileInput.value = '';
718
+ selectedFile = null;
719
+ filePreview.style.display = 'none';
720
+ });
721
+
722
+ const uploadFile = async (file) => {
723
+ const formData = new FormData();
724
+ formData.append('file', file);
725
+
726
+ uploadProgress.style.display = 'block';
727
+
728
+ return new Promise((resolve, reject) => {
729
+ const xhr = new XMLHttpRequest();
730
+ xhr.open('POST', '/api/upload_file', true);
731
+
732
+ xhr.upload.onprogress = (event) => {
733
+ if (event.lengthComputable) {
734
+ const percentComplete = (event.loaded / event.total) * 100;
735
+ uploadProgressBar.style.width = percentComplete + '%';
736
+ }
737
+ };
738
+
739
+ xhr.onload = () => {
740
+ uploadProgress.style.display = 'none';
741
+ uploadProgressBar.style.width = '0%';
742
+ if (xhr.status >= 200 && xhr.status < 300) {
743
+ resolve(JSON.parse(xhr.responseText));
744
+ } else {
745
+ const errorResponse = JSON.parse(xhr.responseText || '{}');
746
+ reject(new Error(errorResponse.error || 'File upload failed'));
747
+ }
748
+ };
749
+
750
+ xhr.onerror = () => {
751
+ uploadProgress.style.display = 'none';
752
+ uploadProgressBar.style.width = '0%';
753
+ reject(new Error('Network error during file upload.'));
754
+ };
755
+
756
+ xhr.send(formData);
757
+ });
758
+ };
759
+
760
  document.getElementById('message-form').addEventListener('submit', async (e) => {
761
  e.preventDefault();
762
  const input = document.getElementById('message-input');
763
  const sendBtn = document.getElementById('send-btn');
764
  const text = input.value.trim();
765
+
766
+ if (!text && !selectedFile) return;
767
+
768
+ input.disabled = true;
769
+ sendBtn.disabled = true;
770
+
771
+ try {
772
+ let fileData = null;
773
+ if (selectedFile) {
774
+ fileData = await uploadFile(selectedFile);
 
 
 
 
 
 
 
 
 
 
775
  }
776
+
777
+ const messagePayload = {
778
+ chatroom_id: activeChatroomId,
779
+ sender_address: currentUser.address,
780
+ text: text
781
+ };
782
+
783
+ if (fileData) {
784
+ messagePayload.file_url = fileData.url;
785
+ messagePayload.original_filename = fileData.filename;
786
+ messagePayload.file_type = fileData.file_type;
787
+ }
788
+
789
+ await apiCall('/api/send_message', {
790
+ method: 'POST',
791
+ headers: { 'Content-Type': 'application/json' },
792
+ body: JSON.stringify(messagePayload)
793
+ });
794
+
795
+ resetMessageForm();
796
+ await fetchMessages(activeChatroomId);
797
+ document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
798
+
799
+ } catch(error) {
800
+ showStatus(`Ошибка отправки: ${error.message}`, 'error');
801
+ }
802
+ finally {
803
+ input.disabled = false;
804
+ sendBtn.disabled = false;
805
+ input.focus();
806
  }
807
  });
808
 
 
974
  '''
975
  return Response(html_content, mimetype='text/html')
976
 
977
+ @app.route('/files/<filename>')
978
+ def uploaded_file(filename):
979
+ return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
980
+
981
+ @app.route('/api/upload_file', methods=['POST'])
982
+ def upload_file():
983
+ if 'file' not in request.files:
984
+ return jsonify({'error': 'No file part'}), 400
985
+ file = request.files['file']
986
+ if file.filename == '':
987
+ return jsonify({'error': 'No selected file'}), 400
988
+ if file:
989
+ original_filename = secure_filename(file.filename)
990
+ extension = os.path.splitext(original_filename)[1]
991
+ unique_filename = f"{uuid.uuid4().hex}{extension}"
992
+
993
+ local_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
994
+ file.save(local_path)
995
+
996
+ upload_to_hf(local_path, f"files/{unique_filename}")
997
+
998
+ file_url = url_for('uploaded_file', filename=unique_filename, _external=True)
999
+ file_type = mimetypes.guess_type(original_filename)[0] or 'application/octet-stream'
1000
+
1001
+ return jsonify({
1002
+ 'url': file_url,
1003
+ 'filename': original_filename,
1004
+ 'file_type': file_type
1005
+ })
1006
+ return jsonify({'error': 'File upload failed'}), 500
1007
+
1008
 
1009
  @app.route('/api/user_data', methods=['POST'])
1010
  def get_user_data():
 
1108
  chatroom_id = data.get('chatroom_id')
1109
  sender_address = data.get('sender_address')
1110
  text = data.get('text')
1111
+ file_url = data.get('file_url')
1112
+ original_filename = data.get('original_filename')
1113
+ file_type = data.get('file_type')
1114
 
1115
+ if not all([chatroom_id, sender_address]) or (not text and not file_url):
1116
  return jsonify({'error': 'Missing data'}), 400
1117
 
1118
  db = read_db()
 
1123
  'id': str(uuid.uuid4()),
1124
  'sender_address': sender_address,
1125
  'text': text,
1126
+ 'timestamp': datetime.utcnow().isoformat() + "Z",
1127
+ 'file_url': file_url,
1128
+ 'original_filename': original_filename,
1129
+ 'file_type': file_type
1130
  }
1131
 
1132
  if len(db['messages'][chatroom_id]) >= 100:
 
1138
 
1139
 
1140
  if __name__ == '__main__':
1141
+ logging.info("Application starting up...")
1142
+ logging.info("Attempting initial sync from Hugging Face...")
1143
+ download_from_hf()
1144
  init_db()
1145
+ logging.info("Initial data load/sync complete.")
1146
+
1147
+ if HF_TOKEN:
1148
+ backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1149
+ backup_thread.start()
1150
+ logging.info("Periodic backup thread started.")
1151
+ else:
1152
+ logging.warning("Periodic backup will NOT run (HF_TOKEN not set).")
1153
+
1154
  app.run(host='0.0.0.0', port=7860)