systems / app.py
Kgshop's picture
Update app.py
83829ea verified
from flask import Flask, render_template_string, request, redirect, url_for, flash
import json
import os
import logging
import threading
import time
from datetime import datetime
from huggingface_hub import HfApi, hf_hub_download
from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
from werkzeug.utils import secure_filename
from dotenv import load_dotenv
import requests
import io
load_dotenv()
app = Flask(__name__)
app.secret_key = 'raina_hvac_secret_key_v2_projects_dynamic'
DATA_FILE = 'data.json'
SYNC_FILES = [DATA_FILE]
REPO_ID = "Kgshop/raina"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
CONTACT_PHONE = "+996 773 901 313"
WHATSAPP_PHONE = "996773901313"
DOWNLOAD_RETRIES = 3
DOWNLOAD_DELAY = 5
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
files_to_download = [specific_file] if specific_file else SYNC_FILES
logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
all_successful = True
for file_name in files_to_download:
success = False
for attempt in range(retries + 1):
try:
logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...")
hf_hub_download(
repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=token_to_use,
local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False
)
logging.info(f"Successfully downloaded {file_name}.")
success = True
break
except RepositoryNotFoundError:
logging.error(f"Repository {REPO_ID} not found. Download cancelled.")
return False
except HfHubHTTPError as e:
if e.response.status_code == 404:
logging.warning(f"File {file_name} not found in repo {REPO_ID}. Creating empty local file.")
if not os.path.exists(file_name):
try:
with open(file_name, 'w', encoding='utf-8') as f:
json.dump({'equipment': [], 'categories': [], 'services': [], 'projects': [], 'settings': {'prices_enabled': True}}, f)
except Exception as create_e:
logging.error(f"Failed to create empty local file {file_name}: {create_e}")
success = True
break
else:
logging.error(f"HTTP error downloading {file_name}: {e}. Retrying...")
except Exception as e:
logging.error(f"Unexpected error downloading {file_name}: {e}. Retrying...", exc_info=True)
if attempt < retries:
time.sleep(delay)
if not success:
logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
all_successful = False
return all_successful
def upload_db_to_hf(specific_file=None):
if not HF_TOKEN_WRITE:
logging.warning("HF_TOKEN (for writing) not set. Skipping upload.")
return
try:
api = HfApi()
files_to_upload = [specific_file] if specific_file else SYNC_FILES
logging.info(f"Starting upload of {files_to_upload} to {REPO_ID}...")
for file_name in files_to_upload:
if os.path.exists(file_name):
api.upload_file(
path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID,
repo_type="dataset", token=HF_TOKEN_WRITE,
commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
logging.info(f"File {file_name} successfully uploaded.")
else:
logging.warning(f"File {file_name} not found locally, skipping upload.")
except Exception as e:
logging.error(f"Error during Hugging Face upload: {e}", exc_info=True)
def periodic_backup():
backup_interval = 1800
while True:
time.sleep(backup_interval)
logging.info("Starting periodic backup...")
upload_db_to_hf()
logging.info("Periodic backup finished.")
def load_data():
default_data = {'equipment': [], 'categories': [], 'services': [], 'projects': [], 'settings': {'prices_enabled': True}}
try:
with open(DATA_FILE, 'r', encoding='utf-8') as file:
data = json.load(file)
if not isinstance(data, dict):
raise ValueError("Data is not a dictionary")
if 'equipment' not in data: data['equipment'] = []
if 'categories' not in data: data['categories'] = []
if 'services' not in data: data['services'] = []
if 'projects' not in data: data['projects'] = []
if 'settings' not in data: data['settings'] = {'prices_enabled': True}
if 'prices_enabled' not in data['settings']: data['settings']['prices_enabled'] = True
return data
except (FileNotFoundError, json.JSONDecodeError, ValueError):
logging.warning(f"Local file {DATA_FILE} not found or corrupt. Attempting download.")
if download_db_from_hf(specific_file=DATA_FILE):
return load_data()
return default_data
def save_data(data):
try:
if not isinstance(data, dict):
logging.error("Attempted to save invalid data structure. Aborting.")
return
with open(DATA_FILE, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
logging.info(f"Data saved to {DATA_FILE}")
upload_db_to_hf(specific_file=DATA_FILE)
except Exception as e:
logging.error(f"Error saving data: {e}", exc_info=True)
LANDING_TEMPLATE = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Раина Климат Систем - Вентиляция и Кондиционирование</title>
<meta name="description" content="Профессиональные услуги по проектированию, монтажу и обслуживанию систем вентиляции и кондиционирования в Кыргызстане.">
<meta name="google-site-verification" content="V6EqDTA9Oj9V1OfNbnBHj5RKrdcXlABD8tqfEFVUHJY" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-dark: #111827;
--bg-medium: #1F2937;
--bg-light-card: #374151;
--text-primary: #F3F4F6;
--text-secondary: #D1D5DB;
--text-muted: #9CA3AF;
--accent-primary: #8B5CF6;
--accent-secondary: #6D28D9;
--accent-glow: rgba(139, 92, 246, 0.25);
--border-color: #374151;
--border-hover-color: var(--accent-primary);
--section-padding: clamp(4rem, 10vw, 6rem);
--card-border-radius: 12px;
--button-border-radius: 30px;
}
* { margin: 0; padding: 0; box-sizing: border-box; scroll-behavior: smooth; }
body {
font-family: 'Montserrat', sans-serif;
background-color: var(--bg-dark);
color: var(--text-primary);
line-height: 1.7;
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.container { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
section { padding: var(--section-padding) 0; overflow: hidden; }
h1, h2, h3 { font-weight: 700; color: var(--text-primary); line-height: 1.3; }
h1 { font-size: clamp(2.5rem, 7vw, 4.5rem); text-shadow: 0 2px 10px rgba(0,0,0,0.3); }
h2 {
font-size: clamp(2rem, 5vw, 3.2rem);
text-align: center;
margin-bottom: 70px;
position: relative;
}
h2::after {
content: ''; display: block; width: 70px; height: 5px;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
margin: 20px auto 0;
border-radius: 3px;
}
h3 { font-size: clamp(1.3rem, 3.5vw, 1.8rem); color: var(--accent-primary); margin-bottom: 15px; }
p { margin-bottom: 1.2rem; color: var(--text-secondary); font-size: 1.05rem; }
.btn {
display: inline-block;
padding: 14px 32px;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
color: #fff;
border-radius: var(--button-border-radius);
text-decoration: none;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 5px 20px var(--accent-glow);
border: none;
cursor: pointer;
}
.btn:hover {
transform: translateY(-4px) scale(1.03);
box-shadow: 0 10px 30px var(--accent-glow);
background: linear-gradient(90deg, var(--accent-secondary), var(--accent-primary));
}
.header {
position: fixed; top: 0; left: 0; width: 100%; z-index: 1000;
padding: 20px 0;
background-color: transparent;
transition: all 0.35s ease-out;
}
.header.scrolled {
padding: 15px 0;
background-color: rgba(17, 24, 39, 0.85);
backdrop-filter: blur(12px);
box-shadow: 0 3px 15px rgba(0,0,0,0.2);
}
.navbar { display: flex; justify-content: space-between; align-items: center; }
.logo { font-size: clamp(1.6rem, 4vw, 2rem); font-weight: 700; color: #fff; text-decoration: none; letter-spacing: -1px; }
.nav-links { display: flex; gap: 35px; list-style: none; }
.nav-links a {
color: var(--text-primary);
text-decoration: none;
font-weight: 500;
padding-bottom: 5px;
position: relative;
}
.nav-links a::after {
content: '';
position: absolute;
width: 0;
height: 2px;
bottom: 0;
left: 50%;
transform: translateX(-50%);
background-color: var(--accent-primary);
transition: width 0.3s ease;
}
.nav-links a:hover { color: var(--accent-primary); }
.nav-links a:hover::after { width: 100%; }
.menu-toggle { display: none; font-size: 1.8rem; cursor: pointer; border: none; background: none; color: white; }
#hero {
min-height: 100vh;
display: flex; align-items: center;
background-image: linear-gradient(rgba(17, 24, 39, 0.75), rgba(17, 24, 39, 1)), url(https://images.unsplash.com/photo-1558221639-2c7158995165?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1740&q=80);
background-size: cover;
background-position: center;
background-attachment: fixed;
}
.hero-content { text-align: center; max-width: 850px; margin: 0 auto; }
.hero-content p { font-size: clamp(1.1rem, 3vw, 1.3rem); margin: 35px 0; max-width: 650px; margin-left: auto; margin-right: auto; color: var(--text-secondary); }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 70px; align-items: center; }
.about-img { width: 100%; border-radius: var(--card-border-radius); box-shadow: 0 15px 40px rgba(0,0,0,0.5); }
.services-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 35px; }
.service-card, .turnkey-card {
background-color: var(--bg-medium);
padding: 35px;
border-radius: var(--card-border-radius);
border: 1px solid var(--border-color);
transition: all 0.3s ease;
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
.service-card:hover, .turnkey-card:hover {
transform: translateY(-8px);
border-color: var(--border-hover-color);
box-shadow: 0 12px 35px var(--accent-glow);
}
.service-card i { font-size: 3rem; color: var(--accent-primary); margin-bottom: 25px; display: block; }
.turnkey-card { padding: 0; display: flex; flex-direction: column; overflow: hidden; }
.turnkey-img { width: 100%; height: 220px; object-fit: cover; border-radius: var(--card-border-radius) var(--card-border-radius) 0 0; transition: transform 0.3s ease; }
.turnkey-card:hover .turnkey-img { transform: scale(1.05); }
.turnkey-content { padding: 35px; flex-grow: 1;}
.turnkey-content h3 i { transition: color 0.3s ease; }
.turnkey-card:hover .turnkey-content h3 i { color: var(--accent-secondary); }
#turnkey { background-color: var(--bg-medium); }
#contact { background-color: var(--bg-medium); }
.equipment-filters { display: flex; justify-content: center; flex-wrap: wrap; gap: 20px; margin-bottom: 50px; }
.filter-btn {
padding: 10px 25px;
border: 1px solid var(--accent-primary);
background-color: transparent;
color: var(--accent-primary);
border-radius: var(--button-border-radius);
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
}
.filter-btn.active, .filter-btn:hover { background-color: var(--accent-primary); color: #fff; }
.equipment-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 35px; }
.equipment-card {
background-color: var(--bg-light-card);
border-radius: var(--card-border-radius);
overflow: hidden;
text-align: center;
padding: 25px;
border: 1px solid var(--border-color);
transition: all 0.3s ease;
cursor: pointer;
}
.equipment-card:hover {
transform: translateY(-8px);
border-color: var(--border-hover-color);
box-shadow: 0 10px 30px var(--accent-glow);
}
.equipment-card img { width: 100%; height: 200px; object-fit: contain; margin-bottom: 20px; transition: transform 0.3s ease; }
.equipment-card:hover img { transform: scale(1.05); }
.equipment-card h3 { font-size: 1.3rem; margin-bottom: 10px; color: var(--text-primary); }
.equipment-card .price { font-size: 1.4rem; font-weight: 700; color: var(--accent-primary); margin: 15px 0; }
.equipment-card .btn { margin-top: 10px; padding: 10px 22px; font-size: 0.95rem; }
.projects-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 35px; }
.project-card {
position: relative;
border-radius: var(--card-border-radius);
overflow: hidden;
min-height: 420px;
cursor: pointer;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
transition: box-shadow 0.3s ease;
}
.project-card:hover { box-shadow: 0 15px 45px rgba(0,0,0,0.4); }
.project-card img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94); }
.project-overlay {
position: absolute; bottom: 0; left: 0; right: 0;
background: linear-gradient(to top, rgba(17, 24, 39, 0.95) 10%, rgba(17, 24, 39, 0) 100%);
padding: 50px 25px 25px;
transition: background 0.4s ease;
}
.project-card h3 { margin-bottom: 8px; font-size: 1.5rem; color: #fff; }
.project-card p {
margin-bottom: 0;
transition: opacity 0.4s ease, max-height 0.5s ease, transform 0.4s ease;
opacity: 0; max-height: 0;
overflow: hidden;
transform: translateY(10px);
color: var(--text-secondary);
}
.project-card:hover img { transform: scale(1.1); }
.project-card:hover .project-overlay { background: linear-gradient(to top, rgba(17, 24, 39, 1) 40%, rgba(17, 24, 39, 0) 100%); }
.project-card:hover p { opacity: 1; max-height: 200px; transform: translateY(0); }
.contact-content { text-align: center; }
.contact-content > p { max-width: 600px; margin-left: auto; margin-right: auto; }
.contact-info { margin-top: 50px; display: flex; flex-direction: column; align-items: center; gap: 25px; }
.contact-info p { font-size: 1.3rem; margin-bottom: 0; color: var(--text-primary); }
.contact-info a { color: var(--accent-primary); text-decoration: none; font-weight: 600; transition: color 0.3s ease; }
.contact-info a:hover { color: var(--accent-secondary); }
.contact-info .btn { font-size: 1.1rem; color: #fff !important; }
.contact-info .btn i { margin-right: 10px; }
.footer { text-align: center; padding: 40px 0; background-color: #0c111d; border-top: 1px solid var(--border-color); }
.footer p { color: var(--text-muted); font-size: 0.95rem; }
@media (max-width: 992px) {
.grid-2 { grid-template-columns: 1fr; text-align: center; }
.about-img { margin-bottom: 40px; max-width: 500px; margin-left: auto; margin-right: auto;}
}
@media (max-width: 768px) {
.nav-links {
position: fixed; top: 0; right: -100%;
width: min(80vw, 320px); height: 100vh;
background-color: var(--bg-medium);
flex-direction: column; justify-content: center; align-items: center;
transition: right 0.45s cubic-bezier(0.68, -0.55, 0.27, 1.55);
box-shadow: -8px 0 25px rgba(0,0,0,0.25);
gap: 40px;
}
.nav-links.active { right: 0; }
.menu-toggle { display: block; z-index: 1001; }
h1 { font-size: 2.2rem; }
h2 { margin-bottom: 50px; font-size: 1.8rem; }
.projects-grid { grid-template-columns: 1fr; }
.services-grid { grid-template-columns: 1fr; }
.equipment-grid { grid-template-columns: 1fr; }
.btn { padding: 12px 28px; }
}
.modal {
display: none;
position: fixed;
z-index: 1001;
left: 0; top: 0; width: 100%; height: 100%;
overflow: auto;
background-color: rgba(17, 24, 39, 0.9);
padding-top: 5vh;
backdrop-filter: blur(5px);
}
.modal-content {
position: relative;
margin: 5% auto;
padding: 30px;
width: 90%;
max-width: 800px;
background-color: var(--bg-medium);
border-radius: var(--card-border-radius);
text-align: center;
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
border: 1px solid var(--border-color);
}
.modal-content img {
max-width: 100%;
max-height: 65vh;
border-radius: 10px;
margin-bottom: 25px;
object-fit: contain;
}
.modal-content h3 { margin-bottom: 15px; font-size: 1.8rem; color: var(--accent-primary); }
.modal-content p { color: var(--text-secondary); font-size: 1.1rem; line-height: 1.8; }
.close-button {
position: absolute;
top: 20px;
right: 25px;
font-size: 2.8rem;
font-weight: bold;
color: var(--text-primary);
cursor: pointer;
background: none;
border: none;
transition: color 0.3s ease, transform 0.3s ease;
}
.close-button:hover, .close-button:focus { color: var(--accent-primary); transform: rotate(90deg); }
.carousel-nav { margin-top: 25px; }
.carousel-nav button {
background-color: var(--accent-primary);
color: white;
border: none;
padding: 12px 18px;
border-radius: var(--button-border-radius);
margin: 0 8px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 1.3rem;
}
.carousel-nav button:hover { background-color: var(--accent-secondary); transform: scale(1.1); }
.carousel-nav button:disabled { background-color: var(--text-muted); cursor: not-allowed; transform: scale(1); }
</style>
</head>
<body>
<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="#turnkey">Под ключ</a></li>
<li><a href="#equipment">Оборудование</a></li>
<li><a href="#contact">Контакты</a></li>
</ul>
<button class="menu-toggle" aria-label="Открыть меню"><i class="fas fa-bars"></i></button>
</div>
</header>
<section id="hero">
<div class="container hero-content">
<h1>Раина Климат Систем: Профессиональные Климатические Решения</h1>
<p>Мы предлагаем комплексный подход к созданию идеального микроклимата в ваших помещениях, обеспечивая высочайшее качество услуг и продукции.</p>
<a href="#contact" class="btn">Получить консультацию</a>
</div>
</section>
<section id="about">
<div class="container">
<h2>О Нашей Компании</h2>
<div class="grid-2">
<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">
<div>
<h3>Профессионализм и Экспертиза</h3>
<p>Наша команда состоит из высококвалифицированных инженеров и техников, обладающих глубокими знаниями и опытом в области HVAC. Мы постоянно совершенствуем свои навыки и внедряем передовые технологии.</p>
<h3>Наша Миссия</h3>
<p>Мы стремимся создавать оптимальный микроклимат для каждого клиента, обеспечивая комфорт, здоровье и высокую производительность через надежные и энергоэффективные климатические системы.</p>
</div>
</div>
</div>
</section>
<section id="services">
<div class="container">
<h2>Наши Услуги</h2>
<div class="services-grid">
<div class="service-card"><i class="fas fa-drafting-compass"></i><h3>Проектирование</h3><p>Точные расчеты, 3D-модели и вся необходимая проектная документация для ваших систем.</p></div>
<div class="service-card"><i class="fas fa-tools"></i><h3>Монтаж</h3><p>Профессиональная установка всех типов систем вентиляции и кондиционирования, от бытовых до промышленных.</p></div>
<div class="service-card"><i class="fas fa-headset"></i><h3>Сервис 24/7</h3><p>Плановое техническое обслуживание и оперативный аварийный ремонт для бесперебойной работы ваших систем.</p></div>
<div class="service-card"><i class="fas fa-sync-alt"></i><h3>Модернизация</h3><p>Повышение энергоэффективности и снижение эксплуатационных расходов за счет оптимизации существующих систем.</p></div>
</div>
</div>
</section>
<section id="turnkey">
<div class="container">
<h2>Услуги "под ключ"</h2>
{% if services %}
<div class="services-grid">
{% for service in services %}
<div class="turnkey-card" onclick="showDetailsModal('service', {{ loop.index0 }})">
{% if service.photo %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/{{ service.photo }}" alt="{{ service.title }}" class="turnkey-img">
{% endif %}
<div class="turnkey-content">
<h3><i class="{{ service.icon }} fa-fw" style="margin-right: 10px;"></i>{{ service.title }}</h3>
<p>{{ service.description }}</p>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p style="text-align: center; color: var(--text-muted);">Информация об услугах "под ключ" скоро появится на сайте.</p>
{% endif %}
</div>
</section>
<section id="equipment">
<div class="container">
<h2>Наше Оборудование</h2>
{% set prices_enabled = data.settings.prices_enabled %}
{% 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="equipment-grid">
{% for item in equipment %}
<div class="equipment-card" data-category="{{ item.get('category', 'all') }}" onclick="showDetailsModal('equipment', {{ loop.index0 }})">
{% if item.photo %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/equipment/{{ item.photo }}" alt="{{ item.name }}">
{% else %}
<img src="https://via.placeholder.com/250x180.png?text=No+Image" alt="No Image" style="filter: grayscale(0.8) opacity(0.6);">
{% endif %}
<h3>{{ item.name }}</h3>
{% if prices_enabled and item.price > 0 %}
<p class="price">{{ "%.2f"|format(item.price) }} KGS</p>
<a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, интересует оборудование: {{ item.name|urlencode }}" target="_blank" class="btn">Запросить</a>
{% else %}
<p class="price">Уточнить цену</p>
<a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, хочу узнать цену {{ item.name|urlencode }}" target="_blank" class="btn">Уточнить цену</a>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<p style="text-align: center; color: var(--text-muted);">Каталог оборудования скоро будет доступен.</p>
{% endif %}
</div>
</section>
<section id="contact">
<div class="container contact-content">
<h2>Контакты</h2>
<p>Свяжитесь с нами для профессиональной консультации и подбора оптимального климатического решения для вашего объекта.</p>
<div class="contact-info">
<p><strong>Телефон:</strong> <a href="tel:{{ contact_phone }}">{{ contact_phone }}</a></p>
<a href="https://api.whatsapp.com/send?phone={{ whatsapp_phone }}&text=Здравствуйте, я хотел(а) бы получить консультацию по вашим услугам." target="_blank" class="btn"><i class="fab fa-whatsapp"></i> Написать в WhatsApp</a>
</div>
<div style="margin-top: 50px; font-size: 0.9rem; color: var(--text-muted);">
<p><strong>Реквизиты:</strong> ОсОО «Раина Климат Систем», ИНН: 00812202110194, ОКПО: 31290279</p>
</div>
</div>
</section>
<footer class="footer">
<p>© {{ now.year }} ОсОО "Раина Климат Систем". Все права защищены.</p>
</footer>
<div id="detailsModal" class="modal">
<div class="modal-content">
<button class="close-button" onclick="closeDetailsModal()" aria-label="Закрыть">×</button>
<div id="modal-body"></div>
<div class="carousel-nav">
<button id="prevBtn" onclick="changeModalItem(-1)" aria-label="Предыдущий">❮</button>
<button id="nextBtn" onclick="changeModalItem(1)" aria-label="Следующий">❯</button>
</div>
</div>
</div>
<script>
let currentData = null;
let currentType = null;
let currentIndex = -1;
let allItems = [];
function showDetailsModal(type, index) {
const data = {{ data | tojson }};
currentData = data;
currentType = type;
currentIndex = index;
if (type === 'service') allItems = data.services || [];
else if (type === 'equipment') allItems = data.equipment || [];
else if (type === 'project') allItems = data.projects || [];
updateModalContent();
document.getElementById('detailsModal').style.display = 'block';
document.body.style.overflow = 'hidden';
}
function updateModalContent() {
if (!allItems || allItems.length === 0 || currentIndex < 0 || currentIndex >= allItems.length) {
return;
}
const item = allItems[currentIndex];
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = '';
let content = '';
const dataSettings = {{ data.settings | tojson }};
const pricesEnabled = dataSettings.prices_enabled;
const whatsappPhone = '{{ whatsapp_phone }}';
if (currentType === 'service') {
content = `
${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/services/${item.photo}" alt="${item.title}">` : ''}
<h3><i class="${item.icon || 'fas fa-tools'} fa-fw" style="margin-right: 10px; color: var(--accent-primary);"></i>${item.title || 'Услуга'}</h3>
<p>${item.description || 'Описание отсутствует.'}</p>
`;
} else if (currentType === 'equipment') {
let priceHtml = '';
let buttonHref = '';
let buttonText = '';
const itemName = item.name || '';
const itemPrice = item.price || 0;
if (pricesEnabled && itemPrice > 0) {
priceHtml = `<p class="price" style="font-size: 1.8rem; color: var(--accent-primary); margin: 20px 0;">${itemPrice.toFixed(2)} KGS</p>`;
buttonText = 'Запросить';
buttonHref = `https://api.whatsapp.com/send?phone=${whatsappPhone}&text=Здравствуйте, интересует оборудование: ${encodeURIComponent(itemName)}`;
} else {
priceHtml = `<p class="price" style="font-size: 1.8rem; color: var(--accent-primary); margin: 20px 0;">Уточнить цену</p>`;
buttonText = 'Уточнить цену';
buttonHref = `https://api.whatsapp.com/send?phone=${whatsappPhone}&text=Здравствуйте, хочу узнать цену ${encodeURIComponent(itemName)}`;
}
content = `
${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/equipment/${item.photo}" alt="${item.name}">` : '<img src="https://via.placeholder.com/400x300.png?text=No+Image" alt="No Image Available">'}
<h3>${itemName}</h3>
<p><strong>Категория:</strong> ${item.category || 'Не указана'}</p>
${priceHtml}
<a href="${buttonHref}" target="_blank" class="btn" style="padding: 14px 30px; font-size: 1.05rem;">${buttonText}</a>
`;
} else if (currentType === 'project') {
content = `
${item.photo ? `<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/projects/${item.photo}" alt="${item.title}">` : '<img src="https://via.placeholder.com/400x300.png?text=Project+Image+Not+Available" alt="Project Image Not Available">'}
<h3>${item.title || 'Проект'}</h3>
<p>${item.description || 'Описание отсутствует.'}</p>
`;
}
modalBody.innerHTML = content;
updateCarouselNav();
}
function changeModalItem(direction) {
if (!allItems || allItems.length === 0) return;
let newIndex = currentIndex + direction;
if (newIndex < 0) newIndex = allItems.length - 1;
if (newIndex >= allItems.length) newIndex = 0;
currentIndex = newIndex;
updateModalContent();
}
function updateCarouselNav() {
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
if (!allItems || allItems.length <= 1) {
prevBtn.style.display = 'none';
nextBtn.style.display = 'none';
} else {
prevBtn.style.display = 'inline-block';
nextBtn.style.display = 'inline-block';
// Since we are wrapping around, navigation is always possible if length > 1
prevBtn.disabled = false; // currentIndex === 0;
nextBtn.disabled = false; // currentIndex === allItems.length - 1;
}
}
function closeDetailsModal() {
document.getElementById('detailsModal').style.display = 'none';
document.body.style.overflow = '';
}
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' || event.key === 'Esc') {
if (document.getElementById('detailsModal').style.display === 'block') {
closeDetailsModal();
}
}
});
document.getElementById('detailsModal').addEventListener('click', function(event) {
if (event.target === this) {
closeDetailsModal();
}
});
document.addEventListener('DOMContentLoaded', function() {
const header = document.querySelector('.header');
const menuToggle = document.querySelector('.menu-toggle');
const navLinks = document.querySelector('.nav-links');
window.addEventListener('scroll', () => {
header.classList.toggle('scrolled', window.scrollY > 50);
});
menuToggle.addEventListener('click', () => {
navLinks.classList.toggle('active');
menuToggle.setAttribute('aria-expanded', navLinks.classList.contains('active'));
});
document.querySelectorAll('.nav-links a').forEach(link => {
link.addEventListener('click', () => {
navLinks.classList.remove('active');
menuToggle.setAttribute('aria-expanded', 'false');
});
});
const filterContainer = document.querySelector('.equipment-filters');
if (filterContainer) {
filterContainer.addEventListener('click', (e) => {
if (!e.target.matches('.filter-btn')) return;
const currentActive = filterContainer.querySelector('.filter-btn.active');
if(currentActive) currentActive.classList.remove('active');
e.target.classList.add('active');
const filter = e.target.dataset.filter;
document.querySelectorAll('.equipment-card').forEach(card => {
card.style.display = (filter === 'all' || card.dataset.category === filter) ? 'block' : 'none';
});
});
}
});
</script>
</body>
</html>
'''
ADMIN_TEMPLATE = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Админ-панель - Раина Климат Систем</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
body { font-family: 'Poppins', sans-serif; background-color: #f4f7f9; color: #333; padding: 20px; line-height: 1.6; }
.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); }
.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;}
h1, h2, h3 { font-weight: 600; color: #6a0dad; margin-bottom: 15px; }
h1 { font-size: 1.8rem; }
h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
.section { margin-bottom: 30px; padding: 20px; background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; }
form { margin-bottom: 20px; }
label { font-weight: 500; margin-top: 10px; display: block; color: #555; font-size: 0.9rem;}
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; }
input[type="file"] { padding: 8px; cursor: pointer; border: 1px solid #ddd;}
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; }
button:hover, .button:hover { background-color: #8e44ad; }
.delete-button { background-color: #e74c3c; }
.delete-button:hover { background-color: #c0392b; }
.item-list { display: grid; gap: 20px; }
.item { background: #fff; padding: 15px 20px; border-radius: 8px; border: 1px solid #eee; }
.item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; }
.edit-form-container { margin-top: 15px; padding: 20px; background: #fdf9ff; border: 1px dashed #ddd; border-radius: 6px; display: none; }
details { background-color: #fafafa; border: 1px solid #e9e9e9; border-radius: 8px; margin-bottom: 20px; }
details > summary { cursor: pointer; font-weight: 600; color: #8e44ad; display: block; padding: 15px; position: relative; list-style: none; }
details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); }
details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
.photo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; object-fit: cover;}
.message { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; }
.message.success { background-color: #d4edda; color: #155724; }
.message.error { background-color: #f8d7da; color: #721c24; }
.message.warning { background-color: #fff3cd; color: #856404; }
.settings-input-group { display: flex; flex-direction: column; align-items: flex-start;}
.settings-input-group label { display: flex; align-items: center; gap: 10px; margin-top: 15px;}
.settings-input-group input[type="checkbox"] { width: auto; margin: 0; }
</style>
</head>
<body>
<div class="container">
<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>
{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}<div class="message {{ category }}">{{ message }}</div>{% endfor %}{% endif %}{% endwith %}
<div class="section">
<h2><i class="fas fa-cog"></i> Настройки сайта</h2>
<div class="settings-input-group">
<form method="POST"><input type="hidden" name="action" value="update_settings">
<label>
<input type="checkbox" name="prices_enabled" value="true" {% if settings.prices_enabled %}checked{% endif %}>
Цены включены (Снимите флажок, чтобы скрыть цены на сайте)
</label>
<button type="submit">Сохранить настройки</button>
</form>
</div>
<h2 style="margin-top: 20px;"><i class="fas fa-sync-alt"></i> Синхронизация</h2>
<form method="POST" action="{{ url_for('force_upload') }}" style="display: inline;"><button type="submit" class="button">Загрузить на сервер</button></form>
<form method="POST" action="{{ url_for('force_download') }}" style="display: inline;"><button type="submit" class="button">Скачать с сервера</button></form>
</div>
<div class="section">
<h2><i class="fas fa-star"></i> Реализованные проекты</h2>
<details><summary>Добавить проект</summary>
<form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="add_project">
<label>Название*:</label><input type="text" name="title" required>
<label>Описание*:</label><textarea name="description" rows="3" required></textarea>
<label>Фото*:</label><input type="file" name="photo" accept="image/*" required>
<button type="submit">Добавить проект</button>
</form>
</details>
<div class="item-list">
{% for project in projects %}
<div class="item">
<p><strong>{{ project.title }}</strong>: {{ project.description }}</p>
{% 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 %}
<div class="item-actions">
<button onclick="toggleEditForm('edit-project-{{ loop.index0 }}')">Редактировать</button>
<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>
</div>
<div id="edit-project-{{ loop.index0 }}" class="edit-form-container">
<form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_project"><input type="hidden" name="index" value="{{ loop.index0 }}">
<label>Название*:</label><input type="text" name="title" value="{{ project.title }}" required>
<label>Описание*:</label><textarea name="description" rows="3" required>{{ project.description }}</textarea>
<label>Заменить фото:</label><input type="file" name="photo" accept="image/*">
<button type="submit">Сохранить</button>
</form>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="section">
<h2><i class="fas fa-concierge-bell"></i> Услуги "под ключ"</h2>
<details><summary>Добавить услугу</summary>
<form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="add_service">
<label>Заголовок*:</label><input type="text" name="title" required>
<label>Иконка (FontAwesome)*:</label><input type="text" name="icon" placeholder="fas fa-tools" required>
<label>Описание*:</label><textarea name="description" rows="3" required></textarea>
<label>Фото:</label><input type="file" name="photo" accept="image/*">
<button type="submit">Добавить услугу</button>
</form>
</details>
<div class="item-list">
{% for service in services %}
<div class="item">
<p><i class="{{ service.icon }} fa-fw"></i> <strong>{{ service.title }}</strong>: {{ service.description }}</p>
{% 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 %}
<div class="item-actions">
<button onclick="toggleEditForm('edit-service-{{ loop.index0 }}')">Редактировать</button>
<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>
</div>
<div id="edit-service-{{ loop.index0 }}" class="edit-form-container">
<form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_service"><input type="hidden" name="index" value="{{ loop.index0 }}">
<label>Заголовок*:</label><input type="text" name="title" value="{{ service.title }}" required>
<label>Иконка*:</label><input type="text" name="icon" value="{{ service.icon }}" required>
<label>Описание*:</label><textarea name="description" rows="3" required>{{ service.description }}</textarea>
<label>Заменить фото:</label><input type="file" name="photo" accept="image/*">
<button type="submit">Сохранить</button>
</form>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="section">
<h2><i class="fas fa-box-open"></i> Оборудование</h2>
<details><summary>Добавить категорию</summary>
<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>
</details>
<div class="item-list">
{% for category in categories %}
<div class="item" style="display: flex; justify-content: space-between; align-items: center;">
<span>{{ category }}</span>
<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>
</div>
{% endfor %}
</div>
<details style="margin-top:20px;"><summary>Добавить оборудование</summary>
<form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="add_equipment">
<label>Название*:</label><input type="text" name="name" required>
<label>Цена (KGS):</label><input type="number" name="price" step="0.01" min="0">
<label>Категория:</label><select name="category"><option value="Без категории">Без категории</option>{% for cat in categories %}<option value="{{ cat }}">{{ cat }}</option>{% endfor %}</select>
<label>Фото:</label><input type="file" name="photo" accept="image/*">
<button type="submit">Добавить</button>
</form>
</details>
<div class="item-list">
{% for item in equipment %}
<div class="item">
<p><strong>{{ item.name }}</strong> ({{ item.category }}) - {{ "%.2f"|format(item.price) }} KGS</p>
{% 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 %}
<div class="item-actions">
<button onclick="toggleEditForm('edit-eq-{{ loop.index0 }}')">Редактировать</button>
<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>
</div>
<div id="edit-eq-{{ loop.index0 }}" class="edit-form-container">
<form method="POST" enctype="multipart/form-data"><input type="hidden" name="action" value="edit_equipment"><input type="hidden" name="index" value="{{ loop.index0 }}">
<label>Название*:</label><input type="text" name="name" value="{{ item.name }}" required>
<label>Цена (KGS):</label><input type="number" name="price" value="{{ item.price if item.price else '' }}" step="0.01" min="0">
<label>Категория:</label><select name="category">{% for cat in categories %}<option value="{{ cat }}" {% if item.category == cat %}selected{% endif %}>{{ cat }}</option>{% endfor %}</select>
<label>Заменить фото:</label><input type="file" name="photo" accept="image/*">
<button type="submit">Сохранить</button>
</form>
</div>
</div>
{% endfor %}
</div>
</div>
<script>function toggleEditForm(id) { document.getElementById(id).style.display = document.getElementById(id).style.display === 'block' ? 'none' : 'block'; }</script>
</body>
</html>
'''
@app.route('/')
def landing():
data = load_data()
return render_template_string(
LANDING_TEMPLATE,
services=data.get('services', []),
equipment=data.get('equipment', []),
categories=sorted(data.get('categories', [])),
projects=data.get('projects', []),
repo_id=REPO_ID,
contact_phone=CONTACT_PHONE,
whatsapp_phone=WHATSAPP_PHONE,
now=datetime.utcnow(),
data=data
)
@app.route('/admin', methods=['GET', 'POST'])
def admin():
data = load_data()
if request.method == 'POST':
action = request.form.get('action')
logging.info(f"Admin action: {action}")
try:
if action == 'update_settings':
data['settings']['prices_enabled'] = 'prices_enabled' in request.form
flash("Настройки сайта обновлены.", 'success')
elif action == 'add_category':
name = request.form.get('category_name', '').strip()
if name and name not in data['categories']:
data['categories'].append(name)
flash(f"Категория '{name}' добавлена.", 'success')
else: flash("Категория уже существует или пуста.", 'error')
elif action == 'delete_category':
name = request.form.get('category_name')
if name in data['categories']:
data['categories'].remove(name)
flash(f"Категория '{name}' удалена.", 'success')
elif action in ['add_equipment', 'edit_equipment']:
name = request.form.get('name', '').strip()
price_str = request.form.get('price', '').strip()
price = round(float(price_str), 2) if price_str else 0
category = request.form.get('category')
if not name:
flash("Название оборудования обязательно.", 'error')
return redirect(url_for('admin'))
item_data = {'name': name, 'price': price, 'category': category}
photo = request.files.get('photo')
if action == 'add_equipment':
if photo and photo.filename:
item_data['photo'] = upload_photo_to_hf(photo, name, 'equipment')
data['equipment'].append(item_data)
flash(f"Оборудование '{name}' добавлено.", 'success')
else:
index = int(request.form.get('index'))
original_item = data['equipment'][index]
if photo and photo.filename:
delete_photo_from_hf(original_item.get('photo'), 'equipment')
item_data['photo'] = upload_photo_to_hf(photo, name, 'equipment')
else:
item_data['photo'] = original_item.get('photo')
data['equipment'][index] = item_data
flash(f"Оборудование '{name}' обновлено.", 'success')
elif action == 'delete_equipment':
index = int(request.form.get('index'))
item = data['equipment'].pop(index)
delete_photo_from_hf(item.get('photo'), 'equipment')
flash(f"Оборудование '{item.get('name')}' удалено.", 'success')
elif action in ['add_service', 'edit_service']:
title = request.form.get('title', '').strip()
item_data = {'title': title, 'icon': request.form.get('icon'), 'description': request.form.get('description')}
photo = request.files.get('photo')
if not title:
flash("Заголовок услуги обязателен.", 'error')
return redirect(url_for('admin'))
if action == 'add_service':
if photo and photo.filename:
item_data['photo'] = upload_photo_to_hf(photo, title, 'services')
data['services'].append(item_data)
flash(f"Услуга '{title}' добавлена.", 'success')
else:
index = int(request.form.get('index'))
original_item = data['services'][index]
if photo and photo.filename:
delete_photo_from_hf(original_item.get('photo'), 'services')
item_data['photo'] = upload_photo_to_hf(photo, title, 'services')
else:
item_data['photo'] = original_item.get('photo')
data['services'][index] = item_data
flash(f"Услуга '{title}' обновлена.", 'success')
elif action == 'delete_service':
index = int(request.form.get('index'))
item = data['services'].pop(index)
delete_photo_from_hf(item.get('photo'), 'services')
flash(f"Услуга '{item.get('title')}' удалена.", 'success')
elif action in ['add_project', 'edit_project']:
title = request.form.get('title', '').strip()
item_data = {'title': title, 'description': request.form.get('description')}
photo = request.files.get('photo')
if not title:
flash("Заголовок проекта обязателен.", 'error')
return redirect(url_for('admin'))
if action == 'add_project':
if photo and photo.filename:
item_data['photo'] = upload_photo_to_hf(photo, title, 'projects')
data['projects'].append(item_data)
flash(f"Проект '{title}' добавлен.", 'success')
else:
flash("Фото обязательно для нового проекта.", 'error')
else:
index = int(request.form.get('index'))
original_item = data['projects'][index]
if photo and photo.filename:
delete_photo_from_hf(original_item.get('photo'), 'projects')
item_data['photo'] = upload_photo_to_hf(photo, title, 'projects')
else:
item_data['photo'] = original_item.get('photo')
data['projects'][index] = item_data
flash(f"Проект '{title}' обновлен.", 'success')
elif action == 'delete_project':
index = int(request.form.get('index'))
item = data['projects'].pop(index)
delete_photo_from_hf(item.get('photo'), 'projects')
flash(f"Проект '{item.get('title')}' удален.", 'success')
save_data(data)
return redirect(url_for('admin'))
except Exception as e:
logging.error(f"Admin action '{action}' failed: {e}", exc_info=True)
flash(f"Произошла ошибка: {e}", 'error')
return redirect(url_for('admin'))
return render_template_string(
ADMIN_TEMPLATE,
equipment=data.get('equipment', []),
categories=sorted(data.get('categories', [])),
services=data.get('services', []),
projects=data.get('projects', []),
repo_id=REPO_ID,
settings=data.get('settings', {'prices_enabled': True})
)
def upload_photo_to_hf(photo, item_name, folder):
if not photo or not photo.filename or not HF_TOKEN_WRITE:
return None
try:
api = HfApi()
safe_name = secure_filename(item_name.replace(' ', '_'))[:50]
ext = os.path.splitext(photo.filename)[1].lower()
if not ext: ext = ".jpg"
photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
photo_file_obj = io.BytesIO(photo.read())
api.upload_file(
path_or_fileobj=photo_file_obj, path_in_repo=f"{folder}/{photo_filename}",
repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
commit_message=f"Upload photo {photo_filename} for {folder}"
)
logging.info(f"Uploaded photo {photo_filename} to {folder}")
return photo_filename
except Exception as e:
logging.error(f"Error uploading photo {photo.filename}: {e}", exc_info=True)
flash(f"Ошибка загрузки фото {photo.filename}. Проверьте формат и размер.", 'error')
return None
def delete_photo_from_hf(photo_filename, folder):
if not photo_filename or not HF_TOKEN_WRITE:
return
try:
api = HfApi()
api.delete_files(
repo_id=REPO_ID, paths_in_repo=[f"{folder}/{photo_filename}"],
repo_type="dataset", token=HF_TOKEN_WRITE,
commit_message=f"Delete photo {photo_filename} from {folder}"
)
logging.info(f"Deleted photo {photo_filename} from {folder}")
except HfHubHTTPError as e:
if e.response.status_code != 404:
logging.error(f"HTTP error deleting photo {photo_filename}: {e}")
else:
logging.info(f"Photo {photo_filename} not found on HF for deletion, or already deleted.")
except Exception as e:
logging.error(f"Unexpected error deleting photo {photo_filename}: {e}")
@app.route('/force_upload', methods=['POST'])
def force_upload():
upload_db_to_hf()
flash("Данные загружены на сервер.", 'success')
return redirect(url_for('admin'))
@app.route('/force_download', methods=['POST'])
def force_download():
if download_db_from_hf():
flash("Данные успешно скачаны с сервера.", 'success')
else:
flash("Ошибка при скачивании данных с сервера.", 'error')
return redirect(url_for('admin'))
if __name__ == '__main__':
logging.info("Application starting up...")
if not download_db_from_hf():
logging.warning("Initial database download failed. Application might start with empty or outdated data.")
if HF_TOKEN_WRITE:
threading.Thread(target=periodic_backup, daemon=True).start()
port = int(os.environ.get('PORT', 7860))
app.run(debug=False, host='0.0.0.0', port=port)