Redesign: add 1-3★ auto-reply pools, filter modes, new settings UI

- app.py: add pool keys for stars 1-3, AUTO_REPLY_STARS_KEY/FILTER_KEY,
  _load_enabled_stars(), _load_filter_mode(), updated _has_review_content()
  with filter_mode param, new /auto-reply-settings POST route, index route
  passes enabled_stars/filter_mode/pools 1-5 to template
- templates/index.html: remove reviews list & controls section, replace pool
  card with full settings card (star toggles, filter mode radios, dynamic
  per-star pool columns), update JS for all 5 stars
- templates/login.html: fix ratings feature text (1-5★), update price to
  7 ₽/день with corrected description
- static/styles.css: login-promo fixed 300px width, logo 140px, add
  settings UI styles (star-toggles, filter-options, pool-columns, star-chips 1-3)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 18:19:38 +03:00
parent 60ea2757bc
commit e5050a7c83
4 changed files with 233 additions and 186 deletions
+64 -4
View File
@@ -28,6 +28,11 @@ AUTO_REPLY_SETTING_KEY = "auto_reply_enabled"
AUTO_REPLY_LAST_RUN_KEY = "auto_reply_last_run_ts"
AUTO_REPLY_POOL_5_KEY = "auto_reply_pool_5"
AUTO_REPLY_POOL_4_KEY = "auto_reply_pool_4"
AUTO_REPLY_POOL_3_KEY = "auto_reply_pool_3"
AUTO_REPLY_POOL_2_KEY = "auto_reply_pool_2"
AUTO_REPLY_POOL_1_KEY = "auto_reply_pool_1"
AUTO_REPLY_STARS_KEY = "auto_reply_stars_json"
AUTO_REPLY_FILTER_KEY = "auto_reply_filter"
AUTO_REPLY_QUEUE_KEY = "auto_reply_queue_json"
AUTO_REPLY_LAST_FETCH_KEY = "auto_reply_last_fetch_ts"
AUTO_REPLY_INTERVAL_MINUTES = int(os.getenv("AUTO_REPLY_INTERVAL_MINUTES", "10"))
@@ -815,8 +820,9 @@ def _save_auto_reply_queue(queue: List[dict]) -> None:
def _build_auto_reply_queue(reviews: List[Review]) -> List[dict]:
queue: List[dict] = []
filter_mode = _load_filter_mode()
for review in reviews:
if _has_review_content(review):
if _has_review_content(review, filter_mode):
logger.info("Auto-reply skip review_id=%s reason=has_review_text", review.id)
db.add_auto_reply_log(
review_id=review.id,
@@ -863,9 +869,32 @@ def _load_reply_pool(star: int) -> List[str]:
if star == 4:
db_value = db.get_setting(AUTO_REPLY_POOL_4_KEY)
return _parse_pool(db_value or ENV_REPLY_POOL_4, DEFAULT_REPLY_POOL_4)
if star in (1, 2, 3):
return _parse_pool(db.get_setting(f"auto_reply_pool_{star}") or "", [])
return []
def _load_enabled_stars() -> Set[int]:
raw = db.get_setting(AUTO_REPLY_STARS_KEY)
if raw:
try:
parsed = json.loads(raw)
if isinstance(parsed, list):
result = {int(s) for s in parsed if isinstance(s, (int, float, str)) and str(s).isdigit() and 1 <= int(s) <= 5}
if result:
return result
except (TypeError, ValueError):
pass
return AUTO_REPLY_STARS
def _load_filter_mode() -> str:
raw = db.get_setting(AUTO_REPLY_FILTER_KEY)
if raw in ("no_text", "empty", "all"):
return raw
return "no_text"
def _pool_to_multiline_text(pool: List[str]) -> str:
return "\n".join(pool)
@@ -877,8 +906,17 @@ def _pick_auto_reply(star: int) -> Optional[str]:
return None
def _has_review_content(review: Review) -> bool:
return bool((review.text or "").strip())
def _has_review_content(review: Review, filter_mode: str = "no_text") -> bool:
if filter_mode == "no_text":
return bool((review.text or "").strip())
if filter_mode == "empty":
return bool(
(review.text or "").strip()
or (review.pros or "").strip()
or (review.cons or "").strip()
)
# filter_mode == "all": reply to everything, treat as no content to skip
return False
def process_auto_replies() -> int:
@@ -906,7 +944,7 @@ def process_auto_replies() -> int:
reviews = client.fetch_reviews(
limit=100,
unanswered_only=True,
allowed_ratings=AUTO_REPLY_STARS,
allowed_ratings=_load_enabled_stars(),
)
except FeedbackApiError:
db.set_setting(AUTO_REPLY_LAST_FETCH_KEY, str(time.time()))
@@ -1265,8 +1303,13 @@ 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,
enabled_stars=list(_load_enabled_stars()),
filter_mode=_load_filter_mode(),
reply_pool_5_text=_pool_to_multiline_text(_load_reply_pool(5)),
reply_pool_4_text=_pool_to_multiline_text(_load_reply_pool(4)),
reply_pool_3_text=_pool_to_multiline_text(_load_reply_pool(3)),
reply_pool_2_text=_pool_to_multiline_text(_load_reply_pool(2)),
reply_pool_1_text=_pool_to_multiline_text(_load_reply_pool(1)),
reply_pool_5_list=_load_reply_pool(5),
reply_pool_4_list=_load_reply_pool(4),
auto_reply_queue=_load_auto_reply_queue(),
@@ -1322,6 +1365,23 @@ def auto_reply_pools():
return redirect(url_for("index", status="pools_saved", action="unanswered", stars=[5, 4]))
@app.route("/auto-reply-settings", methods=["POST"])
@login_required
def auto_reply_settings():
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"))
@app.route("/reply", methods=["POST"])
@login_required
def reply_all():