Kgshop commited on
Commit
e3800fe
·
verified ·
1 Parent(s): 55597e8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +238 -505
app.py CHANGED
@@ -9,13 +9,14 @@ from huggingface_hub import HfApi, hf_hub_download
9
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
10
  from werkzeug.utils import secure_filename
11
  from dotenv import load_dotenv
12
- import requests
13
 
14
  load_dotenv()
15
 
16
  app = Flask(__name__)
17
- app.secret_key = 'raina_hvac_secret_key_v2_projects_dynamic'
18
  DATA_FILE = 'data.json'
 
19
 
20
  SYNC_FILES = [DATA_FILE]
21
 
@@ -33,29 +34,24 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
33
 
34
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
35
  if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
36
- logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
37
  token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
38
  files_to_download = [specific_file] if specific_file else SYNC_FILES
39
- logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
40
  all_successful = True
41
  for file_name in files_to_download:
42
  success = False
43
  for attempt in range(retries + 1):
44
  try:
45
- logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...")
46
  hf_hub_download(
47
  repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=token_to_use,
48
  local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False
49
  )
50
- logging.info(f"Successfully downloaded {file_name}.")
51
  success = True
52
  break
53
  except RepositoryNotFoundError:
54
- logging.error(f"Repository {REPO_ID} not found. Download cancelled.")
55
  return False
56
  except HfHubHTTPError as e:
57
  if e.response.status_code == 404:
58
- logging.warning(f"File {file_name} not found in repo {REPO_ID}. Creating empty local file.")
59
  if not os.path.exists(file_name):
60
  try:
61
  with open(file_name, 'w', encoding='utf-8') as f:
@@ -71,18 +67,15 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
71
  if attempt < retries:
72
  time.sleep(delay)
73
  if not success:
74
- logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
75
  all_successful = False
76
  return all_successful
77
 
78
  def upload_db_to_hf(specific_file=None):
79
  if not HF_TOKEN_WRITE:
80
- logging.warning("HF_TOKEN (for writing) not set. Skipping upload.")
81
  return
82
  try:
83
  api = HfApi()
84
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
85
- logging.info(f"Starting upload of {files_to_upload} to {REPO_ID}...")
86
  for file_name in files_to_upload:
87
  if os.path.exists(file_name):
88
  api.upload_file(
@@ -90,46 +83,32 @@ def upload_db_to_hf(specific_file=None):
90
  repo_type="dataset", token=HF_TOKEN_WRITE,
91
  commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
92
  )
93
- logging.info(f"File {file_name} successfully uploaded.")
94
- else:
95
- logging.warning(f"File {file_name} not found locally, skipping upload.")
96
  except Exception as e:
97
  logging.error(f"Error during Hugging Face upload: {e}", exc_info=True)
98
 
99
  def periodic_backup():
100
- backup_interval = 1800
101
  while True:
102
- time.sleep(backup_interval)
103
- logging.info("Starting periodic backup...")
104
  upload_db_to_hf()
105
- logging.info("Periodic backup finished.")
106
 
107
  def load_data():
108
  default_data = {'equipment': [], 'categories': [], 'services': [], 'projects': []}
109
  try:
110
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
111
  data = json.load(file)
112
- if not isinstance(data, dict):
113
- raise ValueError("Data is not a dictionary")
114
- if 'equipment' not in data: data['equipment'] = []
115
- if 'categories' not in data: data['categories'] = []
116
- if 'services' not in data: data['services'] = []
117
- if 'projects' not in data: data['projects'] = []
118
  return 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
  return load_data()
123
  return default_data
124
 
125
  def save_data(data):
126
  try:
127
- if not isinstance(data, dict):
128
- logging.error("Attempted to save invalid data structure. Aborting.")
129
- return
130
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
131
  json.dump(data, file, ensure_ascii=False, indent=4)
132
- logging.info(f"Data saved to {DATA_FILE}")
133
  upload_db_to_hf(specific_file=DATA_FILE)
134
  except Exception as e:
135
  logging.error(f"Error saving data: {e}", exc_info=True)
@@ -141,15 +120,12 @@ LANDING_TEMPLATE = '''
141
  <meta charset="UTF-8">
142
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
143
  <title>ОсОО "Раина" - Вентиляция и Кондиционирование</title>
144
- <meta name="description" content="Профессиональные услуги по проектированию, монтажу и обслуживанию систем вентиляции и кондиционирования в Кыргызстане. 15 лет опыта, более 1000 проектов.">
145
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
146
  <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
 
147
  <style>
148
- :root {
149
- --dark-bg: #12121c; --card-bg: #1a1a2e; --primary-color: #a955ff;
150
- --secondary-color: #6a0dad; --text-color: #e0e0e0; --text-muted: #a0a0b0;
151
- --accent-glow: rgba(169, 85, 255, 0.3);
152
- }
153
  * { margin: 0; padding: 0; box-sizing: border-box; scroll-behavior: smooth; }
154
  body { font-family: 'Montserrat', sans-serif; background-color: var(--dark-bg); color: var(--text-color); line-height: 1.7; font-size: 16px; }
155
  .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
@@ -160,8 +136,10 @@ LANDING_TEMPLATE = '''
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); }
167
  .navbar { display: flex; justify-content: space-between; align-items: center; }
@@ -176,38 +154,33 @@ LANDING_TEMPLATE = '''
176
  .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items: center; }
177
  .about-img { width: 100%; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); }
178
  .services-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 30px; }
179
- .service-card, .turnkey-card { background-color: var(--card-bg); padding: 30px; border-radius: 15px; border: 1px solid #2a2a4a; transition: all 0.3s ease; }
180
- .service-card:hover, .turnkey-card:hover { transform: translateY(-5px); border-color: var(--primary-color); box-shadow: 0 8px 25px var(--accent-glow); }
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; }
190
- .equipment-card img { width: 100%; height: 180px; object-fit: contain; margin-bottom: 15px; }
191
- .equipment-card h3 { font-size: 1.2rem; }
192
- .equipment-card .price { font-size: 1.3rem; font-weight: 700; color: #fff; margin: 10px 0; }
193
  .projects-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px; }
194
- .project-card { position: relative; border-radius: 15px; overflow: hidden; min-height: 400px; }
195
  .project-card img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.4s ease; }
196
  .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; }
197
- .project-card h3 { margin-bottom: 5px; font-size: 1.3rem; }
198
- .project-card p { margin-bottom: 0; transition: opacity 0.4s ease; opacity: 0; max-height: 0; overflow: hidden; }
199
  .project-card:hover img { transform: scale(1.05); }
200
- .project-card:hover p { opacity: 1; max-height: 200px; }
201
- #contact { background-color: var(--card-bg); }
202
  .contact-content { text-align: center; }
203
  .contact-info { margin-top: 40px; display: flex; flex-direction: column; align-items: center; gap: 20px; }
204
- .contact-info p { font-size: 1.2rem; margin-bottom: 0; }
205
- .contact-info a { color: var(--primary-color); text-decoration: none; font-weight: 600; }
206
  .footer { text-align: center; padding: 30px 0; background-color: #0d0d14; }
207
- @media (max-width: 992px) {
208
- .grid-2 { grid-template-columns: 1fr; text-align: center; }
209
- .about-img { margin-bottom: 30px; max-width: 500px; margin-left: auto; margin-right: auto;}
210
- }
 
 
 
 
 
 
 
211
  @media (max-width: 768px) {
212
  .nav-links { position: fixed; top: 0; right: -100%; width: min(75vw, 300px); height: 100vh; background-color: var(--card-bg); flex-direction: column; justify-content: center; align-items: center; transition: right 0.4s ease-in-out; box-shadow: -5px 0 15px rgba(0,0,0,0.2); }
213
  .nav-links.active { right: 0; }
@@ -218,160 +191,35 @@ LANDING_TEMPLATE = '''
218
  </style>
