+ {{ product['name'] }}
+ {% if product.get('in_stock', True) %}
+ В наличии
+ {% else %}
+ Нет в наличии
+ {% endif %}
+ {% if product.get('is_top', False) %}
+ Топ
+ {% endif %}
+
+
+ '''
+ return logout_response_html
+
+@app.route('/create_order', methods=['POST'])
+def create_order():
+ if 'user' not in session:
+ return jsonify({"error": "Пожалуйста, войдите в систему для создания заказа."}), 401
+
+ order_data = request.get_json()
+
+ if not order_data or 'cart' not in order_data or not isinstance(order_data['cart'], list) or not order_data['cart']:
+ logging.warning("Create order request missing cart data or cart is empty/invalid.")
+ return jsonify({"error": "Корзина пуста или не передана в верном формате."}), 400
+
+ cart_items = order_data['cart']
+ total_price = 0
+ processed_cart = []
+ data_cache = get_data() # Get current product data once
+ products_cache = {p['name']: p for p in data_cache.get('products', [])} # Cache products by name for price lookup
+
+
+ for item in cart_items:
+ # Validate item structure and data types more robustly
+ if not isinstance(item, dict) or not all(k in item for k in ('id', 'name', 'quantity', 'color')):
+ logging.error(f"Invalid cart item structure received: {item}")
+ return jsonify({"error": "Неверный формат товара в корзине."}), 400
+
+ try:
+ quantity = int(item['quantity'])
+ product_name = item['name']
+
+ # Verify price against server-side data cache to prevent tampering
+ if product_name not in products_cache:
+ logging.error(f"Product '{product_name}' from cart not found in server data.")
+ return jsonify({"error": f"Товар '{product_name}' не найден."}), 400
+
+ price = float(products_cache[product_name]['price']) # Use server price
+ photo = products_cache[product_name].get('photos', [None])[0] # Get first photo from server data
+
+
+ if price < 0 or quantity <= 0:
+ raise ValueError("Invalid price or quantity")
+
+ total_price += price * quantity
+ processed_cart.append({
+ "name": product_name,
+ "price": price, # Use validated server price
+ "quantity": quantity,
+ "color": item.get('color', 'N/A'),
+ "photo": photo, # Use photo from server data
+ "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{photo}" if photo else "https://via.placeholder.com/60x60.png?text=N/A"
+ })
+ except (ValueError, TypeError, KeyError) as e:
+ logging.error(f"Invalid data in cart item: {item}. Error: {e}")
+ return jsonify({"error": "Неверные данные (цена, количество или товар) в корзине."}), 400
+
+ order_id = f"{datetime.now().strftime('%y%m%d%H%M%S')}-{uuid.uuid4().hex[:4]}" # Shorter ID
+ order_timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+
+ user_info_for_order = session.get('user_info', {}) # Get from session
+
+ # Create a clean copy for the order, removing potentially empty values
+ user_info_for_order_copy = {
+ k: v for k, v in user_info_for_order.items() if v # Include only non-empty values
+ }
+ # Ensure login is always included if available
+ if 'login' not in user_info_for_order_copy and session.get('user'):
+ user_info_for_order_copy['login'] = session['user']
+
+
+ new_order = {
+ "id": order_id,
+ "created_at": order_timestamp,
+ "cart": processed_cart,
+ "total_price": round(total_price, 2),
+ "user_info": user_info_for_order_copy,
+ "status": "new" # Default status
+ }
+
+ current_data = get_data() # Get current data
+ if 'orders' not in current_data or not isinstance(current_data.get('orders'), dict):
+ current_data['orders'] = {}
+
+ current_data['orders'][order_id] = new_order
+
+ if save_data(current_data): # Save updated data
+ logging.info(f"Order {order_id} created successfully. User: {session.get('user', 'Unknown')}")
+ return jsonify({"order_id": order_id}), 201
+ else:
+ logging.error(f"Failed to save order {order_id} to file/cache.")
+ return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500
+
+
+@app.route('/order/')
+def view_order(order_id):
+ if not order_id:
+ return "Не указан ID заказа", 400
+
+ data = get_data()
+ order = data.get('orders', {}).get(order_id)
+
+ if order:
+ # Optional: Check if the current user is allowed to view this order
+ # current_user_login = session.get('user')
+ # order_user_login = order.get('user_info', {}).get('login')
+ # if current_user_login != order_user_login:
+ # logging.warning(f"User {current_user_login} attempted to view order {order_id} belonging to {order_user_login}")
+ # order = None # Hide order if not owned (or implement admin view later)
+
+ if order:
+ logging.info(f"Displaying order {order_id}. User: {session.get('user', 'Anonymous')}")
+ else:
+ logging.warning(f"Order {order_id} access denied or not found after check. User: {session.get('user', 'Anonymous')}")
+
+ else:
+ logging.warning(f"Order {order_id} not found in data. User: {session.get('user', 'Anonymous')}")
+
+
+ # Pass request object for url_root usage in template
+ return render_template_string(ORDER_TEMPLATE,
+ order=order, # Will be None if not found or access denied
+ repo_id=REPO_ID,
+ currency_code=CURRENCY_CODE,
+ request=request # Pass request context
+ )
+
+@app.route('/admin', methods=['GET', 'POST'])
+def admin():
+ # Basic auth check - replace with a proper role check if needed
+ # if 'user' not in session: # or session.get('user_role') != 'admin':
+ # flash("Доступ запрещен.", 'error')
+ # return redirect(url_for('login'))
+
+ # Load current state from helpers
+ current_data = get_data()
+ current_users = get_users()
+
+ if request.method == 'POST':
+ action = request.form.get('action')
+ logging.info(f"Admin action received: {action}")
+
+ # We operate on copies obtained from get_data/get_users
+ # and then call save_data/save_users if modifications are successful.
+ data_copy = current_data # Already a deepcopy from get_data()
+ users_copy = current_users # Already a deepcopy from get_users()
+ products = data_copy.get('products', [])
+ categories = data_copy.get('categories', [])
+ # users_copy remains the dictionary
+
+ save_needed_data = False
+ save_needed_users = False
+
+ try:
+ 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)
+ categories.sort()
+ data_copy['categories'] = categories
+ save_needed_data = True
+ logging.info(f"Category '{category_name}' staged for adding.")
+ flash(f"Категория '{category_name}' будет добавлена после сохранения.", 'success') # Use future tense
+ elif not category_name:
+ logging.warning("Attempted to add empty category.")
+ flash("Название категории не может быть пустым.", 'error')
+ else:
+ logging.warning(f"Category '{category_name}' already exists.")
+ flash(f"Категория '{category_name}' уже существует.", 'warning')
+
+ elif action == 'delete_category':
+ category_to_delete = request.form.get('category_name')
+ if category_to_delete and category_to_delete in categories:
+ categories.remove(category_to_delete)
+ updated_count = 0
+ for product in products:
+ if product.get('category') == category_to_delete:
+ product['category'] = 'Без категории'
+ updated_count += 1
+ data_copy['categories'] = categories
+ # products list is modified in-place within data_copy
+ save_needed_data = True
+ logging.info(f"Category '{category_to_delete}' staged for deletion. Products to update: {updated_count}.")
+ flash(f"Категория '{category_to_delete}' будет удалена, {updated_count} товаров обновлено после сохранения.", 'success')
+ else:
+ logging.warning(f"Attempted to delete non-existent or empty category: {category_to_delete}")
+ flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
+
+ elif action == 'add_product':
+ name = request.form.get('name', '').strip()
+ price_str = request.form.get('price', '').replace(',', '.')
+ description = request.form.get('description', '').strip()
+ category = request.form.get('category')
+ photos_files = request.files.getlist('photos')
+ colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
+ in_stock = 'in_stock' in request.form
+ is_top = 'is_top' in request.form
+
+ if not name or not price_str:
+ flash("Название и цена товара обязательны.", 'error')
+ # Return early without saving
+ return redirect(url_for('admin'))
+
+ try:
+ price = round(float(price_str), 2)
+ if price < 0: price = 0.0
+ except ValueError:
+ flash("Неверный формат цены.", 'error')
+ return redirect(url_for('admin'))
+
+ photos_list = []
+ if photos_files and any(f.filename for f in photos_files):
+ if not HF_TOKEN_WRITE:
+ flash("HF_TOKEN (write) не настроен. Фотографии не будут загружены на сервер.", "warning")
+ else:
+ uploads_dir = 'uploads_temp'
+ os.makedirs(uploads_dir, exist_ok=True)
+ api = HfApi()
+ photo_limit = 10
+ uploaded_count = 0
+ temp_paths_to_clean = []
+ try:
+ for photo in photos_files:
+ if uploaded_count >= photo_limit:
+ logging.warning(f"Photo limit ({photo_limit}) reached, ignoring remaining photos.")
+ flash(f"Будет загружено только первые {photo_limit} фото.", "warning")
+ break
+ if photo and photo.filename:
+ ext = os.path.splitext(photo.filename)[1].lower()
+ if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
+ logging.warning(f"Skipping non-image file upload: {photo.filename}")
+ flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
+ continue
+
+ # Create unique filename
+ safe_name = secure_filename(name.replace(' ', '_'))[:50].rstrip('_') or "product"
+ photo_filename = f"{safe_name}_{uuid.uuid4().hex[:8]}{ext}"
+ temp_path = os.path.join(uploads_dir, photo_filename)
+ photo.save(temp_path)
+ temp_paths_to_clean.append(temp_path) # Track for cleanup
+ logging.info(f"Uploading photo {photo_filename} to HF for product {name}...")
+ # Perform upload immediately for photos as they aren't part of JSON save
+ 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"Add photo for product {name}"
+ )
+ photos_list.append(photo_filename) # Add to list only on successful upload
+ logging.info(f"Photo {photo_filename} uploaded successfully.")
+ uploaded_count += 1
+ else:
+ logging.info(f"Skipping empty file in photos list for {name}.")
+ except Exception as e:
+ logging.error(f"Error uploading photo to HF during add product: {e}", exc_info=True)
+ flash(f"Ошибка при загрузке фото на сервер. Товар будет добавлен без новых фото.", 'error')
+ photos_list = [] # Clear list if upload failed mid-way
+ finally:
+ # Clean up temporary files
+ for path in temp_paths_to_clean:
+ if os.path.exists(path):
+ try: os.remove(path)
+ except OSError as e: logging.warning(f"Could not remove temp photo {path}: {e}")
+ if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
+ try: os.rmdir(uploads_dir)
+ except OSError as e: logging.warning(f"Could not remove temp upload dir {uploads_dir}: {e}")
+
+ new_product = {
+ 'name': name, 'price': price, 'description': description,
+ 'category': category if category in categories else 'Без категории',
+ 'photos': photos_list, # List of successfully uploaded photos
+ 'colors': colors,
+ 'in_stock': in_stock, 'is_top': is_top
+ }
+ products.append(new_product)
+ # products list is modified in-place within data_copy
+ save_needed_data = True
+ logging.info(f"Product '{name}' staged for adding.")
+ flash(f"Товар '{name}' будет добавлен после сохранения.", 'success')
+
+
+ elif action == 'edit_product':
+ index_str = request.form.get('index')
+ if index_str is None:
+ flash("Ошибка редактирования: индекс товара не передан.", 'error')
+ return redirect(url_for('admin'))
+
+ try:
+ index = int(index_str)
+ # We need original products list to get the correct item by index
+ original_products_sorted = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
+
+ if not (0 <= index < len(original_products_sorted)):
+ raise IndexError("Product index out of range during edit")
+
+ # Find the actual product in our working copy (which might not be sorted yet)
+ # This assumes names are unique identifiers for finding the right dict
+ product_to_find_name = original_products_sorted[index]['name']
+ product_to_edit = next((p for p in products if p['name'] == product_to_find_name), None)
+
+ if product_to_edit is None:
+ raise ValueError(f"Could not find product '{product_to_find_name}' in working data copy.")
+
+ original_name = product_to_edit.get('name', 'N/A')
+ logging.info(f"Editing product '{original_name}' (found at edit time)")
+
+ except (ValueError, IndexError) as e:
+ logging.error(f"Error finding product for edit at index '{index_str}': {e}", exc_info=True)
+ flash(f"Ошибка редактирования: не удалось найти товар (индекс: {index_str}).", 'error')
+ return redirect(url_for('admin')) # Redirect back without saving
+
+
+ product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
+ price_str = request.form.get('price', str(product_to_edit.get('price', 0))).replace(',', '.')
+ product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
+ category = request.form.get('category')
+ product_to_edit['category'] = category if category in categories else 'Без категории'
+ product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
+ product_to_edit['in_stock'] = 'in_stock' in request.form
+ product_to_edit['is_top'] = 'is_top' in request.form
+
+ try:
+ price = round(float(price_str), 2)
+ if price < 0: price = 0.0
+ product_to_edit['price'] = price
+ except ValueError:
+ logging.warning(f"Invalid price format '{price_str}' during edit of product {original_name}. Price not changed.")
+ flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
+ # Continue saving other changes
+
+ photos_files = request.files.getlist('photos')
+ if photos_files and any(f.filename for f in photos_files):
+ if not HF_TOKEN_WRITE:
+ flash("HF_TOKEN (write) не настроен. Фотографии не будут обновлены на сервере.", "warning")
+ else:
+ uploads_dir = 'uploads_temp'
+ os.makedirs(uploads_dir, exist_ok=True)
+ api = HfApi()
+ new_photos_list = []
+ photo_limit = 10
+ uploaded_count = 0
+ temp_paths_to_clean = []
+ logging.info(f"Uploading NEW photos for product {product_to_edit['name']}...")
+ try:
+ for photo in photos_files:
+ if uploaded_count >= photo_limit:
+ flash(f"Будет загружено только первые {photo_limit} фото.", "warning")
+ break
+ if photo and photo.filename:
+ ext = os.path.splitext(photo.filename)[1].lower()
+ if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
+ flash(f"Файл {photo.filename} пропущен (не изображение).", "warning")
+ continue
+
+ safe_name = secure_filename(product_to_edit['name'].replace(' ', '_'))[:50].rstrip('_') or "product"
+ photo_filename = f"{safe_name}_{uuid.uuid4().hex[:8]}{ext}"
+ temp_path = os.path.join(uploads_dir, photo_filename)
+ photo.save(temp_path)
+ temp_paths_to_clean.append(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"Update photo for product {product_to_edit['name']}")
+ new_photos_list.append(photo_filename)
+ logging.info(f"New photo {photo_filename} uploaded successfully.")
+ uploaded_count += 1
+ else:
+ logging.info(f"Skipping empty file in photos list during edit for {product_to_edit['name']}.")
+
+ # If new photos were successfully uploaded, replace the list and delete old ones
+ if new_photos_list:
+ old_photos = product_to_edit.get('photos', [])
+ product_to_edit['photos'] = new_photos_list # Update product data
+ flash("Новые фотографии загружены.", "success")
+ # Now, try to delete old photos from HF
+ if old_photos:
+ logging.info(f"Attempting to delete old photos: {old_photos}")
+ try:
+ # Use list comprehension to filter out potential empty strings or None
+ paths_to_delete = [f"photos/{p}" for p in old_photos if p]
+ if paths_to_delete: # Only call if there are paths to delete
+ api.delete_files(
+ repo_id=REPO_ID,
+ paths_in_repo=paths_to_delete,
+ repo_type="dataset",
+ token=HF_TOKEN_WRITE,
+ commit_message=f"Delete old photos for product {product_to_edit['name']}"
+ )
+ logging.info(f"Old photos for product {product_to_edit['name']} deleted from HF.")
+ else:
+ logging.info("No valid old photo paths to delete.")
+ except Exception as e:
+ # Log error but don't stop the edit process
+ logging.error(f"Error deleting old photos {old_photos} from HF: {e}", exc_info=True)
+ flash("Не удалось удалить старые фотографии с сервера. Новые фото загружены.", "warning")
+ elif uploaded_count == 0 and any(f.filename for f in photos_files):
+ # If files were selected but none uploaded (e.g., wrong format)
+ flash("Не удалось загрузить новые фотографии (возможно, неверный формат). Старые фото сохранены.", "error")
+
+ except Exception as e:
+ logging.error(f"Error during new photo upload/processing for edit: {e}", exc_info=True)
+ flash("Ошибка при загрузке/обработке новых фото.", "error")
+ finally:
+ # Clean up temp files regardless of success/failure
+ for path in temp_paths_to_clean:
+ if os.path.exists(path):
+ try: os.remove(path)
+ except OSError: pass
+ if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
+ try: os.rmdir(uploads_dir)
+ except OSError: pass
+
+ # Mark data for saving regardless of photo outcome
+ save_needed_data = True
+ logging.info(f"Product '{original_name}' staged for update to '{product_to_edit['name']}'.")
+ flash(f"Товар '{product_to_edit['name']}' будет обновлен после сохранения.", 'success')
+
+
+ elif action == 'delete_product':
+ index_str = request.form.get('index')
+ if index_str is None:
+ flash("Ошибка удаления: индекс товара не передан.", 'error')
+ return redirect(url_for('admin'))
+ try:
+ index = int(index_str)
+ # Use original sorted list to identify product by index
+ original_products_sorted = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
+ if not (0 <= index < len(original_products_sorted)): raise IndexError("Product index out of range for deletion")
+
+ product_to_delete_info = original_products_sorted[index]
+ product_name = product_to_delete_info.get('name', 'N/A')
+ photos_to_delete = product_to_delete_info.get('photos', [])
+
+ # Remove the product from the working copy (data_copy['products'])
+ # Find it by name (assuming unique names)
+ initial_len = len(products)
+ products[:] = [p for p in products if p['name'] != product_name] # Filter out by name
+
+ if len(products) == initial_len:
+ raise ValueError(f"Product '{product_name}' not found in working copy for deletion.")
+
+ save_needed_data = True # Mark data for saving
+ logging.info(f"Product '{product_name}' (original index {index}) staged for deletion.")
+
+ # Try deleting photos from HF immediately
+ if photos_to_delete and HF_TOKEN_WRITE:
+ logging.info(f"Attempting to delete photos for product '{product_name}' from HF: {photos_to_delete}")
+ try:
+ api = HfApi()
+ paths_to_delete = [f"photos/{p}" for p in photos_to_delete if p]
+ if paths_to_delete:
+ api.delete_files(
+ repo_id=REPO_ID,
+ paths_in_repo=paths_to_delete,
+ repo_type="dataset",
+ token=HF_TOKEN_WRITE,
+ commit_message=f"Delete photos for deleted product {product_name}"
+ )
+ logging.info(f"Photos for product '{product_name}' deleted from HF.")
+ flash(f"Товар '{product_name}' и его фото будут удалены после сохранения.", 'success')
+ else:
+ flash(f"Товар '{product_name}' будет удален после сохранения (фото не найдены).", 'success')
+
+ except Exception as e:
+ logging.error(f"Error deleting photos {photos_to_delete} for product '{product_name}' from HF: {e}", exc_info=True)
+ flash(f"Товар '{product_name}' будет удален, но не удалось удалить фото с сервера.", "warning")
+ elif photos_to_delete and not HF_TOKEN_WRITE:
+ logging.warning(f"HF_TOKEN (write) not set. Cannot delete photos {photos_to_delete} for deleted product '{product_name}'.")
+ flash(f"Товар '{product_name}' будет удален, но фото ��е удалены с сервера (токен не задан).", "warning")
+ else:
+ flash(f"Товар '{product_name}' будет удален после сохранения.", 'success')
+
+ except (ValueError, IndexError) as e:
+ flash(f"Ошибка удаления: неверный индекс товара '{index_str}' или товар не найден.", 'error')
+ logging.error(f"Error during product deletion: {e}", exc_info=True)
+ # Don't set save_needed_data
+
+
+ elif action == 'add_user':
+ login = request.form.get('login', '').strip()
+ # WARNING: Storing plaintext passwords is a security risk!
+ password = request.form.get('password', '').strip() # No hashing!
+ first_name = request.form.get('first_name', '').strip()
+ last_name = request.form.get('last_name', '').strip()
+ phone = request.form.get('phone', '').strip()
+ country = request.form.get('country', '').strip()
+ city = request.form.get('city', '').strip()
+
+ if not login or not password:
+ flash("Логин и пароль пользователя обязательны.", 'error')
+ return redirect(url_for('admin')) # Return early
+ if login in users_copy:
+ flash(f"Пользователь с логином '{login}' уже существует.", 'error')
+ return redirect(url_for('admin')) # Return early
+
+ users_copy[login] = {
+ 'password': password, # Stored in plaintext!
+ 'first_name': first_name, 'last_name': last_name,
+ 'phone': phone,
+ 'country': country, 'city': city
+ }
+ save_needed_users = True
+ logging.info(f"User '{login}' staged for adding.")
+ flash(f"Пользователь '{login}' будет добавлен после сохранения.", 'success')
+
+ elif action == 'delete_user':
+ login_to_delete = request.form.get('login')
+ if login_to_delete and login_to_delete in users_copy:
+ del users_copy[login_to_delete]
+ save_needed_users = True
+ logging.info(f"User '{login_to_delete}' staged for deletion.")
+ flash(f"Пользователь '{login_to_delete}' будет удален после сохранения.", 'success')
+ elif login_to_delete:
+ logging.warning(f"Attempted to delete non-existent user: {login_to_delete}")
+ flash(f"Пользователь '{login_to_delete}' не найден.", 'error')
+ else:
+ flash("Не указан логин пользователя для удаления.", 'error')
+
+
+ else:
+ logging.warning(f"Received unknown admin action: {action}")
+ flash(f"Неизвестное действие: {action}", 'warning')
+
+
+ # Perform saves if changes were made
+ final_save_success = True
+ if save_needed_data:
+ # Ensure products are sorted before saving
+ data_copy['products'].sort(key=lambda p: p.get('name', '').lower())
+ if not save_data(data_copy):
+ flash("Ошибка при сохранении основных данных (товары/категории).", 'error')
+ final_save_success = False
+ if save_needed_users:
+ if not save_users(users_copy):
+ flash("Ошибка при сохранении данных пользователей.", 'error')
+ final_save_success = False
+
+ if final_save_success and (save_needed_data or save_needed_users):
+ flash("Все изменения успешно сохранены локально.", 'success')
+
+
+ except Exception as e:
+ logging.error(f"Error processing admin action '{action}': {e}", exc_info=True)
+ flash(f"Произошла внутренняя ошибка при обработке действия '{action}'. Подробности в логе сервера.", 'error')
+
+
+ # Redirect back to admin page after processing POST request
+ return redirect(url_for('admin'))
+
+
+ # --- GET Request Handling ---
+ # Get fresh data for rendering the page
+ display_data = get_data()
+ display_users = get_users()
+ display_products = sorted(display_data.get('products', []), key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
+ display_categories = sorted(display_data.get('categories', []))
+ display_users_sorted = dict(sorted(display_users.items()))
+
+ return render_template_string(
+ ADMIN_TEMPLATE,
+ products=display_products,
+ categories=display_categories,
+ users=display_users_sorted,
+ repo_id=REPO_ID,
+ currency_code=CURRENCY_CODE,
+ backup_interval=BACKUP_INTERVAL
+ )
+
+@app.route('/force_upload', methods=['POST'])
+def force_upload():
+ # Add admin check if needed
+ # if 'user' not in session: return redirect(url_for('login'))
+ logging.info("Forcing upload to Hugging Face via admin request...")
+ try:
+ success = upload_db_to_hf() # Uploads both files based on current file content
+ if success:
+ flash("Данные успешно загружены на Hugging Face.", 'success')
+ else:
+ flash("Во время загрузки на Hugging Face произошли ошибки (не все файлы могли быть загружены). Проверьте логи.", 'warning')
+ except Exception as e:
+ logging.error(f"Error during forced upload: {e}", exc_info=True)
+ flash(f"Критическая ошибка при принудительной загрузке на Hugging Face: {e}", 'error')
+ return redirect(url_for('admin'))
+
+@app.route('/force_download', methods=['POST'])
+def force_download():
+ # Add admin check if needed
+ # if 'user' not in session: return redirect(url_for('login'))
+ logging.info("Forcing download from Hugging Face via admin request...")
+ try:
+ if download_db_from_hf(): # Download both files, overwriting local
+ # Reload data into memory *after* successful download
+ load_initial_data()
+ flash("Данные успешно скачаны с Hugging Face и загружены в память. Локальные файлы обновлены.", 'success')
+ else:
+ flash("Не удалось скачать данные с Hugging Face после нескольких попыток. Используются текущие локальные данные. Проверьте логи.", 'error')
+ except Exception as e:
+ logging.error(f"Error during forced download: {e}", exc_info=True)
+ flash(f"Критическая ошибка при принудительном скачивании с Hugging Face: {e}", 'error')
+ return redirect(url_for('admin'))
+
+
+if __name__ == '__main__':
+ logging.info("Application starting up...")
+
+ # Perform initial data load/download *before* starting backup thread or app
+ logging.info("Performing initial data load from local files or HF...")
+ load_initial_data()
+ logging.info("Initial data load complete.")
+
+ # Start periodic backup thread only if token is available
+ if HF_TOKEN_WRITE:
+ backup_thread = threading.Thread(target=periodic_backup, daemon=True)
+ backup_thread.start()
+ logging.info("Periodic backup thread started.")
+ else:
+ logging.warning("Periodic backup thread *not* started (HF_TOKEN_WRITE not set).")
+
+ # Use Waitress or Gunicorn in production instead of Flask's development server
+ port = int(os.environ.get('PORT', 7860))
+ logging.info(f"Starting Flask app server on host 0.0.0.0 and port {port}")
+
+ # For development: app.run(debug=False, host='0.0.0.0', port=port)
+ # For production (example with Waitress):
+ try:
+ from waitress import serve
+ serve(app, host='0.0.0.0', port=port, threads=8) # Adjust threads as needed
+ except ImportError:
+ logging.warning("Waitress not found. Falling back to Flask development server.")
+ logging.warning("Install waitress for a production-ready server: pip install waitress")
+ app.run(debug=False, host='0.0.0.0', port=port)
+
+# --- END OF FILE app.py ---