diff --git "a/app.py" "b/app.py" deleted file mode 100644--- "a/app.py" +++ /dev/null @@ -1,3061 +0,0 @@ - -# -*- coding: utf-8 -*- -from flask import Flask, render_template_string, request, redirect, url_for, send_from_directory, 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, HFValidationError # Import specific errors -from werkzeug.utils import secure_filename -import urllib.parse - -app = Flask(__name__) -# It's highly recommended to set a secret key for flash messages and sessions -app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'a_default_secret_key_change_me') # CHANGE THIS in production! - -DATA_FILE = 'data.json' -UPLOADS_DIR = 'uploads' # Directory for temporary uploads - -# Ensure uploads directory exists -os.makedirs(UPLOADS_DIR, exist_ok=True) - -# Настройки Hugging Face -REPO_ID = "Kgshop/Mebelhause" # Используем ваш репозиторий -HF_TOKEN_WRITE = os.getenv("HF_TOKEN") # Use HF_TOKEN for write access -HF_TOKEN_READ = os.getenv("HF_TOKEN_READ", HF_TOKEN_WRITE) # Use separate READ token if provided, otherwise fallback to WRITE token - -# Ссылка на логотип (обновлено) -LOGO_URL = "https://huggingface.co/spaces/Mebelhause/Kg/resolve/main/Screenshot_20250411-112027.png" -WHATSAPP_NUMBER = "+996700253966" # Номер для WhatsApp - -# Настройка логирования -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - -# --- Функции работы с данными и Hugging Face --- - -# Global lock for file access -data_lock = threading.Lock() - -def download_db_from_hf(): - """Скачивает файл базы данных с Hugging Face.""" - if not HF_TOKEN_READ: - logging.warning("HF_TOKEN_READ не установлен. Пропуск скачивания с Hugging Face.") - return False - try: - logging.info(f"Попытка скачивания {DATA_FILE} из {REPO_ID} (dataset)") - hf_hub_download( - repo_id=REPO_ID, - filename=DATA_FILE, - repo_type="dataset", # Explicitly dataset - token=HF_TOKEN_READ, - local_dir=".", - local_dir_use_symlinks=False, # Recommended for Spaces/Docker - force_download=True, # Принудительно скачивать свежую версию - etag_timeout=10 # Уменьшить таймаут для проверки etag - ) - logging.info(f"Файл {DATA_FILE} успешно скачан из Hugging Face.") - return True - except RepositoryNotFoundError: - logging.error(f"Репозиторий {REPO_ID} (dataset) не найден на Hugging Face.") - return False - except HFValidationError as e: - logging.error(f"Ошибка валидации Hugging Face (возможно, неверный токен?): {e}") - return False - except Exception as e: - # Check for common HTTP errors within the exception string - if "401 Client Error" in str(e): - logging.error(f"Ошибка аутентификации (401) при скачивании с Hugging Face. Проверьте HF_TOKEN_READ. Детали: {e}") - elif "404 Client Error" in str(e): - logging.error(f"Файл {DATA_FILE} не найден в репозитории {REPO_ID} (dataset) на Hugging Face. Детали: {e}") - else: - logging.error(f"Неизвестная ошибка при скачивании JSON базы ({type(e).__name__}): {e}") - return False - -def load_data(): - """Загружает данные из локального JSON файла, предварительно пытаясь скачать с HF.""" - with data_lock: # Ensure only one thread accesses the file load logic at a time - data_loaded_from_hf = False - if download_db_from_hf(): - try: - with open(DATA_FILE, 'r', encoding='utf-8') as f: - data = json.load(f) - logging.info("Данные успешно загружены из локального JSON после скачивания с HF.") - if isinstance(data, dict) and 'products' in data and 'categories' in data: - data_loaded_from_hf = True - return data - else: - logging.warning("Структура JSON файла с HF некорректна. Попытка загрузить существующий локальный файл.") - except FileNotFoundError: - logging.warning("Локальный файл базы данных не найден после попытки скачивания с HF.") - except json.JSONDecodeError as e: - logging.error(f"Ошибка декодирования JSON файла, скачанного с HF: {e}") - except Exception as e: - logging.error(f"Произошла ошибка при чтении локального файла JSON после скачивания с HF: {e}") - - # If download failed or HF file was invalid, try loading existing local file - if not data_loaded_from_hf: - logging.info("Попытка загрузить существующий локальный файл data.json (если есть).") - try: - with open(DATA_FILE, 'r', encoding='utf-8') as f: - data = json.load(f) - logging.info("Данные успешно загружены из существующего локального JSON.") - if isinstance(data, dict) and 'products' in data and 'categories' in data: - return data - else: - logging.warning("Структура существующего локального JSON файла некорректна. Используется пустая структура.") - except FileNotFoundError: - logging.info("Локальный файл data.json не найден. Создание пустой структуры данных.") - except json.JSONDecodeError as e: - logging.error(f"Ошибка декодирования существующего локального JSON: {e}. Создание пустой структуры данных.") - except Exception as e: - logging.error(f"Произошла ошибка при загрузке существующего локального JSON: {e}") - - # Return default empty structure if all loading attempts fail - logging.warning("Не удалось загрузить данные ни с HF, ни локально. Возвращается пустая структура.") - return {'products': [], 'categories': []} - - -def save_data(data): - """Сохраняет данные в локальный JSON файл и загружает на Hugging Face.""" - with data_lock: # Ensure thread safety for saving - try: - # Backup current data file before writing, just in case - if os.path.exists(DATA_FILE): - backup_file = f"{DATA_FILE}.bak_{datetime.now().strftime('%Y%m%d%H%M%S')}" - try: - os.rename(DATA_FILE, backup_file) - logging.info(f"Создана резервная копия старого файла: {backup_file}") - except OSError as e: - logging.error(f"Не удалось создать резервную копию {DATA_FILE}: {e}") - - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump(data, f, ensure_ascii=False, indent=4) - logging.info("Данные успешно сохранены в локальный JSON.") - # Schedule upload to HF in a separate thread to avoid blocking the request - upload_thread = threading.Thread(target=upload_db_to_hf) - upload_thread.start() - except Exception as e: - logging.error(f"Ошибка при сохранении данных в локальный JSON: {e}") - # Optional: Restore from backup if write failed? More complex logic needed. - -def upload_db_to_hf(): - """Загружает файл базы данных на Hugging Face.""" - if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_WRITE не установлен. Пропуск загрузки data.json на Hugging Face.") - return - if not os.path.exists(DATA_FILE): - logging.warning(f"Файл {DATA_FILE} для загрузки на HF не найден.") - return - - # Adding a small delay before upload, allows file system to settle sometimes - time.sleep(1) - - with data_lock: # Acquire lock briefly to ensure file isn't being written to during upload prep - file_path = DATA_FILE - if not os.path.exists(file_path): - logging.warning(f"Файл {file_path} все еще не найден перед загрузкой.") - return - try: - logging.info(f"Попытка загрузки {file_path} в {REPO_ID} (dataset)") - api = HfApi() - api.upload_file( - path_or_fileobj=file_path, - path_in_repo=DATA_FILE, # Upload to root of the dataset repo - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Update database {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - ) - logging.info(f"{DATA_FILE} успешно загружен на Hugging Face.") - except HFValidationError as e: - logging.error(f"Ошибка валидации Hugging Face при загрузке data.json (возможно, неверный токен?): {e}") - except Exception as e: - logging.error(f"Ошибка при загрузке {DATA_FILE} на Hugging Face ({type(e).__name__}): {e}") - - -def upload_photo_to_hf(local_path, filename_in_repo, product_name): - """Загружает файл фотографии на Hugging Face в папку 'photos/'.""" - if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_WRITE не установлен. Пропуск загрузки фото на Hugging Face.") - return False - if not os.path.exists(local_path): - logging.error(f"Локальный файл фото {local_path} не найден для загрузки на HF.") - return False - - path_in_repo = f"photos/{filename_in_repo}" # Ensure it's saved in the photos directory - - try: - logging.info(f"Попытка загрузки фото {local_path} как {path_in_repo} в {REPO_ID} (dataset)") - api = HfApi() - api.upload_file( - path_or_fileobj=local_path, - path_in_repo=path_in_repo, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Upload photo {filename_in_repo} for product {product_name}" - ) - logging.info(f"Фото {filename_in_repo} успешно загружено на Hugging Face.") - return True - except HFValidationError as e: - logging.error(f"Ошибка валидации Hugging Face при загрузке фото {filename_in_repo} (возможно, неверный токен?): {e}") - return False - except Exception as e: - logging.error(f"Ошибка при загрузке фото {filename_in_repo} на Hugging Face ({type(e).__name__}): {e}") - return False - finally: - # Clean up the temporary local file after attempting upload - if os.path.exists(local_path): - try: - os.remove(local_path) - logging.info(f"Удален временный файл фото: {local_path}") - except OSError as e: - logging.error(f"Ошибка при удалении временного файла фото {local_path}: {e}") - -def periodic_backup(): - """Периодически сохраняет резервную копию на Hugging Face.""" - interval_seconds = 900 # 15 minutes - logging.info(f"Периодическое резервное копирование настроено с интервалом {interval_seconds} секунд.") - while True: - time.sleep(interval_seconds) - logging.info("Запуск периодического резервного копирования data.json на HF...") - # Run upload in a separate thread to avoid blocking the backup loop if upload takes time - backup_upload_thread = threading.Thread(target=upload_db_to_hf) - backup_upload_thread.start() - - -# --- HTML Шаблоны --- - -# Общие стили для обеих страниц + Мобильные стили -COMMON_STYLES = ''' - - - -''' - -LANDING_PAGE_HTML = ''' - - - - - - MebelHause KG - Изготовление мебели на заказ в Бишкеке - - - - {{ common_styles | safe }} - - - -
-
- - -