219
  </head>
220
  <body>
221
- <header class="header">
222
- <div class="container navbar">
223
- <a href="#" class="logo">Раина</a>
224
- <ul class="nav-links">
225
- <li><a href="#about">О компании</a></li>
226
- <li><a href="#services">Услуги</a></li>
227
- <li><a href="#turnkey">Под ключ</a></li>
228
- <li><a href="#equipment">Оборудование</a></li>
229
- <li><a href="#projects">Проекты</a></li>
230
- <li><a href="#contact">Контакты</a></li>
231
- </ul>
232
- <button class="menu-toggle" aria-label="Открыть меню"><i class="fas fa-bars"></i></button>
233
- </div>
234
- </header>
235
-
236
- <section id="hero">
237
- <div class="container hero-content">
238
- <h1>ОсОО "Раина": Ваш Партнер в Вентиляции и Кондиционировании</h1>
239
- <p>15 лет опыта, более 1000 реализованных проектов. Мы создаем комфорт и здоровье в любом помещении с помощью самых современных климатических систем.</p>
240
- <a href="#contact" class="btn">Получить консультацию</a>
241
- </div>
242
- </section>
243
-
244
- <section id="about">
245
- <div class="container">
246
- <h2>О Нашей Компании</h2>
247
- <div class="grid-2">
248
- <img src="https://images.unsplash.com/photo-1542744173-8e7e53415bb0?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1740&q=80" alt="Команда Раина" class="about-img">
249
- <div>
250
- <h3>Основание и История</h3>
251
- <p>Компания "Раина" была основана в 2009 году. За эти годы мы зарекомендовали себя как надежный партнер, стремящийся к инновациям и совершенству в области климатических решений.</p>
252
- <h3>Наша Миссия</h3>
253
- <p>Наша миссия — создание оптимального микроклимата для наших клиентов, обеспечивающего комфорт, здоровье и высокую производительность.</p>
254
- <h3>Профессиональная Команда</h3>
255
- <p>Наша команда состоит из высококвалифицированных инженеров и техников, обладающих глубокими знаниями и опытом в области HVAC.</p>
256
- </div>
257
- </div>
258
- </div>
259
- </section>
260
-
261
- <section id="services">
262
- <div class="container">
263
- <h2>Наши Услуги</h2>
264
- <div class="services-grid">
265
- <div class="service-card"><i class="fas fa-drafting-compass"></i><h3>Проектирование</h3><p>Точные расчеты, 3D-модели и вся необходимая проектная документация.</p></div>
266
- <div class="service-card"><i class="fas fa-tools"></i><h3>Монтаж</h3><p>Профессиональная установка всех типов систем HVAC, от бытовых до промышленных.</p></div>
267
- <div class="service-card"><i class="fas fa-headset"></i><h3>Сервис 24/7</h3><p>Плановое обслуживание и оперативный аварийный ремонт в любое время.</p></div>
268
- <div class="service-card"><i class="fas fa-sync-alt"></i><h3>Модернизация</h3><p>Повышение энергоэффективности и снижение расходов на эксплуатацию.</p></div>
269
- </div>
270
- </div>
271
- </section>
272
-
273
- <section id="turnkey" style="background-color: var(--card-bg);">
274
- <div class="container">
275
- <h2>Услуги "под ключ"</h2>
276
- {% if services %}
277
- <div class="services-grid">
278
- {% for service in services %}
279
- <div class="turnkey-card">
280
- {% if service.photo %}
281
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photo }}" alt="{{ service.title }}" class="turnkey-img">
282
- {% endif %}
283
- <div class="turnkey-content">
284
- <h3><i class="{{ service.icon }} fa-fw" style="margin-right: 8px; color: var(--primary-color);"></i>{{ service.title }}</h3>
285
- <p>{{ service.description }}</p>
286
- </div>
287
- </div>
288
- {% endfor %}
289
- </div>
290
- {% else %}
291
- <p style="text-align: center;">Информация об услугах "под ключ" скоро появится на сайте.</p>
292
- {% endif %}
293
- </div>
294
- </section>
295
-
296
- <section id="equipment">
297
- <div class="container">
298
- <h2>Наше Оборудование</h2>
299
- {% if equipment %}
300
- <div class="equipment-filters">
301
- <button class="filter-btn active" data-filter="all">Все</button>
302
- {% for category in categories %}
303
- <button class="filter-btn" data-filter="{{ category }}">{{ category }}</button>
304
- {% endfor %}
305
- </div>
306
- <div class="equipment-grid">
307
- {% for item in equipment %}
308
- <div class="equipment-card" data-category="{{ item.get('category', 'all') }}">
309
- {% if item.photo %}
310
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/equipment/{{ item.photo }}" alt="{{ item.name }}">
311
- {% else %}
312
- <img src="https://via.placeholder.com/250x180.png?text=No+Image" alt="No Image">
313
- {% endif %}
314
- <h3>{{ item.name }}</h3>
315
- <p class="price">{{ "%.2f"|format(item.price) }} KGS</p>
316
- <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>
317
- </div>
318
- {% endfor %}
319
- </div>
320
- {% else %}
321
- <p style="text-align: center;">Каталог оборудования скоро будет доступен.</p>
322
- {% endif %}
323
- </div>
324
- </section>
325
-
326
- <section id="projects">
327
- <div class="container">
328
- <h2>Реализованные Проекты</h2>
329
- {% if projects %}
330
- <div class="projects-grid">
331
- {% for project in projects %}
332
- <div class="project-card">
333
- <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/{{ project.photo }}" alt="{{ project.title }}">
334
- <div class="project-overlay">
335
- <h3>{{ project.title }}</h3>
336
- <p>{{ project.description }}</p>
337
- </div>
338
- </div>
339
- {% endfor %}
340
- </div>
341
- {% else %}
342
- <p style="text-align: center;">Информация о реализованных проектах скоро появится на сайте.</p>
343
- {% endif %}
344
- </div>
345
- </section>
346
-
347
- <section id="contact">
348
- <div class="container contact-content">
349
- <h2>Контакты</h2>
350
- <p>Готовы стать вашим надежным партнером в создании идеального климата.</p>
351
- <div class="contact-info">
352
- <p><strong>Свяжитесь с нами:</strong> <a href="tel:{{ contact_phone }}">{{ contact_phone }}</a></p>
353
- <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, я хотел(а) бы получить консультацию по вашим услугам." target="_blank" class="btn"><i class="fab fa-whatsapp"></i> Написать в WhatsApp</a>
354
- </div>
355
- <div style="margin-top: 40px; font-size: 0.9rem; color: var(--text-muted);">
356
- <p><strong>Реквизиты:</strong> ОсОО «Раина», ИНН: 00812202110194, ОКПО: 31290279</p>
357
- </div>
358
- </div>
359
- </section>
360
-
361
- <footer class="footer">
362
- <p>© {{ now.year }} ОсОО "Раина". Все права защищены.</p>
363
- </footer>
364
-
365
  <script>
