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 = ''' Раина Климат Систем - Вентиляция и Кондиционирование

Раина Климат Систем: Профессиональные Климатические Решения

Мы предлагаем комплексный подход к созданию идеального микроклимата в ваших помещениях, обеспечивая высочайшее качество услуг и продукции.

Получить консультацию

О Нашей Компании

Команда Раина

Профессионализм и Экспертиза

Наша команда состоит из высококвалифицированных инженеров и техников, обладающих глубокими знаниями и опытом в области HVAC. Мы постоянно совершенствуем свои навыки и внедряем передовые технологии.

Наша Миссия

Мы стремимся создавать оптимальный микроклимат для каждого клиента, обеспечивая комфорт, здоровье и высокую производительность через надежные и энергоэффективные климатические системы.

Наши Услуги

Проектирование

Точные расчеты, 3D-модели и вся необходимая проектная документация для ваших систем.

Монтаж

Профессиональная установка всех типов систем вентиляции и кондиционирования, от бытовых до промышленных.

Сервис 24/7

Плановое техническое обслуживание и оперативный аварийный ремонт для бесперебойной работы ваших систем.

Модернизация

Повышение энергоэффективности и снижение эксплуатационных расходов за счет оптимизации существующих систем.

Услуги "под ключ"

{% if services %}
{% for service in services %}
{% if service.photo %} {{ service.title }} {% endif %}

{{ service.title }}

{{ service.description }}

{% endfor %}
{% else %}

Информация об услугах "под ключ" скоро появится на сайте.

{% endif %}

Наше Оборудование

{% set prices_enabled = data.settings.prices_enabled %} {% if equipment %}
{% for category in categories %} {% endfor %}
{% for item in equipment %}
{% if item.photo %} {{ item.name }} {% else %} No Image {% endif %}

{{ item.name }}

{% if prices_enabled and item.price > 0 %}

{{ "%.2f"|format(item.price) }} KGS

Запросить {% else %}

Уточнить цену

Уточнить цену {% endif %}
{% endfor %}
{% else %}

Каталог оборудования скоро будет доступен.

{% endif %}

Контакты

Свяжитесь с нами для профессиональной консультации и подбора оптимального климатического решения для вашего объекта.

Реквизиты: ОсОО «Раина Климат Систем», ИНН: 00812202110194, ОКПО: 31290279

''' ADMIN_TEMPLATE = ''' Админ-панель - Раина Климат Систем

Админ-панель "Раина Климат Систем"

Перейти на сайт
{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %}

Настройки сайта

Синхронизация

Реализованные проекты

Добавить проект
{% for project in projects %}

{{ project.title }}: {{ project.description }}

{% if project.photo %}
Project Photo
{% endif %}
{% endfor %}

Услуги "под ключ"

Добавить услугу
{% for service in services %}

{{ service.title }}: {{ service.description }}

{% if service.photo %}
Service Photo
{% endif %}
{% endfor %}

Оборудование

Добавить категорию
{% for category in categories %}
{{ category }}
{% endfor %}
Добавить оборудование
{% for item in equipment %}

{{ item.name }} ({{ item.category }}) - {{ "%.2f"|format(item.price) }} KGS

{% if item.photo %}
Equipment Photo
{% endif %}
{% endfor %}
''' @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)