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

@@ -6,15 +6,24 @@ from app.extensions import limiter
from app.services.account_service import (
create_api_key,
get_user_by_id,
has_task_access,
list_api_keys,
record_usage_event,
revoke_api_key,
)
from app.services.policy_service import get_usage_summary_for_user
from app.services.credit_config import (
get_all_tool_costs,
get_credits_for_plan,
get_tool_credit_cost,
CREDIT_WINDOW_DAYS,
)
from app.services.credit_service import deduct_credits, get_credit_summary
from app.services.stripe_service import (
is_stripe_configured,
get_stripe_price_id,
)
from app.utils.auth import get_current_user_id
from app.utils.auth import get_current_user_id, has_session_task_access
import stripe
import logging
@@ -38,6 +47,19 @@ def get_usage_route():
return jsonify(get_usage_summary_for_user(user_id, user["plan"])), 200
@account_bp.route("/credit-info", methods=["GET"])
@limiter.limit("60/hour")
def get_credit_info_route():
"""Return public credit/pricing info (no auth required)."""
return jsonify({
"plans": {
"free": {"credits": get_credits_for_plan("free"), "window_days": CREDIT_WINDOW_DAYS},
"pro": {"credits": get_credits_for_plan("pro"), "window_days": CREDIT_WINDOW_DAYS},
},
"tool_costs": get_all_tool_costs(),
}), 200
@account_bp.route("/subscription", methods=["GET"])
@limiter.limit("60/hour")
def get_subscription_status():
@@ -159,3 +181,62 @@ def revoke_api_key_route(key_id: int):
return jsonify({"error": "API key not found or already revoked."}), 404
return jsonify({"message": "API key revoked."}), 200
@account_bp.route("/claim-task", methods=["POST"])
@limiter.limit("60/hour")
def claim_task_route():
"""Adopt an anonymous task into the authenticated user's history.
Called after a guest signs up or logs in to record the previously
processed task in their account and deduct credits.
"""
user_id = get_current_user_id()
if user_id is None:
return jsonify({"error": "Authentication required."}), 401
data = request.get_json(silent=True) or {}
task_id = str(data.get("task_id", "")).strip()
tool = str(data.get("tool", "")).strip()
if not task_id or not tool:
return jsonify({"error": "task_id and tool are required."}), 400
# Verify this task belongs to the caller's session
if not has_session_task_access(task_id):
return jsonify({"error": "Task not found in your session."}), 403
# Skip if already claimed (idempotent)
if has_task_access(user_id, "web", task_id):
summary = get_credit_summary(user_id, "free")
return jsonify({"claimed": True, "credits": summary}), 200
user = get_user_by_id(user_id)
if user is None:
return jsonify({"error": "User not found."}), 404
plan = user.get("plan", "free")
cost = get_tool_credit_cost(tool)
# Deduct credits
try:
deduct_credits(user_id, plan, tool)
except ValueError:
return jsonify({
"error": "Insufficient credits to claim this file.",
"credits_required": cost,
}), 429
# Record usage event so the task appears in history
record_usage_event(
user_id=user_id,
source="web",
tool=tool,
task_id=task_id,
event_type="accepted",
api_key_id=None,
cost_points=cost,
)
summary = get_credit_summary(user_id, plan)
return jsonify({"claimed": True, "credits": summary}), 200

View File

@@ -52,7 +52,7 @@ def generate_barcode_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="barcode")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -38,7 +38,7 @@ def compress_pdf_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="compress-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -43,7 +43,7 @@ def compress_image_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="compress-image")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -33,7 +33,7 @@ def pdf_to_word_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="pdf-to-word")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -75,7 +75,7 @@ def word_to_pdf_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="word-to-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -34,6 +34,13 @@ def download_file(task_id: str, filename: str):
assert_api_task_access(actor, task_id)
else:
actor = resolve_web_actor()
# Download gate: anonymous users must register before downloading
if actor.actor_type == "anonymous":
return (
{"error": "signup_required",
"message": "Create a free account to download your file."},
401,
)
assert_web_task_access(actor, task_id)
except PolicyError as exc:
abort(exc.status_code, exc.message)