366
  document.addEventListener('DOMContentLoaded', function() {
367
  const header = document.querySelector('.header');
368
  const menuToggle = document.querySelector('.menu-toggle');
369
  const navLinks = document.querySelector('.nav-links');
 
 
370
  window.addEventListener('scroll', () => { header.classList.toggle('scrolled', window.scrollY > 50); });
371
  menuToggle.addEventListener('click', () => { navLinks.classList.toggle('active'); });
372
- document.querySelectorAll('.nav-links a').forEach(link => {
373
- link.addEventListener('click', () => { navLinks.classList.remove('active'); });
374
- });
375
  const filterContainer = document.querySelector('.equipment-filters');
376
  if (filterContainer) {
377
  filterContainer.addEventListener('click', (e) => {
@@ -379,194 +227,143 @@ LANDING_TEMPLATE = '''
379
  filterContainer.querySelector('.active').classList.remove('active');
380
  e.target.classList.add('active');
381
  const filter = e.target.dataset.filter;
382
- document.querySelectorAll('.equipment-card').forEach(card => {
383
- card.style.display = (filter === 'all' || card.dataset.category === filter) ? 'block' : 'none';
384
- });
385
  });
386
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  });
388
  </script>
389
  </body>
390
  </html>
391
  '''
392
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  ADMIN_TEMPLATE = '''
394
  <!DOCTYPE html>
395
  <html lang="ru">
396
  <head>
397
- <meta charset="UTF-8">
398
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
399
- <title>Админ-панель - Раина</title>
400
- <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
401
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
402
  <style>
403
- body { font-family: 'Poppins', sans-serif; background-color: #f4f7f9; color: #333; padding: 20px; line-height: 1.6; }
404
- .container { max-width: 1200px; margin: 0 auto; background-color: #fff; padding: 25px; border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.05); }
405
- .header { padding-bottom: 15px; margin-bottom: 25px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;}
406
- h1, h2, h3 { font-weight: 600; color: #6a0dad; margin-bottom: 15px; }
407
- h1 { font-size: 1.8rem; }
408
- h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
409
  .section { margin-bottom: 30px; padding: 20px; background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; }
410
- form { margin-bottom: 20px; }
411
- label { font-weight: 500; margin-top: 10px; display: block; color: #555; font-size: 0.9rem;}
412
- 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; }
413
- input[type="file"] { padding: 8px; cursor: pointer; border: 1px solid #ddd;}
414
- 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; }
415
  button:hover, .button:hover { background-color: #8e44ad; }
416
- .delete-button { background-color: #e74c3c; }
417
- .delete-button:hover { background-color: #c0392b; }
418
- .item-list { display: grid; gap: 20px; }
419
- .item { background: #fff; padding: 15px 20px; border-radius: 8px; border: 1px solid #eee; }
420
- .item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; }
421
- .edit-form-container { margin-top: 15px; padding: 20px; background: #fdf9ff; border: 1px dashed #ddd; border-radius: 6px; display: none; }
422
- details { background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; margin-bottom: 20px; }
423
- details > summary { cursor: pointer; font-weight: 600; color: #8e44ad; display: block; padding: 15px; position: relative; list-style: none; }
424
- details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); }
425
- details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
426
- .photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; object-fit: cover;}
427
- .message { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; }
428
  .message.success { background-color: #d4edda; color: #155724; }
429
  .message.error { background-color: #f8d7da; color: #721c24; }
430
- .message.warning { background-color: #fff3cd; color: #856404; }
431
  </style>
432
  </head>
433
  <body>
434
- <div class="container">
435
- <div class="header"><h1><i class="fas fa-tools"></i> Админ-панель "Раина"</h1><a href="{{ url_for('landing') }}" class="button"><i class="fas fa-home"></i> Перейти на сайт</a></div>
436
- {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}<div class="message {{ category }}">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}
437
-
438
- <div class="section">
439
- <h2><i class="fas fa-sync-alt"></i> Синхронизация</h2>
440
- <form method="POST" action="{{ url_for('force_upload') }}" style="display: inline;"><button type="submit" class="button">Загрузить на сервер</button></form>
441
- <form method="POST" action="{{ url_for('force_download') }}" style="display: inline;"><button type="submit" class="button">Скачать с сервера</button></form>
442
- </div>
443
-
444
- <div class="section">
445
- <h2><i class="fas fa-star"></i> Реализованные проекты</h2>
446
- <details><summary>Добавить проект</summary>
447
- <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="add_project">
448
- <label>Название*:</label><input type="text" name="title" required>
449
- <label>Описание*:</label><textarea name="description" rows="3" required></textarea>
450
- <label>Фото*:</label><input type="file" name="photo" accept="image/*" required>
451
- <button type="submit">Добавить проект</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
  </form>
453
- </details>
454
- <div class="item-list">
455
- {% for project in projects %}
456
- <div class="item">
457
- <p><strong>{{ project.title }}</strong>: {{ project.description }}</p>
458
- <div class="item-actions">
459
- <button onclick="toggleEditForm('edit-project-{{ loop.index0 }}')">Редактировать</button>
460
- <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>
461
- </div>
462
- <div id="edit-project-{{ loop.index0 }}" class="edit-form-container">
463
- <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_project"><input type="hidden" name="index" value="{{ loop.index0 }}">
464
- <label>Название*:</label><input type="text" name="title" value="{{ project.title }}" required>
465
- <label>Описание*:</label><textarea name="description" rows="3" required>{{ project.description }}</textarea>
466
- <label>Заменить фото:</label><input type="file" name="photo" accept="image/*">
467
- <button type="submit">Сохранить</button>
468
- </form>
469
- </div>
470
- </div>
471
- {% endfor %}
472
  </div>
473
  </div>
474
-
475
- <div class="section">
476
- <h2><i class="fas fa-concierge-bell"></i> Услуги "под ключ"</h2>
477
- <details><summary>Добавить услугу</summary>
478
- <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="add_service">
479
- <label>Заголовок*:</label><input type="text" name="title" required>
480
- <label>Иконка (FontAwesome)*:</label><input type="text" name="icon" placeholder="fas fa-tools" required>
481
- <label>Описание*:</label><textarea name="description" rows="3" required></textarea>
482
- <label>Фото:</label><input type="file" name="photo" accept="image/*">
483
- <button type="submit">Добавить услугу</button>
484
- </form>
485
- </details>
486
- <div class="item-list">
487
- {% for service in services %}
488
- <div class="item">
489
- <p><i class="{{ service.icon }} fa-fw"></i> <strong>{{ service.title }}</strong>: {{ service.description }}</p>
490
- <div class="item-actions">
491
- <button onclick="toggleEditForm('edit-service-{{ loop.index0 }}')">Редактировать</button>
492
- <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>
493
- </div>
494
- <div id="edit-service-{{ loop.index0 }}" class="edit-form-container">
495
- <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_service"><input type="hidden" name="index" value="{{ loop.index0 }}">
496
- <label>Заголовок*:</label><input type="text" name="title" value="{{ service.title }}" required>
497
- <label>Иконка*:</label><input type="text" name="icon" value="{{ service.icon }}" required>
498
- <label>Описание*:</label><textarea name="description" rows="3" required>{{ service.description }}</textarea>
499
- <label>Заменить фото:</label><input type="file" name="photo" accept="image/*">
500
- <button type="submit">Сохранить</button>
501
- </form>
502
- </div>
503
- </div>
504
- {% endfor %}
505
- </div>
506
- </div>
507
-
508
- <div class="section">
509
- <h2><i class="fas fa-box-open"></i> Оборудование</h2>
510
- <details><summary>Добавить категорию</summary>
511
- <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>
512
- </details>
513
- <div class="item-list">
514
- {% for category in categories %}
515
- <div class="item" style="display: flex; justify-content: space-between; align-items: center;">
516
- <span>{{ category }}</span>
517
- <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>
518
- </div>
519
- {% endfor %}
520
- </div>
521
-
522
- <details style="margin-top:20px;"><summary>Добавить оборудование</summary>
523
- <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="add_equipment">
524
- <label>Название*:</label><input type="text" name="name" required>
525
- <label>Цена (KGS)*:</label><input type="number" name="price" step="0.01" min="0" required>
526
- <label>Категория:</label><select name="category"><option value="Без категории">Без категории</option>{% for cat in categories %}<option value="{{ cat }}">{{ cat }}</option>{% endfor %}</select>
527
- <label>Фото:</label><input type="file" name="photo" accept="image/*">
528
- <button type="submit">Добавить</button>
529
- </form>
530
- </details>
531
- <div class="item-list">
532
- {% for item in equipment %}
533
- <div class="item">
534
- <p><strong>{{ item.name }}</strong> ({{ item.category }}) - {{ "%.2f"|format(item.price) }} KGS</p>
535
- <div class="item-actions">
536
- <button onclick="toggleEditForm('edit-eq-{{ loop.index0 }}')">Редактировать</button>
537
- <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>
538
- </div>
539
- <div id="edit-eq-{{ loop.index0 }}" class="edit-form-container">
540
- <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_equipment"><input type="hidden" name="index" value="{{ loop.index0 }}">
541
- <label>Название*:</label><input type="text" name="name" value="{{ item.name }}" required>
542
- <label>Цена (KGS)*:</label><input type="number" name="price" value="{{ item.price }}" step="0.01" min="0" required>
543
- <label>Категория:</label><select name="category">{% for cat in categories %}<option value="{{ cat }}" {% if item.category == cat %}selected{% endif %}>{{ cat }}</option>{% endfor %}</select>
544
- <label>Заменить фото:</label><input type="file" name="photo" accept="image/*">
545
- <button type="submit">Сохранить</button>
546
- </form>
547
- </div>
548
- </div>
549
- {% endfor %}
550
- </div>
551
- </div>
552
- <script>function toggleEditForm(id) { document.getElementById(id).style.display = document.getElementById(id).style.display === 'block' ? 'none' : 'block'; }</script>
553
- </body>
554
- </html>
555
  '''
556
 
557
- @app.route('/')
558
- def landing():
559
  data = load_data()
 
 
 
 
 
 
560
  return render_template_string(
561
- LANDING_TEMPLATE,
562
- services=data.get('services', []),
563
- equipment=data.get('equipment', []),
564
- categories=sorted(data.get('categories', [])),
565
- projects=data.get('projects', []),
566
  repo_id=REPO_ID,
567
- contact_phone=CONTACT_PHONE,
568
- whatsapp_phone=WHATSAPP_PHONE,
569
- now=datetime.utcnow()
570
  )
571
 
572
  @app.route('/admin', methods=['GET', 'POST'])
@@ -575,174 +372,110 @@ def admin():
575
 
576
  if request.method == 'POST':
577
  action = request.form.get('action')
578
- logging.info(f"Admin action: {action}")
579
  try:
580
- if action == 'add_category':
581
- name = request.form.get('category_name', '').strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
  if name and name not in data['categories']:
583
  data['categories'].append(name)
584
  flash(f"Категория '{name}' добавлена.", 'success')
585
- else: flash("Категория уже существует или пуста.", 'error')
586
 
587
  elif action == 'delete_category':
588
- name = request.form.get('category_name')
589
  if name in data['categories']:
590
  data['categories'].remove(name)
591
  flash(f"Категория '{name}' удалена.", 'success')
592
 
593
- elif action in ['add_equipment', 'edit_equipment']:
594
- name = request.form.get('name', '').strip()
595
- price = round(float(request.form.get('price', 0)), 2)
596
- category = request.form.get('category')
597
- if not name or price <= 0:
598
- flash("Название и цена обязательны.", 'error')
599
- return redirect(url_for('admin'))
600
-
601
- item_data = {'name': name, 'price': price, 'category': category}
602
- photo = request.files.get('photo')
603
-
604
- if action == 'add_equipment':
605
- if photo and photo.filename:
606
- item_data['photo'] = upload_photo_to_hf(photo, name, 'equipment')
607
- data['equipment'].append(item_data)
608
- flash(f"Оборудование '{name}' добавлено.", 'success')
609
-
610
- else: # edit_equipment
611
- index = int(request.form.get('index'))
612
- original_item = data['equipment'][index]
613
- if photo and photo.filename:
614
- delete_photo_from_hf(original_item.get('photo'), 'equipment')
615
- item_data['photo'] = upload_photo_to_hf(photo, name, 'equipment')
616
- else:
617
- item_data['photo'] = original_item.get('photo')
618
- data['equipment'][index] = item_data
619
- flash(f"Оборудование '{name}' обновлено.", 'success')
620
-
621
- elif action == 'delete_equipment':
622
- index = int(request.form.get('index'))
623
- item = data['equipment'].pop(index)
624
- delete_photo_from_hf(item.get('photo'), 'equipment')
625
- flash(f"Оборудование '{item.get('name')}' удалено.", 'success')
626
-
627
- elif action in ['add_service', 'edit_service']:
628
- title = request.form.get('title', '').strip()
629
- item_data = {'title': title, 'icon': request.form.get('icon'), 'description': request.form.get('description')}
630
- photo = request.files.get('photo')
631
- if action == 'add_service':
632
- if photo and photo.filename:
633
- item_data['photo'] = upload_photo_to_hf(photo, title, 'services')
634
- data['services'].append(item_data)
635
- flash(f"Услуга '{title}' добавлена.", 'success')
636
- else: # edit_service
637
- index = int(request.form.get('index'))
638
- original_item = data['services'][index]
639
- if photo and photo.filename:
640
- delete_photo_from_hf(original_item.get('photo'), 'services')
641
- item_data['photo'] = upload_photo_to_hf(photo, title, 'services')
642
- else:
643
- item_data['photo'] = original_item.get('photo')
644
- data['services'][index] = item_data
645
- flash(f"Услуга '{title}' обновлена.", 'success')
646
-
647
- elif action == 'delete_service':
648
- index = int(request.form.get('index'))
649
- item = data['services'].pop(index)
650
- delete_photo_from_hf(item.get('photo'), 'services')
651
- flash(f"Услуга '{item.get('title')}' удалена.", 'success')
652
-
653
- elif action in ['add_project', 'edit_project']:
654
- title = request.form.get('title', '').strip()
655
- item_data = {'title': title, 'description': request.form.get('description')}
656
- photo = request.files.get('photo')
657
- if action == 'add_project':
658
- if photo and photo.filename:
659
- item_data['photo'] = upload_photo_to_hf(photo, title, 'projects')
660
- data['projects'].append(item_data)
661
- flash(f"Проект '{title}' добавлен.", 'success')
662
- else:
663
- flash("Фото обязательно для нового проекта.", 'error')
664
- else: # edit_project
665
- index = int(request.form.get('index'))
666
- original_item = data['projects'][index]
667
- if photo and photo.filename:
668
- delete_photo_from_hf(original_item.get('photo'), 'projects')
669
- item_data['photo'] = upload_photo_to_hf(photo, title, 'projects')
670
- else:
671
- item_data['photo'] = original_item.get('photo')
672
- data['projects'][index] = item_data
673
- flash(f"Проект '{title}' обновлен.", 'success')
674
-
675
- elif action == 'delete_project':
676
- index = int(request.form.get('index'))
677
- item = data['projects'].pop(index)
678
- delete_photo_from_hf(item.get('photo'), 'projects')
679
- flash(f"Проект '{item.get('title')}' удален.", 'success')
680
-
681
  save_data(data)
682
- return redirect(url_for('admin'))
683
  except Exception as e:
684
  logging.error(f"Admin action '{action}' failed: {e}", exc_info=True)
685
  flash(f"Произошла ошибка: {e}", 'error')
686
- return redirect(url_for('admin'))
687
 
688
- return render_template_string(
689
- ADMIN_TEMPLATE,
690
- equipment=data.get('equipment', []),
691
- categories=sorted(data.get('categories', [])),
692
- services=data.get('services', []),
693
- projects=data.get('projects', [])
694
- )
695
-
696
- def upload_photo_to_hf(photo, item_name, folder):
697
- if not photo or not photo.filename or not HF_TOKEN_WRITE:
698
- return None
699
- try:
700
- api = HfApi()
701
- safe_name = secure_filename(item_name.replace(' ', '_'))[:50]
702
- ext = os.path.splitext(photo.filename)[1].lower()
703
- photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
704
-
705
- api.upload_file(
706
- path_or_fileobj=photo, path_in_repo=f"{folder}/{photo_filename}",
707
- repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE
708
- )
709
- logging.info(f"Uploaded photo {photo_filename} to {folder}")
710
- return photo_filename
711
- except Exception as e:
712
- logging.error(f"Error uploading photo {photo.filename}: {e}")
713
- flash(f"Ошибка загрузки фото {photo.filename}.", 'error')
714
- return None
715
 
716
- def delete_photo_from_hf(photo_filename, folder):
717
- if not photo_filename or not HF_TOKEN_WRITE:
718
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
719
  try:
720
- api = HfApi()
721
- api.delete_files(
722
- repo_id=REPO_ID, paths_in_repo=[f"{folder}/{photo_filename}"],
723
- repo_type="dataset", token=HF_TOKEN_WRITE
724
- )
725
- logging.info(f"Deleted photo {photo_filename} from {folder}")
726
  except HfHubHTTPError as e:
727
- if e.response.status_code != 404:
728
- logging.error(f"Error deleting photo {photo_filename}: {e}")
729
  except Exception as e:
730
- logging.error(f"Error deleting photo {photo_filename}: {e}")
731
 
732
  @app.route('/force_upload', methods=['POST'])
733
  def force_upload():
734
- upload_db_to_hf()
735
- flash("Данные загружены на сервер.", 'success')
736
- return redirect(url_for('admin'))
737
 
738
  @app.route('/force_download', methods=['POST'])
739
  def force_download():
740
- download_db_from_hf()
741
- flash("Данные скачаны с сервера.", 'success')
742
- return redirect(url_for('admin'))
743
 
744
  if __name__ == '__main__':
745
- logging.info("Application starting up...")
746
  download_db_from_hf()
747
  if HF_TOKEN_WRITE:
748
  threading.Thread(target=periodic_backup, daemon=True).start()
 
9
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
10
  from werkzeug.utils import secure_filename
11
  from dotenv import load_dotenv
12
+ import shutil
13
 
14
  load_dotenv()
15
 
16
  app = Flask(__name__)
17
+ app.secret_key = 'raina_hvac_secret_key_v3_modals'
18
  DATA_FILE = 'data.json'
19
+ UPLOADS_DIR = 'uploads_temp'
20
 
21
  SYNC_FILES = [DATA_FILE]
22
 
 
34
 
35
  def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
36
  if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
37
+ logging.warning("HF tokens not set. Download might fail for private repos.")
38
  token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
39
  files_to_download = [specific_file] if specific_file else SYNC_FILES
 
40
  all_successful = True
41
  for file_name in files_to_download:
42
  success = False
43
  for attempt in range(retries + 1):
44
  try:
 
45
  hf_hub_download(
46
  repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=token_to_use,
47
  local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False
48
  )
 
49
  success = True
50
  break
51
  except RepositoryNotFoundError:
 
52
  return False
53
  except HfHubHTTPError as e:
54
  if e.response.status_code == 404:
 
55
  if not os.path.exists(file_name):
56
  try:
57
  with open(file_name, 'w', encoding='utf-8') as f:
 
67
  if attempt < retries:
68
  time.sleep(delay)
69
  if not success:
 
70
  all_successful = False
71
  return all_successful
72
 
73
  def upload_db_to_hf(specific_file=None):
74
  if not HF_TOKEN_WRITE:
 
75
  return
76
  try:
77
  api = HfApi()
78
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
 
79
  for file_name in files_to_upload:
80
  if os.path.exists(file_name):
81
  api.upload_file(
 
83
  repo_type="dataset", token=HF_TOKEN_WRITE,
84
  commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
85
  )
 
 
 
86
  except Exception as e:
87
  logging.error(f"Error during Hugging Face upload: {e}", exc_info=True)
88
 
89
  def periodic_backup():
 
90
  while True:
91
+ time.sleep(1800)
 
92
  upload_db_to_hf()
 
93
 
94
  def load_data():
95
  default_data = {'equipment': [], 'categories': [], 'services': [], 'projects': []}
96
  try:
97
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
98
  data = json.load(file)
99
+ if not isinstance(data, dict): raise ValueError("Data is not a dictionary")
100
+ for key in default_data:
101
+ if key not in data: data[key] = []
 
 
 
102
  return data
103
  except (FileNotFoundError, json.JSONDecodeError, ValueError):
 
104
  if download_db_from_hf(specific_file=DATA_FILE):
105
  return load_data()
106
  return default_data
107
 
108
  def save_data(data):
109
  try:
 
 
 
110
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
111
  json.dump(data, file, ensure_ascii=False, indent=4)
 
112
  upload_db_to_hf(specific_file=DATA_FILE)
113
  except Exception as e:
114
  logging.error(f"Error saving data: {e}", exc_info=True)
 
120
  <meta charset="UTF-8">
121
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
122
  <title>ОсОО "Раина" - Вентиляция и Кондиционирование</title>
123
+ <meta name="description" content="Профессиональные услуги по проектированию, монтажу и обслуживанию систем вентиляции и кондиционирования в Кыргызстане. 15 лет опыта.">
124
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
125
  <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
126
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
127
  <style>
128
+ :root { --dark-bg: #12121c; --card-bg: #1a1a2e; --primary-color: #a955ff; --secondary-color: #6a0dad; --text-color: #e0e0e0; --text-muted: #a0a0b0; --accent-glow: rgba(169, 85, 255, 0.3); }
 
 
 
 
129
  * { margin: 0; padding: 0; box-sizing: border-box; scroll-behavior: smooth; }
130
  body { font-family: 'Montserrat', sans-serif; background-color: var(--dark-bg); color: var(--text-color); line-height: 1.7; font-size: 16px; }
131
  .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
 
136
  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; }
137
  h3 { font-size: clamp(1.2rem, 3vw, 1.5rem); color: var(--primary-color); margin-bottom: 15px; }
138
  p { margin-bottom: 1rem; color: var(--text-muted); }
139
+ .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); border: none; cursor: pointer;}
140
  .btn:hover { transform: translateY(-3px) scale(1.05); box-shadow: 0 8px 25px var(--accent-glow); }
141
+ .btn-outline { background: transparent; border: 2px solid var(--primary-color); color: var(--primary-color); padding: 10px 24px; box-shadow: none; }
142
+ .btn-outline:hover { background: var(--primary-color); color: #fff; box-shadow: 0 4px 15px var(--accent-glow); }
143
  .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; }
144
  .header.scrolled { padding: 10px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.3); }
145
  .navbar { display: flex; justify-content: space-between; align-items: center; }
 
154
  .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items: center; }
155
  .about-img { width: 100%; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); }
