Fix cooldown timer visibility after Start; add real-time modal validation; update context.md with prod server
- Cooldown counter now always rendered (hidden) when auto_reply active, shown dynamically via polling - Modal form validates fields on blur/input with red highlight via .input-invalid class - Telegram: switched back to tel.4mont.ru proxy (working via /etc/hosts on 192.168.33.19) - context.md: added prod server 45.129.3.83, deploy commands, NPM routing notes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1481,7 +1481,9 @@ def request_access():
|
|||||||
f"{geo_line}\n"
|
f"{geo_line}\n"
|
||||||
f"🖥 <b>IP:</b> {ip}"
|
f"🖥 <b>IP:</b> {ip}"
|
||||||
)
|
)
|
||||||
_send_telegram(text)
|
ok = _send_telegram(text)
|
||||||
|
if not ok:
|
||||||
|
return jsonify({"ok": False, "error": "Не удалось отправить запрос. Попробуйте позже."}), 500
|
||||||
return jsonify({"ok": True})
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+61
-27
@@ -1,32 +1,66 @@
|
|||||||
# Контекст проекта Wildberries
|
# Контекст проекта WBfeed
|
||||||
сервер ssh 192.168.33.19
|
|
||||||
путь /home/sites/wild
|
|
||||||
## Что это за проект
|
## Что это за проект
|
||||||
- Flask-приложение для работы с отзывами Wildberries.
|
- Flask-приложение для автоответов на отзывы Wildberries.
|
||||||
- Поддерживает вход/регистрацию, личный кабинет с токенами, админку, ответы на отзывы.
|
- Поддерживает вход, личный кабинет с токенами магазинов, админку, журнал автоответов.
|
||||||
- Есть автоответ для новых неотвеченных отзывов 5★ и 4★ с рандомным выбором ответа из пулов.
|
- Автоответ для 5★, 4★, 3★, 2★, 1★ с настраиваемыми пулами шаблонов.
|
||||||
|
- Уведомления в Telegram через прокси tel.4mont.ru при запросе доступа.
|
||||||
|
|
||||||
## Текущая структура (локальная копия)
|
## Серверы
|
||||||
- `remote_copy/app.py` — основной backend (Flask + SQLite).
|
|
||||||
- `remote_copy/templates/*.html` — шаблоны интерфейса.
|
|
||||||
- `remote_copy/static/styles.css` — стили.
|
|
||||||
- `remote_copy/export_last_answers*.py` — утилиты выгрузки ответов.
|
|
||||||
- `promt.md` — заметка с сервером и путём.
|
|
||||||
|
|
||||||
## Сервер и запуск
|
### Основной (локальная сеть)
|
||||||
- Сервер: `192.168.33.19`
|
- SSH: `root@192.168.33.19`, пароль: `utOgbZ09`
|
||||||
- Путь проекта на сервере: `/home/sites/wild`
|
- Путь проекта: `/home/sites/wild`
|
||||||
- Запуск: Docker Compose, проброс `54119 -> 5000`
|
- Docker Compose, порт `54119 → 5000`
|
||||||
|
- В `/etc/hosts` прописано: `192.168.33.19 tel.4mont.ru` (Telegram прокси)
|
||||||
|
|
||||||
## Что уже реализовано
|
### Продакшн (публичный)
|
||||||
- Пользователи и роли (`is_admin`, `is_active`).
|
- SSH: `root@45.129.3.83`, пароль: `utOgbZ09ruslan`
|
||||||
- Управление токенами в кабинете.
|
- Путь проекта: `/home/docker/wbfeed`
|
||||||
- Получение всех/неотвеченных отзывов + фильтры по звёздам.
|
- Docker Compose, порт `2323 → 5000`, контейнер `wbfeed-app-1`
|
||||||
- Ручной ответ одному/всем отзывам.
|
- NPM (Nginx Proxy Manager) на том же сервере
|
||||||
- Переключатель автоответа.
|
- Для NPM → wbfeed: Forward `172.19.0.1:2323` (шлюз Docker-сети npm)
|
||||||
- Настраиваемые пулы автоответов для 5★ и 4★ через UI.
|
- Для любого нового Docker-проекта через NPM: тот же IP `172.19.0.1`, другой порт
|
||||||
- Журнал автоответов (последние 100 записей).
|
|
||||||
- Базовая мобильная адаптация.
|
|
||||||
|
|
||||||
## Известное ограничение
|
## Локальная структура (только контекст, код деплоится на сервер)
|
||||||
- API Wildberries периодически отдаёт `429 Too Many Requests` при массовой выгрузке исторических ответов.
|
- `app.py` — основной backend (Flask + SQLite)
|
||||||
|
- `templates/*.html` — шаблоны интерфейса
|
||||||
|
- `static/styles.css` — стили
|
||||||
|
- `context.md` — этот файл, хранится локально в git
|
||||||
|
|
||||||
|
## Деплой
|
||||||
|
|
||||||
|
### На основной сервер (192.168.33.19)
|
||||||
|
```bash
|
||||||
|
sshpass -p 'utOgbZ09' scp <файл> root@192.168.33.19:/home/sites/wild/<путь>
|
||||||
|
sshpass -p 'utOgbZ09' ssh root@192.168.33.19 "docker restart wild-app-1"
|
||||||
|
# templates/ и static/ — перезапуск не нужен, Flask видит сразу
|
||||||
|
```
|
||||||
|
|
||||||
|
### На продакшн (45.129.3.83)
|
||||||
|
```bash
|
||||||
|
sshpass -p 'utOgbZ09ruslan' scp -o StrictHostKeyChecking=no <файл> root@45.129.3.83:/home/docker/wbfeed/<путь>
|
||||||
|
sshpass -p 'utOgbZ09ruslan' ssh root@45.129.3.83 "docker restart wbfeed-app-1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Учётные данные
|
||||||
|
- Админ: логин `ruslan`, пароль `utOgbZ09ruslan+`
|
||||||
|
- Git: https://git.ruslan.xyz/ruslan/wildberries.git
|
||||||
|
|
||||||
|
## Telegram уведомления
|
||||||
|
- Бот токен: `8181219074:AAGvqWqb6t10YP4xpMOQnBq_6LrUqAFm5hM`
|
||||||
|
- Chat ID: `54986411`
|
||||||
|
- Прокси URL: `https://tel.4mont.ru/bot{TOKEN}/sendMessage`
|
||||||
|
- На 192.168.33.19 работает через `/etc/hosts` → локальный прокси
|
||||||
|
- На 45.129.3.83 прокси недоступен (разные сети) — уведомления не доходят
|
||||||
|
|
||||||
|
## Технический стек
|
||||||
|
- Python 3.11, Flask 3.0.3, SQLite (tokens.db)
|
||||||
|
- Docker + Docker Compose, Nginx Proxy Manager
|
||||||
|
- WB API rate limit: 1 запрос / 120 сек
|
||||||
|
- Автоответ: цикл каждые 120 сек, 1 ответ за цикл, очередь в SQLite
|
||||||
|
|
||||||
|
## Известные особенности
|
||||||
|
- `tokens.db` монтируется как bind-mount: нужно создавать как файл (`touch`), не как папку
|
||||||
|
- `app.py` монтируется как файл — после `scp` нужен `docker restart` (inode binding)
|
||||||
|
- `templates/` и `static/` монтируются как папки — изменения видны без рестарта
|
||||||
|
|||||||
@@ -1113,6 +1113,11 @@ textarea:focus {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-invalid {
|
||||||
|
border-color: #dc2626 !important;
|
||||||
|
background: #fff5f5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.login-split { flex-direction: column; }
|
.login-split { flex-direction: column; }
|
||||||
.login-promo { padding: 40px 24px; flex: none; }
|
.login-promo { padding: 40px 24px; flex: none; }
|
||||||
|
|||||||
+12
-4
@@ -79,9 +79,9 @@
|
|||||||
<div class="autoreply-status">
|
<div class="autoreply-status">
|
||||||
<span class="status-dot status-dot--green"></span>
|
<span class="status-dot status-dot--green"></span>
|
||||||
<span>Автоответ активен</span>
|
<span>Автоответ активен</span>
|
||||||
{% if api_cooldown_seconds_left and api_cooldown_seconds_left > 0 %}
|
<span id="cooldown-wrap" {% if not api_cooldown_seconds_left or api_cooldown_seconds_left <= 0 %}style="display:none"{% endif %}>
|
||||||
· Следующий ответ через <span id="cooldown-counter" data-seconds="{{ api_cooldown_seconds_left }}">{{ api_cooldown_seconds_left }}</span> сек.
|
· Следующий ответ через <span id="cooldown-counter" data-seconds="{{ api_cooldown_seconds_left or 0 }}">{{ api_cooldown_seconds_left or 0 }}</span> сек.
|
||||||
{% endif %}
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -410,7 +410,15 @@ initPools();
|
|||||||
} else if (data.last_log_id !== lastLogId) {
|
} else if (data.last_log_id !== lastLogId) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
if (cooldownCtrl && data.cooldown > 0) cooldownCtrl.update(data.cooldown);
|
const wrap = document.getElementById('cooldown-wrap');
|
||||||
|
if (wrap) {
|
||||||
|
if (data.cooldown > 0) {
|
||||||
|
wrap.style.display = '';
|
||||||
|
if (cooldownCtrl) cooldownCtrl.update(data.cooldown);
|
||||||
|
} else {
|
||||||
|
wrap.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
if (fetchCtrl && data.next_fetch_seconds > 0) fetchCtrl.update(data.next_fetch_seconds);
|
if (fetchCtrl && data.next_fetch_seconds > 0) fetchCtrl.update(data.next_fetch_seconds);
|
||||||
if (data.next_fetch_seconds === 0 && data.queue_len === 0 && data.auto_reply_enabled) {
|
if (data.next_fetch_seconds === 0 && data.queue_len === 0 && data.auto_reply_enabled) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
|||||||
+33
-15
@@ -189,25 +189,43 @@ closeBtn.addEventListener('click', () => modal.classList.remove('active'));
|
|||||||
modal.addEventListener('click', e => { if (e.target === modal) modal.classList.remove('active'); });
|
modal.addEventListener('click', e => { if (e.target === modal) modal.classList.remove('active'); });
|
||||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') modal.classList.remove('active'); });
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') modal.classList.remove('active'); });
|
||||||
|
|
||||||
document.getElementById('request-form').addEventListener('submit', async e => {
|
const form = document.getElementById('request-form');
|
||||||
|
|
||||||
|
function validateField(name) {
|
||||||
|
const input = form[name];
|
||||||
|
const errEl = document.getElementById('err-' + name);
|
||||||
|
const val = input.value.trim();
|
||||||
|
let msg = '';
|
||||||
|
|
||||||
|
if (name === 'name' && !val)
|
||||||
|
msg = 'Введите имя';
|
||||||
|
if (name === 'email' && (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)))
|
||||||
|
msg = 'Введите корректный email';
|
||||||
|
if (name === 'phone' && val.replace(/\D/g, '').length < 10)
|
||||||
|
msg = 'Введите корректный телефон';
|
||||||
|
|
||||||
|
errEl.textContent = msg;
|
||||||
|
input.classList.toggle('input-invalid', !!msg);
|
||||||
|
return !msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
['name', 'email', 'phone'].forEach(name => {
|
||||||
|
const input = form[name];
|
||||||
|
input.addEventListener('blur', () => validateField(name));
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
if (input.classList.contains('input-invalid')) validateField(name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
form.addEventListener('submit', async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const form = e.target;
|
const validName = validateField('name');
|
||||||
const name = form.name.value.trim();
|
const validEmail = validateField('email');
|
||||||
const email = form.email.value.trim();
|
const validPhone = validateField('phone');
|
||||||
const phone = form.phone.value.trim();
|
if (!validName || !validEmail || !validPhone) return;
|
||||||
let valid = true;
|
|
||||||
|
|
||||||
['name','email','phone'].forEach(f => document.getElementById('err-' + f).textContent = '');
|
|
||||||
document.getElementById('modal-error').style.display = 'none';
|
document.getElementById('modal-error').style.display = 'none';
|
||||||
|
|
||||||
if (!name)
|
|
||||||
{ document.getElementById('err-name').textContent = 'Введите имя'; valid = false; }
|
|
||||||
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
|
|
||||||
{ document.getElementById('err-email').textContent = 'Введите корректный email'; valid = false; }
|
|
||||||
if (!phone || phone.replace(/\D/g, '').length < 10)
|
|
||||||
{ document.getElementById('err-phone').textContent = 'Введите корректный телефон'; valid = false; }
|
|
||||||
if (!valid) return;
|
|
||||||
|
|
||||||
const btn = document.getElementById('request-submit');
|
const btn = document.getElementById('request-submit');
|
||||||
btn.disabled = true; btn.textContent = 'Отправка…';
|
btn.disabled = true; btn.textContent = 'Отправка…';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user