View File

@@ -39,7 +39,7 @@ def extract_flowchart_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="pdf-flowchart")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -75,7 +75,7 @@ def extract_sample_flowchart_route():
"""
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="pdf-flowchart-sample")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -34,7 +34,7 @@ def html_to_pdf_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="html-to-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -54,7 +54,7 @@ def convert_image_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="image-convert")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -127,7 +127,7 @@ def resize_image_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="image-resize")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -180,7 +180,7 @@ def convert_image_to_svg_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="image-to-svg")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -54,7 +54,7 @@ def crop_image_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="image-crop")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -120,7 +120,7 @@ def rotate_flip_image_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="image-rotate-flip")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -52,7 +52,7 @@ def ocr_image_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="ocr-image")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -102,7 +102,7 @@ def ocr_pdf_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="ocr-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -11,6 +11,10 @@ from app.services.policy_service import (
resolve_web_actor,
validate_actor_file,
)
from app.services.translation_guardrails import (
check_page_admission,
TranslationAdmissionError,
)
from app.utils.file_validator import FileValidationError
from app.utils.sanitizer import generate_safe_path
from app.tasks.pdf_ai_tasks import (
@@ -48,7 +52,7 @@ def chat_pdf_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="chat-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -104,7 +108,7 @@ def summarize_pdf_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="summarize-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -161,7 +165,7 @@ def translate_pdf_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="translate-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -175,6 +179,12 @@ def translate_pdf_route():
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
# ── Page-count admission guard ──
try:
page_count = check_page_admission(input_path, actor.plan)
except TranslationAdmissionError as e:
return jsonify({"error": e.message}), e.status_code
task = translate_pdf_task.delay(
input_path,
task_id,
@@ -213,7 +223,7 @@ def extract_tables_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="extract-tables")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -40,7 +40,7 @@ def pdf_to_pptx_route():
file = request.files["file"]
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="pdf-to-pptx")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -77,7 +77,7 @@ def excel_to_pdf_route():
file = request.files["file"]
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="excel-to-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -116,7 +116,7 @@ def pptx_to_pdf_route():
file = request.files["file"]
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="pptx-to-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -167,7 +167,7 @@ def sign_pdf_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="sign-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -54,7 +54,7 @@ def edit_pdf_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="pdf-edit")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -41,7 +41,7 @@ def crop_pdf_route():
file = request.files["file"]
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="crop-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -89,7 +89,7 @@ def flatten_pdf_route():
file = request.files["file"]
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="flatten-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -126,7 +126,7 @@ def repair_pdf_route():
file = request.files["file"]
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="repair-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -184,7 +184,7 @@ def edit_metadata_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="edit-metadata")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -34,7 +34,7 @@ def pdf_to_excel_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="pdf-to-excel")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -56,7 +56,7 @@ def merge_pdfs_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="merge-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -123,7 +123,7 @@ def split_pdf_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="split-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -183,7 +183,7 @@ def rotate_pdf_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="rotate-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -246,7 +246,7 @@ def add_page_numbers_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="page-numbers")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -304,7 +304,7 @@ def pdf_to_images_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="pdf-to-images")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -353,7 +353,7 @@ def images_to_pdf_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="images-to-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -424,7 +424,7 @@ def watermark_pdf_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="watermark-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -480,7 +480,7 @@ def protect_pdf_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="protect-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -532,7 +532,7 @@ def unlock_pdf_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="unlock-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -579,7 +579,7 @@ def remove_watermark_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="remove-watermark")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -638,7 +638,7 @@ def reorder_pdf_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="reorder-pdf")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code
@@ -690,7 +690,7 @@ def extract_pages_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="extract-pages")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -45,7 +45,7 @@ def generate_qr_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="qr-code")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -38,7 +38,7 @@ def remove_bg_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="remove-bg")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code

View File

@@ -59,7 +59,7 @@ def video_to_gif_route():
actor = resolve_web_actor()
try:
assert_quota_available(actor)
assert_quota_available(actor, tool="video-frames")
except PolicyError as e:
return jsonify({"error": e.message}), e.status_code