156
  .services-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 30px; }
157
+ .card { background-color: var(--card-bg); border-radius: 15px; border: 1px solid #2a2a4a; transition: all 0.3s ease; display: flex; flex-direction: column; }
158
+ .card:hover { transform: translateY(-5px); border-color: var(--primary-color); box-shadow: 0 8px 25px var(--accent-glow); }
159
+ .card-img { width: 100%; height: 200px; object-fit: cover; border-radius: 15px 15px 0 0; }
160
+ .card-content { padding: 30px; flex-grow: 1; text-align: center; }
161
+ .card-content p { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
 
162
  .equipment-filters { display: flex; justify-content: center; flex-wrap: wrap; gap: 15px; margin-bottom: 40px; }
163
  .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; }
164
  .filter-btn.active, .filter-btn:hover { background-color: var(--primary-color); color: #fff; }
 
 
 
 
 
165
  .projects-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px; }
166
+ .project-card { position: relative; border-radius: 15px; overflow: hidden; min-height: 400px; background-color: var(--card-bg); cursor: pointer; }
167
  .project-card img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.4s ease; }
168
  .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; }
 
 
169
  .project-card:hover img { transform: scale(1.05); }
 
 
170
  .contact-content { text-align: center; }
171
  .contact-info { margin-top: 40px; display: flex; flex-direction: column; align-items: center; gap: 20px; }
 
 
