diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,107 +1,85 @@ -from flask import Flask, render_template_string, request, redirect, url_for, flash, send_file +from flask import Flask, render_template_string, request, jsonify import json import os import logging -import threading -import time +import uuid from datetime import datetime from huggingface_hub import HfApi, hf_hub_download -from huggingface_hub.utils import RepositoryNotFoundError -from werkzeug.utils import secure_filename -import uuid -import html -import random +from huggingface_hub.utils import HfHubHTTPError +# --- КОНФИГУРАЦИЯ --- app = Flask(__name__) -app.secret_key = os.getenv("FLASK_SECRET_KEY", "zzirix_secret_key_for_cart") -DATA_FILE = 'data_zzirix.json' +# Установите секретный ключ для безопасности, если планируете использовать сессии +app.secret_key = os.getenv("FLASK_SECRET_KEY", "zzirix_secret_key_for_keys") -REPO_ID = "Kgshop/clients" -HF_TOKEN_WRITE = os.getenv("HF_TOKEN") -HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE +# Имя файла для хранения ключей +DATA_FILE = 'keys.json' -LOGO_URL = "https://huggingface.co/spaces/Kgshop/Zzirixadm/resolve/main/Picsart_25-03-20_15-38-36-600.jpg" - -logging.basicConfig(level=logging.INFO) - -def initialize_data_structure(data): - if not isinstance(data, dict): - data = {'categories': [], 'products': [], 'orders': {}} - - data.setdefault('categories', []) - data.setdefault('products', []) - data.setdefault('orders', {}) +# ID репозитория на Hugging Face для хранения ключей +REPO_ID = "Kgshop/Keyspub" - if any(p.get('category') == 'Без категории' for p in data['products']) and 'Без категории' not in data['categories']: - data['categories'].append('Без категории') +# Токены доступа к Hugging Face (ВАЖНО: установите их как переменные окружения) +HF_TOKEN_WRITE = os.getenv("HF_TOKEN") +HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE - for product in data['products']: - if 'id' not in product: - product['id'] = str(uuid.uuid4()) - - product.setdefault('name', 'Без названия') - product.setdefault('description', '') - product.setdefault('category', 'Без категории') - product.setdefault('price', 0.0) - product.setdefault('colors', []) - product.setdefault('models', []) - product.setdefault('photos', []) - product.setdefault('in_stock', True) - product.setdefault('is_top', False) - - if not product['photos'] and 'media' in product and product['media']: - product['photos'] = [m['filename'] for m in product['media'] if m['type'] == 'photo'] - - if 'media' in product: - del product['media'] +# Настройка логирования +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - data['categories'] = sorted(list(set(data['categories'])), key=lambda x: (x != 'Без категории', x)) - - return data +# --- ФУНКЦИИ ДЛЯ РАБОТЫ С ДАННЫМИ И HUGGING FACE --- -def load_data(): +def load_keys(): + """Загружает список ключей с Hugging Face или из локального файла.""" try: - if not os.path.exists(DATA_FILE) or os.path.getsize(DATA_FILE) == 0: - logging.info(f"{DATA_FILE} не найден или пуст, попытка загрузки с HF.") - download_db_from_hf() - + # Сначала пытаемся скачать последнюю версию с HF + download_keys_from_hf() + if os.path.exists(DATA_FILE) and os.path.getsize(DATA_FILE) > 0: - with open(DATA_FILE, 'r', encoding='utf-8') as file: - data = json.load(file) - logging.info("Данные успешно загружены из JSON.") + with open(DATA_FILE, 'r', encoding='utf-8') as f: + data = json.load(f) + # Убеждаемся, что структура корректна + if isinstance(data, dict) and 'keys' in data and isinstance(data['keys'], list): + logging.info(f"Загружено {len(data['keys'])} ключей из файла.") + return data['keys'] + else: + logging.warning("Файл с ключами имеет неверный формат. Возвращается пустой список.") + return [] else: - data = {'products': [], 'categories': [], 'orders': {}} - logging.warning("Файл базы данных пуст или не существует после всех попыток, создается пустая структура.") + logging.info("Файл с ключами не найден или пуст. Возвращается пустой список.") + return [] - return initialize_data_structure(data) - except (json.JSONDecodeError, FileNotFoundError) as e: - logging.error(f"Ошибка при чтении {DATA_FILE}: {e}. Создается пустая структура.") - return initialize_data_structure({}) - except RepositoryNotFoundError as e: - logging.error(f"Репозиторий HF не найден: {e}. Создается локальная база данных.") - return initialize_data_structure({}) except Exception as e: - logging.error(f"Ошибка при загрузке данных: {e}") - return initialize_data_structure({}) + logging.error(f"Ошибка при загрузке ключей: {e}. Возвращается пустой список.") + return [] -def save_data(data): +def save_keys(keys_list): + """Сохраняет список ключей локально и выгружает на Hugging Face.""" try: + # Создаем словарь для сохранения в JSON + data_to_save = {'keys': sorted(list(set(keys_list)))} # Сортируем и удаляем дубликаты + temp_file = DATA_FILE + '.tmp' - with open(temp_file, 'w', encoding='utf-8') as file: - json.dump(data, file, ensure_ascii=False, indent=4) + with open(temp_file, 'w', encoding='utf-8') as f: + json.dump(data_to_save, f, ensure_ascii=False, indent=4) os.replace(temp_file, DATA_FILE) - upload_db_to_hf() - logging.info("Данные сохранены и выгружены в HF") + + logging.info(f"Сохранено {len(data_to_save['keys'])} ключей. Попытка выгрузки в HF.") + upload_keys_to_hf() + except Exception as e: - logging.error(f"Ошибка при сохранении данных: {e}") + logging.error(f"Ошибка при сохранении ключей: {e}") if os.path.exists(temp_file): os.remove(temp_file) raise -def upload_db_to_hf(): +def upload_keys_to_hf(): + """Выгружает файл с ключами в репозиторий Hugging Face.""" if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_WRITE не установлен. Пропуск выгрузки.") + logging.warning("HF_TOKEN_WRITE не установлен. Пропуск выгрузки ключей в HF.") + return + if not os.path.exists(DATA_FILE): + logging.warning(f"Файл {DATA_FILE} для выгрузки не найден.") return + try: api = HfApi() api.upload_file( @@ -110,19 +88,18 @@ def upload_db_to_hf(): repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"Резервная копия {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + commit_message=f"Обновление списка ключей {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) - logging.info("База данных выгружена в Hugging Face") + logging.info("Файл с ключами успешно выгружен на Hugging Face.") except Exception as e: - logging.error(f"Ошибка при выгрузке базы данных: {e}") + logging.error(f"Ошибка при выгрузке ключей на Hugging Face: {e}") -def download_db_from_hf(): +def download_keys_from_hf(): + """Загружает файл с ключами из репозитория Hugging Face.""" if not HF_TOKEN_READ: - logging.warning("HF_TOKEN_READ не установлен. Пропуск загрузки.") - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump(initialize_data_structure({}), f) + logging.warning("HF_TOKEN_READ не установлен. Пропуск загрузки ключей с HF.") return + try: hf_hub_download( repo_id=REPO_ID, @@ -133,2244 +110,217 @@ def download_db_from_hf(): local_dir_use_symlinks=False, force_download=True ) - logging.info("База данных загружена с Hugging Face") - except RepositoryNotFoundError: - logging.error(f"Репозиторий {REPO_ID} не найден. Пропускаем загрузку.") - if not os.path.exists(DATA_FILE): - logging.info("Создается пустой файл базы данных, так как загрузка не удалась и файл не существует.") - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump(initialize_data_structure({}), f) + logging.info("Файл с ключами успешно загружен с Hugging Face.") + except HfHubHTTPError as e: + # Если файл не найден (ошибка 404), это не критично, особенно при первом запуске + if e.response.status_code == 404: + logging.info(f"Файл {DATA_FILE} не найден в репозитории {REPO_ID}. Будет создан новый.") + else: + logging.error(f"HTTP ошибка при загрузке ключей с Hugging Face: {e}") except Exception as e: - logging.error(f"Ошибка при загрузке базы данных: {e}") - if not os.path.exists(DATA_FILE): - logging.info("Создается пустой файл базы данных, так как загрузка не удалась и файл не существует.") - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump(initialize_data_structure({}), f) - -def periodic_backup(): - while True: - time.sleep(3600) - logging.info("Запуск периодического резервного копирования.") - try: - data = load_data() - save_data(data) - except Exception as e: - logging.error(f"Ошибка во время периодического резервного копирования: {e}") + logging.error(f"Неизвестная ошибка при загрузке ключей с Hugging Face: {e}") -def allowed_file(filename): - return '.' in filename and \ - filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg', 'gif', 'webp', 'mp4', 'mov', 'webm'} +def generate_key(): + """Генерирует уникальный лицензионный ключ в формате KEY-XXXX-XXXX-XXXX.""" + parts = str(uuid.uuid4()).upper().split('-') + return f"KEY-{parts[1]}-{parts[2]}-{parts[3]}" -BASE_STYLE = ''' -:root { - --primary-color: #3B82F6; - --primary-dark-color: #2563eb; - --accent-color: #10b981; - --accent-dark-color: #059669; - --danger-color: #ef4444; - --danger-dark-color: #dc2626; - --background-light: linear-gradient(135deg, #f0f2f5, #e0e5ec); - --background-dark: linear-gradient(135deg, #1f2937, #374151); - --card-background-light: #ffffff; - --card-background-dark: #2d3748; - --text-color-light: #2d3748; - --text-color-dark: #e2e8f0; - --secondary-text-color-light: #718096; - --secondary-text-color-dark: #a0aec0; - --border-color-light: #e2e8f0; - --border-color-dark: #4a5568; - --shadow-light: 0 8px 25px rgba(0, 0, 0, 0.1); - --shadow-hover-light: 0 12px 35px rgba(0, 0, 0, 0.18); - --shadow-dark: 0 8px 25px rgba(0, 0, 0, 0.35); - --shadow-hover-dark: 0 12px 35px rgba(0, 0, 0, 0.5); - --border-radius-large: 22px; - --border-radius-medium: 12px; - --border-radius-small: 8px; -} -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} -body { - font-family: 'Roboto', sans-serif; - background: var(--background-light); - color: var(--text-color-light); - line-height: 1.6; - transition: background 0.3s, color 0.3s; - min-height: 100vh; - display: flex; - flex-direction: column; -} -body.dark-mode { - background: var(--background-dark); - color: var(--text-color-dark); -} -.container { - max-width: 1400px; - margin: 0 auto; - padding: 30px; - flex-grow: 1; -} -.header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px 30px; - border-bottom: none; - margin-bottom: 30px; - box-shadow: 0 2px 10px rgba(0,0,0,0.05); - border-radius: var(--border-radius-large); - background: var(--card-background-light); -} -body.dark-mode .header { - border-bottom-color: var(--border-color-dark); - box-shadow: 0 2px 10px rgba(0,0,0,0.2); - background: var(--card-background-dark); -} -.header-info { - display: flex; - align-items: center; -} -.header-logo { - width: 60px; - height: 60px; - border-radius: 50%; - object-fit: cover; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); - transition: transform 0.3s ease, box-shadow 0.3s ease; - margin-right: 15px; -} -.header-logo:hover { - transform: scale(1.08); - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); -} -.header h1 { - font-size: 2rem; - font-weight: 800; - color: var(--primary-color); -} -.theme-toggle { - background: none; - border: none; - font-size: 1.8rem; - cursor: pointer; - color: var(--secondary-text-color-light); - transition: color 0.3s ease, transform 0.2s ease; - padding: 5px; -} -body.dark-mode .theme-toggle { - color: var(--secondary-text-color-dark); -} -.theme-toggle:hover { - color: var(--accent-color); - transform: rotate(30deg); -} +# --- HTML И СТИЛИ ДЛЯ АДМИН-ПАНЕЛИ --- -.flash { - padding: 18px; - margin-bottom: 25px; - border-radius: var(--border-radius-medium); - font-weight: 500; - border: 1px solid transparent; - font-size: 1.1rem; -} -.flash.success { - background-color: var(--accent-color); - color: white; - border-color: var(--accent-dark-color); -} -.flash.error { - background-color: var(--danger-color); - color: white; - border-color: var(--danger-dark-color); -} - -.filters-container { - margin: 30px 0; - display: flex; - flex-wrap: wrap; - gap: 15px; - justify-content: center; -} -.search-container { - margin: 25px 0; - text-align: center; -} -#search-input { - width: 90%; - max-width: 600px; - padding: 14px 22px; - font-size: 1rem; - border: none; - border-radius: var(--border-radius-medium); - outline: none; - box-shadow: var(--shadow-light); - transition: all 0.3s ease; - background-color: var(--card-background-light); - color: var(--text-color-light); -} -body.dark-mode #search-input { - border-color: var(--border-color-dark); - background-color: var(--card-background-dark); - color: var(--text-color-dark); - box-shadow: var(--shadow-dark); -} -#search-input:focus { - border-color: var(--primary-color); - box-shadow: 0 0 10px rgba(59, 130, 246, 0.4); -} -.category-filter { - padding: 12px 25px; - border: none; - border-radius: var(--border-radius-medium); - background-color: var(--card-background-light); - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - font-size: 1rem; - font-weight: 500; - color: var(--text-color-light); - box-shadow: var(--shadow-light); -} -body.dark-mode .category-filter { - border-color: var(--border-color-dark); - background-color: var(--card-background-dark); - color: var(--text-color-dark); - box-shadow: var(--shadow-dark); -} -.category-filter.active, .category-filter:hover { - background-color: var(--primary-color); - color: white; - border-color: var(--primary-color); - box-shadow: var(--shadow-hover-light); -} -body.dark-mode .category-filter.active, body.dark-mode .category-filter:hover { - background-color: var(--primary-color); - border-color: var(--primary-color); - box-shadow: var(--shadow-hover-dark); -} - -.products-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 25px; - padding: 15px; -} -.product { - background: var(--card-background-light); - border-radius: var(--border-radius-large); - padding: 20px; - box-shadow: var(--shadow-light); - transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease; - overflow: hidden; - display: flex; - flex-direction: column; - justify-content: space-between; -} -body.dark-mode .product { - background: var(--card-background-dark); - box-shadow: var(--shadow-dark); -} -.product:hover { - transform: translateY(-10px) scale(1.03); - box-shadow: var(--shadow-hover-light); -} -body.dark-mode .product:hover { - box-shadow: var(--shadow-hover-dark); -} -.product-image { - width: 100%; - aspect-ratio: 1; - background-color: #f7f7f7; - border-radius: var(--border-radius-medium); - overflow: hidden; - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 15px; - cursor: pointer; -} -body.dark-mode .product-image { - background-color: #343e4d; -} -.product-image img, .product-image video { - max-width: 100%; - max-height: 100%; - object-fit: contain; - transition: transform 0.3s ease; - border-radius: var(--border-radius-medium); -} -.product-image img:hover, .product-image video:hover { - transform: scale(1.05); -} -.product h2 { - font-size: 1.2rem; - font-weight: 700; - margin: 8px 0; - text-align: center; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: var(--text-color-light); -} -body.dark-mode .product h2 { - color: var(--text-color-dark); -} -.product-price { - font-size: 1.3rem; - color: var(--danger-color); - font-weight: 700; - text-align: center; - margin: 8px 0 12px; -} -.product-description { - font-size: 0.9rem; - color: var(--secondary-text-color-light); - text-align: center; - margin-bottom: 20px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -body.dark-mode .product-description { - color: var(--secondary-text-color-dark); -} -.product-actions { - display: flex; - flex-direction: column; - gap: 10px; - margin-top: auto; -} -.product-button { - display: block; - width: 100%; - padding: 12px; - border: none; - border-radius: var(--border-radius-medium); - background-color: var(--primary-color); - color: white; - font-size: 1rem; - font-weight: 500; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - text-align: center; - text-decoration: none; - box-shadow: 0 3px 10px rgba(0,0,0,0.1); -} -.product-button:hover { - background-color: var(--primary-dark-color); - box-shadow: 0 6px 15px rgba(59, 130, 246, 0.4); - transform: translateY(-2px); -} -.add-to-cart { - background-color: var(--accent-color); - box-shadow: 0 3px 10px rgba(16, 185, 129, 0.2); -} -.add-to-cart:hover { - background-color: var(--accent-dark-color); - box-shadow: 0 6px 15px rgba(16, 185, 129, 0.4); -} -#cart-button { - position: fixed; - bottom: 30px; - right: 30px; - background-color: var(--danger-color); - color: white; - border: none; - border-radius: 50%; - width: 60px; - height: 60px; - font-size: 1.5rem; - cursor: pointer; - box-shadow: 0 8px 25px rgba(239, 68, 68, 0.3); - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - z-index: 1000; - display: flex; - align-items: center; - justify-content: center; -} -#cart-button:hover { - background-color: var(--danger-dark-color); - transform: translateY(-5px) scale(1.08); - box-shadow: 0 12px 35px rgba(239, 68, 68, 0.5); -} -.cart-count { - position: absolute; - top: -5px; - right: -5px; - background-color: var(--accent-color); - color: white; - border-radius: 50%; - padding: 4px 8px; - font-size: 0.8rem; - min-width: 22px; - text-align: center; - font-weight: 600; -} - -.modal { - display: none; - position: fixed; - z-index: 1001; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: rgba(0,0,0,0.6); - backdrop-filter: blur(8px); - overflow-y: auto; - padding: 20px; -} -.modal-content { - background: var(--card-background-light); - margin: 50px auto; - padding: 40px; - border-radius: var(--border-radius-large); - width: 95%; - max-width: 750px; - box-shadow: var(--shadow-hover-light); - animation: fadeInScale 0.3s ease-out; - max-height: 90vh; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - position: relative; -} -body.dark-mode .modal-content { - background: var(--card-background-dark); - color: var(--text-color-dark); - box-shadow: var(--shadow-hover-dark); -} -@keyframes fadeInScale { - from { opacity: 0; transform: translateY(-30px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} -.close { - position: absolute; - top: 20px; - right: 25px; - font-size: 2rem; - color: var(--secondary-text-color-light); - cursor: pointer; - transition: color 0.3s, transform 0.2s; -} -.close:hover { - color: var(--danger-color); - transform: rotate(90deg); -} -body.dark-mode .close { - color: var(--secondary-text-color-dark); -} -body.dark-mode .close:hover { - color: var(--danger-color); -} - -.modal h2 { - font-size: 1.8rem; - font-weight: 700; - margin-bottom: 25px; - text-align: center; -} -.cart-item { - display: flex; - align-items: center; - padding: 18px 0; - border-bottom: 1px solid var(--border-color-light); -} -body.dark-mode .cart-item { - border-bottom-color: var(--border-color-dark); -} -.cart-item:last-child { - border-bottom: none; -} -.cart-item img { - width: 70px; - height: 70px; - object-fit: contain; - border-radius: var(--border-radius-small); - margin-right: 20px; - background-color: #f7f7f7; -} -body.dark-mode .cart-item img { - background-color: #343e4d; -} -.cart-item-details { - flex-grow: 1; -} -.cart-item-details strong { - font-size: 1.2rem; - font-weight: 600; -} -.cart-item-details p { - font-size: 0.95rem; - color: var(--secondary-text-color-light); -} -body.dark-mode .cart-item-details p { - color: var(--secondary-text-color-dark); -} -.cart-item-total { - font-size: 1.1rem; - font-weight: 700; - color: var(--danger-color); -} -.quantity-input, .color-select, .model-select { - width: 100%; - padding: 12px; - border: 1px solid var(--border-color-light); - border-radius: var(--border-radius-medium); - font-size: 1rem; - margin: 10px 0; - background-color: var(--card-background-light); - color: var(--text-color-light); - box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); -} -body.dark-mode .quantity-input, body.dark-mode .color-select, body.dark-mode .model-select { - border-color: var(--border-color-dark); - background-color: var(--card-background-dark); - color: var(--text-color-dark); - box-shadow: inset 0 1px 3px rgba(0,0,0,0.2); -} -.modal-buttons { - margin-top: 30px; - display: flex; - justify-content: flex-end; - gap: 15px; -} -.modal-buttons .product-button { - width: auto; - padding: 12px 25px; -} -.clear-cart { - background-color: var(--danger-color); -} -.clear-cart:hover { - background-color: var(--danger-dark-color); - box-shadow: 0 6px 15px rgba(239, 68, 68, 0.4); -} -.order-button { - background-color: var(--accent-color); -} -.order-button:hover { - background-color: var(--accent-dark-color); - box-shadow: 0 6px 15px rgba(16, 185, 129, 0.4); -} - -.swiper-container { - max-width: 500px; - margin: 0 auto 30px; - border-radius: var(--border-radius-large); - overflow: hidden; - box-shadow: var(--shadow-light); -} -body.dark-mode .swiper-container { - box-shadow: var(--shadow-dark); -} -.swiper-slide { - background-color: #f7f7f7; - display: flex; - justify-content: center; - align-items: center; - min-height: 300px; -} -body.dark-mode .swiper-slide { - background-color: #343e4d; -} -.swiper-slide img, .swiper-slide video { - max-width: 100%; - max-height: 350px; - object-fit: contain; -} -.swiper-button-next, .swiper-button-prev { - color: var(--primary-color) !important; - background-color: rgba(255,255,255,0.9); - border-radius: 50%; - width: 45px; - height: 45px; - display: flex; - align-items: center; - justify-content: center; - transition: background-color 0.3s; - font-size: 1.8rem; -} -.swiper-button-next:hover, .swiper-button-prev:hover { - background-color: #fff; -} -body.dark-mode .swiper-button-next, body.dark-mode .swiper-button-prev { - background-color: rgba(45, 55, 72, 0.9); -} -body.dark-mode .swiper-button-next:hover, body.dark-mode .swiper-button-prev:hover { - background-color: #2d3748; -} -.swiper-pagination-bullet { - background-color: var(--primary-color) !important; -} - -.product-detail-modal-inner { - background: transparent; - padding: 0; - margin: 0; - box-shadow: none; - position: static; -} -body.dark-mode .product-detail-modal-inner { - background: transparent; - box-shadow: none; -} -.product-detail-modal-inner h2 { - font-size: 2.2rem; - font-weight: 700; - margin-bottom: 25px; - text-align: center; - color: var(--text-color-light); -} -body.dark-mode .product-detail-modal-inner h2 { - color: var(--text-color-dark); -} -.product-detail-modal-inner p { - margin-bottom: 10px; - font-size: 1rem; -} -.product-detail-modal-inner p strong { - color: var(--text-color-light); -} -body.dark-mode .product-detail-modal-inner p strong { - color: var(--text-color-dark); -} -.product-detail-modal-inner .price { - font-size: 1.8rem; - color: var(--danger-color); - font-weight: 700; - margin-bottom: 20px; - text-align: center; -} -.product-detail-modal-inner .description { - margin-bottom: 20px; - white-space: pre-wrap; -} -.product-detail-modal-actions { - display: flex; - justify-content: center; - gap: 15px; - margin-top: 30px; -} -.product-detail-modal-actions .product-button { - width: auto; - padding: 12px 25px; -} - -@media (max-width: 768px) { - .container { - padding: 15px; - } - .header h1 { - font-size: 1.6rem; - } - .category-filter { - font-size: 0.85rem; - padding: 8px 15px; - } - .products-grid { - grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); - gap: 18px; - } - .product h2 { - font-size: 1.05rem; - } - .product-price { - font-size: 1.15rem; - } - .product-description { - font-size: 0.8rem; - } - .product-button { - font-size: 0.9rem; - padding: 10px; - } - #cart-button { - width: 50px; - height: 50px; - font-size: 1.2rem; - bottom: 20px; - right: 20px; - } - .modal-content { - margin: 20px auto; - padding: 25px; - max-height: 95vh; - } - .close { - font-size: 1.6rem; - top: 15px; - right: 18px; - } - .modal h2 { - font-size: 1.6rem; - } - .cart-item img { - width: 55px; - height: 55px; - } - .product-detail-modal-inner h2 { - font-size: 2rem; - } - .product-detail-modal-inner .price { - font-size: 1.6rem; - } - .product-detail-modal-actions { - flex-direction: column; - gap: 10px; - } -} -''' - -@app.route('/') -def catalog(): - data = load_data() - products = data.get('products', []) - categories = data.get('categories', []) - - products.sort(key=lambda p: p['name'].lower()) - - return render_template_string(''' +ADMIN_TEMPLATE = '''
-{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}
-Товаров пока нет. Загляните позже!
- {% endfor %} -{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}
-В этой категории пока нет товаров.
- {% endfor %} +ID: {{ product.id }}
-Категория: {{ product.get('category', 'Без категории') }}
-Цена: {{ "%.2f"|format(product['price']) }} с
-Описание: {{ product['description'] }}
-Цвета: {{ product.get('colors', ['Нет цветов'])|join(', ') }}
-Модели: {{ product.get('models', ['Нет моделей'])|join(', ') }}
- {% if product.get('photos') and product['photos']|length > 0 %} -Перейдите на /admin для управления ключами или на /keys для получения списка в JSON.
' -@app.route('/download', methods=['GET']) -def download(): - try: - download_db_from_hf() - if os.path.exists(DATA_FILE): - return send_file(DATA_FILE, as_attachment=True, mimetype='application/json', download_name='data_zzirix_backup.json') - flash('Файл базы данных не найден после попытки скачивания.', 'error') - return redirect(url_for('admin')) - except Exception as e: - logging.error(f"Ошибка при скачивании базы данных: {e}") - flash(f'Ошибка при скачивании базы данных: {e}', 'error') - return redirect(url_for('admin')) if __name__ == '__main__': - uploads_dir = 'uploads' - os.makedirs(uploads_dir, exist_ok=True) - - logging.info("Начальная проверка и обновление структуры данных...") - try: - current_data = load_data() - save_data(current_data) - logging.info("Структура данных проверена и при необходимости обновлена.") - except Exception as e: - logging.error(f"Ошибка во время начальной проверки/обновления структуры данных: {e}") - - backup_thread = threading.Thread(target=periodic_backup, daemon=True) - backup_thread.start() - logging.info("Запуск Flask приложения...") + # При запуске приложения пытаемся загрузить актуальные ключи + logging.info("Запуск приложения и первоначальная синхронизация ключей...") + load_keys() + app.run(debug=True, host='0.0.0.0', port=7860) \ No newline at end of file