fix: Add scrollable container to ToolSelectorModal for small screens

- Add max-h-[90vh] and flex-col to modal content container
- Wrap tools grid in max-h-[50vh] overflow-y-auto container
- Add overscroll-contain for smooth scroll behavior on mobile
- Fixes issue where 21 PDF tools overflow viewport on small screens
This commit is contained in:
Your Name
2026-04-01 22:22:48 +02:00
parent 3e1c0e5f99
commit 314f847ece
49 changed files with 2142 additions and 361 deletions

View File

@@ -0,0 +1,86 @@
"""Guest demo budget enforcement.
Anonymous visitors get a small usage budget tracked by IP address
via Redis (with Flask session fallback). The budget prevents abuse
of expensive tools before the download-gate forces registration.
"""
import os
from flask import request, session
from app.services.credit_config import GUEST_DEMO_BUDGET, GUEST_DEMO_TTL_HOURS
_TTL_SECONDS = GUEST_DEMO_TTL_HOURS * 3600
# ── Redis helpers ──────────────────────────────────────────────
def _get_redis():
try:
import redis
redis_url = os.getenv("REDIS_URL", "redis://localhost:6379/0")
return redis.Redis.from_url(redis_url, decode_responses=True)
except Exception:
return None
def _guest_redis_key(ip: str) -> str:
return f"guest_demo:{ip}"
def _get_client_ip() -> str:
"""Return the best-effort client IP for rate tracking."""
forwarded = request.headers.get("X-Forwarded-For", "")
if forwarded:
return forwarded.split(",")[0].strip()
return request.remote_addr or "unknown"
# ── Public API ─────────────────────────────────────────────────
def get_guest_remaining() -> int:
"""Return how many demo operations the current guest has left."""
ip = _get_client_ip()
r = _get_redis()
if r is not None:
try:
used = r.get(_guest_redis_key(ip))
if used is None:
return GUEST_DEMO_BUDGET
return max(0, GUEST_DEMO_BUDGET - int(str(used)))
except Exception:
pass
# Fallback: Flask session
used = session.get("guest_demo_used", 0)
return max(0, GUEST_DEMO_BUDGET - used)
def record_guest_usage() -> None:
"""Increment the guest demo counter for the current visitor."""
ip = _get_client_ip()
r = _get_redis()
if r is not None:
try:
key = _guest_redis_key(ip)
pipe = r.pipeline()
pipe.incr(key)
pipe.expire(key, _TTL_SECONDS)
pipe.execute()
return
except Exception:
pass
# Fallback: Flask session
session["guest_demo_used"] = session.get("guest_demo_used", 0) + 1
def assert_guest_budget_available() -> None:
"""Raise ValueError if the guest has exhausted their demo budget."""
remaining = get_guest_remaining()
if remaining <= 0:
raise ValueError(
"You have used all your free demo tries. "
"Create a free account to continue."
)