Kgshop commited on
Commit
7d33a2b
·
verified ·
1 Parent(s): 7f56766

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +279 -360
app.py CHANGED
@@ -119,17 +119,7 @@ def load_data():
119
  except (FileNotFoundError, json.JSONDecodeError, ValueError):
120
  logging.warning(f"Local file {DATA_FILE} not found or corrupt. Attempting download.")
121
  if download_db_from_hf(specific_file=DATA_FILE):
122
- try:
123
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
124
- data = json.load(file)
125
- if not isinstance(data, dict): data = default_data
126
- if 'equipment' not in data: data['equipment'] = []
127
- if 'categories' not in data: data['categories'] = []
128
- if 'services' not in data: data['services'] = []
129
- if 'projects' not in data: data['projects'] = []
130
- return data
131
- except:
132
- return default_data
133
  return default_data
134
 
135
  def save_data(data):
@@ -170,7 +160,7 @@ LANDING_TEMPLATE = '''
170
  h2::after { content: ''; display: block; width: 80px; height: 4px; background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); margin: 15px auto 0; border-radius: 2px; }
171
  h3 { font-size: clamp(1.2rem, 3vw, 1.5rem); color: var(--primary-color); margin-bottom: 15px; }
172
  p { margin-bottom: 1rem; color: var(--text-muted); }
173
- .btn { display: inline-block; padding: 12px 28px; background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); color: #fff; border-radius: 50px; text-decoration: none; font-weight: 600; transition: all 0.3s ease; box-shadow: 0 4px 15px var(--accent-glow); cursor: pointer; border: none; }
174
  .btn:hover { transform: translateY(-3px) scale(1.05); box-shadow: 0 8px 25px var(--accent-glow); }
175
  .header { position: fixed; top: 0; left: 0; width: 100%; z-index: 1000; padding: 15px 0; background-color: rgba(18, 18, 28, 0.85); backdrop-filter: blur(10px); transition: all 0.3s ease; }
176
  .header.scrolled { padding: 10px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.3); }
@@ -191,43 +181,30 @@ LANDING_TEMPLATE = '''
191
  .service-card i { font-size: 2.5rem; color: var(--primary-color); margin-bottom: 20px; }
192
  .turnkey-card { padding: 0; display: flex; flex-direction: column; }
193
  .turnkey-img { width: 100%; height: 200px; object-fit: cover; border-radius: 15px 15px 0 0; }
194
- .turnkey-content { padding: 30px; flex-grow: 1; display: flex; flex-direction: column;}
195
- .turnkey-content p { flex-grow: 1; }
196
  .equipment-filters { display: flex; justify-content: center; flex-wrap: wrap; gap: 15px; margin-bottom: 40px; }
197
  .filter-btn { padding: 8px 20px; border: 1px solid var(--primary-color); background-color: transparent; color: var(--primary-color); border-radius: 20px; cursor: pointer; transition: all 0.3s; }
198
  .filter-btn.active, .filter-btn:hover { background-color: var(--primary-color); color: #fff; }
199
  .equipment-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 30px; }
200
- .equipment-card { background-color: var(--card-bg); border-radius: 15px; overflow: hidden; text-align: center; padding: 20px; border: 1px solid #2a2a4a; transition: all 0.3s ease; display:flex; flex-direction:column; justify-content:space-between; }
 
201
  .equipment-card img { width: 100%; height: 180px; object-fit: contain; margin-bottom: 15px; }
202
  .equipment-card h3 { font-size: 1.2rem; }
203
  .equipment-card .price { font-size: 1.3rem; font-weight: 700; color: #fff; margin: 10px 0; }
204
  .projects-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px; }
205
- .project-card { position: relative; border-radius: 15px; overflow: hidden; min-height: 400px; display: flex; flex-direction: column;}
206
- .project-card img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.4s ease; flex-grow: 1;}
207
- .project-overlay { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(to top, rgba(18,18,28,1) 0%, rgba(18,18,28,0) 100%); padding: 40px 20px 20px; text-align: center;}
208
  .project-card h3 { margin-bottom: 5px; font-size: 1.3rem; }
209
- .project-card .project-description-summary { margin-bottom: 10px; color: var(--text-muted); font-size: 0.9em; }
210
  .project-card:hover img { transform: scale(1.05); }
 
211
  #contact { background-color: var(--card-bg); }
212
  .contact-content { text-align: center; }
213
  .contact-info { margin-top: 40px; display: flex; flex-direction: column; align-items: center; gap: 20px; }
214
  .contact-info p { font-size: 1.2rem; margin-bottom: 0; }
215
  .contact-info a { color: var(--primary-color); text-decoration: none; font-weight: 600; }
