From ee946f7f1f75f1a71115a92ecc355fee0d26d9c8 Mon Sep 17 00:00:00 2001 From: ruslan Date: Fri, 15 May 2026 21:11:56 +0300 Subject: [PATCH] Redesign: Start/Stop buttons, locked settings, queue countdown, wb2 logo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace tumbler with Start/Stop buttons (green/red) - Lock settings form with
when auto-reply active - Clear queue + reset fetch timer on settings save - Show 'Очередь подгрузится через X сек.' countdown in queue section - API /status returns next_fetch_seconds, queue_len, auto_reply_enabled - Login: price 15₽/день, up to 144 replies/day, wb2 logo, promo panel 25% width - Replace wb.png → wb2.png across all templates Co-Authored-By: Claude Sonnet 4.6 --- app.py | 36 ++++++++++++++--- static/styles.css | 76 +++++++++++++++++++---------------- templates/admin.html | 2 +- templates/cabinet.html | 2 +- templates/index.html | 87 +++++++++++++++++++++------------------- templates/login.html | 8 ++-- templates/register.html | 2 +- wb2.png | Bin 0 -> 2093192 bytes 8 files changed, 123 insertions(+), 90 deletions(-) create mode 100644 wb2.png diff --git a/app.py b/app.py index d5b948e..2bd2371 100644 --- a/app.py +++ b/app.py @@ -1051,6 +1051,17 @@ def _next_auto_reply_meta() -> Tuple[Optional[datetime], Optional[int]]: return next_dt, seconds_left +def _next_fetch_seconds_left() -> int: + raw = db.get_setting(AUTO_REPLY_LAST_FETCH_KEY) + if not raw: + return 0 + try: + last_fetch = float(raw) + except ValueError: + return 0 + return max(0, int(last_fetch + AUTO_REPLY_FETCH_INTERVAL_SECONDS - time.time())) + + def auto_reply_loop() -> None: while True: try: @@ -1275,6 +1286,8 @@ def index(): success_message = f"Отправлено ответов: {count}" elif status == "pools_saved": success_message = "Пулы автоответов сохранены." + elif status == "settings_saved": + success_message = "Настройки сохранены. Очередь будет обновлена." elif status == "reply_failed": error_text = request.args.get("error") or "Не удалось отправить ответы." error_message = error_text @@ -1297,11 +1310,10 @@ def index(): next_auto_reply_at=next_auto_reply_at, next_auto_reply_in_seconds=next_auto_reply_in_seconds, api_cooldown_seconds_left=api_cooldown_seconds_left, + next_fetch_seconds_left=_next_fetch_seconds_left(), enabled_stars=list(_load_enabled_stars()), filter_mode=_load_filter_mode(), reply_pools={n: _pool_to_multiline_text(_load_reply_pool(n)) for n in range(1, 6)}, - reply_pool_5_list=_load_reply_pool(5), - reply_pool_4_list=_load_reply_pool(4), auto_reply_queue=_load_auto_reply_queue(), auto_reply_logs=db.list_auto_reply_logs(limit=100), current_user=g.user, @@ -1319,7 +1331,15 @@ def api_status(): logs = db.list_auto_reply_logs(limit=1) last_id = logs[0]["id"] if logs else None cooldown = _get_api_cooldown_seconds_left() - return jsonify({"last_log_id": last_id, "cooldown": cooldown}) + next_fetch = _next_fetch_seconds_left() + queue_len = len(_load_auto_reply_queue()) + return jsonify({ + "last_log_id": last_id, + "cooldown": cooldown, + "next_fetch_seconds": next_fetch, + "queue_len": queue_len, + "auto_reply_enabled": is_auto_reply_enabled(), + }) @app.route("/auto-reply-toggle", methods=["POST"]) @@ -1327,7 +1347,7 @@ def api_status(): def auto_reply_toggle(): enabled = request.form.get("enabled") == "1" set_auto_reply_enabled(enabled) - return redirect(url_for("index", action="unanswered", stars=[5, 4])) + return redirect(url_for("index")) @app.route("/auto-reply-pools", methods=["POST"]) @@ -1358,18 +1378,22 @@ def auto_reply_pools(): @app.route("/auto-reply-settings", methods=["POST"]) @login_required def auto_reply_settings(): + if is_auto_reply_enabled(): + return redirect(url_for("index")) stars = [int(s) for s in request.form.getlist("stars") if s.isdigit() and 1 <= int(s) <= 5] filter_mode = request.form.get("filter_mode", "no_text") if filter_mode not in ("no_text", "empty", "all"): filter_mode = "no_text" db.set_setting(AUTO_REPLY_STARS_KEY, json.dumps(stars)) db.set_setting(AUTO_REPLY_FILTER_KEY, filter_mode) - # Save pools for each enabled star for star in range(1, 6): items = [i.strip() for i in request.form.getlist(f"pool_{star}_item") if i.strip()] if items: db.set_setting(f"auto_reply_pool_{star}", _pool_to_multiline_text(items)) - return redirect(url_for("index", status="pools_saved")) + # Clear queue and reset fetch window so next cycle rebuilds immediately + db.set_setting("auto_reply_queue_json", "[]") + db.set_setting(AUTO_REPLY_LAST_FETCH_KEY, "0") + return redirect(url_for("index", status="settings_saved")) @app.route("/reply", methods=["POST"]) diff --git a/static/styles.css b/static/styles.css index b430407..bad5de1 100644 --- a/static/styles.css +++ b/static/styles.css @@ -897,7 +897,8 @@ textarea:focus { } .login-promo { - flex: 0 0 300px; + flex: 0 0 25%; + min-width: 260px; background: linear-gradient(145deg, #3D0066 0%, #6B0FA8 45%, #CB11AB 100%); display: flex; align-items: center; @@ -1024,7 +1025,7 @@ textarea:focus { } .login-form-wrap { - flex: 0 0 420px; + flex: 1; display: flex; align-items: center; justify-content: center; @@ -1374,47 +1375,52 @@ textarea:focus { } /* ── Тумблер автоответа ─────────────────────────────────── */ -.tumbler { +.btn-start, +.btn-stop { display: inline-flex; align-items: center; + gap: 6px; + padding: 8px 20px; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + border: none; cursor: pointer; - user-select: none; + transition: opacity var(--t), transform var(--t); } -.tumbler__input { - position: absolute; - width: 0; - height: 0; - opacity: 0; +.btn-start { + background: #22C55E; + color: #fff; } -.tumbler__track { - position: relative; - display: inline-block; - width: 52px; - height: 28px; - background: var(--c-border); - border-radius: 14px; - transition: background var(--t); - flex-shrink: 0; +.btn-stop { + background: #EF4444; + color: #fff; } -.tumbler__thumb { - position: absolute; - top: 3px; - left: 3px; - width: 22px; - height: 22px; - background: #fff; - border-radius: 50%; - transition: transform var(--t); - box-shadow: 0 1px 4px rgba(0,0,0,.25); +.btn-start:hover, +.btn-stop:hover { + opacity: 0.88; + transform: translateY(-1px); } -.tumbler__input:checked ~ .tumbler__track { - background: #CB11AB; + +.autoreply-status { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--c-text-muted); + padding: 8px 0 12px; + border-bottom: 1px solid var(--c-border); + margin-bottom: 16px; } -.tumbler__input:checked ~ .tumbler__track .tumbler__thumb { - transform: translateX(24px); -} -.tumbler__track:hover { - filter: brightness(0.92); + +fieldset:disabled .star-toggle, +fieldset:disabled .filter-option, +fieldset:disabled .pool-item-input, +fieldset:disabled .btn-add-item, +fieldset:disabled button[type="submit"] { + opacity: 0.45; + cursor: not-allowed; + pointer-events: none; } /* ── Тег артикула ───────────────────────────────────────── */ diff --git a/templates/admin.html b/templates/admin.html index 3105cc8..8493f99 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -11,7 +11,7 @@