from flask import Flask, render_template_string, request, redirect, url_for, jsonify, session
import json
import os
import logging
import threading
import time
from datetime import datetime, timedelta, timezone
import pytz # Keep pytz if needed elsewhere, but timezone is now preferred for basic UTC handling
from huggingface_hub import HfApi, hf_hub_download
from huggingface_hub.utils import RepositoryNotFoundError
from werkzeug.utils import secure_filename
from werkzeug.security import generate_password_hash, check_password_hash
app = Flask(__name__)
# SECURITY: Ensure this key is strong and loaded from environment in production
app.secret_key = os.getenv("FLASK_SECRET_KEY", "a_much_stronger_default_secret_key_please_change_v3") # Changed secret key significantly
app.config['SESSION_COOKIE_SAMESITE'] = 'None'
app.config['SESSION_COOKIE_SECURE'] = True # Keep secure cookies
# Set session lifetime (e.g., 30 days)
app.permanent_session_lifetime = timedelta(days=30)
DATA_FILE = 'data_ultra_flowers.json' # Changed filename
USER_DATA_FILE = 'data_ultra_flowerusers.json' # Changed filename
REPO_ID = "Kgshop/Testbasebase" # <<< UPDATED REPOSITORY ID
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # Use the same token if read/write access is the same
# Placeholder for a new logo if available, otherwise keep the old one or remove
LOGO_URL = "https://huggingface.co/spaces/shopflo0/flowerbase/resolve/main/Gemini_Generated_Image_ebfuonebfuonebfu.jpg" # Placeholder logo
logging.basicConfig(level=logging.INFO) # Changed level to INFO for less verbosity in production
# --- Helper Function & Jinja Filter ---
def format_iso_datetime_filter(iso_str):
"""
Parses an ISO 8601 datetime string (handling 'Z' for UTC)
and returns a timezone-aware datetime object (UTC).
Returns None if parsing fails.
"""
if not iso_str: return None
try:
# Check if it's already a datetime object (e.g., from previous processing)
if isinstance(iso_str, datetime):
dt = iso_str
# Ensure it's timezone-aware (assume UTC if naive)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
else:
# Process string format
expiry_dt_str = str(iso_str) # Ensure it's a string
if 'Z' in expiry_dt_str:
expiry_dt_str = expiry_dt_str.replace('Z', '+00:00')
# Handle potential missing timezone info by assuming UTC
dt = datetime.fromisoformat(expiry_dt_str)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
except (ValueError, TypeError) as e:
logging.warning(f"Could not parse date string: {iso_str}. Error: {e}")
return None
# Register the function as a Jinja2 filter
app.template_filter('format_iso_datetime')(format_iso_datetime_filter)
# --- Data Loading/Saving Functions ---
def load_data():
try:
# Attempt to download only if tokens are set, otherwise use local
if HF_TOKEN_READ:
try:
download_db_from_hf()
except RepositoryNotFoundError:
logging.warning(f"Репозиторий HF {REPO_ID} не найден. Попытка использовать локальный файл {DATA_FILE}.")
except Exception as download_err:
logging.error(f"Ошибка загрузки {DATA_FILE} с HF: {download_err}. Попытка использовать локальный файл.")
if not os.path.exists(DATA_FILE):
logging.warning(f"Локальный файл базы данных '{DATA_FILE}' не найден. Создается пустая база данных.")
return {'products': [], 'categories': [], 'news': []}
with open(DATA_FILE, 'r', encoding='utf-8') as file:
data = json.load(file)
logging.info(f"Данные магазина цветов успешно загружены из '{DATA_FILE}'")
if not isinstance(data, dict):
logging.warning("Структура JSON некорректна, создается структура по умолчанию.")
return {'products': [], 'categories': [], 'news': []}
# Ensure essential keys exist
data.setdefault('products', [])
data.setdefault('categories', [])
data.setdefault('news', [])
# News expiry check
current_time_utc = datetime.now(timezone.utc)
updated_news = []
news_list = data.get('news', [])
if not isinstance(news_list, list): # Handle case where 'news' might not be a list
logging.warning("Ключ 'news' в JSON не является списком. Новости будут проигнорированы.")
news_list = []
for news_item in news_list:
if not isinstance(news_item, dict): # Skip non-dict items
logging.warning(f"Найден несловарный элемент в списке новостей: {news_item}")
continue
expiry_str = news_item.get('expiry')
if expiry_str:
expiry_datetime = format_iso_datetime_filter(expiry_str) # Use the filter function for parsing
if expiry_datetime:
# Compare timezone-aware datetimes
if expiry_datetime > current_time_utc:
updated_news.append(news_item)
else:
logging.info(f"Новость '{news_item.get('title', 'N/A')}' истекла и удалена.")
else:
# Keep news if date format is wrong, but log error
logging.error(f"Ошибка парсинга даты истечения для новости '{news_item.get('title', 'N/A')}'. Новость сохранена.")
updated_news.append(news_item)
else:
updated_news.append(news_item) # Keep news without expiry
data['news'] = updated_news
return data
except FileNotFoundError:
# This case is now handled by the check after download attempt
logging.warning(f"Локальный файл базы данных '{DATA_FILE}' не найден и не удалось скачать. Создается пустая база данных.")
return {'products': [], 'categories': [], 'news': []}
except json.JSONDecodeError:
logging.error(f"Ошибка: Невозможно декодировать JSON-файл '{DATA_FILE}'. Создается пустая база данных.")
return {'products': [], 'categories': [], 'news': []}
except Exception as e:
logging.error(f"Неожиданная ошибка при загрузке данных магазина цветов: {e}", exc_info=True)
return {'products': [], 'categories': [], 'news': []}
def save_data(data):
try:
data_to_save = data.copy()
# Ensure datetime objects are converted to ISO format strings before saving
if 'news' in data_to_save and isinstance(data_to_save['news'], list):
for news_item in data_to_save['news']:
if not isinstance(news_item, dict): continue # Skip non-dict items
# Ensure timestamp and expiry are strings
if 'timestamp' in news_item and isinstance(news_item['timestamp'], datetime):
# Ensure UTC before formatting
ts_dt = news_item['timestamp']
if ts_dt.tzinfo is None:
ts_dt = ts_dt.replace(tzinfo=timezone.utc)
else:
ts_dt = ts_dt.astimezone(timezone.utc)
news_item['timestamp'] = ts_dt.isoformat()
if 'expiry' in news_item and isinstance(news_item['expiry'], datetime):
exp_dt = news_item['expiry']
if exp_dt.tzinfo is None:
exp_dt = exp_dt.replace(tzinfo=timezone.utc)
else:
exp_dt = exp_dt.astimezone(timezone.utc)
news_item['expiry'] = exp_dt.isoformat()
with open(DATA_FILE, 'w', encoding='utf-8') as file:
json.dump(data_to_save, file, ensure_ascii=False, indent=4)
logging.info(f"Данные магазина цветов успешно сохранены в '{DATA_FILE}'")
# Attempt to upload only if write token is set
if HF_TOKEN_WRITE:
upload_db_to_hf()
except Exception as e:
logging.error(f"Ошибка при сохранении данных магазина цветов: {e}", exc_info=True)
# Decide if you want to raise the exception or just log it
# raise
def upload_db_to_hf():
if not HF_TOKEN_WRITE:
logging.info("HF_TOKEN_WRITE не установлен, загрузка на Hugging Face пропущена.")
return
if not os.path.exists(DATA_FILE):
logging.warning(f"Файл {DATA_FILE} не найден для загрузки на HF.")
return
try:
api = HfApi()
api.upload_file(
path_or_fileobj=DATA_FILE,
path_in_repo=DATA_FILE,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Автоматическое резервное копирование базы данных цветов {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
logging.info(f"Резервная копия JSON-базы данных цветов загружена в {REPO_ID}.")
except Exception as e:
logging.error(f"Ошибка при загрузке резервной копии базы данных цветов {DATA_FILE}: {e}")
def download_db_from_hf():
if not HF_TOKEN_READ:
logging.info("HF_TOKEN_READ не установлен, загрузка с Hugging Face пропущена.")
return # Don't raise error, just skip download attempt
try:
logging.info(f"Попытка загрузки {DATA_FILE} из {REPO_ID}...")
hf_hub_download(
repo_id=REPO_ID,
filename=DATA_FILE,
repo_type="dataset",
token=HF_TOKEN_READ,
local_dir=".",
force_filename=DATA_FILE, # Ensure it overwrites with the correct name
local_dir_use_symlinks=False # Recommended for compatibility
)
logging.info(f"JSON-база данных цветов '{DATA_FILE}' успешно загружена из {REPO_ID}.")
except RepositoryNotFoundError as e:
logging.warning(f"Репозиторий {REPO_ID} не найден на Hugging Face: {e}.")
raise # Re-raise to be caught in load_data
except Exception as e:
# Log other potential download errors (network issues, permissions, file not found on remote)
logging.error(f"Ошибка при загрузке JSON-базы данных цветов '{DATA_FILE}' с Hugging Face: {e}")
# Check if it's specifically a file not found error (often HTTP 404)
if "404" in str(e):
logging.warning(f"Файл {DATA_FILE} не найден в репозитории {REPO_ID}.")
raise # Re-raise to be caught in load_data
# --- User Data Functions (Similar adjustments) ---
def load_user_data():
try:
if HF_TOKEN_READ:
try:
download_user_db_from_hf()
except RepositoryNotFoundError:
logging.warning(f"Репозиторий HF {REPO_ID} для пользователей не найден. Попытка использовать локальный файл {USER_DATA_FILE}.")
except Exception as download_err:
logging.error(f"Ошибка загрузки {USER_DATA_FILE} с HF: {download_err}. Попытка использовать локальный файл.")
if not os.path.exists(USER_DATA_FILE):
logging.warning(f"Локальный файл базы данных пользователей '{USER_DATA_FILE}' не найден. Создается пустая база данных.")
return {'users': []}
with open(USER_DATA_FILE, 'r', encoding='utf-8') as file:
user_data = json.load(file)
logging.info(f"Данные пользователей успешно загружены из '{USER_DATA_FILE}'")
if not isinstance(user_data, dict):
logging.warning("Структура JSON пользователей некорректна, создается структура по умолчанию.")
return {'users': []}
user_data.setdefault('users', [])
# Add basic validation for users list if needed
if not isinstance(user_data['users'], list):
logging.warning("Ключ 'users' в JSON пользователей не является списком. Создается пустой список.")
user_data['users'] = []
return user_data
except FileNotFoundError:
# Handled by os.path.exists check now
logging.warning(f"Локальный файл базы данных пользователей '{USER_DATA_FILE}' не найден и не удалось скачать. Создается пустая база данных.")
return {'users': []}
except json.JSONDecodeError:
logging.error(f"Ошибка: Невозможно декодировать JSON-файл пользователей '{USER_DATA_FILE}'.")
return {'users': []}
except Exception as e:
logging.error(f"Неожиданная ошибка при загрузке данных пользователей: {e}", exc_info=True)
return {'users': []}
def save_user_data(user_data):
try:
# Ensure datetime objects in order history are strings (in UTC ISO format)
for user in user_data.get('users', []):
if 'order_history' in user and isinstance(user['order_history'], list):
for order in user['order_history']:
if not isinstance(order, dict): continue # Skip non-dict items
if 'timestamp' in order and isinstance(order['timestamp'], datetime):
ts_dt = order['timestamp']
if ts_dt.tzinfo is None:
ts_dt = ts_dt.replace(tzinfo=timezone.utc)
else:
ts_dt = ts_dt.astimezone(timezone.utc)
order['timestamp'] = ts_dt.isoformat()
with open(USER_DATA_FILE, 'w', encoding='utf-8') as file:
json.dump(user_data, file, ensure_ascii=False, indent=4)
logging.info(f"Данные пользователей успешно сохранены в '{USER_DATA_FILE}'")
if HF_TOKEN_WRITE:
upload_user_db_to_hf()
except Exception as e:
logging.error(f"Ошибка при сохранении данных пользователей: {e}", exc_info=True)
# raise
def upload_user_db_to_hf():
if not HF_TOKEN_WRITE:
logging.info("HF_TOKEN_WRITE не установлен, загрузка пользовательской базы на Hugging Face пропущена.")
return
if not os.path.exists(USER_DATA_FILE):
logging.warning(f"Файл {USER_DATA_FILE} не найден для загрузки на HF.")
return
try:
api = HfApi()
api.upload_file(
path_or_fileobj=USER_DATA_FILE,
path_in_repo=USER_DATA_FILE,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Автоматическое резервное копирование базы данных пользователей {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
logging.info(f"Резервная копия JSON-базы данных пользователей загружена в {REPO_ID}.")
except Exception as e:
logging.error(f"Ошибка при загрузке резервной копии базы данных пользователей {USER_DATA_FILE}: {e}")
def download_user_db_from_hf():
if not HF_TOKEN_READ:
logging.info("HF_TOKEN_READ не установлен, загрузка пользовательской базы с Hugging Face пропущена.")
return # Don't raise error
try:
logging.info(f"Попытка загрузки {USER_DATA_FILE} из {REPO_ID}...")
hf_hub_download(
repo_id=REPO_ID,
filename=USER_DATA_FILE,
repo_type="dataset",
token=HF_TOKEN_READ,
local_dir=".",
force_filename=USER_DATA_FILE, # Ensure it overwrites with the correct name
local_dir_use_symlinks=False
)
logging.info(f"JSON-база данных пользователей '{USER_DATA_FILE}' успешно загружена из {REPO_ID}.")
except RepositoryNotFoundError as e:
logging.warning(f"Репозиторий {REPO_ID} (для пользователей) не найден на Hugging Face: {e}.")
raise # Re-raise
except Exception as e:
logging.error(f"Ошибка при загрузке JSON-базы данных пользователей '{USER_DATA_FILE}' с Hugging Face: {e}")
if "404" in str(e):
logging.warning(f"Файл {USER_DATA_FILE} не найден в репозитории {REPO_ID}.")
raise # Re-raise
# --- Background Backup Thread ---
# Global lock for saving data to prevent race conditions between request threads and backup thread
data_lock = threading.Lock()
user_data_lock = threading.Lock()
def periodic_backup():
while True:
time.sleep(900) # Backup every 15 minutes
logging.info("Запуск периодического резервного копирования...")
try:
# No need to reload data here if save functions acquire lock
# Just trigger the save which handles upload internally
with data_lock:
current_data = load_data() # Load latest state inside lock just before saving
save_data(current_data) # save_data now includes upload if token exists
with user_data_lock:
current_user_data = load_user_data() # Load latest state inside lock
save_user_data(current_user_data) # save_user_data includes upload if token exists
logging.info("Периодическое резервное копирование завершено.")
except Exception as e:
logging.error(f"Ошибка во время периодического резервного копирования: {e}", exc_info=True)
# --- Helper Functions ---
def get_category_counts(products):
counts = {}
if not isinstance(products, list): # Add type check
return counts
for product in products:
if isinstance(product, dict): # Check if product is a dict
category = product.get('category', 'Без категории')
counts[category] = counts.get(category, 0) + 1
return counts
# --- User Management Functions ---
# Wrap data saving in locks to ensure thread safety
def register_user(login, password, phone, address):
with user_data_lock:
user_data_dict = load_user_data() # Load latest data inside lock
users = user_data_dict.get('users', [])
if any(user.get('login') == login for user in users if isinstance(user, dict)):
return False, "Логин уже занят."
if len(password) < 6: # Basic password length check
return False, "Пароль должен быть не менее 6 символов."
hashed_password = generate_password_hash(password)
new_user = {
'login': login,
'password': hashed_password,
'phone': phone,
'address': address,
'points': 0,
'order_history': []
}
# Ensure users is a list before appending
if not isinstance(users, list):
users = []
users.append(new_user)
user_data_dict['users'] = users
save_user_data(user_data_dict) # Save data inside lock
return True, "Регистрация прошла успешно."
def authenticate_user(login, password):
# No lock needed for read-only operations usually, but load_user_data handles potential downloads
user_data_dict = load_user_data()
users = user_data_dict.get('users', [])
if not isinstance(users, list): return None # Safety check
for user in users:
if isinstance(user, dict) and user.get('login') == login:
if check_password_hash(user.get('password', ''), password):
return user # Return the user dictionary
return None
def get_user_profile(login):
# Read-only, no lock needed unless concerned about reads during backup write
user_data_dict = load_user_data()
users = user_data_dict.get('users', [])
if not isinstance(users, list): return None # Safety check
for user in users:
if isinstance(user, dict) and user.get('login') == login:
return user
return None
def update_user_profile(login, phone, address):
user_found = False
with user_data_lock:
user_data_dict = load_user_data()
users = user_data_dict.get('users', [])
if not isinstance(users, list):
save_user_data(user_data_dict) # Save even if structure was bad
return False, "Ошибка структуры данных пользователей."
for user in users:
if isinstance(user, dict) and user.get('login') == login:
user['phone'] = phone
user['address'] = address
user_found = True
break
if user_found:
user_data_dict['users'] = users # Ensure the modified list is assigned back
save_user_data(user_data_dict) # Save updated data
return True, "Профиль обновлен."
else:
# Save data even if user not found, in case load fixed something
save_user_data(user_data_dict)
return False, "Пользователь не найден."
def add_points_to_user(login, points):
user_found = False
with user_data_lock:
user_data_dict = load_user_data()
users = user_data_dict.get('users', [])
if not isinstance(users, list):
save_user_data(user_data_dict)
return False # Indicate failure
for user in users:
if isinstance(user, dict) and user.get('login') == login:
user['points'] = user.get('points', 0) + points
user_found = True
break
if user_found:
user_data_dict['users'] = users
save_user_data(user_data_dict)
return True
else:
save_user_data(user_data_dict)
return False # User not found
def redeem_points_from_user(login, points_to_redeem):
user_found = False
success = False
message = "Пользователь не найден."
with user_data_lock:
user_data_dict = load_user_data()
users = user_data_dict.get('users', [])
if not isinstance(users, list):
save_user_data(user_data_dict)
return False, "Ошибка структуры данных пользователей."
for user in users:
if isinstance(user, dict) and user.get('login') == login:
user_found = True
current_points = user.get('points', 0)
if current_points >= points_to_redeem:
user['points'] = current_points - points_to_redeem
success = True
message = "Баллы успешно списаны"
else:
message = "Недостаточно баллов для списания."
break
if user_found and success:
user_data_dict['users'] = users
save_user_data(user_data_dict)
return True, message
else:
# Save even on failure if load fixed something or if user was found but points insufficient
save_user_data(user_data_dict)
return False, message
def save_order_to_history(login, order_details):
user_found = False
# Use UTC for storage consistency
timestamp_utc = datetime.now(timezone.utc)
with user_data_lock:
user_data_dict = load_user_data()
users = user_data_dict.get('users', [])
if not isinstance(users, list):
save_user_data(user_data_dict)
return False, "Ошибка структуры данных пользователей."
for user in users:
if isinstance(user, dict) and user.get('login') == login:
if 'order_history' not in user or not isinstance(user['order_history'], list):
user['order_history'] = [] # Ensure it exists and is a list
# Add timestamp as ISO string (UTC)
order_details_copy = order_details.copy() # Avoid modifying original dict if passed by reference
order_details_copy['timestamp'] = timestamp_utc.isoformat()
user['order_history'].append(order_details_copy)
user_found = True
break
if user_found:
user_data_dict['users'] = users
save_user_data(user_data_dict)
return True, "Заказ сохранен в истории."
else:
save_user_data(user_data_dict)
return False, "Пользователь не найден."
def get_order_history(login):
# Read-only, lock might be overkill but load_user_data handles potential downloads
user_data_dict = load_user_data()
users = user_data_dict.get('users', [])
if not isinstance(users, list): return []
user = next((user for user in users if isinstance(user, dict) and user.get('login') == login), None)
if user:
# Return history, newest first
# Sort using the datetime filter function for robustness
history = user.get('order_history', [])
if not isinstance(history, list): return [] # Safety check
# Default datetime for sorting if timestamp is missing or invalid
min_utc_dt = datetime.min.replace(tzinfo=timezone.utc)
sorted_history = sorted(
[item for item in history if isinstance(item, dict)], # Filter out non-dict items
key=lambda x: format_iso_datetime_filter(x.get('timestamp')) or min_utc_dt,
reverse=True
)
return sorted_history
return []
# --- Flask Routes ---
@app.route('/')
def menu():
# Use lock for loading main data as it might involve download/initial creation
with data_lock:
data = load_data()
products = data.get('products', [])
categories = data.get('categories', [])
news_list = data.get('news', [])
# User data loading is handled within called functions (get_user_profile)
logged_in = 'user_login' in session
user_login = session.get('user_login')
user_profile = get_user_profile(user_login) if logged_in else None
user_points = user_profile.get('points', 0) if user_profile else 0
category_counts = get_category_counts(products)
# Sort news by timestamp (creation time) descending, most recent first
min_utc_dt = datetime.min.replace(tzinfo=timezone.utc)
news_for_template = sorted(
[item for item in news_list if isinstance(item, dict)], # Filter non-dict news
key=lambda item: format_iso_datetime_filter(item.get('timestamp')) or min_utc_dt,
reverse=True
)
# HTML Template (incorporating fixes for header and logo animation)
menu_html = '''
Ultra Flowers - Магазин Цветов
Ultra Flowers
Создаем красоту для каждого момента.
{% for category in categories %}
{# Use .get for safety #}
{% endfor %}
{% for product in products %}
{% if product is mapping %} {# Check if product is a dictionary-like object #}
{% set photos = product.get('photos', []) %}
{% if photos and photos is iterable and photos|length > 0 %}
{# Added onerror handler #}
{% else %}
{% endif %}
Войдите или зарегистрируйтесь, чтобы управлять заказами и копить баллы.
{% endif %}
×
Регистрация
×
Вход
×
Редактировать профиль
×
Новости и Акции
{% if news_for_template %}
{% for news_item in news_for_template %}
{% if news_item is mapping %} {# Check if news_item is a dictionary #}
{% set photo = news_item.get('photo') %}
{% if photo %}
{# Hide image on error #}
{% endif %}
{{ news_item.get('title', 'Без заголовка') }}
{{ news_item.get('text', '') | safe }}
{# Allow basic HTML in news text #}
{# --- UPDATED DATE FORMATTING using the registered filter --- #}
{% set timestamp_dt = news_item.timestamp | format_iso_datetime %}
{% if timestamp_dt %}
Опубликовано: {{ timestamp_dt.strftime('%d.%m.%Y %H:%M') }} UTC
{% endif %}
{% set expiry = news_item.get('expiry') %}
{% if expiry %}
{% set expiry_dt = expiry | format_iso_datetime %}
{% if expiry_dt %}
Актуально до: {{ expiry_dt.strftime('%d.%m.%Y %H:%M') }} UTC
{% else %}
{% endif %}
{% endif %}
{# --- END UPDATED DATE FORMATTING --- #}
{% endif %} {# End check if news_item is mapping #}
{% endfor %}
{% else %}
Пока нет новостей.
{% endif %}
×
Программа лояльности
{% if logged_in %}
Вы получаете 5% кэшбэка баллами с каждого оплаченного заказа.
Ваш текущий баланс: {{ user_points }} баллов.
Баллами можно оплатить часть следующих заказов (до полной суммы минус 1 ₸).
{% else %}
Авторизуйтесь, чтобы копить баллы и узнавать о специальных предложениях!
Зарегистрированные пользователи получают 5% кэшбэка баллами с каждого заказа.
{% endif %}
{# Updated jQuery version #}
{# #} {# Popper not strictly needed for current setup #}
'''
current_year = datetime.now().year
# Pass necessary variables to the template
return render_template_string(
menu_html,
products=products,
categories=categories,
category_counts=category_counts,
repo_id=REPO_ID,
LOGO_URL=LOGO_URL,
logged_in=logged_in,
user_login=user_login,
user_points=user_points,
user_profile=user_profile,
news_for_template=news_for_template,
current_year=current_year
)
@app.route('/product/')
def product_detail(index):
# This route *could* load product details specifically for the modal via AJAX,
# but the current implementation loads all product data initially and uses JS.
# Keeping this route might be useful for direct linking or future enhancements.
# For now, let's make it return the data needed by the JS `loadProductDetails` if called directly.
with data_lock: # Use lock for reading main data
data = load_data()
products = data.get('products', [])
if 0 <= index < len(products) and isinstance(products[index], dict):
product = products[index]
# Return JSON data for the specific product
return jsonify(product)
else:
return jsonify({'error': 'Товар не найден или данные некорректны'}), 404
@app.route('/admin', methods=['GET', 'POST'])
def admin():
# Basic Auth - Consider a more robust system for production
auth = request.authorization
# Load credentials securely from environment variables
ADMIN_USERNAME = os.getenv("ADMIN_USER", "admin") # Default only for dev
ADMIN_PASSWORD = os.getenv("ADMIN_PASS", "secret") # Default only for dev
# Check if credentials are provided and valid
if not auth or not (auth.username == ADMIN_USERNAME and auth.password == ADMIN_PASSWORD):
# Log failed attempt, potentially rate-limit
logging.warning(f"Failed admin login attempt for user: {auth.username if auth else 'None'} from IP: {request.remote_addr}")
# Return 401 Unauthorized with WWW-Authenticate header to prompt login
return ('Доступ запрещен. Требуется авторизация.', 401, {'WWW-Authenticate': 'Basic realm="Admin Login Required"'})
# --- If authenticated, proceed ---
# Use locks for loading/saving data
with data_lock:
data = load_data()
products = data.get('products', [])
categories = data.get('categories', [])
news_list = data.get('news', [])
# Ensure data structures are lists
if not isinstance(products, list): products = []
if not isinstance(categories, list): categories = []
if not isinstance(news_list, list): news_list = []
if request.method == 'POST':
action = request.form.get('action')
try:
# --- Category Management ---
if action == 'add_category':
category_name = request.form.get('category_name', '').strip()
if category_name and category_name not in categories:
with data_lock:
data = load_data() # Reload fresh data inside lock
categories = data.get('categories', [])
if not isinstance(categories, list): categories = [] # Ensure list
if category_name not in categories: # Double check
categories.append(category_name)
data['categories'] = categories
save_data(data) # Save inside lock
else:
logging.warning(f"Категория '{category_name}' уже существует (обнаружена при добавлении).")
elif not category_name:
logging.warning("Попытка добавить пустую категорию.")
else:
logging.warning(f"Категория '{category_name}' уже существует.")
return redirect(url_for('admin'))
elif action == 'delete_category':
category_index_str = request.form.get('category_index')
if category_index_str is not None:
category_index = int(category_index_str)
with data_lock:
data = load_data()
categories = data.get('categories', [])
products = data.get('products', [])
if not isinstance(categories, list): categories = []
if not isinstance(products, list): products = []
if 0 <= category_index < len(categories):
category_to_delete = categories.pop(category_index)
# Update products using this category
updated_products = []
for product in products:
if isinstance(product, dict) and product.get('category') == category_to_delete:
product['category'] = 'Без категории'
updated_products.append(product)
data['categories'] = categories
data['products'] = updated_products
save_data(data)
logging.info(f"Категория '{category_to_delete}' удалена.")
else:
logging.warning(f"Попытка удаления категории с неверным индексом: {category_index}")
else:
logging.warning("Индекс категории не предоставлен для удаления.")
return redirect(url_for('admin'))
# --- Product Management ---
elif action == 'add':
name = request.form.get('name', '').strip()
price_str = request.form.get('price', '0').replace(',', '.').strip()
price = float(price_str) if price_str else 0.0
description = request.form.get('description', '').strip()
category = request.form.get('category', 'Без категории')
photos_files = request.files.getlist('photos')
option_names = request.form.getlist('option_names')
option_prices = request.form.getlist('option_prices')
photos_list = []
options_list = []
# Basic validation
if not name or price < 0:
logging.error("Ошибка добавления: Название и неотрицательная цена обязательны.")
# Add flash message for user feedback here if desired
return redirect(url_for('admin'))
# Upload photos to HF
if HF_TOKEN_WRITE:
api = HfApi()
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
for photo in photos_files[:10]: # Limit photos
if photo and photo.filename:
# Sanitize filename and make unique
base, ext = os.path.splitext(photo.filename)
safe_base = secure_filename(base)
photo_filename = f"{safe_base}_{int(time.time())}{ext}"
temp_path = os.path.join(uploads_dir, photo_filename)
try:
photo.save(temp_path)
api.upload_file(
path_or_fileobj=temp_path,
path_in_repo=f"photos/{photo_filename}",
repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
commit_message=f"Добавлено фото для товара {name}"
)
photos_list.append(photo_filename)
logging.info(f"Загружено фото {photo_filename} на HF.")
except Exception as upload_err:
logging.error(f"Ошибка загрузки фото {photo_filename} на HF: {upload_err}")
finally:
if os.path.exists(temp_path):
try:
os.remove(temp_path)
except OSError as rm_err:
logging.error(f"Не удалось удалить временный файл {temp_path}: {rm_err}")
# Clean up temp dir
try:
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
os.rmdir(uploads_dir)
except OSError as e:
logging.error(f"Ошибка удаления временной папки {uploads_dir}: {e}")
elif photos_files and any(p.filename for p in photos_files):
logging.warning("HF_TOKEN_WRITE не установлен, загрузка фото пропущена.")
# Process options
for opt_name, opt_price_str in zip(option_names, option_prices):
opt_name = opt_name.strip()
opt_price_str = opt_price_str.replace(',', '.').strip()
if opt_name and opt_price_str is not None: # Ensure price string exists
try:
options_list.append({
'name': opt_name,
'price': float(opt_price_str)
})
except ValueError:
logging.warning(f"Неверный формат цены для опции '{opt_name}': '{opt_price_str}'. Опция пропущена.")
new_product = {
'name': name, 'price': price, 'description': description,
'category': category if category in categories else 'Без категории',
'photos': photos_list, 'options': options_list
}
with data_lock:
data = load_data()
products = data.get('products', [])
if not isinstance(products, list): products = []
products.append(new_product)
data['products'] = products
save_data(data)
logging.info(f"Добавлен товар: {name}")
return redirect(url_for('admin'))
elif action == 'edit':
product_index_str = request.form.get('product_index')
if product_index_str is not None:
product_index = int(product_index_str)
with data_lock:
data = load_data()
products = data.get('products', [])
categories = data.get('categories', []) # Load categories too for validation
if not isinstance(products, list): products = []
if not isinstance(categories, list): categories = []
if 0 <= product_index < len(products) and isinstance(products[product_index], dict):
product = products[product_index] # Get the product dict
# Update fields similar to 'add' action
new_name = request.form.get('name', product.get('name', '')).strip()
price_str = request.form.get('price', str(product.get('price', 0))).replace(',', '.')
new_price = float(price_str) if price_str else product.get('price', 0)
new_description = request.form.get('description', product.get('description', '')).strip()
new_category = request.form.get('category', product.get('category', 'Без категории'))
# Basic validation
if not new_name or new_price < 0:
logging.error("Ошибка редактирования: Название и неотрицательная цена обязательны.")
# Add flash message
return redirect(url_for('admin'))
product['name'] = new_name
product['price'] = new_price
product['description'] = new_description
product['category'] = new_category if new_category in categories else 'Без категории'
# Handle photos: keep existing + add new
existing_photos_to_keep = request.form.getlist('existing_photos')
new_photos_files = request.files.getlist('photos')
# Filter out any empty strings or invalid values from existing_photos_to_keep
current_photos = [p for p in existing_photos_to_keep if p and isinstance(p, str)]
# Upload new photos
if HF_TOKEN_WRITE and new_photos_files:
api = HfApi()
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
photo_limit = 10
for photo in new_photos_files:
if len(current_photos) >= photo_limit:
logging.warning(f"Достигнут лимит фото ({photo_limit}) для товара {product['name']}. Остальные фото пропущены.")
break
if photo and photo.filename:
base, ext = os.path.splitext(photo.filename)
safe_base = secure_filename(base)
photo_filename = f"{safe_base}_{int(time.time())}{ext}"
temp_path = os.path.join(uploads_dir, photo_filename)
try:
photo.save(temp_path)
api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}",
repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
commit_message=f"Обновлено фото для товара {product['name']}")
current_photos.append(photo_filename)
logging.info(f"Загружено новое фото {photo_filename} при редактировании.")
except Exception as upload_err:
logging.error(f"Ошибка загрузки фото {photo_filename} на HF при редактировании: {upload_err}")
finally:
if os.path.exists(temp_path):
try: os.remove(temp_path)
except OSError as rm_err: logging.error(f"Не удалось удалить {temp_path}: {rm_err}")
# Clean up temp dir
try:
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
os.rmdir(uploads_dir)
except OSError as e:
logging.error(f"Ошибка удаления временной папки {uploads_dir}: {e}")
elif new_photos_files and any(p.filename for p in new_photos_files):
logging.warning("HF_TOKEN_WRITE не установлен, загрузка новых фото при редактировании пропущена.")
product['photos'] = current_photos
# Update options (replace all existing with new list from form)
option_names = request.form.getlist('option_names')
option_prices = request.form.getlist('option_prices')
options_list = []
for opt_name, opt_price_str in zip(option_names, option_prices):
opt_name = opt_name.strip()
opt_price_str = opt_price_str.replace(',', '.').strip()
if opt_name and opt_price_str is not None:
try:
options_list.append({'name': opt_name, 'price': float(opt_price_str)})
except ValueError:
logging.warning(f"Неверный формат цены для опции '{opt_name}': '{opt_price_str}' при редактировании. Опция пропущена.")
product['options'] = options_list
# products[product_index] = product # Update the list (already modified by reference)
data['products'] = products # Assign back just in case
save_data(data) # Save changes
logging.info(f"Товар '{product['name']}' (индекс {product_index}) обновлен.")
else:
logging.warning(f"Попытка редактирования товара с неверным индексом или неверными данными: {product_index}")
else:
logging.warning("Индекс товара не предоставлен для редактирования.")
return redirect(url_for('admin'))
elif action == 'delete':
product_index_str = request.form.get('product_index')
if product_index_str is not None:
product_index = int(product_index_str)
deleted_product_name = "N/A"
photos_to_delete = []
with data_lock:
data = load_data()
products = data.get('products', [])
if not isinstance(products, list): products = []
if 0 <= product_index < len(products):
# Optionally: Delete associated photos from HF? Risky.
# For now, just remove the product entry.
deleted_product = products.pop(product_index)
deleted_product_name = deleted_product.get('name', 'N/A')
photos_to_delete = deleted_product.get('photos', []) # Get photos for potential later deletion
data['products'] = products
save_data(data)
logging.info(f"Удален товар: {deleted_product_name} (индекс {product_index})")
# Add logic here later to delete photos_to_delete from HF if desired
else:
logging.warning(f"Попытка удаления товара с неверным индексом: {product_index}")
else:
logging.warning("Индекс товара не предоставлен для удаления.")
return redirect(url_for('admin'))
# --- News Management ---
elif action == 'add_news':
news_title = request.form.get('news_title', '').strip()
news_text = request.form.get('news_text', '').strip() # Consider sanitizing HTML
news_photo_file = request.files.get('news_photo')
# Get expiry values safely, default to 0
expiry_days = int(request.form.get('expiry_days') or 0)
expiry_hours = int(request.form.get('expiry_hours') or 0)
expiry_minutes = int(request.form.get('expiry_minutes') or 0)
if not news_title or not news_text:
logging.error("Ошибка добавления новости: Заголовок и текст обязательны.")
# Add flash message
return redirect(url_for('admin'))
news_photo_filename = None
if HF_TOKEN_WRITE and news_photo_file and news_photo_file.filename:
base, ext = os.path.splitext(news_photo_file.filename)
safe_base = secure_filename(base)
news_photo_filename = f"news_{safe_base}_{int(time.time())}{ext}"
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
temp_path = os.path.join(uploads_dir, news_photo_filename)
try:
news_photo_file.save(temp_path)
api = HfApi()
api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{news_photo_filename}",
repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
commit_message=f"Добавлено фото для новости {news_title}")
logging.info(f"Загружено фото новости {news_photo_filename} на HF.")
except Exception as upload_err:
logging.error(f"Ошибка загрузки фото новости {news_photo_filename} на HF: {upload_err}")
news_photo_filename = None # Reset filename if upload failed
finally:
if os.path.exists(temp_path):
try: os.remove(temp_path)
except OSError as rm_err: logging.error(f"Не удалось удалить {temp_path}: {rm_err}")
try:
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
os.rmdir(uploads_dir)
except OSError as e:
logging.error(f"Ошибка удаления временной папки {uploads_dir}: {e}")
elif news_photo_file and news_photo_file.filename:
logging.warning("HF_TOKEN_WRITE не установлен, загрузка фото новости пропущена.")
expiry_time = None
total_delta = timedelta(days=expiry_days, hours=expiry_hours, minutes=expiry_minutes)
if total_delta > timedelta(0):
# Store expiry in UTC
expiry_time = datetime.now(timezone.utc) + total_delta
new_news_item = {
'title': news_title,
'text': news_text, # Be careful with HTML injection if not sanitized
'photo': news_photo_filename,
# Store as ISO string (UTC) or None
'expiry': expiry_time.isoformat() if expiry_time else None,
'timestamp': datetime.now(timezone.utc).isoformat() # Add creation timestamp (UTC ISO)
}
with data_lock:
data = load_data()
news_list = data.get('news', [])
if not isinstance(news_list, list): news_list = []
news_list.append(new_news_item)
data['news'] = news_list
save_data(data)
logging.info(f"Добавлена новость: {news_title}")
return redirect(url_for('admin'))
elif action == 'delete_news':
news_index_str = request.form.get('news_index') # This index refers to the sorted list in the admin view
if news_index_str is not None:
news_index_view = int(news_index_str)
with data_lock:
data = load_data()
all_news_raw = data.get('news', [])
if not isinstance(all_news_raw, list): all_news_raw = []
# Re-sort raw news like in admin view to find the correct item
min_utc_dt = datetime.min.replace(tzinfo=timezone.utc)
sorted_news_for_admin = sorted(
[item for item in all_news_raw if isinstance(item, dict)],
key=lambda item: format_iso_datetime_filter(item.get('timestamp')) or min_utc_dt,
reverse=True
)
if 0 <= news_index_view < len(sorted_news_for_admin):
item_to_delete = sorted_news_for_admin[news_index_view]
# Find the item in the original raw list to remove it
# This assumes timestamps or titles are unique enough, might need a unique ID later
original_index_to_delete = -1
for i, item in enumerate(all_news_raw):
if item == item_to_delete: # Compare dictionaries
original_index_to_delete = i
break
if original_index_to_delete != -1:
deleted_news = all_news_raw.pop(original_index_to_delete)
logging.info(f"Удалена новость: {deleted_news.get('title', 'N/A')}")
# Consider deleting photo from HF here if needed
data['news'] = all_news_raw # Update the main data dictionary
save_data(data)
else:
logging.error(f"Не удалось найти новость для удаления в исходном списке (индекс вида {news_index_view}).")
else:
logging.warning(f"Попытка удаления новости с неверным индексом вида: {news_index_view}")
else:
logging.warning("Индекс новости не предоставлен для удаления.")
return redirect(url_for('admin'))
except ValueError as ve:
logging.error(f"Ошибка преобразования значения в админке (action={action}): {ve}")
# Optionally: add flash message to show user
except Exception as e:
logging.error(f"Непредвиденная ошибка в админке (action={action}): {e}", exc_info=True)
# Optionally: add flash message
# Redirect even on error for simplicity, consider flash messages
return redirect(url_for('admin'))
# --- Admin Panel HTML ---
# Sort news list for display in admin panel (use the same sorting as main page)
min_utc_dt = datetime.min.replace(tzinfo=timezone.utc)
admin_news_list = sorted(
[item for item in news_list if isinstance(item, dict)], # Filter non-dicts
key=lambda item: format_iso_datetime_filter(item.get('timestamp')) or min_utc_dt,
reverse=True
)
# Sort products alphabetically for admin view
admin_products_list = sorted(
[p for p in products if isinstance(p, dict)], # Filter non-dicts
key=lambda p: p.get('name', '').lower()
)
admin_html = '''
Админ-панель - Ultra Flowers
Админ-панель Ultra Flowers
ℹ️ Если кнопки "Редактировать", "Удалить" или "+ Добавить опцию" не работают, проверьте консоль разработчика в браузере (обычно клавиша F12) на наличие ошибок JavaScript.
Управление категориями
Существующие категории:
{% for category in categories %}
{{ category }}
{% else %}
Нет категорий.
{% endfor %}
Добавить Товар (Букет/Композицию)
Список Товаров ({{ admin_products_list|length }})
{% for product in admin_products_list %} {# Using sorted list #}
{% if product is mapping %} {# Check if product is dict #}
{% set photos = product.get('photos', []) %}
{% if photos and photos is iterable and photos|length > 0 %}
{% else %}
Нет фото
{% endif %}
{{ product.get('name', 'Без названия') }} ({{ product.get('category', 'Без категории') }}) - {{ product.get('price', 0) }} ₸{{ product.get('description', '')[:100] }}{% if product.get('description', '')|length > 100 %}...{% endif %}
{% set options = product.get('options', []) %}
{% if options and options is iterable and options|length > 0 %}
Опции: {{ options|map(attribute='name')|join(', ') }}
{% endif %}
{# FIX 1: Ensure onclick calls openEditModal with the correct ORIGINAL index #}
{# Find original index #}
{% endif %} {# End check if product is mapping #}
{% else %}
{% for news_item in admin_news_list %} {# Using sorted list for display #}
{% if news_item is mapping %} {# Check if item is dict #}
{% set photo = news_item.get('photo') %}
{% if photo %}
{% else %}
{% endif %}
{{ news_item.get('title', 'Без заголовка') }}{{ news_item.get('text', '')[:150] | safe }}{% if news_item.get('text', '')|length > 150 %}...{% endif %}
{# --- UPDATED DATE FORMATTING using the registered filter --- #}
{% set ts_dt = news_item.timestamp | format_iso_datetime %}
Добавлено: {{ ts_dt.strftime('%d.%m.%Y %H:%M') if ts_dt else 'N/A' }} UTC
{% set expiry = news_item.get('expiry') %}
{% if expiry %}
{% set expiry_dt = expiry | format_iso_datetime %}
{% if expiry_dt %}
Актуально до: {{ expiry_dt.strftime('%d.%m.%Y %H:%M') }} UTC
{% endif %}
{% endif %}
{# --- END UPDATED DATE FORMATTING --- #}
{% endif %} {# End check if item is mapping #}
{% else %}
Нет добавленных новостей.
{% endfor %}
Редактировать Товар
{# Ensure form targets the correct endpoint #}
'''
return render_template_string(
admin_html,
products=products, # Pass original list for index finding
admin_products_list=admin_products_list, # Pass sorted list for display
categories=categories,
repo_id=REPO_ID,
admin_news_list=admin_news_list # Pass sorted news list
)
# --- API Routes (Login, Register, Profile, Points, History) ---
@app.route('/register', methods=['POST'])
def register():
# Simplified data extraction
login = request.form.get('registerLogin')
password = request.form.get('registerPassword')
phone = request.form.get('registerPhone')
address = request.form.get('registerAddress')
if not all([login, password, phone, address]):
return jsonify({'status': 'error', 'message': 'Все поля обязательны для заполнения.'}), 400
# Input validation (basic)
if len(password) < 6:
return jsonify({'status': 'error', 'message': 'Пароль должен быть не менее 6 символов.'}), 400
if len(login) < 3:
return jsonify({'status': 'error', 'message': 'Логин должен быть не менее 3 символов.'}), 400
success, message = register_user(login, password, phone, address)
status_code = 201 if success else 409 # Use 409 Conflict if user exists
return jsonify({'status': 'success' if success else 'error', 'message': message}), status_code
@app.route('/login', methods=['POST'])
def login():
# Simplified data extraction
login_username = request.form.get('loginUsername')
login_password = request.form.get('loginPassword')
if not login_username or not login_password:
return jsonify({'status': 'error', 'message': 'Логин и пароль обязательны.'}), 400
user = authenticate_user(login_username, login_password)
if user:
session.permanent = True # Make session persistent (uses app.permanent_session_lifetime)
session['user_login'] = user['login'] # Store login in session AFTER setting permanent
logging.info(f"Пользователь '{user['login']}' успешно вошел в систему. Session ID: {session.sid if hasattr(session, 'sid') else 'N/A'}")
return jsonify({'status': 'success', 'message': 'Вход выполнен успешно.'})
else:
logging.warning(f"Неудачная попытка входа для пользователя '{login_username}'.")
return jsonify({'status': 'error', 'message': 'Неверный логин или пароль.'}), 401
@app.route('/logout')
def logout():
user = session.pop('user_login', None)
session.clear() # Clear entire session for good measure
if user:
logging.info(f"Пользователь '{user}' вышел из системы.")
# Redirect to main page after logout
return redirect(url_for('menu'))
@app.route('/update_profile', methods=['POST'])
def update_profile():
if 'user_login' not in session:
return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401
login = session['user_login']
# Simplified data extraction
phone = request.form.get('editPhone')
address = request.form.get('editAddress')
if not phone or not address:
return jsonify({'status': 'error', 'message': 'Телефон и адрес обязательны.'}), 400
success, message = update_user_profile(login, phone, address)
if success:
logging.info(f"Профиль пользователя {login} обновлен.")
return jsonify({'status': 'success', 'message': message}), 200
else:
# Log failure reason if possible (e.g., user not found, save error)
logging.warning(f"Не удалось обновить профиль для {login}: {message}")
# Determine appropriate status code (404 if not found, 500 if save failed)
status_code = 404 if "не найден" in message else 500
return jsonify({'status': 'error', 'message': message}), status_code
@app.route('/add_points', methods=['POST'])
def add_points():
if 'user_login' not in session:
return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401
login = session['user_login']
try:
# Assume form data from finalizeOrderWhatsApp AJAX call
points_str = request.form.get('points')
if points_str is None:
return jsonify({'status': 'error', 'message': 'Параметр points отсутствует.'}), 400
points = int(points_str)
if points < 0: # Allow 0 points (e.g., if order total was 0 after redemption)
return jsonify({'status': 'error', 'message': 'Нельзя начислить отрицательные баллы.'}), 400
except ValueError:
return jsonify({'status': 'error', 'message': 'Неверное количество баллов.'}), 400
success = add_points_to_user(login, points)
if success:
logging.info(f"Начислено {points} баллов пользователю {login}")
return jsonify({'status': 'success', 'message': f'{points} баллов начислено.'})
else:
# This case should ideally not happen if user is logged in, means data inconsistency
logging.error(f"Ошибка начисления баллов: пользователь {login} не найден при попытке добавления.")
return jsonify({'status': 'error', 'message': 'Ошибка начисления баллов: пользователь не найден.'}), 500
@app.route('/redeem_points', methods=['POST'])
def redeem_points():
if 'user_login' not in session:
return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401
login = session['user_login']
try:
# Assume form data from orderViaWhatsApp AJAX call
points_str = request.form.get('points')
if points_str is None:
return jsonify({'status': 'error', 'message': 'Параметр points отсутствует.'}), 400
points = int(points_str)
if points <= 0: # Must redeem a positive amount
return jsonify({'status': 'error', 'message': 'Количество списываемых баллов должно быть положительным.'}), 400
except ValueError:
return jsonify({'status': 'error', 'message': 'Неверное количество баллов.'}), 400
success, message = redeem_points_from_user(login, points)
status_code = 200 if success else 400 # 400 for insufficient points or user error
if success:
logging.info(f"Списано {points} баллов у пользователя {login}")
else:
logging.warning(f"Ошибка списания {points} баллов у пользователя {login}: {message}")
return jsonify({'status': 'success' if success else 'error', 'message': message}), status_code
@app.route('/save_order_history', methods=['POST'])
def save_order_history_route(): # Renamed route function slightly
if 'user_login' not in session:
return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401
login = session['user_login']
if not request.is_json:
logging.warning(f"Получен не-JSON запрос на /save_order_history от {login}")
return jsonify({'status': 'error', 'message': 'Запрос должен быть в формате JSON.'}), 400
order_details = request.get_json()
# Validate basic structure
if not isinstance(order_details, dict) or not isinstance(order_details.get('items'), list) or not isinstance(order_details.get('total_amount'), (int, float)):
logging.warning(f"Получены неполные или некорректные данные заказа для сохранения в историю от {login}: {order_details}")
return jsonify({'status': 'error', 'message': 'Неполные или некорректные данные заказа.'}), 400
success, message = save_order_to_history(login, order_details)
status_code = 201 if success else 500 # 500 if save failed unexpectedly
if success:
logging.info(f"Заказ сохранен в историю для пользователя {login}")
else:
logging.error(f"Ошибка сохранения заказа в историю для пользователя {login}: {message}")
return jsonify({'status': 'success' if success else 'error', 'message': message}), status_code
# --- App Initialization ---
if __name__ == '__main__':
# Start background backup thread only if HF tokens are set for writing
if HF_TOKEN_WRITE:
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
backup_thread.start()
logging.info("Поток периодического резервного копирования запущен.")
else:
logging.info("HF_TOKEN_WRITE не установлен, резервное копирование на HF отключено.")
# Use Waitress or Gunicorn for production instead of Flask's built-in server
# For development:
port = int(os.environ.get("PORT", 7860)) # Use environment variable for port
# Set debug=False for production/semi-production, host='0.0.0.0' for external access
app.run(debug=False, host='0.0.0.0', port=port)