360 / app.py
Kgshop's picture
Rename app (16) (5).py to app.py
b0eff02 verified
Raw
History Blame Contribute Delete
104 kB
import flask
from flask import Flask, render_template_string, request, redirect, url_for, jsonify, send_from_directory
import json
import os
import logging
import threading
import time
import uuid
from datetime import datetime
from huggingface_hub import HfApi, hf_hub_download, list_repo_files
from huggingface_hub.utils import RepositoryNotFoundError, EntryNotFoundError
from werkzeug.utils import secure_filename
import io
import urllib.parse
app = Flask(__name__)
DATA_FILE = 'data_360_panorama.json'
PANORAMA_UPLOADS = 'uploads/panoramas'
PRODUCT_PHOTO_UPLOADS = 'uploads/product_photos'
os.makedirs(PANORAMA_UPLOADS, exist_ok=True)
os.makedirs(PRODUCT_PHOTO_UPLOADS, exist_ok=True)
REPO_ID = "Kgshop/360testb"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ", HF_TOKEN_WRITE)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
app_data = {'points': [], 'categories': [], 'standalone_products': [], 'whatsapp_number': ''}
def generate_id():
return str(uuid.uuid4())
def get_hf_image_url(repo_id, file_path_in_repo):
if not file_path_in_repo:
return None
if file_path_in_repo.startswith('/'):
file_path_in_repo = file_path_in_repo[1:]
return f"https://huggingface.co/datasets/{repo_id}/resolve/main/{file_path_in_repo}"
def upload_file_to_hf(local_path, path_in_repo, repo_id, token, commit_message):
if not token:
logging.error("Hugging Face write token (HF_TOKEN) не установлен. Загрузка пропущена.")
return False
try:
api = HfApi()
api.upload_file(
path_or_fileobj=local_path,
path_in_repo=path_in_repo,
repo_id=repo_id,
repo_type="dataset",
token=token,
commit_message=commit_message
)
logging.info(f"Успешно загружен {local_path} в {repo_id}/{path_in_repo}")
return True
except Exception as e:
logging.error(f"Ошибка загрузки {local_path} в Hugging Face: {e}")
return False
def delete_file_from_hf(path_in_repo, repo_id, token, commit_message):
if not token:
logging.error("Hugging Face write token (HF_TOKEN) не установлен. Удаление пропущено.")
return False
if not path_in_repo:
logging.warning("Попытка удалить файл из HF с пустым путем.")
return True
try:
api = HfApi()
api.delete_file(
path_in_repo=path_in_repo,
repo_id=repo_id,
repo_type="dataset",
token=token,
commit_message=commit_message
)
logging.info(f"Успешно удален {path_in_repo} из {repo_id}")
return True
except EntryNotFoundError:
logging.warning(f"Файл {path_in_repo} не найден в репозитории {repo_id} для удаления.")
return True
except Exception as e:
logging.error(f"Ошибка удаления {path_in_repo} из Hugging Face: {e}")
return False
def upload_db_to_hf():
if not HF_TOKEN_WRITE:
logging.error("Hugging Face write token (HF_TOKEN) не установлен. Загрузка БД пропущена.")
return
try:
api = HfApi()
db_content = json.dumps(app_data, ensure_ascii=False, indent=4).encode('utf-8')
db_stream = io.BytesIO(db_content)
api.upload_file(
path_or_fileobj=db_stream,
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"База данных {DATA_FILE} успешно загружена в Hugging Face.")
except Exception as e:
logging.error(f"Ошибка загрузки базы данных в Hugging Face: {e}")
def download_db_from_hf():
global app_data
default_data = {'points': [], 'categories': [], 'standalone_products': [], 'whatsapp_number': ''}
if not HF_TOKEN_READ:
logging.error("Hugging Face read token не установлен. Невозможно скачать БД.")
try:
with open(DATA_FILE, 'r', encoding='utf-8') as f:
app_data = json.load(f)
logging.info(f"Загружены данные из локального файла {DATA_FILE} из-за отсутствия read token.")
app_data.setdefault('standalone_products', [])
app_data.setdefault('whatsapp_number', '')
except FileNotFoundError:
logging.warning(f"Локальный файл {DATA_FILE} не найден и read token отсутствует. Запуск с пустыми данными.")
app_data = default_data
except json.JSONDecodeError:
logging.error(f"Ошибка декодирования локального JSON файла {DATA_FILE}. Запуск с пустыми данными.")
app_data = default_data
return
try:
local_path = hf_hub_download(
repo_id=REPO_ID,
filename=DATA_FILE,
repo_type="dataset",
token=HF_TOKEN_READ,
local_dir=".",
local_dir_use_symlinks=False,
force_download=True,
resume_download=False
)
with open(local_path, 'r', encoding='utf-8') as file:
app_data = json.load(file)
if not isinstance(app_data, dict):
raise ValueError("Данные не являются словарем")
if 'points' not in app_data or not isinstance(app_data['points'], list):
app_data['points'] = []
logging.warning("Отсутствует или неверный ключ 'points' в загруженных данных, инициализирован.")
if 'categories' not in app_data or not isinstance(app_data['categories'], list):
app_data['categories'] = []
logging.warning("Отсутствует или неверный ключ 'categories' в загруженных данных, инициализирован.")
if 'standalone_products' not in app_data or not isinstance(app_data['standalone_products'], list):
app_data['standalone_products'] = []
logging.warning("Отсутствует или неверный ключ 'standalone_products' в загруженных данных, инициализирован.")
if 'whatsapp_number' not in app_data:
app_data['whatsapp_number'] = ''
logging.warning("Отсутствует ключ 'whatsapp_number' в загруженных данных, инициализирован.")
logging.info(f"База данных {DATA_FILE} успешно скачана и загружена из Hugging Face.")
except RepositoryNotFoundError:
logging.error(f"Репозиторий Hugging Face {REPO_ID} не найден. Запуск с пустыми данными.")
app_data = default_data
except EntryNotFoundError:
logging.warning(f"Файл базы данных {DATA_FILE} не найден в репозитории {REPO_ID}. Запуск с пустыми данными.")
app_data = default_data
except json.JSONDecodeError:
logging.error(f"Ошибка декодирования JSON из скачанного файла {DATA_FILE}. Запуск с пустыми данными.")
app_data = default_data
except Exception as e:
logging.error(f"Произошла непредвиденная ошибка при скачивании базы данных: {e}")
app_data = default_data
def save_data():
global app_data
try:
app_data.setdefault('points', [])
app_data.setdefault('categories', [])
app_data.setdefault('standalone_products', [])
app_data.setdefault('whatsapp_number', '')
with open(DATA_FILE, 'w', encoding='utf-8') as file:
json.dump(app_data, file, ensure_ascii=False, indent=4)
logging.info(f"Данные успешно сохранены локально в {DATA_FILE}")
upload_db_to_hf()
except Exception as e:
logging.error(f"Ошибка сохранения данных: {e}")
def periodic_backup():
while True:
time.sleep(800)
logging.info("Выполнение периодического резервного копирования базы данных...")
save_data()
def find_point(point_id):
return next((p for p in app_data.get('points', []) if p.get('id') == point_id), None)
def find_category(category_id):
return next((c for c in app_data.get('categories', []) if c.get('id') == category_id), None)
def find_product_in_category(category_id, product_id):
category = find_category(category_id)
if category:
return next((p for p in category.get('products', []) if p.get('id') == product_id), None)
return None
def find_standalone_product(product_id):
return next((p for p in app_data.get('standalone_products', []) if p.get('id') == product_id), None)
def delete_product_files_from_hf(product):
hf_delete_failed = False
product_name_for_log = product.get('name', product.get('id', 'N/A'))
photo_paths = product.get('photo_paths', [])
for hf_path in photo_paths:
commit_msg = f"Удалить фото {os.path.basename(hf_path)} для удаленного товара {product_name_for_log}"
if not delete_file_from_hf(hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg):
logging.warning(f"Не удалось удалить фото товара {hf_path} из Hugging Face для товара {product_name_for_log}.")
hf_delete_failed = True
return hf_delete_failed
def cleanup_local_product_files(product):
photo_paths = product.get('photo_paths', [])
for hf_path in photo_paths:
local_filename = os.path.basename(hf_path)
local_path = os.path.join(PRODUCT_PHOTO_UPLOADS, local_filename)
if os.path.exists(local_path):
try:
os.remove(local_path)
except OSError as e:
logging.warning(f"Не удалось удалить остаточный локальный файл фото товара {local_path}: {e}")
@app.route('/')
def index():
points_for_frontend = []
for point in app_data.get('points', []):
hf_panorama_url = get_hf_image_url(REPO_ID, point.get('panorama_image_path', ''))
hotspots_for_frontend = []
for hs in point.get('hotspots', []):
hotspots_for_frontend.append({
'id': hs.get('id'),
'pitch': hs.get('pitch'),
'yaw': hs.get('yaw'),
'type': hs.get('type'),
'target': hs.get('target'),
'text': hs.get('text', 'Инфо'),
'cssClass': 'custom-hotspot-button',
})
points_for_frontend.append({
'id': point.get('id'),
'name': point.get('name'),
'panoramaUrl': hf_panorama_url,
'hotspots': hotspots_for_frontend
})
index_html = """
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>360 Панорамный Каталог</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pannellum@2.5.6/build/pannellum.css"/>
<style>
html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; font-family: Arial, sans-serif; }
#panorama-container { width: 100%; height: 100%; }
.custom-hotspot-button-wrapper { z-index: 10; }
.custom-hotspot-button {
background-color: rgba(0, 123, 255, 0.8);
color: white;
padding: 8px 15px;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.7);
font-size: 14px;
font-weight: bold;
cursor: pointer;
text-align: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
transition: background-color 0.3s ease, transform 0.2s ease;
white-space: nowrap;
display: block;
}
.custom-hotspot-button:hover {
background-color: rgba(0, 86, 179, 0.9);
transform: scale(1.05);
}
.modal {
display: none; position: fixed; z-index: 1001; left: 0; top: 0;
width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.7);
align-items: center; justify-content: center;
}
.modal-content {
background-color: #f8f9fa; margin: auto; padding: 25px; border: 1px solid #ccc;
width: 85%; max-width: 800px; border-radius: 15px; max-height: 85vh; overflow-y: auto;
box-shadow: 0 5px 15px rgba(0,0,0,0.2); position: relative;
}
.modal-close {
color: #aaa; position: absolute; top: 10px; right: 20px;
font-size: 32px; font-weight: bold; cursor: pointer; line-height: 1;
}
.modal-close:hover, .modal-close:focus { color: black; text-decoration: none; cursor: pointer; }
.product-list { display: flex; flex-direction: column; gap: 20px; margin-top: 20px;}
.product-item {
display: flex; gap: 15px; border: 1px solid #ddd; padding: 15px;
background-color: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.product-item img {
width: 120px; height: 120px; object-fit: cover; border-radius: 5px; flex-shrink: 0;
}
.product-details { flex-grow: 1; }
.product-item h4 { font-size: 1.1em; margin-bottom: 8px; color: #333; }
.product-item .price { font-weight: bold; color: #007bff; margin-top: 8px; font-size: 1.1em; }
.product-description { display: none; margin-top: 10px; font-size: 0.9em; color: #555; padding-top: 10px; border-top: 1px solid #eee; }
.product-actions button {
background-color: #007bff; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9em; margin-right: 5px; margin-top: 10px;
}
.product-actions button:hover { background-color: #0056b3; }
.product-actions .details-button { background-color: #6c757d; }
.product-actions .details-button:hover { background-color: #5a6268; }
.no-image-placeholder {
width:120px; height:120px; background:#eee; display:flex; align-items:center; justify-content:center; color:#aaa; border-radius:5px; flex-shrink:0; font-size: 12px; text-align: center;
}
#single-product-modal .product-item img {
width: 150px; height: 150px;
}
#single-product-modal .product-description {
display: block;
}
#floating-cart-button {
position: fixed; bottom: 20px; right: 20px; background-color: #dc3545;
color: white; padding: 12px 20px; border-radius: 50px; font-size: 16px;
font-weight: bold; cursor: pointer; box-shadow: 0 4px 8px rgba(0,0,0,0.3);
z-index: 1005; display: none; transition: transform 0.2s ease-in-out;
}
#floating-cart-button:hover { transform: scale(1.1); }
#cart-modal .modal-content { max-width: 600px; }
.cart-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #eee; }
.cart-item:last-child { border-bottom: none; }
.cart-item-details { flex-grow: 1; margin-right: 10px; }
.cart-item-name { font-weight: bold; }
.cart-item-price { color: #555; font-size: 0.9em; }
.cart-item-quantity input { width: 50px; text-align: center; margin: 0 5px; }
.cart-total { text-align: right; font-size: 1.2em; font-weight: bold; margin-top: 20px; padding-top: 15px; border-top: 2px solid #333;}
.cart-actions button { margin-left: 10px; }
.remove-item-btn { background-color: #dc3545; }
.remove-item-btn:hover { background-color: #c82333; }
.quantity-input { width: 40px; padding: 5px; text-align: center; border-radius: 4px; border: 1px solid #ccc; margin-left: 5px; }
</style>
</head>
<body>
<div id="panorama-container"></div>
<div id="product-modal" class="modal">
<div class="modal-content">
<span class="modal-close" onclick="closeModal('product-modal')">×</span>
<h2 id="modal-category-name">Товары Категории</h2>
<div id="modal-product-list" class="product-list">
</div>
</div>
</div>
<div id="single-product-modal" class="modal">
<div class="modal-content">
<span class="modal-close" onclick="closeModal('single-product-modal')">×</span>
<h2 id="single-product-modal-title">Информация о Товаре</h2>
<div id="single-product-content">
</div>
</div>
</div>
<div id="cart-modal" class="modal">
<div class="modal-content">
<span class="modal-close" onclick="closeModal('cart-modal')">×</span>
<h2>Ваша Корзина</h2>
<div id="cart-items-list">
</div>
<div id="cart-total" class="cart-total">
Итого: 0 SOM
</div>
<div style="text-align: right; margin-top: 20px;">
<button class="button" onclick="openWhatsAppOrder()">Оформить заказ</button>
<button class="delete-button" onclick="clearCart()">Очистить корзину</button>
</div>
</div>
</div>
<div id="floating-cart-button" onclick="showCartModal()">Корзина (0)</div>
<script src="https://cdn.jsdelivr.net/npm/pannellum@2.5.6/build/pannellum.js"></script>
<script>
const pointsData = {{ points_data | tojson }};
const whatsappNumber = "{{ whatsapp_number }}";
let currentViewer = null;
const panoramaContainer = document.getElementById('panorama-container');
const floatingCartButton = document.getElementById('floating-cart-button');
function getModal(modalId) { return document.getElementById(modalId); }
function closeModal(modalId) { getModal(modalId).style.display = 'none'; }
function showModal(modalId) { getModal(modalId).style.display = 'flex'; }
function createHotspotElement(hotSpotDiv, args) {
hotSpotDiv.classList.add('custom-hotspot-button-wrapper');
const button = document.createElement('button');
button.classList.add('custom-hotspot-button');
button.textContent = args.text || 'Инфо';
hotSpotDiv.style.pointerEvents = 'auto';
button.style.pointerEvents = 'auto';
button.onclick = (e) => {
e.stopPropagation();
handleHotspotClick(args);
};
hotSpotDiv.appendChild(button);
}
function toggleProductDescription(productId, modalId = 'product-modal') {
const descriptionDiv = getModal(modalId).querySelector(`#desc-${productId}`);
if (descriptionDiv) {
descriptionDiv.style.display = descriptionDiv.style.display === 'block' ? 'none' : 'block';
}
}
function renderProductItem(product, isSingleView = false) {
let imgHtml = product.photoUrl
? `<img src="${product.photoUrl}" alt="${product.name}">`
: '<div class="no-image-placeholder">Нет Изображения</div>';
const descriptionStyle = isSingleView ? 'style="display: block;"' : '';
return `
<div class="product-item" id="product-${product.id}">
${imgHtml}
<div class="product-details">
<h4>${product.name}</h4>
<p class="price">${product.price} SOM</p>
<div class="product-actions">
${!isSingleView ? `<button class="details-button" onclick="toggleProductDescription('${product.id}', 'product-modal')">Детали</button>` : ''}
<button onclick="addToCart('${product.id}', '${product.name}', ${product.price}, document.getElementById('quantity-${product.id}').value)">В корзину</button>
<input type="number" id="quantity-${product.id}" class="quantity-input" value="1" min="1">
</div>
<div class="product-description" id="desc-${product.id}" ${descriptionStyle}>
<p>${product.description || 'Описание отсутствует.'}</p>
</div>
</div>
</div>`;
}
function showCategoryProductModal(categoryName, products) {
const modalList = getModal('product-modal').querySelector('#modal-product-list');
getModal('product-modal').querySelector('#modal-category-name').textContent = categoryName || 'Товары Категории';
modalList.innerHTML = '';
if (products && products.length > 0) {
products.forEach(product => {
modalList.innerHTML += renderProductItem(product);
});
} else {
modalList.innerHTML = '<p>Товары в этой категории не найдены.</p>';
}
showModal('product-modal');
}
function showSingleProductModal(product) {
const modalContent = getModal('single-product-modal').querySelector('#single-product-content');
getModal('single-product-modal').querySelector('#single-product-modal-title').textContent = product.name || 'Информация о Товаре';
modalContent.innerHTML = renderProductItem(product, true);
showModal('single-product-modal');
}
function getCart() {
const cart = localStorage.getItem('shoppingCart');
return cart ? JSON.parse(cart) : [];
}
function saveCart(cart) {
localStorage.setItem('shoppingCart', JSON.stringify(cart));
updateCartButton();
renderCartItems();
}
function addToCart(productId, name, price, quantity) {
const cart = getCart();
const existingItemIndex = cart.findIndex(item => item.id === productId);
const qty = parseInt(quantity, 10);
if (existingItemIndex > -1) {
cart[existingItemIndex].quantity += qty;
} else {
cart.push({ id: productId, name: name, price: price, quantity: qty });
}
saveCart(cart);
alert(`"${name}" добавлен в корзину в количестве ${qty} шт.!`);
}
function updateCartQuantity(productId, change) {
const cart = getCart();
const itemIndex = cart.findIndex(item => item.id === productId);
if (itemIndex > -1) {
cart[itemIndex].quantity += change;
if (cart[itemIndex].quantity <= 0) {
cart.splice(itemIndex, 1);
}
saveCart(cart);
}
}
function removeFromCart(productId) {
let cart = getCart();
cart = cart.filter(item => item.id !== productId);
saveCart(cart);
}
function clearCart() {
if (confirm('Вы уверены, что хотите очистить корзину?')) {
saveCart([]);
closeModal('cart-modal');
}
}
function updateCartButton() {
const cart = getCart();
const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);
if (totalItems > 0) {
floatingCartButton.textContent = `Корзина (${totalItems})`;
floatingCartButton.style.display = 'block';
} else {
floatingCartButton.style.display = 'none';
}
}
function renderCartItems() {
const cart = getCart();
const cartListDiv = getModal('cart-modal').querySelector('#cart-items-list');
const cartTotalDiv = getModal('cart-modal').querySelector('#cart-total');
let totalAmount = 0;
cartListDiv.innerHTML = '';
if (cart.length === 0) {
cartListDiv.innerHTML = '<p>Ваша корзина пуста.</p>';
cartTotalDiv.textContent = 'Итого: 0 SOM';
return;
}
cart.forEach(item => {
const itemTotal = item.price * item.quantity;
totalAmount += itemTotal;
const itemDiv = document.createElement('div');
itemDiv.classList.add('cart-item');
itemDiv.innerHTML = `
<div class="cart-item-details">
<div class="cart-item-name">${item.name}</div>
<div class="cart-item-price">${item.quantity} x ${item.price} SOM = ${itemTotal.toFixed(2)} SOM</div>
</div>
<div class="cart-item-quantity">
<button onclick="updateCartQuantity('${item.id}', -1)" class="cart-actions">-</button>
<span>${item.quantity}</span>
<button onclick="updateCartQuantity('${item.id}', 1)" class="cart-actions">+</button>
</div>
<button class="cart-actions remove-item-btn" onclick="removeFromCart('${item.id}')">Удалить</button>
`;
cartListDiv.appendChild(itemDiv);
});
cartTotalDiv.textContent = `Итого: ${totalAmount.toFixed(2)} SOM`;
}
function showCartModal() {
renderCartItems();
showModal('cart-modal');
}
function openWhatsAppOrder() {
const cart = getCart();
if (cart.length === 0) {
alert('Корзина пуста. Добавьте товары для оформления заказа.');
return;
}
let orderMessage = "Здравствуйте! Я хочу оформить заказ:\\n";
let totalSum = 0;
cart.forEach(item => {
const itemTotal = item.price * item.quantity;
totalSum += itemTotal;
orderMessage += `- ${item.name} - ${item.quantity} шт. (Цена за шт: ${item.price} SOM, Итого: ${itemTotal.toFixed(2)} SOM)\\n`;
});
orderMessage += `\\nОбщая сумма заказа: ${totalSum.toFixed(2)} SOM`;
const whatsappUrl = `https://wa.me/${whatsappNumber}?text=${encodeURIComponent(orderMessage)}`;
window.open(whatsappUrl, '_blank');
}
function handleHotspotClick(args) {
console.log("Хотспот нажат:", args);
if (!args || !args.type) {
console.warn("Нажатый хотспот не имеет типа в аргументах");
return;
}
switch(args.type) {
case 'point':
if (args.target && pointsData.find(p => p.id === args.target)) {
loadPanorama(args.target);
} else {
console.error(`Целевой поинт ID ${args.target} не найден.`);
alert("Ошибка: Связанная панорама не найдена.");
}
break;
case 'link':
if (args.target) {
window.open(args.target, '_blank');
} else {
console.error("Хотспот-ссылка не имеет целевого URL.");
alert("Ошибка: URL ссылки отсутствует.");
}
break;
case 'category':
if (args.target) {
fetch(`/category_products/${args.target}`)
.then(response => {
if (!response.ok) throw new Error(`HTTP ошибка! Статус: ${response.status}`);
return response.json();
})
.then(data => {
showCategoryProductModal(data.category_name, data.products);
})
.catch(error => {
console.error('Ошибка загрузки товаров категории:', error);
alert(`Ошибка загрузки товаров: ${error.message}`);
showCategoryProductModal('Ошибка', []);
});
} else {
console.error("Хотспот категории не имеет ID целевой категории.");
alert("Ошибка: Ссылка на категорию повреждена.");
}
break;
case 'product':
if (args.target) {
fetch(`/standalone_product_details/${args.target}`)
.then(response => {
if (!response.ok) throw new Error(`HTTP ошибка! Статус: ${response.status}`);
return response.json();
})
.then(data => {
if (data.error) {
throw new Error(data.error);
}
showSingleProductModal(data.product);
})
.catch(error => {
console.error('Ошибка загрузки информации о товаре:', error);
alert(`Ошибка загрузки товара: ${error.message}`);
});
} else {
console.error("Хотспот товара не имеет ID целевого товара.");
alert("Ошибка: Ссылка на товар повреждена.");
}
break;
default:
console.warn(`Неизвестный тип хотспота: ${args.type}`);
}
}
function loadPanorama(pointId) {
const point = pointsData.find(p => p.id === pointId);
if (!point) {
console.error(`Поинт с ID ${pointId} не найден в pointsData.`);
if(pointsData.length > 0 && pointId !== pointsData[0].id) {
loadPanorama(pointsData[0].id);
} else {
panoramaContainer.innerHTML = '<p style="text-align: center; padding-top: 50px;">Ошибка: Данные панорамы не найдены.</p>';
}
return;
}
const configuredHotspots = point.hotspots.map(hs => ({
pitch: hs.pitch,
yaw: hs.yaw,
createTooltipFunc: createHotspotElement,
createTooltipArgs: { type: hs.type, target: hs.target, text: hs.text }
}));
if (currentViewer) {
try {
currentViewer.destroy();
} catch (e) {
console.warn("Ошибка уничтожения предыдущего экземпляра Pannellum:", e);
}
}
try {
currentViewer = pannellum.viewer('panorama-container', {
"type": "equirectangular",
"panorama": point.panoramaUrl,
"autoLoad": true,
"autoRotate": -2,
"hfov": 90,
"pitch": 0,
"yaw": 0,
"hotSpots": configuredHotspots,
"strings": {
"loadButtonLabel": "Нажмите для загрузки",
"loadingLabel": "Загрузка...",
"fullscreenNotSupported": "Полноэкранный режим не поддерживается",
"errorLoading": "Ошибка загрузки панорамы."
}
});
currentViewer.on('error', function(err) {
console.error('Ошибка Pannellum viewer:', err);
panoramaContainer.innerHTML = `<p style="text-align: center; padding-top: 50px;">Ошибка загрузки панорамы: ${err}</p>`;
});
currentViewer.on('load', function() {
console.log(`Панорама загружена: ${point.name}`);
});
} catch (e) {
console.error("Не удалось инициализировать Pannellum viewer:", e);
panoramaContainer.innerHTML = '<p style="text-align: center; padding-top: 50px;">Не удалось инициализировать просмотрщик панорам.</p>';
}
}
document.addEventListener('DOMContentLoaded', () => {
if (pointsData && pointsData.length > 0) {
loadPanorama(pointsData[0].id);
} else {
console.warn("Данные поинтов панорамы не найдены.");
panoramaContainer.innerHTML = '<p style="text-align: center; padding-top: 50px;">Панорамы отсутствуют. Пожалуйста, добавьте их в админ-панели.</p>';
}
updateCartButton();
});
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
closeModal(event.target.id);
}
}
</script>
</body>
</html>
"""
return render_template_string(index_html, points_data=points_for_frontend, whatsapp_number=app_data.get('whatsapp_number', ''))
@app.route('/category_products/<category_id>')
def get_category_products(category_id):
category = find_category(category_id)
if not category:
return jsonify({"error": "Категория не найдена"}), 404
products_with_urls = []
for product in category.get('products', []):
photo_url = None
if product.get('photo_paths') and len(product['photo_paths']) > 0:
photo_url = get_hf_image_url(REPO_ID, product['photo_paths'][0])
products_with_urls.append({
"id": product.get('id'),
"name": product.get('name', 'N/A'),
"price": product.get('price', 0),
"description": product.get('description', ''),
"photoUrl": photo_url
})
return jsonify({
"category_id": category_id,
"category_name": category.get('name', 'Неизвестная Категория'),
"products": products_with_urls
})
@app.route('/standalone_product_details/<product_id>')
def get_standalone_product_details(product_id):
product = find_standalone_product(product_id)
if not product:
return jsonify({"error": "Товар не найден"}), 404
photo_url = None
if product.get('photo_paths') and len(product['photo_paths']) > 0:
photo_url = get_hf_image_url(REPO_ID, product['photo_paths'][0])
product_details = {
"id": product.get('id'),
"name": product.get('name', 'N/A'),
"price": product.get('price', 0),
"description": product.get('description', ''),
"photoUrl": photo_url,
"allPhotoUrls": [get_hf_image_url(REPO_ID, p) for p in product.get('photo_paths', []) if p]
}
return jsonify({"product": product_details})
@app.route('/admin', methods=['GET'])
def admin_panel():
admin_html = """
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Админ-панель</title>
<style>
body { font-family: sans-serif; padding: 20px; background-color: #f4f4f4; }
.container { max-width: 1200px; margin: auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h1, h2, h3 { color: #333; border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 20px; }
.section { margin-bottom: 30px; padding: 20px; border: 1px solid #ddd; border-radius: 5px; background-color: #fafafa; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type=text], input[type=number], input[type=url], textarea, select {
width: calc(100% - 22px); padding: 10px; margin-bottom: 15px; border: 1px solid #ccc; border-radius: 4px;
}
input[type=file] { margin-bottom: 15px; }
button, .button {
background-color: #5cb85c; color: white; padding: 10px 15px; border: none; border-radius: 4px;
cursor: pointer; text-decoration: none; display: inline-block; margin-right: 5px;
}
button:hover, .button:hover { background-color: #4cae4c; }
.delete-button { background-color: #d9534f; }
.delete-button:hover { background-color: #c9302c; }
.edit-button { background-color: #f0ad4e; }
.edit-button:hover { background-color: #ec971f; }
.view-button { background-color: #5bc0de; }
.view-button:hover { background-color: #46b8da; }
ul { list-style: none; padding: 0; }
li { background: #fff; border: 1px solid #eee; margin-bottom: 10px; padding: 15px; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; }
li span { flex-grow: 1; margin-right: 10px; }
.form-inline button { margin-top: 10px; }
.hidden { display: none; }
</style>
</head>
<body>
<div class="container">
<h1>Админ-панель 360° Каталога</h1>
<div class="section">
<h2>Управление Поинтами (Панорамами)</h2>
<form action="/admin/points/add" method="post" enctype="multipart/form-data">
<h3>Добавить новый Поинт</h3>
<label for="point_name">Название Поинта:</label>
<input type="text" id="point_name" name="point_name" required>
<label for="panorama_image">Файл панорамы (JPG):</label>
<input type="file" id="panorama_image" name="panorama_image" accept="image/jpeg, image/jpg" required>
<button type="submit">Добавить Поинт</button>
</form>
<h3>Существующие Поинты</h3>
<ul id="points-list">
{% for point in points %}
<li>
<span>{{ point.name }} (ID: {{ point.id }})</span>
<div>
<a href="{{ url_for('admin_edit_point', point_id=point.id) }}" class="button view-button">Редактировать Хотспоты</a>
<form action="/admin/points/delete/{{ point.id }}" method="post" style="display: inline;">
<button type="submit" class="delete-button" onclick="return confirm('Вы уверены, что хотите удалить этот поинт и все его хотспоты?');">Удалить</button>
</form>
</div>
</li>
{% else %}
<p>Пока нет ни одного поинта.</p>
{% endfor %}
</ul>
</div>
<div class="section">
<h2>Управление Категориями</h2>
<form action="/admin/categories/add" method="post">
<h3>Добавить новую Категорию</h3>
<label for="category_name">Название Категории:</label>
<input type="text" id="category_name" name="category_name" required>
<button type="submit">Добавить Категорию</button>
</form>
<h3>Существующие Категории</h3>
<ul id="categories-list">
{% for category in categories %}
<li>
<span>{{ category.name }} (ID: {{ category.id }})</span>
<div>
<a href="{{ url_for('admin_manage_category_products', category_id=category.id) }}" class="button view-button">Управлять Товарами</a>
<form action="/admin/categories/delete/{{ category.id }}" method="post" style="display: inline;">
<button type="submit" class="delete-button" onclick="return confirm('Вы уверены, что хотите удалить эту категорию и ВСЕ товары в ней? Это действие необратимо.');">Удалить</button>
</form>
</div>
</li>
{% else %}
<p>Пока нет ни одной категории.</p>
{% endfor %}
</ul>
</div>
<div class="section">
<h2>Управление Отдельными Товарами</h2>
<p>Эти товары можно будет выбирать напрямую при создании хотспота типа "Товар".</p>
<a href="{{ url_for('admin_manage_standalone_products') }}" class="button view-button">Перейти к управлению отдельными товарами</a>
</div>
<div class="section">
<h2>Настройки WhatsApp</h2>
<form action="{{ url_for('admin_save_whatsapp_number') }}" method="post">
<label for="whatsapp_number">Номер WhatsApp для заказов:</label>
<input type="text" id="whatsapp_number" name="whatsapp_number" value="{{ whatsapp_number }}">
<button type="submit">Сохранить номер WhatsApp</button>
</form>
</div>
<div class="section">
<h2>Резервное копирование</h2>
<p>База данных автоматически сохраняется на Hugging Face при каждом изменении и периодически.</p>
<form action="{{ url_for('manual_backup') }}" method="post" style="display: inline;">
<button type="submit">Создать резервную копию БД сейчас</button>
</form>
<p><i>Примечание: Изображения (панорамы, фото товаров) загружаются на Hugging Face сразу при их добавлении/изменении.</i></p>
</div>
</div>
</body>
</html>
"""
return render_template_string(admin_html,
points=app_data.get('points', []),
categories=app_data.get('categories', []),
standalone_products=app_data.get('standalone_products', []),
whatsapp_number=app_data.get('whatsapp_number', ''))
@app.route('/admin/settings/whatsapp', methods=['POST'])
def admin_save_whatsapp_number():
whatsapp_number = request.form.get('whatsapp_number')
app_data['whatsapp_number'] = whatsapp_number
save_data()
return redirect(url_for('admin_panel'))
@app.route('/admin/points/add', methods=['POST'])
def admin_add_point():
point_name = request.form.get('point_name')
panorama_file = request.files.get('panorama_image')
if not point_name or not panorama_file or not panorama_file.filename:
return redirect(url_for('admin_panel'))
if panorama_file:
filename = secure_filename(panorama_file.filename)
unique_filename = f"{generate_id()}_{filename}"
local_path = os.path.join(PANORAMA_UPLOADS, unique_filename)
hf_path = f"panoramas/{unique_filename}"
try:
panorama_file.save(local_path)
logging.info(f"Панорама сохранена локально в {local_path}")
commit_msg = f"Добавить панораму для поинта: {point_name}"
if upload_file_to_hf(local_path, hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg):
new_point = {
"id": generate_id(),
"name": point_name,
"panorama_image_path": hf_path,
"hotspots": []
}
app_data.setdefault('points', []).append(new_point)
save_data()
logging.info(f"Поинт '{point_name}' успешно добавлен.")
try:
os.remove(local_path)
logging.info(f"Удален временный локальный файл {local_path}")
except OSError as e:
logging.warning(f"Не удалось удалить временный локальный файл {local_path}: {e}")
else:
logging.error(f"Не удалось загрузить панораму для поинта {point_name} в Hugging Face. Поинт не добавлен.")
try:
os.remove(local_path)
except OSError: pass
except Exception as e:
logging.error(f"Ошибка обработки загрузки панорамы для {point_name}: {e}")
return redirect(url_for('admin_panel'))
@app.route('/admin/points/delete/<point_id>', methods=['POST'])
def admin_delete_point(point_id):
point_index = next((index for (index, p) in enumerate(app_data.get('points', [])) if p.get('id') == point_id), None)
if point_index is not None:
point = app_data['points'][point_index]
panorama_hf_path = point.get('panorama_image_path')
deleted_from_hf = False
if panorama_hf_path:
commit_msg = f"Удалить панораму для поинта: {point.get('name', point_id)}"
if delete_file_from_hf(panorama_hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg):
deleted_from_hf = True
else:
logging.warning(f"Не удалось удалить панораму {panorama_hf_path} из Hugging Face. Данные поинта все равно будут удалены.")
deleted_point = app_data['points'].pop(point_index)
save_data()
logging.info(f"Удален поинт: {deleted_point.get('name', point_id)}")
if panorama_hf_path:
local_filename = os.path.basename(panorama_hf_path)
local_path = os.path.join(PANORAMA_UPLOADS, local_filename)
if os.path.exists(local_path):
try:
os.remove(local_path)
except OSError as e:
logging.warning(f"Не удалось удалить остаточный локальный файл {local_path}: {e}")
else:
logging.warning(f"Попытка удалить несуществующий поинт с ID: {point_id}")
return redirect(url_for('admin_panel'))
@app.route('/admin/points/edit/<point_id>', methods=['GET'])
def admin_edit_point(point_id):
point = find_point(point_id)
if not point:
return redirect(url_for('admin_panel'))
panorama_url = get_hf_image_url(REPO_ID, point.get('panorama_image_path', ''))
all_points = [{"id": p['id'], "name": p['name']} for p in app_data.get('points', []) if p['id'] != point_id]
all_categories = [{"id": c['id'], "name": c['name']} for c in app_data.get('categories', [])]
all_standalone_products = [{"id": p['id'], "name": p['name']} for p in app_data.get('standalone_products', [])]
edit_point_html = """
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Редактировать Поинт: {{ point.name }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pannellum@2.5.6/build/pannellum.css"/>
<style>
body { font-family: sans-serif; padding: 20px; background-color: #f4f4f4; }
.container { max-width: 1200px; margin: auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h1, h2, h3 { color: #333; border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 20px; }
.section { margin-bottom: 30px; padding: 20px; border: 1px solid #ddd; border-radius: 5px; background-color: #fafafa; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type=text], input[type=number], input[type=url], textarea, select {
width: calc(100% - 22px); padding: 10px; margin-bottom: 15px; border: 1px solid #ccc; border-radius: 4px;
}
button, .button {
background-color: #5cb85c; color: white; padding: 10px 15px; border: none; border-radius: 4px;
cursor: pointer; text-decoration: none; display: inline-block; margin-right: 5px;
}
button:hover, .button:hover { background-color: #4cae4c; }
.delete-button { background-color: #d9534f; }
.delete-button:hover { background-color: #c9302c; }
ul { list-style: none; padding: 0; }
li { background: #fff; border: 1px solid #eee; margin-bottom: 10px; padding: 15px; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; }
li span { flex-grow: 1; margin-right: 10px; word-break: break-all; }
.hidden { display: none; }
#panorama-preview-container { width: 100%; height: 450px; border: 1px solid #ccc; margin-bottom: 15px; position: relative; background-color: #eee; cursor: crosshair; }
#panorama-preview-container::after {
content: 'Кликните на панораму для добавления хотспота в эту точку'; position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.6); color: white; padding: 5px; border-radius: 3px; font-size: 12px; pointer-events: none; z-index: 10;
}
.hotspot-list-item { background-color: #e9e9e9; font-size: 0.9em;}
.hotspot-form-section { margin-top: 20px; padding: 15px; background-color: #eef; border-radius: 5px; }
.target-field { margin-top: 10px; padding-top: 10px; border-top: 1px dashed #ccc; }
</style>
</head>
<body>
<div class="container">
<h1>Редактировать Поинт: {{ point.name }}</h1>
<a href="{{ url_for('admin_panel') }}" class="button">« Назад к админ-панели</a>
<div class="section">
<h2>Предпросмотр Панорамы и Хотспоты</h2>
<div id="panorama-preview-container"></div>
<p>Кликните на панораму, чтобы получить координаты (Pitch/Yaw) для нового хотспота.</p>
<div id="hotspot-form-section" class="hotspot-form-section">
<h3>Добавить Хотспот</h3>
<form id="hotspot-form" action="/admin/points/{{ point.id }}/hotspots/save" method="post">
<input type="hidden" name="hotspot_id" id="hotspot_id">
<label for="hotspot_pitch">Pitch (Вертикальный угол):</label>
<input type="number" step="any" id="hotspot_pitch" name="pitch" required readonly placeholder="Кликните на панораму">
<label for="hotspot_yaw">Yaw (Горизонтальный угол):</label>
<input type="number" step="any" id="hotspot_yaw" name="yaw" required readonly placeholder="Кликните на панораму">
<label for="hotspot_text">Текст для кнопки хотспота:</label>
<input type="text" id="hotspot_text" name="text" value="Подробнее" required>
<label for="hotspot_type">Тип Хотспота:</label>
<select id="hotspot_type" name="type" required onchange="updateHotspotTarget()">
<option value="">-- Выберите тип --</option>
<option value="point">Переход на другой Поинт</option>
<option value="link">Внешняя Ссылка</option>
<option value="category">Показать Категорию Товаров</option>
<option value="product">Показать Отдельный Товар</option>
</select>
<div id="target-point" class="hidden target-field">
<label for="target_point_id">Целевой Поинт:</label>
<select id="target_point_id" name="target_point_id">
<option value="">-- Выберите поинт --</option>
{% for p in all_points %}
<option value="{{ p.id }}">{{ p.name }}</option>
{% endfor %}
</select>
</div>
<div id="target-link" class="hidden target-field">
<label for="target_link_url">URL Ссылки:</label>
<input type="url" id="target_link_url" name="target_link_url" placeholder="https://example.com">
</div>
<div id="target-category" class="hidden target-field">
<label for="target_category_id">Категория Товаров:</label>
<select id="target_category_id" name="target_category_id">
<option value="">-- Выберите категорию --</option>
{% for c in all_categories %}
<option value="{{ c.id }}">{{ c.name }}</option>
{% endfor %}
</select>
</div>
<div id="target-product" class="hidden target-field">
<label for="target_product_id">Отдельный Товар:</label>
<select id="target_product_id" name="target_product_id">
<option value="">-- Выберите товар --</option>
{% for p in all_standalone_products %}
<option value="{{ p.id }}">{{ p.name }}</option>
{% endfor %}
</select>
{% if not all_standalone_products %}
<small style="color: #888;">Нет отдельных товаров. <a href="{{ url_for('admin_manage_standalone_products') }}">Добавить?</a></small>
{% endif %}
</div>
<button type="submit">Сохранить Хотспот</button>
<button type="button" onclick="clearHotspotForm()">Очистить форму</button>
</form>
</div>
<h3>Существующие Хотспоты</h3>
<ul id="hotspots-list">
{% for hotspot in point.get('hotspots', []) %}
<li class="hotspot-list-item" id="hotspot-item-{{ hotspot.id }}">
<span>
<strong>Тип:</strong> {{ hotspot.type }} |
<strong>Цель:</strong> {{ hotspot.target }} |
<strong>Текст:</strong> {{ hotspot.text }} |
<strong>Коорд:</strong> (P: {{ "%.2f"|format(hotspot.pitch) }}, Y: {{ "%.2f"|format(hotspot.yaw) }})
</span>
<div>
<form action="/admin/points/{{ point.id }}/hotspots/delete/{{ hotspot.id }}" method="post" style="display: inline;">
<button type="submit" class="delete-button" onclick="return confirm('Удалить этот хотспот?');">Удалить</button>
</form>
</div>
</li>
{% else %}
<p>Нет хотспотов для этого поинта.</p>
{% endfor %}
</ul>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/pannellum@2.5.6/build/pannellum.js"></script>
<script>
const panoramaUrl = "{{ panorama_url }}";
const pointId = "{{ point.id }}";
const existingHotspots = {{ point.get('hotspots', []) | tojson }};
let editorViewer = null;
function updateHotspotTarget() {
const type = document.getElementById('hotspot_type').value;
document.querySelectorAll('.target-field').forEach(el => el.classList.add('hidden'));
document.getElementById('target_point_id').required = false;
document.getElementById('target_link_url').required = false;
document.getElementById('target_category_id').required = false;
document.getElementById('target_product_id').required = false;
if (type === 'point') {
document.getElementById('target-point').classList.remove('hidden');
document.getElementById('target_point_id').required = true;
} else if (type === 'link') {
document.getElementById('target-link').classList.remove('hidden');
document.getElementById('target_link_url').required = true;
} else if (type === 'category') {
document.getElementById('target-category').classList.remove('hidden');
document.getElementById('target_category_id').required = true;
} else if (type === 'product') {
document.getElementById('target-product').classList.remove('hidden');
document.getElementById('target_product_id').required = true;
}
}
function clearHotspotForm() {
document.getElementById('hotspot-form').reset();
document.getElementById('hotspot_id').value = '';
document.getElementById('hotspot_pitch').readOnly = true;
document.getElementById('hotspot_yaw').readOnly = true;
document.getElementById('hotspot_pitch').placeholder = 'Кликните на панораму';
document.getElementById('hotspot_yaw').placeholder = 'Кликните на панораму';
updateHotspotTarget();
}
try {
editorViewer = pannellum.viewer('panorama-preview-container', {
"type": "equirectangular",
"panorama": panoramaUrl,
"autoLoad": true,
"showControls": true,
"hotSpotDebug": false,
"hotSpots": existingHotspots.map(hs => ({
"pitch": hs.pitch,
"yaw": hs.yaw,
"cssClass": "pannellum-info-hotspot",
"text": `[${hs.type}] ${hs.text}`
}))
});
editorViewer.on('mousedown', function(event) {
if (event.target.classList.contains('pnlm-hotspot')) return;
const coords = editorViewer.mouseEventToCoords(event);
document.getElementById('hotspot_pitch').value = coords[0].toFixed(4);
document.getElementById('hotspot_yaw').value = coords[1].toFixed(4);
document.getElementById('hotspot_pitch').readOnly = true;
document.getElementById('hotspot_yaw').readOnly = true;
document.getElementById('hotspot_pitch').placeholder = '';
document.getElementById('hotspot_yaw').placeholder = '';
document.getElementById('hotspot_text').focus();
});
editorViewer.on('error', function(err) {
console.error('Ошибка редактора Pannellum:', err);
document.getElementById('panorama-preview-container').innerHTML = '<p style="color: red; text-align: center;">Ошибка загрузки предпросмотра панорамы: ' + err + '</p>';
});
} catch (e) {
console.error("Не удалось инициализировать редактор Pannellum:", e);
document.getElementById('panorama-preview-container').innerHTML = '<p style="color: red; text-align: center;">Не удалось инициализировать просмотрщик панорам.</p>';
}
updateHotspotTarget();
</script>
</body>
</html>
"""
return render_template_string(edit_point_html, point=point, panorama_url=panorama_url,
all_points=all_points, all_categories=all_categories,
all_standalone_products=all_standalone_products)
@app.route('/admin/points/<point_id>/hotspots/save', methods=['POST'])
def admin_save_hotspot(point_id):
point = find_point(point_id)
if not point:
return redirect(url_for('admin_panel'))
try:
pitch_str = request.form.get('pitch')
yaw_str = request.form.get('yaw')
text = request.form.get('text')
type = request.form.get('type')
hotspot_id = request.form.get('hotspot_id')
if not pitch_str or not yaw_str:
logging.warning("Координаты Pitch/Yaw отсутствуют. Кликните на панораму.")
return redirect(url_for('admin_edit_point', point_id=point_id))
pitch = float(pitch_str)
yaw = float(yaw_str)
target = None
if type == 'point':
target = request.form.get('target_point_id')
elif type == 'link':
target = request.form.get('target_link_url')
elif type == 'category':
target = request.form.get('target_category_id')
elif type == 'product':
target = request.form.get('target_product_id')
if not all([pitch is not None, yaw is not None, text, type, target]):
logging.warning("Отсутствуют данные для сохранения хотспота.")
return redirect(url_for('admin_edit_point', point_id=point_id))
new_hotspot = {
"id": generate_id(),
"pitch": pitch,
"yaw": yaw,
"type": type,
"target": target,
"text": text
}
point.setdefault('hotspots', []).append(new_hotspot)
save_data()
logging.info(f"Добавлен хотспот к поинту {point_id}: Тип={type}, Цель={target}")
except ValueError:
logging.error("Неверный формат числа для pitch или yaw.")
except Exception as e:
logging.error(f"Ошибка сохранения хотспота для поинта {point_id}: {e}")
return redirect(url_for('admin_edit_point', point_id=point_id))
@app.route('/admin/points/<point_id>/hotspots/delete/<hotspot_id>', methods=['POST'])
def admin_delete_hotspot(point_id, hotspot_id):
point = find_point(point_id)
if point and 'hotspots' in point:
initial_length = len(point['hotspots'])
point['hotspots'] = [hs for hs in point['hotspots'] if hs.get('id') != hotspot_id]
if len(point['hotspots']) < initial_length:
save_data()
logging.info(f"Удален хотспот {hotspot_id} из поинта {point_id}")
else:
logging.warning(f"Хотспот {hotspot_id} не найден в поинте {point_id} для удаления.")
else:
logging.warning(f"Поинт {point_id} не найден или не имеет хотспотов для удаления.")
return redirect(url_for('admin_edit_point', point_id=point_id))
@app.route('/admin/categories/add', methods=['POST'])
def admin_add_category():
category_name = request.form.get('category_name')
if category_name:
if any(c.get('name', '').lower() == category_name.lower() for c in app_data.get('categories', [])):
logging.warning(f"Попытка добавить дубликат имени категории: {category_name}")
else:
new_category = {
"id": generate_id(),
"name": category_name,
"products": []
}
app_data.setdefault('categories', []).append(new_category)
save_data()
logging.info(f"Добавлена категория: {category_name}")
else:
logging.warning("Попытка добавить категорию с пустым именем.")
return redirect(url_for('admin_panel'))
@app.route('/admin/categories/delete/<category_id>', methods=['POST'])
def admin_delete_category(category_id):
category_index = next((index for (index, c) in enumerate(app_data.get('categories', [])) if c.get('id') == category_id), None)
if category_index is not None:
category = app_data['categories'][category_index]
category_name = category.get('name', category_id)
products_to_delete = category.get('products', [])
hf_delete_failed_overall = False
for product in products_to_delete:
if delete_product_files_from_hf(product):
hf_delete_failed_overall = True
deleted_category = app_data['categories'].pop(category_index)
save_data()
logging.info(f"Удалена категория: {category_name} и ее {len(products_to_delete)} товаров.")
for product in products_to_delete:
cleanup_local_product_files(product)
if hf_delete_failed_overall:
pass
else:
pass
else:
logging.warning(f"Попытка удалить несуществующую категорию с ID: {category_id}")
return redirect(url_for('admin_panel'))
@app.route('/admin/category/<category_id>/products', methods=['GET'])
def admin_manage_category_products(category_id):
category = find_category(category_id)
if not category:
return redirect(url_for('admin_panel'))
products_with_urls = []
for product in category.get('products', []):
photo_urls = [get_hf_image_url(REPO_ID, p) for p in product.get('photo_paths', []) if p]
products_with_urls.append({**product, 'photo_urls': photo_urls})
manage_products_html = """
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Управление Товарами: {{ category.name }}</title>
<style>
body { font-family: sans-serif; padding: 20px; background-color: #f4f4f4; }
.container { max-width: 1200px; margin: auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h1, h2, h3 { color: #333; border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 20px; }
.section { margin-bottom: 30px; padding: 20px; border: 1px solid #ddd; border-radius: 5px; background-color: #fafafa; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type=text], input[type=number], textarea, select {
width: calc(100% - 22px); padding: 10px; margin-bottom: 15px; border: 1px solid #ccc; border-radius: 4px;
}
input[type=file] { margin-bottom: 15px; }
button, .button {
background-color: #5cb85c; color: white; padding: 10px 15px; border: none; border-radius: 4px;
cursor: pointer; text-decoration: none; display: inline-block; margin-right: 5px;
}
button:hover, .button:hover { background-color: #4cae4c; }
.delete-button { background-color: #d9534f; }
.delete-button:hover { background-color: #c9302c; }
.edit-button { background-color: #f0ad4e; }
.edit-button:hover { background-color: #ec971f; }
ul { list-style: none; padding: 0; }
li { background: #fff; border: 1px solid #eee; margin-bottom: 10px; padding: 15px; border-radius: 4px; display: flex; flex-direction: column; }
.product-info { display: flex; justify-content: space-between; align-items: flex-start; width: 100%; gap: 15px; }
.product-details { flex-grow: 1; margin-right: 10px; }
.product-actions { flex-shrink: 0; white-space: nowrap; }
.product-photos { margin-top: 10px; }
.product-photos img { max-width: 80px; max-height: 80px; margin-right: 5px; border: 1px solid #eee; vertical-align: middle; border-radius: 4px;}
.edit-form { margin-top: 15px; padding: 15px; background: #f9f9f9; border: 1px dashed #ccc; border-radius: 5px; display: none; }
.edit-form.visible { display: block; }
</style>
</head>
<body>
<div class="container">
<h1>Управление Товарами в Категории: {{ category.name }}</h1>
<a href="{{ url_for('admin_panel') }}" class="button">« Назад к админ-панели</a>
<div class="section">
<h2>Добавить Новый Товар</h2>
<form action="{{ url_for('admin_add_product_to_category', category_id=category.id) }}" method="post" enctype="multipart/form-data">
<label for="product_name">Название:</label>
<input type="text" id="product_name" name="name" required>
<label for="product_price">Цена (SOM):</label>
<input type="number" id="product_price" name="price" step="0.01" min="0" required>
<label for="product_description">Описание:</label>
<textarea id="product_description" name="description" rows="3" required></textarea>
<label for="product_photos">Фотографии (до 10, JPG/PNG/GIF):</label>
<input type="file" id="product_photos" name="photos" accept="image/*" multiple>
<button type="submit">Добавить Товар в Категорию</button>
</form>
</div>
<div class="section">
<h2>Существующие Товары в "{{ category.name }}"</h2>
<ul id="products-list">
{% for product in products %}
<li id="product-item-{{ product.id }}">
<div class="product-info">
<div class="product-details">
<strong>{{ product.name }}</strong><br>
Цена: {{ product.price }} SOM<br>
Описание: {{ product.description[:100] }}{% if product.description|length > 100 %}...{% endif %}<br>
<small>ID: {{ product.id }}</small>
</div>
<div class="product-actions">
<button class="edit-button" onclick="toggleEditForm('{{ product.id }}')">Редактировать</button>
<form action="{{ url_for('admin_delete_product_from_category', category_id=category.id, product_id=product.id) }}" method="post" style="display: inline;">
<button type="submit" class="delete-button" onclick="return confirm('Вы уверены, что хотите удалить этот товар из категории?');">Удалить</button>
</form>
</div>
</div>
<div class="product-photos">
{% if product.photo_urls %}
{% for url in product.photo_urls %}
<img src="{{ url }}" alt="{{ product.name }} фото">
{% endfor %}
{% else %}
<small>Нет фото</small>
{% endif %}
</div>
<div id="edit-form-{{ product.id }}" class="edit-form">
<h3>Редактировать: {{ product.name }}</h3>
<form action="{{ url_for('admin_edit_product_in_category', category_id=category.id, product_id=product.id) }}" method="post" enctype="multipart/form-data">
<label>Название:</label>
<input type="text" name="name" value="{{ product.name }}" required>
<label>Цена (SOM):</label>
<input type="number" name="price" step="0.01" min="0" value="{{ product.price }}" required>
<label>Описание:</label>
<textarea name="description" rows="3" required>{{ product.description }}</textarea>
<label>Заменить Фотографии (опционально, до 10):</label>
<input type="file" name="photos" accept="image/*" multiple>
<small>Если выбраны новые фото, они ЗАМЕНЯТ все старые.</small><br><br>
<button type="submit">Сохранить Изменения</button>
<button type="button" onclick="toggleEditForm('{{ product.id }}')">Отмена</button>
</form>
</div>
</li>
{% else %}
<p>В этой категории пока нет товаров.</p>
{% endfor %}
</ul>
</div>
</div>
<script>
function toggleEditForm(productId) {
const form = document.getElementById(`edit-form-${productId}`);
form.classList.toggle('visible');
}
</script>
</body>
</html>
"""
return render_template_string(manage_products_html, category=category, products=products_with_urls)
@app.route('/admin/category/<category_id>/products/add', methods=['POST'])
def admin_add_product_to_category(category_id):
category = find_category(category_id)
if not category:
return redirect(url_for('admin_panel'))
name = request.form.get('name')
price = request.form.get('price')
description = request.form.get('description')
photos = request.files.getlist('photos')
if not name or price is None or description is None:
return redirect(url_for('admin_manage_category_products', category_id=category_id))
try:
price_float = float(price)
if price_float < 0: raise ValueError("Цена не может быть отрицательной")
except ValueError:
return redirect(url_for('admin_manage_category_products', category_id=category_id))
photo_hf_paths = []
upload_failed = False
uploaded_local_paths = []
for photo in photos[:10]:
if photo and photo.filename:
filename = secure_filename(photo.filename)
unique_filename = f"{generate_id()}_{filename}"
local_path = os.path.join(PRODUCT_PHOTO_UPLOADS, unique_filename)
hf_path = f"product_photos/{unique_filename}"
uploaded_local_paths.append(local_path)
try:
photo.save(local_path)
commit_msg = f"Добавить фото для товара: {name} в категорию {category.get('name')}"
if upload_file_to_hf(local_path, hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg):
photo_hf_paths.append(hf_path)
else:
upload_failed = True
logging.error(f"Не удалось загрузить фото товара {filename} в HF.")
except Exception as e:
upload_failed = True
logging.error(f"Ошибка обработки фото товара {filename}: {e}")
new_product = {
"id": generate_id(),
"name": name,
"price": price_float,
"description": description,
"photo_paths": photo_hf_paths
}
category.setdefault('products', []).append(new_product)
save_data()
logging.info(f"Добавлен товар '{name}' в категорию '{category.get('name')}'. Фото: {len(photo_hf_paths)}.")
if upload_failed:
pass
else:
pass
for local_path in uploaded_local_paths:
if os.path.exists(local_path):
try:
os.remove(local_path)
except OSError as e:
logging.warning(f"Не удалось удалить временный локальный файл фото товара {local_path}: {e}")
return redirect(url_for('admin_manage_category_products', category_id=category_id))
@app.route('/admin/category/<category_id>/products/edit/<product_id>', methods=['POST'])
def admin_edit_product_in_category(category_id, product_id):
category = find_category(category_id)
product = find_product_in_category(category_id, product_id)
if not category or not product:
return redirect(url_for('admin_panel'))
name = request.form.get('name')
price = request.form.get('price')
description = request.form.get('description')
new_photos = request.files.getlist('photos')
if not name or price is None or description is None:
return redirect(url_for('admin_manage_category_products', category_id=category_id))
try:
price_float = float(price)
if price_float < 0: raise ValueError("Цена не может быть отрицательной")
except ValueError:
return redirect(url_for('admin_manage_category_products', category_id=category_id))
product['name'] = name
product['price'] = price_float
product['description'] = description
if new_photos and any(p.filename for p in new_photos):
logging.info(f"Замена фото для товара {product_id} в категории {category_id}...")
old_photo_paths = product.get('photo_paths', [])
new_photo_hf_paths = []
upload_failed = False
uploaded_local_paths = []
for photo in new_photos[:10]:
if photo and photo.filename:
filename = secure_filename(photo.filename)
unique_filename = f"{generate_id()}_{filename}"
local_path = os.path.join(PRODUCT_PHOTO_UPLOADS, unique_filename)
hf_path = f"product_photos/{unique_filename}"
uploaded_local_paths.append(local_path)
try:
photo.save(local_path)
commit_msg = f"Обновить фото для товара: {name}"
if upload_file_to_hf(local_path, hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg):
new_photo_hf_paths.append(hf_path)
else:
upload_failed = True
logging.error(f"Не удалось загрузить новое фото {filename} в HF.")
except Exception as e:
upload_failed = True
logging.error(f"Ошибка обработки нового фото {filename}: {e}")
if len(new_photo_hf_paths) > 0:
logging.info(f"Удаление {len(old_photo_paths)} старых фото для товара {product_id} из HF...")
hf_delete_failed = False
for old_hf_path in old_photo_paths:
commit_msg = f"Удалить старое фото {os.path.basename(old_hf_path)} для товара {name} при обновлении"
if not delete_file_from_hf(old_hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg):
logging.warning(f"Не удалось удалить старое фото {old_hf_path} из HF при обновлении.")
hf_delete_failed = True
product['photo_paths'] = new_photo_hf_paths
if hf_delete_failed:
pass
elif upload_failed:
logging.error(f"Замена фото не удалась для товара {product_id}. Старые фото сохранены.")
for local_path in uploaded_local_paths:
if os.path.exists(local_path):
try:
os.remove(local_path)
except OSError as e:
logging.warning(f"Не удалось удалить временный локальный файл нового фото {local_path}: {e}")
save_data()
logging.info(f"Обновлен товар '{name}' (ID: {product_id}) в категории '{category.get('name')}'.")
return redirect(url_for('admin_manage_category_products', category_id=category_id))
@app.route('/admin/category/<category_id>/products/delete/<product_id>', methods=['POST'])
def admin_delete_product_from_category(category_id, product_id):
category = find_category(category_id)
if not category or 'products' not in category:
return redirect(url_for('admin_panel'))
product_index = next((index for (index, p) in enumerate(category['products']) if p.get('id') == product_id), None)
if product_index is not None:
product = category['products'][product_index]
product_name = product.get('name', product_id)
hf_delete_failed = delete_product_files_from_hf(product)
deleted_product = category['products'].pop(product_index)
save_data()
logging.info(f"Удален товар: {product_name} из категории {category.get('name')}")
cleanup_local_product_files(product)
if hf_delete_failed:
pass
else:
pass
else:
logging.warning(f"Товар {product_id} не найден в категории {category_id} для удаления.")
return redirect(url_for('admin_manage_category_products', category_id=category_id))
@app.route('/admin/products/standalone', methods=['GET'])
def admin_manage_standalone_products():
products_with_urls = []
for product in app_data.get('standalone_products', []):
photo_urls = [get_hf_image_url(REPO_ID, p) for p in product.get('photo_paths', []) if p]
products_with_urls.append({**product, 'photo_urls': photo_urls})
manage_standalone_html = """
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Управление Отдельными Товарами</title>
<style>
body { font-family: sans-serif; padding: 20px; background-color: #f4f4f4; }
.container { max-width: 1200px; margin: auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h1, h2, h3 { color: #333; border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 20px; }
.section { margin-bottom: 30px; padding: 20px; border: 1px solid #ddd; border-radius: 5px; background-color: #fafafa; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type=text], input[type=number], textarea, select {
width: calc(100% - 22px); padding: 10px; margin-bottom: 15px; border: 1px solid #ccc; border-radius: 4px;
}
input[type=file] { margin-bottom: 15px; }
button, .button {
background-color: #5cb85c; color: white; padding: 10px 15px; border: none; border-radius: 4px;
cursor: pointer; text-decoration: none; display: inline-block; margin-right: 5px;
}
button:hover, .button:hover { background-color: #4cae4c; }
.delete-button { background-color: #d9534f; }
.delete-button:hover { background-color: #c9302c; }
.edit-button { background-color: #f0ad4e; }
.edit-button:hover { background-color: #ec971f; }
ul { list-style: none; padding: 0; }
li { background: #fff; border: 1px solid #eee; margin-bottom: 10px; padding: 15px; border-radius: 4px; display: flex; flex-direction: column; }
.product-info { display: flex; justify-content: space-between; align-items: flex-start; width: 100%; gap: 15px;}
.product-details { flex-grow: 1; margin-right: 10px; }
.product-actions { flex-shrink: 0; white-space: nowrap; }
.product-photos { margin-top: 10px; }
.product-photos img { max-width: 80px; max-height: 80px; margin-right: 5px; border: 1px solid #eee; vertical-align: middle; border-radius: 4px;}
.edit-form { margin-top: 15px; padding: 15px; background: #f9f9f9; border: 1px dashed #ccc; border-radius: 5px; display: none; }
.edit-form.visible { display: block; }
</style>
</head>
<body>
<div class="container">
<h1>Управление Отдельными Товарами</h1>
<p>Эти товары можно выбирать напрямую при создании хотспота типа "Товар". Они не принадлежат к какой-либо категории.</p>
<a href="{{ url_for('admin_panel') }}" class="button">« Назад к главной админ-панели</a>
<div class="section">
<h2>Добавить Новый Отдельный Товар</h2>
<form action="{{ url_for('admin_add_standalone_product') }}" method="post" enctype="multipart/form-data">
<label for="product_name">Название:</label>
<input type="text" id="product_name" name="name" required>
<label for="product_price">Цена (SOM):</label>
<input type="number" id="product_price" name="price" step="0.01" min="0" required>
<label for="product_description">Описание:</label>
<textarea id="product_description" name="description" rows="3" required></textarea>
<label for="product_photos">Фотографии (до 10, JPG/PNG/GIF):</label>
<input type="file" id="product_photos" name="photos" accept="image/*" multiple>
<button type="submit">Добавить Отдельный Товар</button>
</form>
</div>
<div class="section">
<h2>Существующие Отдельные Товары</h2>
<ul id="products-list">
{% for product in products %}
<li id="product-item-{{ product.id }}">
<div class="product-info">
<div class="product-details">
<strong>{{ product.name }}</strong><br>
Цена: {{ product.price }} SOM<br>
Описание: {{ product.description[:100] }}{% if product.description|length > 100 %}...{% endif %}<br>
<small>ID: {{ product.id }}</small>
</div>
<div class="product-actions">
<button class="edit-button" onclick="toggleEditForm('{{ product.id }}')">Редактировать</button>
<form action="{{ url_for('admin_delete_standalone_product', product_id=product.id) }}" method="post" style="display: inline;">
<button type="submit" class="delete-button" onclick="return confirm('Вы уверены, что хотите удалить этот отдельный товар? Это действие необратимо.');">Удалить</button>
</form>
</div>
</div>
<div class="product-photos">
{% if product.photo_urls %}
{% for url in product.photo_urls %}
<img src="{{ url }}" alt="{{ product.name }} фото">
{% endfor %}
{% else %}
<small>Нет фото</small>
{% endif %}
</div>
<div id="edit-form-{{ product.id }}" class="edit-form">
<h3>Редактировать: {{ product.name }}</h3>
<form action="{{ url_for('admin_edit_standalone_product', product_id=product.id) }}" method="post" enctype="multipart/form-data">
<label>Название:</label>
<input type="text" name="name" value="{{ product.name }}" required>
<label>Цена (SOM):</label>
<input type="number" name="price" step="0.01" min="0" value="{{ product.price }}" required>
<label>Описание:</label>
<textarea name="description" rows="3" required>{{ product.description }}</textarea>
<label>Заменить Фотографии (опционально, до 10):</label>
<input type="file" name="photos" accept="image/*" multiple>
<small>Если выбраны новые фото, они ЗАМЕНЯТ все старые.</small><br><br>
<button type="submit">Сохранить Изменения</button>
<button type="button" onclick="toggleEditForm('{{ product.id }}')">Отмена</button>
</form>
</div>
</li>
{% else %}
<p>Пока нет отдельных товаров.</p>
{% endfor %}
</ul>
</div>
</div>
<script>
function toggleEditForm(productId) {
const form = document.getElementById(`edit-form-${productId}`);
form.classList.toggle('visible');
}
</script>
</body>
</html>
"""
return render_template_string(manage_standalone_html, products=products_with_urls)
@app.route('/admin/products/standalone/add', methods=['POST'])
def admin_add_standalone_product():
name = request.form.get('name')
price = request.form.get('price')
description = request.form.get('description')
photos = request.files.getlist('photos')
if not name or price is None or description is None:
return redirect(url_for('admin_manage_standalone_products'))
try:
price_float = float(price)
if price_float < 0: raise ValueError("Цена не может быть отрицательной")
except ValueError:
return redirect(url_for('admin_manage_standalone_products'))
photo_hf_paths = []
upload_failed = False
uploaded_local_paths = []
for photo in photos[:10]:
if photo and photo.filename:
filename = secure_filename(photo.filename)
unique_filename = f"{generate_id()}_{filename}"
local_path = os.path.join(PRODUCT_PHOTO_UPLOADS, unique_filename)
hf_path = f"product_photos/standalone/{unique_filename}"
uploaded_local_paths.append(local_path)
try:
photo.save(local_path)
commit_msg = f"Добавить фото для отдельного товара: {name}"
if upload_file_to_hf(local_path, hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg):
photo_hf_paths.append(hf_path)
else:
upload_failed = True
logging.error(f"Не удалось загрузить фото отдельного товара {filename} в HF.")
except Exception as e:
upload_failed = True
logging.error(f"Ошибка обработки фото отдельного товара {filename}: {e}")
new_product = {
"id": generate_id(),
"name": name,
"price": price_float,
"description": description,
"photo_paths": photo_hf_paths
}
app_data.setdefault('standalone_products', []).append(new_product)
save_data()
logging.info(f"Добавлен отдельный товар '{name}'. Фото: {len(photo_hf_paths)}.")
if upload_failed:
pass
else:
pass
for local_path in uploaded_local_paths:
if os.path.exists(local_path):
try:
os.remove(local_path)
except OSError as e:
logging.warning(f"Не удалось удалить временный локальный файл фото товара {local_path}: {e}")
return redirect(url_for('admin_manage_standalone_products'))
@app.route('/admin/products/standalone/edit/<product_id>', methods=['POST'])
def admin_edit_standalone_product(product_id):
product = find_standalone_product(product_id)
if not product:
return redirect(url_for('admin_manage_standalone_products'))
name = request.form.get('name')
price = request.form.get('price')
description = request.form.get('description')
new_photos = request.files.getlist('photos')
if not name or price is None or description is None:
return redirect(url_for('admin_manage_standalone_products'))
try:
price_float = float(price)
if price_float < 0: raise ValueError("Цена не может быть отрицательной")
except ValueError:
return redirect(url_for('admin_manage_standalone_products'))
product['name'] = name
product['price'] = price_float
product['description'] = description
if new_photos and any(p.filename for p in new_photos):
logging.info(f"Замена фото для отдельного товара {product_id}...")
old_photo_paths = product.get('photo_paths', [])
new_photo_hf_paths = []
upload_failed = False
uploaded_local_paths = []
for photo in new_photos[:10]:
if photo and photo.filename:
filename = secure_filename(photo.filename)
unique_filename = f"{generate_id()}_{filename}"
local_path = os.path.join(PRODUCT_PHOTO_UPLOADS, unique_filename)
hf_path = f"product_photos/standalone/{unique_filename}"
uploaded_local_paths.append(local_path)
try:
photo.save(local_path)
commit_msg = f"Обновить фото для отдельного товара: {name}"
if upload_file_to_hf(local_path, hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg):
new_photo_hf_paths.append(hf_path)
else:
upload_failed = True
logging.error(f"Не удалось загрузить новое фото {filename} в HF.")
except Exception as e:
upload_failed = True
logging.error(f"Ошибка обработки нового фото {filename}: {e}")
if len(new_photo_hf_paths) > 0:
logging.info(f"Удаление {len(old_photo_paths)} старых фото для товара {product_id} из HF...")
hf_delete_failed = False
for old_hf_path in old_photo_paths:
commit_msg = f"Удалить старое фото {os.path.basename(old_hf_path)} для товара {name} при обновлении"
if not delete_file_from_hf(old_hf_path, REPO_ID, HF_TOKEN_WRITE, commit_msg):
logging.warning(f"Не удалось удалить старое фото {old_hf_path} из HF при обновлении.")
hf_delete_failed = True
product['photo_paths'] = new_photo_hf_paths
if hf_delete_failed:
pass
elif upload_failed:
logging.error(f"Замена фото не удалась для товара {product_id}. Старые фото сохранены.")
for local_path in uploaded_local_paths:
if os.path.exists(local_path):
try:
os.remove(local_path)
except OSError as e:
logging.warning(f"Не удалось удалить временный локальный файл нового фото {local_path}: {e}")
save_data()
logging.info(f"Обновлен отдельный товар '{name}' (ID: {product_id}).")
return redirect(url_for('admin_manage_standalone_products', product_id=product_id))
@app.route('/admin/products/standalone/delete/<product_id>', methods=['POST'])
def admin_delete_standalone_product(product_id):
product_index = next((index for (index, p) in enumerate(app_data.get('standalone_products', [])) if p.get('id') == product_id), None)
if product_index is not None:
product = app_data['standalone_products'][product_index]
product_name = product.get('name', product_id)
hf_delete_failed = delete_product_files_from_hf(product)
deleted_product = app_data['standalone_products'].pop(product_index)
save_data()
logging.info(f"Удален отдельный товар: {product_name}")
cleanup_local_product_files(product)
if hf_delete_failed:
pass
else:
pass
else:
logging.warning(f"Отдельный товар {product_id} не найден для удаления.")
return redirect(url_for('admin_manage_standalone_products'))
@app.route('/backup', methods=['POST'])
def manual_backup():
logging.info("Запущено ручное резервное копирование базы данных.")
save_data()
return redirect(url_for('admin_panel'))
if __name__ == '__main__':
logging.info("Попытка начальной загрузки данных...")
download_db_from_hf()
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
backup_thread.start()
logging.info("Поток периодического резервного копирования запущен.")
logging.info("Запуск приложения Flask...")
app.run(debug=False, host='0.0.0.0', port=7860)