MebelHause KG

-
- -
- -
-
-

Мебель Вашей Мечты - На Заказ

-

Создаем стильную и функциональную корпусную мебель в Бишкеке с учетом всех ваших пожеланий. Качество, дизайн и точность в каждой детали.

- Рассчитать стоимость - Смотреть готовые работы -
- -
-

Почему выбирают нас?

-
-
- -

Индивидуальные размеры

-

Изготавливаем мебель точно под ваше пространство.

-
-
- -

Широкий выбор материалов

-

ЛДСП, МДФ, акрил, шпон - подберем идеальный вариант.

-
-
- -

Уникальный дизайн

-

Воплотим ваши идеи или предложим свои решения.

-
-
- -

Гарантия качества

-

Используем надежную фурнитуру и контролируем сборку.

-
-
-
- -
-

Быстрый Расчет Стоимости

-
-

Заполните форму ниже, и мы свяжемся с вами в WhatsApp для уточнения деталей и расчета.

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
-
- - - -
- - - - - - - - -''' - -CATALOG_PAGE_HTML = ''' - - - - - - Каталог Готовых Изделий - MebelHause KG - - - {{ common_styles | safe }} - - - - -
-
- - -

MebelHause KG

-
- -
- -
-
-

Каталог Готовых Изделий

-
- -
- - {% for category in categories %} - - {% endfor %} -
- -
- - -
- -
- {% for product in products %} -
- -
- {% set photo_url = 'https://via.placeholder.com/300x300.png?text=Нет+фото' %} {# Default image #} - {% if product.get('photos') and product['photos']|length > 0 %} - {% set photo_url = 'https://huggingface.co/datasets/' + repo_id + '/resolve/main/photos/' + product['photos'][0] %} - {% endif %} - {{ product['name'] }} {# Fallback on image load error #} -
- -
-
{# Top content wrapper #} -

{{ product['name'] }}

-
{{ product['price'] }} сом
-

{{ product.get('description', 'Описание отсутствует.') }}

{# Add title for full text hover #} -
-
- - -
-
-
- {% endfor %} - {# Message for empty grid is handled by CSS :empty selector #} -
-
- - -
- - - - - - - - - - - - - - -
- - - - - - - - -''' - -ADMIN_PAGE_HTML = ''' - - - - - - Админ-панель - MebelHause KG - - {{ common_styles | safe }} - - - -
-
- - -

Админ-панель

-
- -
- -
- {# Flash Messages Display #} - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - - {% endif %} - {% endwith %} - - -
-

Управление Категориями

-
-

Добавить новую категорию

-
- - - - -
-
- -
-

Список категорий

- {% if categories %} -
- {% for category in categories %} -
-

{{ category }}

-
-
- - - -
- -
-
- {% endfor %} -
- {% else %} -

Категорий пока нет.

- {% endif %} -
-
- - - -
-

Управление Товарами

-
-

Добавить новый товар

-
- - - - - - - - - - - - - - - - - - -
-
- - -
-
- - - -
-
- -
-

Список товаров

- {% if products %} -
- {% for product in products %} -
-

{{ product['name'] }}

-

Категория: {{ product.get('category', 'Без категории') }}

-

Цена: {{ product['price'] }} сом

-

Описание: {{ product.get('description', '')[:100] | escape }}{% if product.get('description', '')|length > 100 %}...{% endif %}

-

Цвета: {{ (product.get('colors') or ['нет']) | join(', ') }}

{# Display 'нет' if empty #} - - {% set current_photos = product.get('photos', []) %} - {% if current_photos %} -
- {% for photo in current_photos %} - {% set photo_url = 'https://huggingface.co/datasets/' + repo_id + '/resolve/main/photos/' + photo %} - {{ product['name'] }} - фото {{ loop.index }} - {% endfor %} -
- {% else %} -

Фото нет

- {% endif %} - -
-
- Редактировать -
-
- - - - - - - - - - - - - - - - - - - Старые фото останутся, если новые не выбраны. - {% if current_photos %} -
Т��кущие фото: {{ current_photos | join(', ') }}. Удалить все текущие - - {% endif %} -
- - - -
- {% set current_colors = product.get('colors', []) %} - {% if current_colors %} - {% for color in current_colors %} -
- - -
- {% endfor %} - {% else %} - {# Show one empty field if no colors exist #} -
- - -
- {% endif %} -
- - - -
-
-
- -
- - - -
-
-
- {% endfor %} -
- {% else %} -

Товаров пока нет. Добавьте первый товар с помощью формы выше.

- {% endif %} -
-
- - -
-

Управление Базой Данных (Hugging Face)

-

- Данные сохраняются на Hugging Face при каждом изменении и автоматически каждые 15 минут. - Вы можете принудительно синхронизировать данные кнопками ниже. -

-
-
- - -
-
- - -
-
-
-
-
- - -
- - - - -''' - - -# --- Маршруты Flask --- - -@app.route('/') -def landing_page(): - """Отображает главную страницу (лендинг).""" - current_year = datetime.now().year - return render_template_string( - LANDING_PAGE_HTML, - common_styles=COMMON_STYLES, - logo_url=LOGO_URL, - whatsapp_number=WHATSAPP_NUMBER.replace("+", ""), # Убираем + для ссылки wa.me - current_year=current_year - ) - -@app.route('/catalog') -def catalog(): - """Отображает страницу каталога.""" - data = load_data() - # Ensure products and categories are lists, even if missing/null in data.json - products = data.get('products', []) if isinstance(data.get('products'), list) else [] - categories = data.get('categories', []) if isinstance(data.get('categories'), list) else [] - current_year = datetime.now().year - return render_template_string( - CATALOG_PAGE_HTML, - common_styles=COMMON_STYLES, - logo_url=LOGO_URL, - products=products, - categories=categories, - repo_id=REPO_ID, - whatsapp_number=WHATSAPP_NUMBER.replace("+", ""), - current_year=current_year - ) - -# Note: The /product/ route for modal content is no longer needed -# as the JavaScript now generates the modal content directly from the 'products' array. - -@app.route('/admin', methods=['GET', 'POST']) -def admin(): - """Отображает админ-панель и обрабатывает действия администратора.""" - data = load_data() - # Ensure products and categories are lists - products = data.get('products', []) if isinstance(data.get('products'), list) else [] - categories = data.get('categories', []) if isinstance(data.get('categories'), list) else [] - - if request.method == 'POST': - action = request.form.get('action') - logging.info(f"Admin action received: {action}") - - try: - # --- Category Actions --- - if action == 'add_category': - category_name = request.form.get('category_name', '').strip() - if category_name and category_name not in categories: - categories.append(category_name) - save_data(data) # Save triggers async HF upload - flash(f"Категория '{category_name}' успешно добавлена.", 'success') - logging.info(f"Category added: {category_name}") - elif category_name in categories: - flash(f"Ошибка: Категория '{category_name}' уже существует.", 'error') - logging.warning(f"Attempted to add duplicate category: {category_name}") - else: - flash("Ошибка: Название категории не может быть пустым.", 'error') - logging.warning("Attempted to add empty category name.") - return redirect(url_for('admin')) # Redirect even on error to show flash - - elif action == 'delete_category': - category_index_str = request.form.get('category_index') - if category_index_str is not None: - try: - category_index = int(category_index_str) - if 0 <= category_index < len(categories): - deleted_category = categories.pop(category_index) - # Update products using this category - updated_count = 0 - for product in products: - if product.get('category') == deleted_category: - product['category'] = 'Без категории' - updated_count += 1 - save_data(data) - flash(f"Категория '{deleted_category}' удалена. {updated_count} товаров обновлено.", 'success') - logging.info(f"Category deleted: {deleted_category}, updated {updated_count} products.") - else: - flash("Ошибка: Неверный индекс категории для удаления.", 'error') - logging.warning(f"Invalid category index for delete: {category_index}") - except ValueError: - flash("Ошибка: Некорректный индекс категории.", 'error') - logging.warning(f"Non-integer category index received: {category_index_str}") - else: - flash("Ошибка: Индекс категории не указан.", 'error') - logging.warning("Category index missing for delete action.") - return redirect(url_for('admin')) - - # --- Product Actions --- - elif action == 'add' or action == 'edit': - index = -1 # Default for 'add' - if action == 'edit': - index_str = request.form.get('index') - if index_str is None: - flash("Ошибка: Индекс товара не указан для редактирования.", 'error') - return redirect(url_for('admin')) - try: - index = int(index_str) - if not (0 <= index < len(products)): - flash("Ошибка: Неверный индекс товара для редактирования.", 'error') - logging.warning(f"Invalid product index for edit: {index}") - return redirect(url_for('admin')) - except ValueError: - flash("Ошибка: Некорректный индекс товара для редактирования.", 'error') - logging.warning(f"Non-integer product index for edit: {index_str}") - return redirect(url_for('admin')) - - # --- Field Validation --- - name = request.form.get('name', '').strip() - price_str = request.form.get('price', '0').replace(',', '.') - description = request.form.get('description', '').strip() - category = request.form.get('category', 'Без категории') - colors = sorted(list(set(c.strip() for c in request.form.getlist('colors') if c.strip()))) # Unique, sorted, non-empty - photos_files = request.files.getlist('photos') - delete_current_photos = request.form.get('delete_current_photos') == 'true' - - error = False - if not name: - flash("Ошибка: Название товара не может быть пустым.", 'error') - error = True - if not description: - flash("Ошибка: Описание товара не может быть пустым.", 'error') - error = True - try: - price = int(float(price_str)) # Allow float input, convert to int - if price < 0: - flash("Ошибка: Цена не может быть отрицательной.", 'error') - error = True - except ValueError: - flash("Ошибка: Некорректный формат цены.", 'error') - error = True - price = 0 # Set default on error - - if error: - # Need to re-render form with errors, ideally preserving input - # This is complex without a form library. Redirecting for now. - logging.warning(f"Validation error during product {'edit' if action=='edit' else 'add'}.") - return redirect(url_for('admin')) - - # --- Photo Handling --- - MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB - MAX_PHOTOS = 10 - allowed_extensions = {'png', 'jpg', 'jpeg', 'webp'} - - def allowed_file(filename): - return '.' in filename and \ - filename.rsplit('.', 1)[1].lower() in allowed_extensions - - newly_uploaded_filenames = [] - current_photos = products[index].get('photos', []) if action == 'edit' else [] - - # Filter out empty file inputs - valid_photos_files = [pf for pf in photos_files if pf and pf.filename] - - if len(current_photos) + len(valid_photos_files) - (len(current_photos) if delete_current_photos else 0) > MAX_PHOTOS: - flash(f"Ошибка: Превышен лимит в {MAX_PHOTOS} фотографий на товар.", 'error') - return redirect(url_for('admin')) - - - for photo in valid_photos_files: - if allowed_file(photo.filename): - # Check file size - file_size = len(photo.read()) - photo.seek(0) # Reset stream position after reading size - if file_size > MAX_FILE_SIZE: - flash(f"Ошибка: Файл '{photo.filename}' слишком большой (макс 5MB).", 'warning') - logging.warning(f"File too large: {photo.filename} ({file_size} bytes)") - continue # Skip this file - - original_filename = secure_filename(photo.filename) - timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f") - unique_filename = f"{timestamp}_{original_filename}" - temp_path = os.path.join(UPLOADS_DIR, unique_filename) - - try: - photo.save(temp_path) - logging.info(f"Photo saved temporarily to {temp_path}") - # Schedule photo upload in a separate thread - photo_upload_thread = threading.Thread( - target=upload_photo_to_hf, - args=(temp_path, unique_filename, name) - # temp_path will be deleted inside upload_photo_to_hf - ) - photo_upload_thread.start() - newly_uploaded_filenames.append(unique_filename) - except Exception as e: - flash(f"Ошибка при сохранении фото '{original_filename}': {e}", 'error') - logging.error(f"Error saving photo {original_filename} locally: {e}") - # Clean up if temp file exists but HF upload wasn't started - if os.path.exists(temp_path): - try: os.remove(temp_path) - except OSError as rm_err: logging.error(f"Error removing temp file {temp_path} after save error: {rm_err}") - else: - flash(f"Ошибка: Недопустимый тип файла для '{photo.filename}'. Разрешены: {', '.join(allowed_extensions)}.", 'warning') - logging.warning(f"Invalid file type uploaded: {photo.filename}") - - # --- Update Data Structure --- - final_photos = current_photos - if action == 'edit' and delete_current_photos: - # TODO: Optionally delete old photos from HF here (requires careful implementation) - # Need to store old filenames before clearing - logging.info(f"Deleting current photos for product index {index}") - final_photos = [] - - # Add newly uploaded photos - final_photos.extend(newly_uploaded_filenames) - final_photos = list(dict.fromkeys(final_photos)) # Keep unique and order - - product_data = { - 'name': name, - 'price': price, - 'description': description, - 'category': category if category in categories else 'Без категории', - 'photos': final_photos, - 'colors': colors - } - - if action == 'add': - products.append(product_data) - flash(f"Товар '{name}' успешно добавлен.", 'success') - logging.info(f"Product added: {name}") - elif action == 'edit': - products[index].update(product_data) - flash(f"Товар '{name}' успешно обновлен.", 'success') - logging.info(f"Product updated: {name} (index {index})") - - save_data(data) - return redirect(url_for('admin')) - - elif action == 'delete': - index_str = request.form.get('index') - if index_str is not None: - try: - index = int(index_str) - if 0 <= index < len(products): - deleted_product = products.pop(index) - # TODO: Optionally delete associated photos from HF - # photos_to_delete = deleted_product.get('photos', []) - # Implement deletion logic here if needed, likely async - save_data(data) - flash(f"Товар '{deleted_product.get('name', 'N/A')}' удален.", 'success') - logging.info(f"Product deleted: {deleted_product.get('name', 'N/A')} (index {index})") - else: - flash("Ошибка: Неверный индекс товара для удаления.", 'error') - logging.warning(f"Invalid product index for delete: {index}") - except ValueError: - flash("Ошибка: Некорректный индекс товара.", 'error') - logging.warning(f"Non-integer product index for delete: {index_str}") - else: - flash("Ошибка: Индекс товара не указан для удаления.", 'error') - logging.warning("Product index missing for delete action.") - return redirect(url_for('admin')) - - except Exception as e: - logging.exception("Произошла ошибка при обработке POST запроса в /admin") - flash(f"Внутренняя ошибка сервера: {e}. См. логи.", 'error') - # Redirect to avoid resubmission on refresh - return redirect(url_for('admin')) - - - # --- GET Request --- - current_year = datetime.now().year - # Pass flash messages implicitly via get_flashed_messages in template - return render_template_string( - ADMIN_PAGE_HTML, - common_styles=COMMON_STYLES, - logo_url=LOGO_URL, - products=products, - categories=categories, - repo_id=REPO_ID, - current_year=current_year - ) - - -@app.route('/backup', methods=['POST']) -def backup(): - """Запускает принудительное резервное копирование data.json на HF.""" - status_js = "window.parent.updateStatus('Не удалось запустить резервное копирование.', 'backup-loading');" # Default error - if not HF_TOKEN_WRITE: - status_js = "window.parent.updateStatus('Ошибка: Токен для записи (HF_TOKEN) не установлен.', 'backup-loading');" - logging.warning("Backup requested but HF_TOKEN_WRITE is not set.") - else: - try: - # Use a thread to avoid blocking the response - upload_thread = threading.Thread(target=upload_db_to_hf) - upload_thread.start() - # Give immediate feedback, actual status comes from logs - status_js = "window.parent.updateStatus('Запущено резервное копирование на Hugging Face...', 'backup-loading');" - logging.info("Manual backup to HF triggered.") - except Exception as e: - status_js = f"window.parent.updateStatus('Ошибка запуска копирования: {str(e)}.', 'backup-loading');" - logging.error(f"Error starting manual backup thread: {e}") - - # Return JS to update the parent window's status indicator - # Use text/html content type for script tag execution in iframe/target - response = app.make_response(f"") - response.headers['Content-Type'] = 'text/html' - return response - - -@app.route('/download', methods=['POST']) -def download(): - """Запускает принудительное скачивание базы данных с HF.""" - status_js = "window.parent.updateStatus('Не удалось запустить скачивание.', 'download-loading');" - if not HF_TOKEN_READ: - status_js = "window.parent.updateStatus('Ошибка: Токен для чтения (HF_TOKEN_READ) не установлен.', 'download-loading');" - logging.warning("Download requested but HF_TOKEN_READ is not set.") - else: - try: - # Perform download synchronously as it affects the data used immediately after - if download_db_from_hf(): - status_js = "window.parent.updateStatus('База данных успешно скачана с HF. Перезагрузите страницу.', 'download-loading');" - logging.info("Manual download from HF successful.") - else: - status_js = "window.parent.updateStatus('Ошибка скачивания с HF (см. логи).', 'download-loading');" - logging.warning("Manual download from HF failed (check logs for details).") - except Exception as e: - status_js = f"window.parent.updateStatus('Ошибка скачивания: {str(e)}.', 'download-loading');" - logging.error(f"Error during manual download: {e}") - - response = app.make_response(f"") - response.headers['Content-Type'] = 'text/html' - return response - -# Optional: Route to serve uploaded images directly if needed, but HF link is better -# @app.route('/uploads/') -# def uploaded_file(filename): -# return send_from_directory(app.config['UPLOADS_DIR'], filename) - -# --- Запуск приложения --- - -if __name__ == '__main__': - print("Starting MebelHause KG Flask Application...") # Use print before logging is fully configured if needed - - # --- Token Checks --- - if not HF_TOKEN_WRITE: - logging.warning("(!) Переменная окружения HF_TOKEN (для записи) не установлена. Загрузка данных и фото на Hugging Face НЕ БУДЕТ РАБОТАТЬ.") - if not HF_TOKEN_READ: - logging.warning("(!) Переменная окружения HF_TOKEN_READ (для чтения) не установлена. Скачивание данных с Hugging Face может не работать (если отличается от HF_TOKEN).") - if not app.secret_key or app.secret_key == 'a_default_secret_key_change_me': - logging.warning("(!) Секретный ключ Flask (FLASK_SECRET_KEY) не установлен или используется значение по умолчанию. Установите его для безопасности!") - - # --- Initial Data Load --- - try: - logging.info("Загрузка начальных данных...") - load_data() # Attempt to load/download data on startup - logging.info("Начальная загрузка данных завершена.") - except Exception as e: - logging.exception("(!) КРИТИЧЕСКАЯ ОШИБКА: Не удалось инициализировать базу данных при запуске!") - # Depending on severity, you might want to exit here - # sys.exit(1) - - # --- Start Periodic Backup Thread --- - if HF_TOKEN_WRITE: - backup_thread = threading.Thread(target=periodic_backup, daemon=True) - backup_thread.start() - logging.info("Запущен поток для периодического резервного копирования на Hugging Face.") - else: - logging.info("Поток периодического резервного копирования не запущен (HF_TOKEN не установлен).") - - # --- Run Flask App --- - # Use environment variable for port, default to 7860 for HF Spaces compatibility - port = int(os.environ.get("PORT", 7860)) - host = '0.0.0.0' # Listen on all available network interfaces - - logging.info(f"Запуск Flask приложения на http://{host}:{port}") - - # Use Waitress for production deployment - try: - from waitress import serve - serve(app, host=host, port=port, threads=8) # Adjust threads as needed - except ImportError: - logging.warning("Waitress не найден. Запуск с использованием встроенного сервера Flask (НЕ РЕКОМЕНДУЕТСЯ для продакшена).") - # Fallback to Flask's built-in server (for development/testing only) - # DO NOT use debug=True in production - app.run(host=host, port=port, debug=False)