216
  .footer { text-align: center; padding: 30px 0; background-color: #0d0d14; }
217
-
218
- .modal { display:none; position:fixed; z-index:2000; left:0; top:0; width:100%; height:100%; overflow:auto; background-color:rgba(0,0,0,0.7); backdrop-filter: blur(5px); }
219
- .modal-content-wrapper { margin: 5% auto; padding:30px; width:90%; max-width:700px; background-color:var(--card-bg); border-radius:15px; position:relative; animation: fadeInModal 0.4s ease-out; border: 1px solid #2a2a4a; box-shadow: 0 10px 40px var(--accent-glow); }
220
- .close-modal-btn { color:var(--text-muted); position: absolute; top: 15px; right: 20px; font-size:32px; font-weight:bold; cursor:pointer; transition: color 0.3s; }
221
- .close-modal-btn:hover { color: var(--primary-color); }
222
- #modalPhoto { width:100%; max-height:400px; object-fit:cover; border-radius:10px; margin-bottom:25px; display:none; border: 1px solid #2a2a4a;}
223
- #modalTitle { color:var(--primary-color); margin-bottom:15px; font-size: 1.8rem; }
224
- #modalDescription { color:var(--text-color); max-height: 300px; overflow-y: auto; line-height:1.8; font-size:1.05rem; }
225
- #modalDescription::-webkit-scrollbar { width: 8px; }
226
- #modalDescription::-webkit-scrollbar-thumb { background-color: var(--primary-color); border-radius: 4px; }
227
- #modalDescription::-webkit-scrollbar-track { background-color: #2a2a4a; }
228
- #modalPrice { font-size:1.4rem; font-weight:bold; color: #fff; margin-top:20px; display:none; text-align:right; }
229
- @keyframes fadeInModal { from { opacity:0; transform: translateY(-20px) scale(0.98); } to { opacity:1; transform: translateY(0) scale(1); } }
230
-
231
  @media (max-width: 992px) {
232
  .grid-2 { grid-template-columns: 1fr; text-align: center; }
233
  .about-img { margin-bottom: 30px; max-width: 500px; margin-left: auto; margin-right: auto;}
@@ -238,10 +215,59 @@ LANDING_TEMPLATE = '''
238
  .menu-toggle { display: block; z-index: 1001; }
239
  h2 { margin-bottom: 40px; }
240
  .projects-grid { grid-template-columns: 1fr; }
241
- .modal-content-wrapper { width: 95%; margin: 10% auto; }
242
- #modalTitle { font-size: 1.5rem; }
243
- #modalDescription { font-size: 1rem; }
244
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  </style>
246
  </head>
247
  <body>
@@ -297,25 +323,19 @@ LANDING_TEMPLATE = '''
297
  </div>
298
  </section>
299
 
300
- <section id="turnkey" style="background-color: var(--dark-bg); border-top: 1px solid #2a2a4a; border-bottom: 1px solid #2a2a4a;">
301
  <div class="container">
302
  <h2>Услуги "под ключ"</h2>
303
  {% if services %}
304
  <div class="services-grid">
305
  {% for service in services %}
306
- <div class="turnkey-card">
307
  {% if service.photo %}
308
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photo }}" alt="{{ service.title }}" class="turnkey-img">
309
  {% endif %}
310
  <div class="turnkey-content">
311
  <h3><i class="{{ service.icon }} fa-fw" style="margin-right: 8px; color: var(--primary-color);"></i>{{ service.title }}</h3>
312
- <p>{{ service.description | truncate(120, True) if service.description else '' }}</p>
313
- <button class="btn detail-btn" style="margin-top:auto; padding: 10px 20px; font-size:0.9rem;"
314
- data-title="{{ service.title }}"
315
- data-description="{{ service.description | e }}"
316
- data-photo-url="{% if service.photo %}https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photo }}{% endif %}">
317
- Подробнее
318
- </button>
319
  </div>
320
  </div>
321
  {% endfor %}
@@ -338,26 +358,15 @@ LANDING_TEMPLATE = '''
338
  </div>
339
  <div class="equipment-grid">
340
  {% for item in equipment %}
341
- <div class="equipment-card" data-category="{{ item.get('category', 'all') }}">
342
- <div>
343
- {% if item.photo %}
344
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/equipment/{{ item.photo }}" alt="{{ item.name }}">
345
- {% else %}
346
- <img src="https://via.placeholder.com/250x180.png?text=No+Image" alt="No Image">
347
- {% endif %}
348
- <h3>{{ item.name }}</h3>
349
- <p class="price">{{ "%.2f"|format(item.price) }} KGS</p>
350
- </div>
351
- <div style="display:flex; flex-direction: column; gap:10px; justify-content:center; margin-top:15px;">
352
- <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, интересует оборудование: {{ item.name | urlencode }}" target="_blank" class="btn" style="padding: 10px 20px; font-size: 0.9rem;">Запросить</a>
353
- <button class="btn detail-btn" style="padding: 10px 20px; font-size:0.9rem; background: var(--text-muted);"
354
- data-title="{{ item.name }}"
355
- data-description="{{ item.get('description', 'Подробное описание скоро появится.') | e }}"
356
- data-price="{{ '%.2f KGS'|format(item.price) }}"
357
- data-photo-url="{% if item.photo %}https://huggingface.co/datasets/{{ repo_id }}/resolve/main/equipment/{{ item.photo }}{% endif %}">
358
- Детали
359
- </button>
360
- </div>
361
  </div>
362
  {% endfor %}
363
  </div>
@@ -367,23 +376,17 @@ LANDING_TEMPLATE = '''
367
  </div>
368
  </section>
369
 
370
- <section id="projects" style="background-color: var(--card-bg);">
371
  <div class="container">
372
  <h2>Реализованные Проекты</h2>
373
  {% if projects %}
374
  <div class="projects-grid">
375
  {% for project in projects %}
376
- <div class="project-card">
377
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/{{ project.photo }}" alt="{{ project.title }}">
378
  <div class="project-overlay">
379
  <h3>{{ project.title }}</h3>
380
- <p class="project-description-summary">{{ project.description | truncate(80, True) if project.description else '' }}</p>
381
- <button class="btn detail-btn" style="margin-top:10px; padding: 8px 16px; font-size:0.85rem; opacity:0.9;"
382
- data-title="{{ project.title }}"
383
- data-description="{{ project.description | e }}"
384
- data-photo-url="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/{{ project.photo }}">
385
- Узнать больше
386
- </button>
387
  </div>
388
  </div>
389
  {% endfor %}
@@ -412,85 +415,131 @@ LANDING_TEMPLATE = '''
412
  <p>© {{ now.year }} ОсОО "Раина". Все права защищены.</p>
413
  </footer>
414
 
415
- <div id="itemDetailModal" class="modal">
416
- <div class="modal-content-wrapper">
417
- <span class="close-modal-btn">×</span>
418
- <img id="modalPhoto" src="" alt="Detail photo">
419
- <h3 id="modalTitle"></h3>
420
- <div id="modalDescription"></div>
421
- <p id="modalPrice"></p>
 
422
  </div>
423
  </div>
424
 
425
  <script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  document.addEventListener('DOMContentLoaded', function() {
427
  const header = document.querySelector('.header');
428
  const menuToggle = document.querySelector('.menu-toggle');
429
  const navLinks = document.querySelector('.nav-links');
430
- window.addEventListener('scroll', () => { header.classList.toggle('scrolled', window.scrollY > 50); });
431
- menuToggle.addEventListener('click', () => { navLinks.classList.toggle('active'); });
 
 
 
 
 
 
 
432
  document.querySelectorAll('.nav-links a').forEach(link => {
433
- link.addEventListener('click', () => { navLinks.classList.remove('active'); });
 
 
434
  });
 
435
  const filterContainer = document.querySelector('.equipment-filters');
436
  if (filterContainer) {
437
  filterContainer.addEventListener('click', (e) => {
438
  if (!e.target.matches('.filter-btn')) return;
439
- if (filterContainer.querySelector('.active')) {
440
- filterContainer.querySelector('.active').classList.remove('active');
441
- }
442
  e.target.classList.add('active');
443
  const filter = e.target.dataset.filter;
444
  document.querySelectorAll('.equipment-card').forEach(card => {
445
- card.style.display = (filter === 'all' || card.dataset.category === filter) ? 'flex' : 'none';
446
  });
447
  });
448
  }
449
-
450
- const modal = document.getElementById('itemDetailModal');
451
- const modalPhoto = document.getElementById('modalPhoto');
452
- const modalTitle = document.getElementById('modalTitle');
453
- const modalDescription = document.getElementById('modalDescription');
454
- const modalPrice = document.getElementById('modalPrice');
455
- const closeModalBtn = modal.querySelector('.close-modal-btn');
456
-
457
- function openItemModal(data) {
458
- modalTitle.textContent = data.title || '';
459
- modalDescription.innerHTML = data.description ? data.description.replace(/\\n/g, '<br>').replace(/\n/g, '<br>') : '';
460
-
461
- if (data.photoUrl && data.photoUrl !== "None" && data.photoUrl !== "") {
462
- modalPhoto.src = data.photoUrl;
463
- modalPhoto.style.display = 'block';
464
- modalPhoto.alt = data.title || 'Detail photo';
465
- } else {
466
- modalPhoto.style.display = 'none';
467
- }
468
-
469
- if (data.price) {
470
- modalPrice.textContent = data.price;
471
- modalPrice.style.display = 'block';
472
- } else {
473
- modalPrice.style.display = 'none';
474
- }
475
- modal.style.display = 'block';
476
- }
477
-
478
- closeModalBtn.onclick = function() { modal.style.display = 'none'; }
479
- window.onclick = function(event) {
480
- if (event.target == modal) { modal.style.display = 'none'; }
481
- }
482
-
483
- document.querySelectorAll('.detail-btn').forEach(button => {
484
- button.addEventListener('click', function() {
485
- const data = {
486
- title: this.dataset.title,
487
- description: this.dataset.description,
488
- photoUrl: this.dataset.photoUrl,
489
- price: this.dataset.price
490
- };
491
- openItemModal(data);
492
- });
493
- });
494
  });
495
  </script>
496
  </body>
@@ -517,8 +566,8 @@ ADMIN_TEMPLATE = '''
517
  form { margin-bottom: 20px; }
518
  label { font-weight: 500; margin-top: 10px; display: block; color: #555; font-size: 0.9rem;}
519
  input[type="text"], input[type="number"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 5px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; }
520
- input[type="file"] { padding: 8px; cursor: pointer; border: 1px solid #ddd; width: 100%; box-sizing: border-box; margin-top:5px; }
521
- button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #9b59b6; color: white; font-weight: 500; cursor: pointer; transition: all 0.3s ease; margin-top: 15px; text-decoration: none; font-size: 0.95rem;}
522
  button:hover, .button:hover { background-color: #8e44ad; }
523
  .delete-button { background-color: #e74c3c; }
524
  .delete-button:hover { background-color: #c0392b; }
@@ -528,15 +577,13 @@ ADMIN_TEMPLATE = '''
528
  .edit-form-container { margin-top: 15px; padding: 20px; background: #fdf9ff; border: 1px dashed #ddd; border-radius: 6px; display: none; }
529
  details { background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; margin-bottom: 20px; }
530
  details > summary { cursor: pointer; font-weight: 600; color: #8e44ad; display: block; padding: 15px; position: relative; list-style: none; }
531
- details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); transition: transform 0.2s; }
532
  details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
533
  .photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; object-fit: cover;}
534
- .message { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; font-size: 0.9rem;}
535
- .message.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb;}
536
- .message.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb;}
537
- .message.warning { background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba;}
538
- .current-photo { font-size: 0.85em; color: #777; margin-top: 5px;}
539
- .current-photo img { max-width: 50px; max-height: 50px; vertical-align: middle; margin-right: 5px; border-radius: 3px;}
540
  </style>
541
  </head>
542
  <body>
@@ -546,8 +593,8 @@ ADMIN_TEMPLATE = '''
546
 
547
  <div class="section">
548
  <h2><i class="fas fa-sync-alt"></i> Синхронизация</h2>
549
- <form method="POST" action="{{ url_for('force_upload') }}" style="display: inline-block; margin-right:10px;"><button type="submit" class="button"><i class="fas fa-cloud-upload-alt"></i> Загрузить на сервер</button></form>
550
- <form method="POST" action="{{ url_for('force_download') }}" style="display: inline-block;"><button type="submit" class="button"><i class="fas fa-cloud-download-alt"></i> Скачать с сервера</button></form>
551
  </div>
552
 
553
  <div class="section">
@@ -563,18 +610,17 @@ ADMIN_TEMPLATE = '''
563
  <div class="item-list">
564
  {% for project in projects %}
565
  <div class="item">
566
- <p><strong>{{ project.title }}</strong>: {{ project.description | truncate(100) }}</p>
567
- {% if project.photo %}<div class="current-photo"><img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/{{ project.photo }}" alt="Current photo"> {{ project.photo }}</div>{% endif %}
568
  <div class="item-actions">
569
- <button onclick="toggleEditForm('edit-project-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button>
570
- <form method="POST" style="margin:0;"><input type="hidden" name="action" value="delete_project"><input type="hidden" name="index" value="{{ loop.index0 }}"><button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button></form>
571
  </div>
572
  <div id="edit-project-{{ loop.index0 }}" class="edit-form-container">
573
  <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_project"><input type="hidden" name="index" value="{{ loop.index0 }}">
574
  <label>Название*:</label><input type="text" name="title" value="{{ project.title }}" required>
575
  <label>Описание*:</label><textarea name="description" rows="3" required>{{ project.description }}</textarea>
576
- <label>Заменить фото (оставьте пустым, чтобы не менять):</label><input type="file" name="photo" accept="image/*">
577
- {% if project.photo %}<p class="current-photo">Текущее фото: {{ project.photo }}</p>{% endif %}
578
  <button type="submit">Сохранить</button>
579
  </form>
580
  </div>
@@ -590,26 +636,25 @@ ADMIN_TEMPLATE = '''
590
  <label>Заголовок*:</label><input type="text" name="title" required>
591
  <label>Иконка (FontAwesome)*:</label><input type="text" name="icon" placeholder="fas fa-tools" required>
592
  <label>Описание*:</label><textarea name="description" rows="3" required></textarea>
593
- <label>Фото (опционально):</label><input type="file" name="photo" accept="image/*">
594
  <button type="submit">Добавить услугу</button>
595
  </form>
596
  </details>
597
  <div class="item-list">
598
  {% for service in services %}
599
  <div class="item">
600
- <p><i class="{{ service.icon }} fa-fw"></i> <strong>{{ service.title }}</strong>: {{ service.description | truncate(100) }}</p>
601
- {% if service.photo %}<div class="current-photo"><img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photo }}" alt="Current photo"> {{ service.photo }}</div>{% endif %}
602
  <div class="item-actions">
603
- <button onclick="toggleEditForm('edit-service-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button>
604
- <form method="POST" style="margin:0;"><input type="hidden" name="action" value="delete_service"><input type="hidden" name="index" value="{{ loop.index0 }}"><button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button></form>
605
  </div>
606
  <div id="edit-service-{{ loop.index0 }}" class="edit-form-container">
607
  <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_service"><input type="hidden" name="index" value="{{ loop.index0 }}">
608
  <label>Заголовок*:</label><input type="text" name="title" value="{{ service.title }}" required>
609
  <label>Иконка*:</label><input type="text" name="icon" value="{{ service.icon }}" required>
610
  <label>Описание*:</label><textarea name="description" rows="3" required>{{ service.description }}</textarea>
611
- <label>Заменить фото (оставьте пустым, чтобы не менять):</label><input type="file" name="photo" accept="image/*">
612
- {% if service.photo %}<p class="current-photo">Текущее фото: {{ service.photo }}</p>{% endif %}
613
  <button type="submit">Сохранить</button>
614
  </form>
615
  </div>
@@ -619,54 +664,43 @@ ADMIN_TEMPLATE = '''
619
  </div>
620
 
621
  <div class="section">
622
- <h2><i class="fas fa-tags"></i> Категории оборудования</h2>
623
  <details><summary>Добавить категорию</summary>
624
- <form method="POST"><input type="hidden" name="action" value="add_category"><label>Название категории*:</label><input type="text" name="category_name" required><button type="submit">Добавить</button></form>
625
  </details>
626
- {% if categories %}
627
- <div class="item-list" style="grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));">
628
  {% for category in categories %}
629
- <div class="item" style="display: flex; justify-content: space-between; align-items: center; padding: 10px 15px;">
630
  <span>{{ category }}</span>
631
- <form method="POST" style="margin: 0;"><input type="hidden" name="action" value="delete_category"><input type="hidden" name="category_name" value="{{ category }}"><button type="submit" class="delete-button" style="margin:0; padding: 5px 10px; font-size: 0.8rem;"><i class="fas fa-trash-alt"></i></button></form>
632
  </div>
633
  {% endfor %}
634
  </div>
635
- {% else %}
636
- <p>Категорий пока нет.</p>
637
- {% endif %}
638
- </div>
639
 
640
- <div class="section">
641
- <h2><i class="fas fa-box-open"></i> Оборудование</h2>
642
  <details style="margin-top:20px;"><summary>Добавить оборудование</summary>
643
  <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="add_equipment">
644
  <label>Название*:</label><input type="text" name="name" required>
645
  <label>Цена (KGS)*:</label><input type="number" name="price" step="0.01" min="0" required>
646
- <label>Описание (опционально):</label><textarea name="description" rows="3"></textarea>
647
  <label>Категория:</label><select name="category"><option value="Без категории">Без категории</option>{% for cat in categories %}<option value="{{ cat }}">{{ cat }}</option>{% endfor %}</select>
648
- <label>Фото (опционально):</label><input type="file" name="photo" accept="image/*">
649
  <button type="submit">Добавить</button>
650
  </form>
651
  </details>
652
  <div class="item-list">
653
  {% for item in equipment %}
654
  <div class="item">
655
- <p><strong>{{ item.name }}</strong> ({{ item.get('category', 'Без категории') }}) - {{ "%.2f"|format(item.price) }} KGS</p>
656
- {% if item.get('description') %}<p style="font-size:0.9em; color:#555;">Описание: {{ item.description | truncate(100) }}</p>{% endif %}
657
- {% if item.photo %}<div class="current-photo"><img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/equipment/{{ item.photo }}" alt="Current photo"> {{ item.photo }}</div>{% endif %}
658
  <div class="item-actions">
659
- <button onclick="toggleEditForm('edit-eq-{{ loop.index0 }}')"><i class="fas fa-edit"></i> Редактировать</button>
660
- <form method="POST" style="margin:0;"><input type="hidden" name="action" value="delete_equipment"><input type="hidden" name="index" value="{{ loop.index0 }}"><button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button></form>
661
  </div>
662
  <div id="edit-eq-{{ loop.index0 }}" class="edit-form-container">
663
  <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_equipment"><input type="hidden" name="index" value="{{ loop.index0 }}">
664
  <label>Название*:</label><input type="text" name="name" value="{{ item.name }}" required>
665
  <label>Цена (KGS)*:</label><input type="number" name="price" value="{{ item.price }}" step="0.01" min="0" required>
666
- <label>Описание (опционально):</label><textarea name="description" rows="3">{{ item.get('description', '') }}</textarea>
667
- <label>Категория:</label><select name="category"><option value="Без категории" {% if not item.category or item.category == "Без категории" %}selected{% endif %}>Без категории</option>{% for cat in categories %}<option value="{{ cat }}" {% if item.category == cat %}selected{% endif %}>{{ cat }}</option>{% endfor %}</select>
668
- <label>Заменить фото (оставьте пустым, чтобы не менять):</label><input type="file" name="photo" accept="image/*">
669
- {% if item.photo %}<p class="current-photo">Текущее фото: {{ item.photo }}</p>{% endif %}
670
  <button type="submit">Сохранить</button>
671
  </form>
672
  </div>
@@ -674,7 +708,7 @@ ADMIN_TEMPLATE = '''
674
  {% endfor %}
675
  </div>
676
  </div>
677
- <script>function toggleEditForm(id) { var el = document.getElementById(id); el.style.display = el.style.display === 'block' ? 'none' : 'block'; }</script>
678
  </body>
679
  </html>
680
  '''
@@ -686,12 +720,13 @@ def landing():
686
  LANDING_TEMPLATE,
687
  services=data.get('services', []),
688
  equipment=data.get('equipment', []),
689
- categories=sorted(list(set(item.get('category', 'Без категории') for item in data.get('equipment', [])) - {'Без категории'})),
690
  projects=data.get('projects', []),
691
  repo_id=REPO_ID,
692
  contact_phone=CONTACT_PHONE,
693
  whatsapp_phone=WHATSAPP_PHONE,
694
- now=datetime.utcnow()
 
695
  )
696
 
697
  @app.route('/admin', methods=['GET', 'POST'])
@@ -704,72 +739,42 @@ def admin():
704
  try:
705
  if action == 'add_category':
706
  name = request.form.get('category_name', '').strip()
707
- if name and name not in data.get('categories', []):
708
- if 'categories' not in data: data['categories'] = []
709
  data['categories'].append(name)
710
- data['categories'] = sorted(list(set(data['categories'])))
711
  flash(f"Категория '{name}' добавлена.", 'success')
712
- elif not name:
713
- flash("Название категории не может быть пустым.", 'error')
714
- else: flash(f"Категория '{name}' уже существует.", 'warning')
715
 
716
  elif action == 'delete_category':
717
  name = request.form.get('category_name')
718
- if name in data.get('categories', []):
719
  data['categories'].remove(name)
720
- for item in data.get('equipment', []):
721
- if item.get('category') == name:
722
- item['category'] = 'Без категории'
723
- flash(f"Категория '{name}' удалена. Оборудование этой категории перемещено в 'Без категории'.", 'success')
724
- else:
725
- flash(f"Категория '{name}' не найдена.", 'error')
726
-
727
 
728
  elif action in ['add_equipment', 'edit_equipment']:
729
  name = request.form.get('name', '').strip()
730
- price_str = request.form.get('price')
731
  category = request.form.get('category')
732
- description = request.form.get('description', '').strip()
733
-
734
- if not name:
735
- flash("Название оборудования обязательно.", 'error')
736
  return redirect(url_for('admin'))
737
- try:
738
- price = round(float(price_str), 2)
739
- if price < 0:
740
- flash("Цена не может быть отрицательной.", 'error')
741
- return redirect(url_for('admin'))
742
- except (ValueError, TypeError):
743
- flash("Некорректный формат цены.", 'error')
744
- return redirect(url_for('admin'))
745
-
746
- item_data = {'name': name, 'price': price, 'category': category, 'description': description}
747
- photo_file = request.files.get('photo')
748
 
749
  if action == 'add_equipment':
750
- if photo_file and photo_file.filename:
751
- uploaded_filename = upload_photo_to_hf(photo_file, name, 'equipment')
752
- if uploaded_filename:
753
- item_data['photo'] = uploaded_filename
754
- else:
755
- flash(f"Фото для '{name}' не было загружено из-за ошибки или отсутствия токена.", 'warning')
756
  data['equipment'].append(item_data)
757
  flash(f"Оборудование '{name}' добавлено.", 'success')
758
 
759
  else:
760
  index = int(request.form.get('index'))
761
  original_item = data['equipment'][index]
762
- item_data['photo'] = original_item.get('photo')
763
-
764
- if photo_file and photo_file.filename:
765
  delete_photo_from_hf(original_item.get('photo'), 'equipment')
766
- uploaded_filename = upload_photo_to_hf(photo_file, name, 'equipment')
767
- if uploaded_filename:
768
- item_data['photo'] = uploaded_filename
769
- else:
770
- flash(f"Новое фото для '{name}' не было загружено, старое фото (если было) удалено.", 'warning')
771
- item_data['photo'] = None # Ensure old photo is cleared if new one failed
772
-
773
  data['equipment'][index] = item_data
774
  flash(f"Оборудование '{name}' обновлено.", 'success')
775
 
@@ -781,38 +786,21 @@ def admin():
781
 
782
  elif action in ['add_service', 'edit_service']:
783
  title = request.form.get('title', '').strip()
784
- icon = request.form.get('icon', 'fas fa-tools').strip()
785
- description = request.form.get('description', '').strip()
786
-
787
- if not title or not description or not icon:
788
- flash("Заголовок, иконка и описание услуги обязательны.", 'error')
789
- return redirect(url_for('admin'))
790
-
791
- item_data = {'title': title, 'icon': icon, 'description': description}
792
- photo_file = request.files.get('photo')
793
-
794
  if action == 'add_service':
795
- if photo_file and photo_file.filename:
796
- uploaded_filename = upload_photo_to_hf(photo_file, title, 'services')
797
- if uploaded_filename:
798
- item_data['photo'] = uploaded_filename
799
- else:
800
- flash(f"Фото для услуги '{title}' не было загружено.", 'warning')
801
  data['services'].append(item_data)
802
  flash(f"Услуга '{title}' добавлена.", 'success')
803
  else:
804
  index = int(request.form.get('index'))
805
  original_item = data['services'][index]
806
- item_data['photo'] = original_item.get('photo')
807
-
808
- if photo_file and photo_file.filename:
809
  delete_photo_from_hf(original_item.get('photo'), 'services')
810
- uploaded_filename = upload_photo_to_hf(photo_file, title, 'services')
811
- if uploaded_filename:
812
- item_data['photo'] = uploaded_filename
813
- else:
814
- flash(f"Новое фото для услуги '{title}' не было загружено.", 'warning')
815
- item_data['photo'] = None
816
  data['services'][index] = item_data
817
  flash(f"Услуга '{title}' обновлена.", 'success')
818
 
@@ -824,49 +812,26 @@ def admin():
824
 
825
  elif action in ['add_project', 'edit_project']:
826
  title = request.form.get('title', '').strip()
827
- description = request.form.get('description', '').strip()
828
- photo_file = request.files.get('photo')
829
-
830
- if not title or not description:
831
- flash("Название и описание проекта обязательны.", 'error')
832
- return redirect(url_for('admin'))
833
-
834
- item_data = {'title': title, 'description': description}
835
-
836
  if action == 'add_project':
837
- if photo_file and photo_file.filename:
838
- uploaded_filename = upload_photo_to_hf(photo_file, title, 'projects')
839
- if uploaded_filename:
840
- item_data['photo'] = uploaded_filename
841
- data['projects'].append(item_data)
842
- flash(f"Проект '{title}' добавлен.", 'success')
843
- else:
844
- flash(f"Фото для проекта '{title}' не было загружено. Проект не добавлен.", 'error')
845
  else:
846
  flash("Фото обязательно для нового проекта.", 'error')
847
  else:
848
  index = int(request.form.get('index'))
849
  original_item = data['projects'][index]
850
- item_data['photo'] = original_item.get('photo')
851
-
852
- if photo_file and photo_file.filename:
853
  delete_photo_from_hf(original_item.get('photo'), 'projects')
854
- uploaded_filename = upload_photo_to_hf(photo_file, title, 'projects')
855
- if uploaded_filename:
856
- item_data['photo'] = uploaded_filename
857
- else:
858
- flash(f"Новое фото для проекта '{title}' не было загружено. Проект может остаться без фото или со старым, если оно не было удалено.", 'warning')
859
- item_data['photo'] = None # Or decide to keep original_item.get('photo') if upload fails
860
-
861
- if not item_data.get('photo') and not original_item.get('photo'): # If no photo existed and no new one provided/uploaded
862
- flash(f"Проект '{title}' не имеет фото. Пожалуйста, загрузите фото.", 'warning')
863
- # Do not save if photo is mandatory for projects
864
- # return redirect(url_for('admin'))
865
-
866
  data['projects'][index] = item_data
867
  flash(f"Проект '{title}' обновлен.", 'success')
868
 
869
-
870
  elif action == 'delete_project':
871
  index = int(request.form.get('index'))
872
  item = data['projects'].pop(index)
@@ -874,118 +839,72 @@ def admin():
874
  flash(f"Проект '{item.get('title')}' удален.", 'success')
875
 
876
  save_data(data)
 
877
  except Exception as e:
878
  logging.error(f"Admin action '{action}' failed: {e}", exc_info=True)
879
- flash(f"Произошла ошибка при выполнении '{action}': {str(e)}", 'error')
880
- return redirect(url_for('admin'))
881
 
882
  return render_template_string(
883
  ADMIN_TEMPLATE,
884
  equipment=data.get('equipment', []),
885
  categories=sorted(data.get('categories', [])),
886
  services=data.get('services', []),
887
- projects=data.get('projects', []),
888
- repo_id=REPO_ID
889
  )
890
 
891
- def upload_photo_to_hf(photo_storage, item_name, folder):
892
- if not photo_storage or not photo_storage.filename:
893
- logging.warning("upload_photo_to_hf: No photo or filename provided.")
894
  return None
895
- if not HF_TOKEN_WRITE:
896
- logging.warning("upload_photo_to_hf: HF_TOKEN_WRITE is not set. Cannot upload photo.")
897
- flash("Токен для записи на Hugging Face (HF_TOKEN) не установлен. Загрузка фото невозможна.", "error")
898
- return None
899
-
900
  try:
901
  api = HfApi()
902
- safe_item_name = secure_filename(item_name.replace(' ', '_'))[:50]
903
- if not safe_item_name:
904
- safe_item_name = "untitled"
905
 
906
- original_ext = os.path.splitext(photo_storage.filename)[1].lower()
907
- if not original_ext: original_ext = ".jpg" # Default extension
908
-
909
- photo_filename_in_repo = f"{safe_item_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{original_ext}"
910
-
911
- path_in_repo = f"{folder}/{photo_filename_in_repo}"
912
-
913
- photo_storage.stream.seek(0) # Ensure stream is at the beginning
914
-
915
  api.upload_file(
916
- path_or_fileobj=photo_storage.stream,
917
- path_in_repo=path_in_repo,
918
- repo_id=REPO_ID,
919
- repo_type="dataset",
920
- token=HF_TOKEN_WRITE,
921
- commit_message=f"Upload photo {photo_filename_in_repo} to {folder}"
922
  )
923
- logging.info(f"Successfully uploaded photo {photo_filename_in_repo} to Hugging Face Hub: {folder}/{photo_filename_in_repo}")
924
- return photo_filename_in_repo
925
  except Exception as e:
926
- logging.error(f"Error uploading photo {photo_storage.filename} for item '{item_name}' to folder '{folder}': {e}", exc_info=True)
927
- flash(f"Ошибка загрузки фото '{photo_storage.filename}': {str(e)}", 'error')
928
  return None
929
 
930
  def delete_photo_from_hf(photo_filename, folder):
931
- if not photo_filename or not folder:
932
- logging.info(f"Skipping deletion of photo: No photo_filename or folder provided. Filename: '{photo_filename}', Folder: '{folder}'")
933
- return
934
- if not HF_TOKEN_WRITE:
935
- logging.warning("HF_TOKEN_WRITE not set. Skipping photo deletion from Hugging Face Hub.")
936
  return
937
-
938
  try:
939
  api = HfApi()
940
- path_in_repo_to_delete = f"{folder}/{photo_filename}"
941
- logging.info(f"Attempting to delete photo {path_in_repo_to_delete} from Hugging Face Hub repo {REPO_ID}")
942
  api.delete_files(
943
- repo_id=REPO_ID,
944
- paths_in_repo=[path_in_repo_to_delete],
945
- repo_type="dataset",
946
- token=HF_TOKEN_WRITE,
947
- commit_message=f"Delete photo {photo_filename} from {folder}"
948
  )
949
- logging.info(f"Successfully deleted photo {path_in_repo_to_delete} from Hugging Face Hub.")
950
  except HfHubHTTPError as e:
951
- if e.response.status_code == 404:
952
- logging.warning(f"Photo {path_in_repo_to_delete} not found on Hugging Face Hub for deletion (HTTP 404). It might have been already deleted or never uploaded.")
953
- else:
954
- logging.error(f"HTTP error deleting photo {path_in_repo_to_delete} from Hugging Face Hub: {e}", exc_info=True)
955
- # flash(f"Ошибка удаления фото '{photo_filename}' с сервера: HTTP {e.response.status_code}", 'warning') # Flashing here can be noisy
956
  except Exception as e:
957
- logging.error(f"Unexpected error deleting photo {path_in_repo_to_delete} from Hugging Face Hub: {e}", exc_info=True)
958
- # flash(f"Неожиданная ошибка при удалении фото '{photo_filename}': {str(e)}", 'warning')
959
-
960
 
961
  @app.route('/force_upload', methods=['POST'])
962
  def force_upload():
963
  upload_db_to_hf()
964
- flash("Данные синхронизированы (выгружены на сервер).", 'success')
965
  return redirect(url_for('admin'))
966
 
967
  @app.route('/force_download', methods=['POST'])
968
  def force_download():
969
- if download_db_from_hf():
970
- flash("Данные синхронизированы (загружены с сервера).", 'success')
971
- else:
972
- flash("Ошибка при загрузке данных с сервера.", 'error')
973
  return redirect(url_for('admin'))
974
 
975
  if __name__ == '__main__':
976
  logging.info("Application starting up...")
977
- if not os.path.exists(DATA_FILE):
978
- logging.info(f"{DATA_FILE} not found locally. Attempting initial download.")
979
- download_db_from_hf()
980
- else:
981
- logging.info(f"{DATA_FILE} found locally. Skipping initial download unless forced or file is corrupt.")
982
-
983
  if HF_TOKEN_WRITE:
984
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
985
- backup_thread.start()
986
- logging.info("Periodic backup thread started.")
987
- else:
988
- logging.warning("HF_TOKEN_WRITE not set, periodic backups to Hugging Face Hub will be disabled.")
989
-
990
  port = int(os.environ.get('PORT', 7860))
991
  app.run(debug=False, host='0.0.0.0', port=port)
 
119
  except (FileNotFoundError, json.JSONDecodeError, ValueError):
120
  logging.warning(f"Local file {DATA_FILE} not found or corrupt. Attempting download.")
121
  if download_db_from_hf(specific_file=DATA_FILE):
122
+ return load_data()
 
 
 
 
 
 
 
 
 
 
123
  return default_data
124
 
125
  def save_data(data):
 
160
  h2::after { content: ''; display: block; width: 80px; height: 4px; background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); margin: 15px auto 0; border-radius: 2px; }
161
  h3 { font-size: clamp(1.2rem, 3vw, 1.5rem); color: var(--primary-color); margin-bottom: 15px; }
162
  p { margin-bottom: 1rem; color: var(--text-muted); }
163
+ .btn { display: inline-block; padding: 12px 28px; background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); color: #fff; border-radius: 50px; text-decoration: none; font-weight: 600; transition: all 0.3s ease; box-shadow: 0 4px 15px var(--accent-glow); }
164
  .btn:hover { transform: translateY(-3px) scale(1.05); box-shadow: 0 8px 25px var(--accent-glow); }
165
  .header { position: fixed; top: 0; left: 0; width: 100%; z-index: 1000; padding: 15px 0; background-color: rgba(18, 18, 28, 0.85); backdrop-filter: blur(10px); transition: all 0.3s ease; }
166
  .header.scrolled { padding: 10px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.3); }
 
181
  .service-card i { font-size: 2.5rem; color: var(--primary-color); margin-bottom: 20px; }
182
  .turnkey-card { padding: 0; display: flex; flex-direction: column; }
183
  .turnkey-img { width: 100%; height: 200px; object-fit: cover; border-radius: 15px 15px 0 0; }
184
+ .turnkey-content { padding: 30px; flex-grow: 1;}
 
185
  .equipment-filters { display: flex; justify-content: center; flex-wrap: wrap; gap: 15px; margin-bottom: 40px; }
186
  .filter-btn { padding: 8px 20px; border: 1px solid var(--primary-color); background-color: transparent; color: var(--primary-color); border-radius: 20px; cursor: pointer; transition: all 0.3s; }
187
  .filter-btn.active, .filter-btn:hover { background-color: var(--primary-color); color: #fff; }
188
  .equipment-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 30px; }
189
+ .equipment-card { background-color: var(--card-bg); border-radius: 15px; overflow: hidden; text-align: center; padding: 20px; border: 1px solid #2a2a4a; transition: all 0.3s ease; cursor: pointer; }
190
+ .equipment-card:hover { transform: translateY(-5px); border-color: var(--primary-color); box-shadow: 0 8px 25px var(--accent-glow); }
191
  .equipment-card img { width: 100%; height: 180px; object-fit: contain; margin-bottom: 15px; }
192
  .equipment-card h3 { font-size: 1.2rem; }
193
  .equipment-card .price { font-size: 1.3rem; font-weight: 700; color: #fff; margin: 10px 0; }
194
  .projects-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px; }
195
+ .project-card { position: relative; border-radius: 15px; overflow: hidden; min-height: 400px; cursor: pointer; }
196
+ .project-card img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.4s ease; }
197
+ .project-overlay { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(to top, rgba(18,18,28,1) 0%, rgba(18,18,28,0) 100%); padding: 40px 20px 20px; }
198
  .project-card h3 { margin-bottom: 5px; font-size: 1.3rem; }
199
+ .project-card p { margin-bottom: 0; transition: opacity 0.4s ease; opacity: 0; max-height: 0; overflow: hidden; }
200
  .project-card:hover img { transform: scale(1.05); }
201
+ .project-card:hover p { opacity: 1; max-height: 200px; }
202
  #contact { background-color: var(--card-bg); }
203
  .contact-content { text-align: center; }
204
  .contact-info { margin-top: 40px; display: flex; flex-direction: column; align-items: center; gap: 20px; }
205
  .contact-info p { font-size: 1.2rem; margin-bottom: 0; }
206
  .contact-info a { color: var(--primary-color); text-decoration: none; font-weight: 600; }
207
  .footer { text-align: center; padding: 30px 0; background-color: #0d0d14; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  @media (max-width: 992px) {
209
  .grid-2 { grid-template-columns: 1fr; text-align: center; }
210
  .about-img { margin-bottom: 30px; max-width: 500px; margin-left: auto; margin-right: auto;}
 
215
  .menu-toggle { display: block; z-index: 1001; }
216
  h2 { margin-bottom: 40px; }
217
  .projects-grid { grid-template-columns: 1fr; }
 
 
 
218
  }
219
+ .modal {
220
+ display: none;
221
+ position: fixed;
222
+ z-index: 1001;
223
+ left: 0; top: 0; width: 100%; height: 100%;
224
+ overflow: auto;
225
+ background-color: rgba(0,0,0,0.8);
226
+ padding-top: 60px;
227
+ }
228
+ .modal-content {
229
+ position: relative;
230
+ margin: 5% auto;
231
+ padding: 20px;
232
+ width: 90%;
233
+ max-width: 800px;
234
+ background-color: var(--card-bg);
235
+ border-radius: 15px;
236
+ text-align: center;
237
+ }
238
+ .modal-content img {
239
+ max-width: 100%;
240
+ max-height: 70vh;
241
+ border-radius: 10px;
242
+ margin-bottom: 20px;
243
+ }
244
+ .modal-content h3 { margin-bottom: 10px; }
245
+ .modal-content p { color: var(--text-muted); font-size: 1.1rem;}
246
+ .close-button {
247
+ position: absolute;
248
+ top: 15px;
249
+ right: 25px;
250
+ font-size: 2.5rem;
251
+ font-weight: bold;
252
+ color: #fff;
253
+ cursor: pointer;
254
+ background: none;
255
+ border: none;
256
+ }
257
+ .close-button:hover, .close-button:focus { color: var(--primary-color); text-decoration: none; }
258
+ .carousel-nav { margin-top: 15px; }
259
+ .carousel-nav button {
260
+ background-color: var(--primary-color);
261
+ color: white;
262
+ border: none;
263
+ padding: 10px 15px;
264
+ border-radius: 50px;
265
+ margin: 0 5px;
266
+ cursor: pointer;
267
+ transition: all 0.3s ease;
268
+ font-size: 1.2rem;
269
+ }
270
+ .carousel-nav button:hover { background-color: var(--secondary-color); transform: scale(1.1); }
271
  </style>
272
  </head>
273
  <body>
 
323
  </div>
324
  </section>
325
 
326
+ <section id="turnkey" style="background-color: var(--card-bg);">
327
  <div class="container">
328
  <h2>Услуги "под ключ"</h2>
329
  {% if services %}
330
  <div class="services-grid">
331
  {% for service in services %}
332
+ <div class="turnkey-card" onclick="showDetailsModal('service', {{ loop.index0 }})">
333
  {% if service.photo %}
334
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photo }}" alt="{{ service.title }}" class="turnkey-img">
335
  {% endif %}
336
  <div class="turnkey-content">
337
  <h3><i class="{{ service.icon }} fa-fw" style="margin-right: 8px; color: var(--primary-color);"></i>{{ service.title }}</h3>
338
+ <p>{{ service.description }}</p>
 
 
 
 
 
 
339
  </div>
340
  </div>
341
  {% endfor %}
 
358
  </div>
359
  <div class="equipment-grid">
360
  {% for item in equipment %}
361
+ <div class="equipment-card" data-category="{{ item.get('category', 'all') }}" onclick="showDetailsModal('equipment', {{ loop.index0 }})">
362
+ {% if item.photo %}
363
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/equipment/{{ item.photo }}" alt="{{ item.name }}">
364
+ {% else %}
365
+ <img src="https://via.placeholder.com/250x180.png?text=No+Image" alt="No Image">
366
+ {% endif %}
367
+ <h3>{{ item.name }}</h3>
368
+ <p class="price">{{ "%.2f"|format(item.price) }} KGS</p>
369
+ <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, интересует оборудование: {{ item.name }}" target="_blank" class="btn" style="padding: 8px 20px; font-size: 0.9rem;">Запросить</a>
 
 
 
 
 
 
 
 
 
 
 
370
  </div>
371
  {% endfor %}
372
  </div>
 
376
  </div>
377
  </section>
378
 
379
+ <section id="projects">
380
  <div class="container">
381
  <h2>Реализованные Проекты</h2>
382
  {% if projects %}
383
  <div class="projects-grid">
384
  {% for project in projects %}
385
+ <div class="project-card" onclick="showDetailsModal('project', {{ loop.index0 }})">
386
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/{{ project.photo }}" alt="{{ project.title }}">
387
  <div class="project-overlay">
388
  <h3>{{ project.title }}</h3>
389
+ <p>{{ project.description }}</p>
 
 
 
 
 
 
390
  </div>
391
  </div>
392
  {% endfor %}
 
415
  <p>© {{ now.year }} ОсОО "Раина". Все права защищены.</p>
416
  </footer>
417
 
418
+ <div id="detailsModal" class="modal">
419
+ <div class="modal-content">
420
+ <button class="close-button" onclick="closeDetailsModal()">×</button>
421
+ <div id="modal-body"></div>
422
+ <div class="carousel-nav">
423
+ <button id="prevBtn" onclick="changeModalItem(-1)">❮</button>
424
+ <button id="nextBtn" onclick="changeModalItem(1)">❯</button>
425
+ </div>
426
  </div>
427
  </div>
428
 
429
  <script>
430
+ let currentData = null;
431
+ let currentType = null;
432
+ let currentIndex = -1;
433
+ let allItems = [];
434
+
435
+ function showDetailsModal(type, index) {
436
+ const data = {{ data | tojson }};
437
+ currentData = data;
438
+ currentType = type;
439
+ currentIndex = index;
440
+
441
+ if (type === 'service') allItems = data.services;
442
+ else if (type === 'equipment') allItems = data.equipment;
443
+ else if (type === 'project') allItems = data.projects;
444
+
445
+ updateModalContent();
446
+ document.getElementById('detailsModal').style.display = 'block';
447
+ document.body.style.overflow = 'hidden';
448
+ }
449
+
450
+ function updateModalContent() {
451
+ if (!allItems || currentIndex < 0 || currentIndex >= allItems.length) return;
452
+
453
+ const item = allItems[currentIndex];
454
+ const modalBody = document.getElementById('modal-body');
455
+ modalBody.innerHTML = '';
456
+ let content = '';
457
+
458
+ if (currentType === 'service') {
459
+ content = `
460
+ ${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/${item.photo}" alt="${item.title}">` : ''}
461
+ <h3><i class="${item.icon} fa-fw" style="margin-right: 8px; color: var(--primary-color);"></i>${item.title}</h3>
462
+ <p>${item.description}</p>
463
+ `;
464
+ } else if (currentType === 'equipment') {
465
+ content = `
466
+ ${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/equipment/${item.photo}" alt="${item.name}">` : ''}
467
+ <h3>${item.name}</h3>
468
+ <p><strong>Категория:</strong> ${item.category || 'Не указана'}</p>
469
+ <p class="price" style="font-size: 1.5rem; color: var(--primary-color);">${item.price.toFixed(2)} KGS</p>
470
+ <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, интересует оборудование: ${encodeURIComponent(item.name)}" target="_blank" class="btn" style="padding: 12px 28px; font-size: 1rem;">Запросить</a>
471
+ `;
472
+ } else if (currentType === 'project') {
473
+ content = `
474
+ <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/${item.photo}" alt="${item.title}">
475
+ <h3>${item.title}</h3>
476
+ <p>${item.description}</p>
477
+ `;
478
+ }
479
+ modalBody.innerHTML = content;
480
+ updateCarouselNav();
481
+ }
482
+
483
+ function changeModalItem(direction) {
484
+ let newIndex = currentIndex + direction;
485
+ if (newIndex < 0) newIndex = allItems.length - 1;
486
+ if (newIndex >= allItems.length) newIndex = 0;
487
+ currentIndex = newIndex;
488
+ updateModalContent();
489
+ }
490
+
491
+ function updateCarouselNav() {
492
+ const prevBtn = document.getElementById('prevBtn');
493
+ const nextBtn = document.getElementById('nextBtn');
494
+ if (allItems.length <= 1) {
495
+ prevBtn.style.display = 'none';
496
+ nextBtn.style.display = 'none';
497
+ } else {
498
+ prevBtn.style.display = 'inline-block';
499
+ nextBtn.style.display = 'inline-block';
500
+ }
501
+ }
502
+
503
+ function closeDetailsModal() {
504
+ document.getElementById('detailsModal').style.display = 'none';
505
+ document.body.style.overflow = '';
506
+ currentData = null;
507
+ currentType = null;
508
+ currentIndex = -1;
509
+ allItems = [];
510
+ }
511
+
512
  document.addEventListener('DOMContentLoaded', function() {
513
  const header = document.querySelector('.header');
514
  const menuToggle = document.querySelector('.menu-toggle');
515
  const navLinks = document.querySelector('.nav-links');
516
+
517
+ window.addEventListener('scroll', () => {
518
+ header.classList.toggle('scrolled', window.scrollY > 50);
519
+ });
520
+
521
+ menuToggle.addEventListener('click', () => {
522
+ navLinks.classList.toggle('active');
523
+ });
524
+
525
  document.querySelectorAll('.nav-links a').forEach(link => {
526
+ link.addEventListener('click', () => {
527
+ navLinks.classList.remove('active');
528
+ });
529
  });
530
+
531
  const filterContainer = document.querySelector('.equipment-filters');
532
  if (filterContainer) {
533
  filterContainer.addEventListener('click', (e) => {
534
  if (!e.target.matches('.filter-btn')) return;
535
+ filterContainer.querySelector('.active').classList.remove('active');
 
 
536
  e.target.classList.add('active');
537
  const filter = e.target.dataset.filter;
538
  document.querySelectorAll('.equipment-card').forEach(card => {
539
+ card.style.display = (filter === 'all' || card.dataset.category === filter) ? 'block' : 'none';
540
  });
541
  });
542
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
  });
544
  </script>
545
  </body>
 
566
  form { margin-bottom: 20px; }
567
  label { font-weight: 500; margin-top: 10px; display: block; color: #555; font-size: 0.9rem;}
568
  input[type="text"], input[type="number"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 5px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; }
569
+ input[type="file"] { padding: 8px; cursor: pointer; border: 1px solid #ddd;}
570
+ button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #9b59b6; color: white; font-weight: 500; cursor: pointer; transition: all 0.3s ease; margin-top: 15px; text-decoration: none; }
571
  button:hover, .button:hover { background-color: #8e44ad; }
572
  .delete-button { background-color: #e74c3c; }
573
  .delete-button:hover { background-color: #c0392b; }
 
577
  .edit-form-container { margin-top: 15px; padding: 20px; background: #fdf9ff; border: 1px dashed #ddd; border-radius: 6px; display: none; }
578
  details { background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; margin-bottom: 20px; }
579
  details > summary { cursor: pointer; font-weight: 600; color: #8e44ad; display: block; padding: 15px; position: relative; list-style: none; }
580
+ details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); }
581
  details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
582
  .photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; object-fit: cover;}
583
+ .message { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; }
584
+ .message.success { background-color: #d4edda; color: #155724; }
585
+ .message.error { background-color: #f8d7da; color: #721c24; }
586
+ .message.warning { background-color: #fff3cd; color: #856404; }
 
 
587
  </style>
588
  </head>
589
  <body>
 
593
 
594
  <div class="section">
595
  <h2><i class="fas fa-sync-alt"></i> Синхронизация</h2>
596
+ <form method="POST" action="{{ url_for('force_upload') }}" style="display: inline;"><button type="submit" class="button">Загрузить на сервер</button></form>
597
+ <form method="POST" action="{{ url_for('force_download') }}" style="display: inline;"><button type="submit" class="button">Скачать с сервера</button></form>
598
  </div>
599
 
600
  <div class="section">
 
610
  <div class="item-list">
611
  {% for project in projects %}
612
  <div class="item">
613
+ <p><strong>{{ project.title }}</strong>: {{ project.description }}</p>
614
+ {% if project.photo %}<div class="photo-preview"><img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/{{ project.photo }}" alt="Project Photo"></div>{% endif %}
615
  <div class="item-actions">
616
+ <button onclick="toggleEditForm('edit-project-{{ loop.index0 }}')">Редактировать</button>
617
+ <form method="POST" style="margin:0;"><input type="hidden" name="action" value="delete_project"><input type="hidden" name="index" value="{{ loop.index0 }}"><button type="submit" class="delete-button">Удалить</button></form>
618
  </div>
619
  <div id="edit-project-{{ loop.index0 }}" class="edit-form-container">
620
  <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_project"><input type="hidden" name="index" value="{{ loop.index0 }}">
621
  <label>Название*:</label><input type="text" name="title" value="{{ project.title }}" required>
622
  <label>Описание*:</label><textarea name="description" rows="3" required>{{ project.description }}</textarea>
623
+ <label>Заменить фото:</label><input type="file" name="photo" accept="image/*">
 
624
  <button type="submit">Сохранить</button>
625
  </form>
626
  </div>
 
636
  <label>Заголовок*:</label><input type="text" name="title" required>
637
  <label>Иконка (FontAwesome)*:</label><input type="text" name="icon" placeholder="fas fa-tools" required>
638
  <label>Описание*:</label><textarea name="description" rows="3" required></textarea>
639
+ <label>Фото:</label><input type="file" name="photo" accept="image/*">
640
  <button type="submit">Добавить услугу</button>
641
  </form>
642
  </details>
643
  <div class="item-list">
644
  {% for service in services %}
645
  <div class="item">
646
+ <p><i class="{{ service.icon }} fa-fw"></i> <strong>{{ service.title }}</strong>: {{ service.description }}</p>
647
+ {% if service.photo %}<div class="photo-preview"><img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photo }}" alt="Service Photo"></div>{% endif %}
648
  <div class="item-actions">
649
+ <button onclick="toggleEditForm('edit-service-{{ loop.index0 }}')">Редактировать</button>
650
+ <form method="POST" style="margin:0;"><input type="hidden" name="action" value="delete_service"><input type="hidden" name="index" value="{{ loop.index0 }}"><button type="submit" class="delete-button">Удалить</button></form>
651
  </div>
652
  <div id="edit-service-{{ loop.index0 }}" class="edit-form-container">
653
  <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_service"><input type="hidden" name="index" value="{{ loop.index0 }}">
654
  <label>Заголовок*:</label><input type="text" name="title" value="{{ service.title }}" required>
655
  <label>Иконка*:</label><input type="text" name="icon" value="{{ service.icon }}" required>
656
  <label>Описание*:</label><textarea name="description" rows="3" required>{{ service.description }}</textarea>
657
+ <label>Заменить фото:</label><input type="file" name="photo" accept="image/*">
 
658
  <button type="submit">Сохранить</button>
659
  </form>
660
  </div>
 
664
  </div>
665
 
666
  <div class="section">
667
+ <h2><i class="fas fa-box-open"></i> Оборудование</h2>
668
  <details><summary>Добавить категорию</summary>
669
+ <form method="POST"><input type="hidden" name="action" value="add_category"><label>Название:</label><input type="text" name="category_name" required><button type="submit">Добавить</button></form>
670
  </details>
671
+ <div class="item-list">
 
672
  {% for category in categories %}
673
+ <div class="item" style="display: flex; justify-content: space-between; align-items: center;">
674
  <span>{{ category }}</span>
675
+ <form method="POST" style="margin: 0;"><input type="hidden" name="action" value="delete_category"><input type="hidden" name="category_name" value="{{ category }}"><button type="submit" class="delete-button" style="margin:0;">Удалить</button></form>
676
  </div>
677
  {% endfor %}
678
  </div>
 
 
 
 
679
 
 
 
680
  <details style="margin-top:20px;"><summary>Добавить оборудование</summary>
681
  <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="add_equipment">
682
  <label>Название*:</label><input type="text" name="name" required>
683
  <label>Цена (KGS)*:</label><input type="number" name="price" step="0.01" min="0" required>
 
684
  <label>Категория:</label><select name="category"><option value="Без категории">Без категории</option>{% for cat in categories %}<option value="{{ cat }}">{{ cat }}</option>{% endfor %}</select>
685
+ <label>Фото:</label><input type="file" name="photo" accept="image/*">
686
  <button type="submit">Добавить</button>
687
  </form>
688
  </details>
689
  <div class="item-list">
690
  {% for item in equipment %}
691
  <div class="item">
692
+ <p><strong>{{ item.name }}</strong> ({{ item.category }}) - {{ "%.2f"|format(item.price) }} KGS</p>
693
+ {% if item.photo %}<div class="photo-preview"><img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/equipment/{{ item.photo }}" alt="Equipment Photo"></div>{% endif %}
 
694
  <div class="item-actions">
695
+ <button onclick="toggleEditForm('edit-eq-{{ loop.index0 }}')">Редактировать</button>
696
+ <form method="POST" style="margin:0;"><input type="hidden" name="action" value="delete_equipment"><input type="hidden" name="index" value="{{ loop.index0 }}"><button type="submit" class="delete-button">Удалить</button></form>
697
  </div>
698
  <div id="edit-eq-{{ loop.index0 }}" class="edit-form-container">
699
  <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_equipment"><input type="hidden" name="index" value="{{ loop.index0 }}">
700
  <label>Название*:</label><input type="text" name="name" value="{{ item.name }}" required>
701
  <label>Цена (KGS)*:</label><input type="number" name="price" value="{{ item.price }}" step="0.01" min="0" required>
702
+ <label>Категория:</label><select name="category">{% for cat in categories %}<option value="{{ cat }}" {% if item.category == cat %}selected{% endif %}>{{ cat }}</option>{% endfor %}</select>
703
+ <label>Заменить фото:</label><input type="file" name="photo" accept="image/*">
 
 
704
  <button type="submit">Сохранить</button>
705
  </form>
706
  </div>
 
708
  {% endfor %}
709
  </div>
710
  </div>
711
+ <script>function toggleEditForm(id) { document.getElementById(id).style.display = document.getElementById(id).style.display === 'block' ? 'none' : 'block'; }</script>
712
  </body>
713
  </html>
714
  '''
 
720
  LANDING_TEMPLATE,
721
  services=data.get('services', []),
722
  equipment=data.get('equipment', []),
723
+ categories=sorted(data.get('categories', [])),
724
  projects=data.get('projects', []),
725
  repo_id=REPO_ID,
726
  contact_phone=CONTACT_PHONE,
727
  whatsapp_phone=WHATSAPP_PHONE,
728
+ now=datetime.utcnow(),
729
+ data=data
730
  )
731
 
732
  @app.route('/admin', methods=['GET', 'POST'])
 
739
  try:
740
  if action == 'add_category':
741
  name = request.form.get('category_name', '').strip()
742
+ if name and name not in data['categories']:
 
743
  data['categories'].append(name)
 
744
  flash(f"Категория '{name}' добавлена.", 'success')
745
+ else: flash("Категория уже существует или пуста.", 'error')
 
 
746
 
747
  elif action == 'delete_category':
748
  name = request.form.get('category_name')
749
+ if name in data['categories']:
750
  data['categories'].remove(name)
751
+ flash(f"Категория '{name}' удалена.", 'success')
 
 
 
 
 
 
752
 
753
  elif action in ['add_equipment', 'edit_equipment']:
754
  name = request.form.get('name', '').strip()
755
+ price = round(float(request.form.get('price', 0)), 2)
756
  category = request.form.get('category')
757
+ if not name or price <= 0:
758
+ flash("Название и цена обязательны.", 'error')
 
 
759
  return redirect(url_for('admin'))
760
+
761
+ item_data = {'name': name, 'price': price, 'category': category}
762
+ photo = request.files.get('photo')
 
 
 
 
 
 
 
 
763
 
764
  if action == 'add_equipment':
765
+ if photo and photo.filename:
766
+ item_data['photo'] = upload_photo_to_hf(photo, name, 'equipment')
 
 
 
 
767
  data['equipment'].append(item_data)
768
  flash(f"Оборудование '{name}' добавлено.", 'success')
769
 
770
  else:
771
  index = int(request.form.get('index'))
772
  original_item = data['equipment'][index]
773
+ if photo and photo.filename:
 
 
774
  delete_photo_from_hf(original_item.get('photo'), 'equipment')
775
+ item_data['photo'] = upload_photo_to_hf(photo, name, 'equipment')
776
+ else:
777
+ item_data['photo'] = original_item.get('photo')
 
 
 
 
778
  data['equipment'][index] = item_data
779
  flash(f"Оборудование '{name}' обновлено.", 'success')
780
 
 
786
 
787
  elif action in ['add_service', 'edit_service']:
788
  title = request.form.get('title', '').strip()
789
+ item_data = {'title': title, 'icon': request.form.get('icon'), 'description': request.form.get('description')}
790
+ photo = request.files.get('photo')
 
 
 
 
 
 
 
 
791
  if action == 'add_service':
792
+ if photo and photo.filename:
793
+ item_data['photo'] = upload_photo_to_hf(photo, title, 'services')
 
 
 
 
794
  data['services'].append(item_data)
795
  flash(f"Услуга '{title}' добавлена.", 'success')
796
  else:
797
  index = int(request.form.get('index'))
798
  original_item = data['services'][index]
799
+ if photo and photo.filename:
 
 
800
  delete_photo_from_hf(original_item.get('photo'), 'services')
801
+ item_data['photo'] = upload_photo_to_hf(photo, title, 'services')
802
+ else:
803
+ item_data['photo'] = original_item.get('photo')
 
 
 
804
  data['services'][index] = item_data
805
  flash(f"Услуга '{title}' обновлена.", 'success')
806
 
 
812
 
813
  elif action in ['add_project', 'edit_project']:
814
  title = request.form.get('title', '').strip()
815
+ item_data = {'title': title, 'description': request.form.get('description')}
816
+ photo = request.files.get('photo')
 
 
 
 
 
 
 
817
  if action == 'add_project':
818
+ if photo and photo.filename:
819
+ item_data['photo'] = upload_photo_to_hf(photo, title, 'projects')
820
+ data['projects'].append(item_data)
821
+ flash(f"Проект '{title}' добавлен.", 'success')
 
 
 
 
822
  else:
823
  flash("Фото обязательно для нового проекта.", 'error')
824
  else:
825
  index = int(request.form.get('index'))
826
  original_item = data['projects'][index]
827
+ if photo and photo.filename:
 
 
828
  delete_photo_from_hf(original_item.get('photo'), 'projects')
829
+ item_data['photo'] = upload_photo_to_hf(photo, title, 'projects')
830
+ else:
831
+ item_data['photo'] = original_item.get('photo')
 
 
 
 
 
 
 
 
 
832
  data['projects'][index] = item_data
833
  flash(f"Проект '{title}' обновлен.", 'success')
834
 
 
835
  elif action == 'delete_project':
836
  index = int(request.form.get('index'))
837
  item = data['projects'].pop(index)
 
839
  flash(f"Проект '{item.get('title')}' удален.", 'success')
840
 
841
  save_data(data)
842
+ return redirect(url_for('admin'))
843
  except Exception as e:
844
  logging.error(f"Admin action '{action}' failed: {e}", exc_info=True)
845
+ flash(f"Произошла ошибка: {e}", 'error')
846
+ return redirect(url_for('admin'))
847
 
848
  return render_template_string(
849
  ADMIN_TEMPLATE,
850
  equipment=data.get('equipment', []),
851
  categories=sorted(data.get('categories', [])),
852
  services=data.get('services', []),
853
+ projects=data.get('projects', [])
 
854
  )
855
 
856
+ def upload_photo_to_hf(photo, item_name, folder):
857
+ if not photo or not photo.filename or not HF_TOKEN_WRITE:
 
858
  return None
 
 
 
 
 
859
  try:
860
  api = HfApi()
861
+ safe_name = secure_filename(item_name.replace(' ', '_'))[:50]
862
+ ext = os.path.splitext(photo.filename)[1].lower()
863
+ photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
864
 
 
 
 
 
 
 
 
 
 
865
  api.upload_file(
866
+ path_or_fileobj=photo, path_in_repo=f"{folder}/{photo_filename}",
867
+ repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE
 
 
 
 
868
  )
869
+ logging.info(f"Uploaded photo {photo_filename} to {folder}")
870
+ return photo_filename
871
  except Exception as e:
872
+ logging.error(f"Error uploading photo {photo.filename}: {e}")
873
+ flash(f"Ошибка загрузки фото {photo.filename}.", 'error')
874
  return None
875
 
876
  def delete_photo_from_hf(photo_filename, folder):
877
+ if not photo_filename or not HF_TOKEN_WRITE:
 
 
 
 
878
  return
 
879
  try:
880
  api = HfApi()
 
 
881
  api.delete_files(
882
+ repo_id=REPO_ID, paths_in_repo=[f"{folder}/{photo_filename}"],
883
+ repo_type="dataset", token=HF_TOKEN_WRITE
 
 
 
884
  )
885
+ logging.info(f"Deleted photo {photo_filename} from {folder}")
886
  except HfHubHTTPError as e:
887
+ if e.response.status_code != 404:
888
+ logging.error(f"Error deleting photo {photo_filename}: {e}")
 
 
 
889
  except Exception as e:
890
+ logging.error(f"Error deleting photo {photo_filename}: {e}")
 
 
891
 
892
  @app.route('/force_upload', methods=['POST'])
893
  def force_upload():
894
  upload_db_to_hf()
895
+ flash("Данные загружены на сервер.", 'success')
896
  return redirect(url_for('admin'))
897
 
898
  @app.route('/force_download', methods=['POST'])
899
  def force_download():
900
+ download_db_from_hf()
901
+ flash("Данные скачаны с сервера.", 'success')
 
 
902
  return redirect(url_for('admin'))
903
 
904
  if __name__ == '__main__':
905
  logging.info("Application starting up...")
906
+ download_db_from_hf()
 
 
 
 
 
907
  if HF_TOKEN_WRITE:
908
+ threading.Thread(target=periodic_backup, daemon=True).start()
 
 
 
 
 
909
  port = int(os.environ.get('PORT', 7860))
910
  app.run(debug=False, host='0.0.0.0', port=port)