172
  .footer { text-align: center; padding: 30px 0; background-color: #0d0d14; }
173
+ .modal { display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.8); backdrop-filter: blur(5px); }
174
+ .modal-content { position: relative; background: var(--dark-bg); margin: 5% auto; padding: 30px; border: 1px solid var(--primary-color); border-radius: 15px; width: 90%; max-width: 800px; box-shadow: 0 0 40px var(--accent-glow); animation: slideIn 0.4s ease-out; }
175
+ @keyframes slideIn { from { transform: translateY(-50px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
176
+ .close-btn { position: absolute; top: 15px; right: 20px; color: #fff; font-size: 2rem; font-weight: bold; cursor: pointer; transition: color 0.3s; }
177
+ .close-btn:hover { color: var(--primary-color); }
178
+ .swiper { width: 100%; height: 400px; margin-bottom: 25px; border-radius: 10px; background-color: #000; }
179
+ .swiper-slide { text-align: center; font-size: 18px; background: #000; display: flex; justify-content: center; align-items: center; }
180
+ .swiper-slide img { display: block; width: auto; height: auto; max-width: 100%; max-height: 100%; object-fit: contain; }
181
+ .swiper-button-next, .swiper-button-prev { color: var(--primary-color); }
182
+ .swiper-pagination-bullet-active { background: var(--primary-color); }
183
+ @media (max-width: 992px) { .grid-2 { grid-template-columns: 1fr; text-align: center; } .about-img { margin-bottom: 30px; max-width: 500px; margin-left: auto; margin-right: auto;} }
184
  @media (max-width: 768px) {
185
  .nav-links { position: fixed; top: 0; right: -100%; width: min(75vw, 300px); height: 100vh; background-color: var(--card-bg); flex-direction: column; justify-content: center; align-items: center; transition: right 0.4s ease-in-out; box-shadow: -5px 0 15px rgba(0,0,0,0.2); }
186
  .nav-links.active { right: 0; }
 
191
  </style>
192
  </head>
193
  <body>
194
+ <header class="header"><div class="container navbar"><a href="#" class="logo">Раина</a><ul class="nav-links"><li><a href="#about">О компании</a></li><li><a href="#services">Услуги</a></li><li><a href="#equipment">Оборудование</a></li><li><a href="#projects">Проекты</a></li><li><a href="#contact">Контакты</a></li></ul><button class="menu-toggle" aria-label="Открыть меню"><i class="fas fa-bars"></i></button></div></header>
195
+ <section id="hero"><div class="container hero-content"><h1>Ваш Партнер в Вентиляции и Кондиционировании</h1><p>15 лет опыта, тысячи реализованных проектов. Мы создаем комфорт и здоровье в любом помещении с помощью самых современных климатических систем.</p><a href="#contact" class="btn">Получить консультацию</a></div></section>
196
+ <section id="about"><div class="container"><h2>О Нашей Компании</h2><div class="grid-2"><img src="https://images.unsplash.com/photo-1542744173-8e7e53415bb0?auto=format&fit=crop&q=80" alt="Команда Раина" class="about-img"><div><h3>Основание и История</h3><p>Компания "Раина" была основана в 2009 году. За эти годы мы зарекомендовали себя как надежный партнер, стремящийся к инновациям и совершенству.</p><h3>Наша Миссия</h3><p>Создание оптимального микроклимата для наших клиентов, обеспечивающего комфорт, здоровье и высокую производительность.</p><h3>Профессиональная Команда</h3><p>Наша команда состоит из высококвалифицированных инженеров и техников, обладающих глубокими знаниями и опытом в области HVAC.</p></div></div></div></section>
197
+ <section id="services"><div class="container"><h2>Услуги "под ключ"</h2>
198
+ {% if services %}<div class="services-grid">
199
+ {% for service in services %}<div class="card"><img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photos[0] if service.photos else 'placeholder.jpg' }}" alt="{{ service.title }}" class="card-img"><div class="card-content"><h3><i class="{{ service.icon }} fa-fw"></i> {{ service.title }}</h3><p>{{ service.description }}</p><button class="btn btn-outline" onclick="openModal('service', {{loop.index0}})">Подробнее</button></div></div>{% endfor %}
200
+ </div>{% else %}<p style="text-align: center;">Информация об услугах скоро появится.</p>{% endif %}</div></section>
201
+ <section id="equipment"><div class="container"><h2>Наше Оборудование</h2>
202
+ {% if equipment %}<div class="equipment-filters"><button class="filter-btn active" data-filter="all">Все</button>{% for category in categories %}<button class="filter-btn" data-filter="{{ category }}">{{ category }}</button>{% endfor %}</div><div class="services-grid">
203
+ {% for item in equipment %}<div class="card" data-category="{{ item.get('category', 'all') }}"><img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/equipment/{{ item.photos[0] if item.photos else 'placeholder.jpg' }}" alt="{{ item.name }}" class="card-img"><div class="card-content"><h3>{{ item.name }}</h3><p style="font-size: 1.3rem; font-weight: 700; color: #fff;">{{ "%.2f"|format(item.price) }} KGS</p><button class="btn btn-outline" onclick="openModal('equipment', {{loop.index0}})">Подробнее</button></div></div>{% endfor %}
204
+ </div>{% else %}<p style="text-align: center;">Каталог оборудования скоро будет доступен.</p>{% endif %}</div></section>
205
+ <section id="projects"><div class="container"><h2>Реализованные Проекты</h2>
206
+ {% if projects %}<div class="projects-grid">
207
+ {% for project in projects %}<div class="project-card" onclick="openModal('project', {{loop.index0}})"><img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/{{ project.photos[0] if project.photos else 'placeholder.jpg' }}" alt="{{ project.title }}"><div class="project-overlay"><h3>{{ project.title }}</h3></div></div>{% endfor %}
208
+ </div>{% else %}<p style="text-align: center;">Информация о проектах скоро появится.</p>{% endif %}</div></section>
209
+ <section id="contact"><div class="container contact-content"><h2>Контакты</h2><p>Готовы стать вашим надежным партнером в создании идеального климата.</p><div class="contact-info"><p><a href="tel:{{ contact_phone }}">{{ contact_phone }}</a></p><a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}" target="_blank" class="btn"><i class="fab fa-whatsapp"></i> Написать в WhatsApp</a></div></div></section>
210
+ <footer class="footer"><p>© {{ now.year }} ОсОО "Раина". Все права защищены.</p></footer>
211
+ <div id="detailModal" class="modal"><div id="modalContent" class="modal-content"></div></div>
212
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  <script>
214
  document.addEventListener('DOMContentLoaded', function() {
215
  const header = document.querySelector('.header');
216
  const menuToggle = document.querySelector('.menu-toggle');
217
  const navLinks = document.querySelector('.nav-links');
218
+ const modal = document.getElementById('detailModal');
219
+ const modalContent = document.getElementById('modalContent');
220
  window.addEventListener('scroll', () => { header.classList.toggle('scrolled', window.scrollY > 50); });
221
  menuToggle.addEventListener('click', () => { navLinks.classList.toggle('active'); });
222
+ document.querySelectorAll('.nav-links a').forEach(link => { link.addEventListener('click', () => { navLinks.classList.remove('active'); }); });
 
 
223
  const filterContainer = document.querySelector('.equipment-filters');
224
  if (filterContainer) {
225
  filterContainer.addEventListener('click', (e) => {
 
227
  filterContainer.querySelector('.active').classList.remove('active');
228
  e.target.classList.add('active');
229
  const filter = e.target.dataset.filter;
230
+ document.querySelectorAll('#equipment .card').forEach(card => { card.style.display = (filter === 'all' || card.dataset.category === filter) ? 'flex' : 'none'; });
 
 
231
  });
232
  }
233
+ window.openModal = (type, index) => {
234
+ fetch(`/details/${type}/${index}`).then(res => res.text()).then(html => {
235
+ modalContent.innerHTML = html;
236
+ modal.style.display = 'block';
237
+ document.body.style.overflow = 'hidden';
238
+ const swiper = modal.querySelector('.swiper');
239
+ if(swiper) new Swiper(swiper, { loop: true, pagination: { el: '.swiper-pagination', clickable: true }, navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' } });
240
+ });
241
+ };
242
+ window.closeModal = () => {
243
+ modal.style.display = 'none';
244
+ modalContent.innerHTML = '';
245
+ document.body.style.overflow = 'auto';
246
+ };
247
+ modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); });
248
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modal.style.display === 'block') closeModal(); });
249
  });
250
  </script>
251
  </body>
252
  </html>
253
  '''
254
 
255
+ DETAIL_MODAL_TEMPLATE = '''
256
+ <span class="close-btn" onclick="closeModal()">×</span>
257
+ {% if item.photos and item.photos|length > 0 %}
258
+ <div class="swiper">
259
+ <div class="swiper-wrapper">
260
+ {% for photo in item.photos %}
261
+ <div class="swiper-slide"><img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ item_type }}/{{ photo }}" alt="Фото"></div>
262
+ {% endfor %}
263
+ </div>
264
+ {% if item.photos|length > 1 %}
265
+ <div class="swiper-pagination"></div>
266
+ <div class="swiper-button-prev"></div>
267
+ <div class="swiper-button-next"></div>
268
+ {% endif %}
269
+ </div>
270
+ {% endif %}
271
+ <h3>{% if item.icon %}<i class="{{ item.icon }} fa-fw"></i> {% endif %}{{ item.title or item.name }}</h3>
272
+ {% if item.price %}
273
+ <p style="font-size: 1.5rem; font-weight: 700; color: #fff;">{{ "%.2f"|format(item.price) }} KGS</p>
274
+ {% endif %}
275
+ <p style="white-space: pre-wrap;">{{ item.description }}</p>
276
+ <a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, интересует: {{ item.title or item.name }}" target="_blank" class="btn" style="margin-top: 20px;"><i class="fab fa-whatsapp"></i> Обсудить в WhatsApp</a>
277
+ '''
278
+
279
  ADMIN_TEMPLATE = '''
280
  <!DOCTYPE html>
281
  <html lang="ru">
282
  <head>
283
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Админ-панель</title>
 
 
 
284
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
285
  <style>
286
+ body { font-family: sans-serif; background-color: #f4f7f9; color: #333; padding: 20px; }
287
+ .container { max-width: 1200px; margin: 0 auto; background-color: #fff; padding: 25px; border-radius: 10px; }
288
+ h1, h2 { color: #6a0dad; }
 
 
 
289
  .section { margin-bottom: 30px; padding: 20px; background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; }
290
+ form { margin-bottom: 1rem; }
291
+ label { font-weight: 500; margin-top: 10px; display: block; }
292
+ input, textarea, select { width: 100%; padding: 10px; margin-top: 5px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; }
293
+ button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #9b59b6; color: white; cursor: pointer; text-decoration: none; display: inline-block; margin-top: 10px; }
 
294
  button:hover, .button:hover { background-color: #8e44ad; }
295
+ .delete-button { background-color: #e74c3c; } .delete-button:hover { background-color: #c0392b; }
296
+ .item { background: #fff; padding: 15px; border-radius: 8px; border: 1px solid #eee; margin-bottom: 10px; }
297
+ .item-actions { margin-top: 10px; display: flex; gap: 10px; }
298
+ .edit-form { display: none; margin-top: 15px; padding: 15px; background: #fdf9ff; border: 1px dashed #ddd; }
299
+ .message { padding: 10px; border-radius: 6px; margin-bottom: 15px; }
 
 
 
 
 
 
 
300
  .message.success { background-color: #d4edda; color: #155724; }
301
  .message.error { background-color: #f8d7da; color: #721c24; }
 
302
  </style>
303
  </head>
304
  <body>
305
+ <div class="container">
306
+ <div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #eee; padding-bottom:1rem; margin-bottom:1rem;"><h1>Админ-панель</h1><a href="/" class="button">На сайт</a></div>
307
+ {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}<div class="message {{ category }}">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}
308
+ <div class="section"><h2>Синхронизация</h2><form method="POST" action="{{ url_for('force_upload') }}" style="display:inline;"><button type="submit">Загрузить на сервер</button></form><form method="POST" action="{{ url_for('force_download') }}" style="display:inline;"><button type="submit">Скачать с сервера</button></form></div>
309
+
310
+ {% set sections = [
311
+ ('projects', 'Проекты', ['title', 'description', 'photos']),
312
+ ('services', 'Услуги', ['title', 'icon', 'description', 'photos']),
313
+ ('equipment', 'Оборудование', ['name', 'price', 'category', 'photos'])
314
+ ] %}
315
+ {% for key, title, fields in sections %}
316
+ <div class="section"><h2>{{ title }}</h2><button onclick="toggleForm('add-{{key}}')">Добавить</button><div id="add-{{key}}" style="display:none;">
317
+ <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="add_{{key}}">
318
+ {% for field in fields %}<label>{{field}}:</label>
319
+ {% if field == 'photos' %}<input type="file" name="photos" accept="image/*" multiple>
320
+ {% elif field == 'description' %}<textarea name="{{field}}"></textarea>
321
+ {% elif field == 'category' %}<select name="category">{% for cat in categories %}<option value="{{cat}}">{{cat}}</option>{% endfor %}</select>
322
+ {% else %}<input type="{{'number' if field=='price' else 'text'}}" name="{{field}}" {{'step=0.01' if field=='price'}}>
323
+ {% endif %}
324
+ {% endfor %}<button type="submit">Сохранить</button>
325
+ </form></div>
326
+ {% for item in data[key] %}
327
+ <div class="item"><strong>{{ item.title or item.name }}</strong>
328
+ <div class="item-actions"><button onclick="toggleForm('edit-{{key}}-{{loop.index0}}')">Редактировать</button><form method="POST" style="margin:0;"><input type="hidden" name="action" value="delete_{{key}}"><input type="hidden" name="index" value="{{loop.index0}}"><button type="submit" class="delete-button">Удалить</button></form></div>
329
+ <div id="edit-{{key}}-{{loop.index0}}" class="edit-form">
330
+ <form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_{{key}}"><input type="hidden" name="index" value="{{loop.index0}}">
331
+ {% for field in fields %}<label>{{field}}:</label>
332
+ {% if field == 'photos' %}<input type="file" name="photos" accept="image/*" multiple>
333
+ {% elif field == 'description' %}<textarea name="{{field}}">{{item[field]}}</textarea>
334
+ {% elif field == 'category' %}<select name="category">{% for cat in categories %}<option value="{{cat}}" {%if item[field]==cat%}selected{%endif%}>{{cat}}</option>{% endfor %}</select>
335
+ {% else %}<input type="{{'number' if field=='price' else 'text'}}" name="{{field}}" value="{{item[field]}}" {{'step=0.01' if field=='price'}}>
336
+ {% endif %}
337
+ {% endfor %}<button type="submit">Сохранить</button>
338
  </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  </div>
340
  </div>
341
+ {% endfor %}
342
+ </div>
343
+ {% endfor %}
344
+ <div class="section"><h2>Категории оборудования</h2><button onclick="toggleForm('add-category')">Добавить</button><div id="add-category" style="display:none;"><form method="POST"><input type="hidden" name="action" value="add_category"><label>Название:</label><input type="text" name="name" required><button type="submit">Добавить</button></form></div>
345
+ {% for cat in categories %}<div class="item" style="display:flex;justify-content:space-between;">{{cat}}<form method="POST" style="margin:0;"><input type="hidden" name="action" value="delete_category"><input type="hidden" name="name" value="{{cat}}"><button class="delete-button" type="submit">Удалить</button></form></div>{% endfor %}
346
+ </div>
347
+ </div>
348
+ <script>function toggleForm(id){const el=document.getElementById(id);el.style.display=el.style.display==='block'?'none':'block';}</script>
349
+ </body></html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  '''
351
 
352
+ @app.route('/details/<item_type>/<int:index>')
353
+ def get_details(item_type, index):
354
  data = load_data()
355
+ item_list = data.get(item_type[:-1] + 's' if item_type[-1] != 's' else item_type)
356
+ if not item_list or index >= len(item_list):
357
+ return "Элемент не найден", 404
358
+
359
+ item = item_list[index]
360
+
361
  return render_template_string(
362
+ DETAIL_MODAL_TEMPLATE,
363
+ item=item,
364
+ item_type=item_type[:-1] + 's' if item_type[-1] != 's' else item_type,
 
 
365
  repo_id=REPO_ID,
366
+ whatsapp_phone=WHATSAPP_PHONE
 
 
367
  )
368
 
369
  @app.route('/admin', methods=['GET', 'POST'])
 
372
 
373
  if request.method == 'POST':
374
  action = request.form.get('action')
 
375
  try:
376
+ entity_map = {
377
+ 'project': ('projects', ['title', 'description', 'photos']),
378
+ 'service': ('services', ['title', 'icon', 'description', 'photos']),
379
+ 'equipment': ('equipment', ['name', 'price', 'category', 'photos']),
380
+ }
381
+
382
+ action_type, _, entity_name = action.partition('_')
383
+
384
+ if entity_name in entity_map:
385
+ data_key, fields = entity_map[entity_name]
386
+
387
+ if action_type in ['add', 'edit']:
388
+ item_data = {f: request.form.get(f) for f in fields if f != 'photos'}
389
+ if 'price' in item_data: item_data['price'] = float(item_data['price'] or 0)
390
+
391
+ photos = request.files.getlist('photos')
392
+ uploaded_photos = upload_photos_to_hf(photos, item_data.get('title') or item_data.get('name'), data_key)
393
+
394
+ if action_type == 'add':
395
+ item_data['photos'] = uploaded_photos
396
+ data[data_key].append(item_data)
397
+ flash(f'"{item_data.get("title") or item_data.get("name")}" добавлен.', 'success')
398
+ else:
399
+ index = int(request.form.get('index'))
400
+ original_item = data[data_key][index]
401
+ if uploaded_photos:
402
+ delete_photos_from_hf(original_item.get('photos', []), data_key)
403
+ item_data['photos'] = uploaded_photos
404
+ else:
405
+ item_data['photos'] = original_item.get('photos', [])
406
+ data[data_key][index] = item_data
407
+ flash(f'"{item_data.get("title") or item_data.get("name")}" обновлен.', 'success')
408
+
409
+ elif action_type == 'delete':
410
+ index = int(request.form.get('index'))
411
+ item = data[data_key].pop(index)
412
+ delete_photos_from_hf(item.get('photos', []), data_key)
413
+ flash(f'"{item.get("title") or item.get("name")}" удален.', 'success')
414
+
415
+ elif action == 'add_category':
416
+ name = request.form.get('name', '').strip()
417
  if name and name not in data['categories']:
418
  data['categories'].append(name)
419
  flash(f"Категория '{name}' добавлена.", 'success')
 
420
 
421
  elif action == 'delete_category':
422
+ name = request.form.get('name')
423
  if name in data['categories']:
424
  data['categories'].remove(name)
425
  flash(f"Категория '{name}' удалена.", 'success')
426
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  save_data(data)
 
428
  except Exception as e:
429
  logging.error(f"Admin action '{action}' failed: {e}", exc_info=True)
430
  flash(f"Произошла ошибка: {e}", 'error')
431
+ return redirect(url_for('admin'))
432
 
433
+ return render_template_string(ADMIN_TEMPLATE, data=data, categories=sorted(data.get('categories', [])))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
 
435
+ def upload_photos_to_hf(photo_files, item_name, folder):
436
+ if not photo_files or not HF_TOKEN_WRITE: return []
437
+ os.makedirs(UPLOADS_DIR, exist_ok=True)
438
+ api = HfApi()
439
+ uploaded_photos = []
440
+ for photo in photo_files:
441
+ if photo and photo.filename:
442
+ try:
443
+ safe_name = secure_filename(item_name.replace(' ', '_'))[:50]
444
+ ext = os.path.splitext(photo.filename)[1].lower()
445
+ temp_filename = f"{safe_name}_{datetime.now().strftime('%f')}{ext}"
446
+ temp_path = os.path.join(UPLOADS_DIR, temp_filename)
447
+ photo.save(temp_path)
448
+
449
+ repo_path = f"{folder}/{temp_filename}"
450
+ api.upload_file(path_or_fileobj=temp_path, path_in_repo=repo_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
451
+ uploaded_photos.append(temp_filename)
452
+ except Exception as e:
453
+ logging.error(f"Error uploading {photo.filename}: {e}")
454
+ finally:
455
+ if os.path.exists(temp_path): os.remove(temp_path)
456
+ return uploaded_photos
457
+
458
+ def delete_photos_from_hf(photo_list, folder):
459
+ if not photo_list or not HF_TOKEN_WRITE: return
460
+ api = HfApi()
461
+ paths_in_repo = [f"{folder}/{p}" for p in photo_list if p]
462
+ if not paths_in_repo: return
463
  try:
464
+ api.delete_files(repo_id=REPO_ID, paths_in_repo=paths_in_repo, repo_type="dataset", token=HF_TOKEN_WRITE)
 
 
 
 
 
465
  except HfHubHTTPError as e:
466
+ if e.response.status_code != 404: logging.error(f"Error deleting photos: {e}")
 
467
  except Exception as e:
468
+ logging.error(f"Error deleting photos: {e}")
469
 
470
  @app.route('/force_upload', methods=['POST'])
471
  def force_upload():
472
+ upload_db_to_hf(); flash("Данные загружены.", 'success'); return redirect(url_for('admin'))
 
 
473
 
474
  @app.route('/force_download', methods=['POST'])
475
  def force_download():
476
+ download_db_from_hf(); flash("Данные скачаны.", 'success'); return redirect(url_for('admin'))
 
 
477
 
478
  if __name__ == '__main__':
 
479
  download_db_from_hf()
480
  if HF_TOKEN_WRITE:
481
  threading.Thread(target=periodic_backup, daemon=True).start()