"""Public web routes for the SmartTools UI.""" from __future__ import annotations from types import SimpleNamespace from typing import Any, Dict, List, Optional, Tuple from markupsafe import Markup, escape from flask import current_app, redirect, render_template, request, session, url_for from smarttools.registry.db import connect_db, query_all, query_one from . import web_bp def _api_get(path: str, params: Optional[Dict[str, Any]] = None, token: Optional[str] = None) -> Tuple[int, Dict[str, Any]]: client = current_app.test_client() headers = {} if token: headers["Authorization"] = f"Bearer {token}" response = client.get(path, query_string=params or {}, headers=headers) return response.status_code, response.get_json(silent=True) or {} def _api_post(path: str, data: Optional[Dict[str, Any]] = None, token: Optional[str] = None) -> Tuple[int, Dict[str, Any]]: client = current_app.test_client() headers = {"Content-Type": "application/json"} if token: headers["Authorization"] = f"Bearer {token}" import json response = client.post(path, data=json.dumps(data or {}), headers=headers) return response.status_code, response.get_json(silent=True) or {} def _api_delete(path: str, token: Optional[str] = None) -> Tuple[int, Dict[str, Any]]: client = current_app.test_client() headers = {} if token: headers["Authorization"] = f"Bearer {token}" response = client.delete(path, headers=headers) return response.status_code, response.get_json(silent=True) or {} def _title_case(value: str) -> str: return value.replace("-", " ").title() def _build_pagination(meta: Dict[str, Any]) -> SimpleNamespace: page = int(meta.get("page", 1)) total_pages = int(meta.get("total_pages", 1)) return SimpleNamespace( page=page, pages=total_pages, has_prev=page > 1, has_next=page < total_pages, prev_num=page - 1, next_num=page + 1, ) def _render_readme(readme: Optional[str]) -> str: if not readme: return "" escaped = escape(readme) return Markup("
") + escaped + Markup("
") def _load_categories() -> List[SimpleNamespace]: status, payload = _api_get("/api/v1/categories", params={"per_page": 100}) if status != 200: return [] categories = [] for item in payload.get("data", []): name = item.get("name") if not name: continue categories.append(SimpleNamespace( name=name, display_name=_title_case(name), count=item.get("tool_count", 0), description=item.get("description"), icon=item.get("icon"), )) return categories def _load_publisher(slug: str) -> Optional[Dict[str, Any]]: conn = connect_db() try: row = query_one( conn, "SELECT slug, display_name, bio, website, verified, created_at FROM publishers WHERE slug = ?", [slug], ) return dict(row) if row else None finally: conn.close() def _load_publisher_tools(slug: str) -> List[Dict[str, Any]]: conn = connect_db() try: rows = query_all( conn, """ WITH latest AS ( SELECT owner, name, MAX(id) AS max_id FROM tools WHERE owner = ? AND version NOT LIKE '%-%' GROUP BY owner, name ) SELECT t.owner, t.name, t.version, t.description, t.category, t.downloads, t.published_at FROM tools t JOIN latest ON t.owner = latest.owner AND t.name = latest.name AND t.id = latest.max_id ORDER BY t.downloads DESC, t.published_at DESC """, [slug], ) return [dict(row) for row in rows] finally: conn.close() def _load_tool_versions(owner: str, name: str) -> List[Dict[str, Any]]: conn = connect_db() try: rows = query_all( conn, "SELECT version, published_at FROM tools WHERE owner = ? AND name = ? ORDER BY id DESC", [owner, name], ) return [dict(row) for row in rows] finally: conn.close() def _load_tool_id(owner: str, name: str, version: str) -> Optional[int]: conn = connect_db() try: row = query_one( conn, "SELECT id FROM tools WHERE owner = ? AND name = ? AND version = ?", [owner, name, version], ) return int(row["id"]) if row else None finally: conn.close() def _load_current_publisher() -> Optional[Dict[str, Any]]: slug = session.get("user", {}).get("slug") if not slug: return None conn = connect_db() try: row = query_one( conn, """ SELECT id, slug, display_name, email, verified, bio, website, created_at FROM publishers WHERE slug = ? """, [slug], ) return dict(row) if row else None finally: conn.close() def _load_pending_prs(publisher_id: int) -> List[Dict[str, Any]]: conn = connect_db() try: rows = query_all( conn, """ SELECT owner, name, version, pr_url, status, created_at FROM pending_prs WHERE publisher_id = ? ORDER BY created_at DESC """, [publisher_id], ) return [dict(row) for row in rows] finally: conn.close() def _require_login() -> Optional[Any]: if not session.get("auth_token"): next_url = request.path if request.query_string: next_url = f"{next_url}?{request.query_string.decode('utf-8')}" return redirect(url_for("web.login", next=next_url)) return None @web_bp.route("/", endpoint="home") def home(): status, payload = _api_get("/api/v1/stats/popular", params={"limit": 6}) featured_tools = payload.get("data", []) if status == 200 else [] show_ads = current_app.config.get("SHOW_ADS", False) return render_template( "pages/index.html", featured_tools=featured_tools, featured_contributor=None, show_ads=show_ads, ) def _home_alias(): return home() web_bp.add_url_rule("/", endpoint="index", view_func=_home_alias) @web_bp.route("/tools", endpoint="tools") def tools(): return _render_tools() def _render_tools(category_override: Optional[str] = None): page = request.args.get("page", 1) sort = request.args.get("sort", "downloads") category = category_override or request.args.get("category") query = request.args.get("q") params = {"page": page, "per_page": 12, "sort": sort} if category: params["category"] = category status, payload = _api_get("/api/v1/tools", params=params) if status != 200: return render_template("errors/500.html"), 500 meta = payload.get("meta", {}) categories = _load_categories() # Sum all category counts for the "All Tools" total all_tools_total = sum(c.count for c in categories) return render_template( "pages/tools.html", tools=payload.get("data", []), categories=categories, current_category=category, total_count=all_tools_total, sort=sort, query=query, pagination=_build_pagination(meta), ) def _tools_alias(): return tools() web_bp.add_url_rule("/tools", endpoint="tools_browse", view_func=_tools_alias) @web_bp.route("/category/", endpoint="category") def category(name: str): query = request.args.get("q") if query: return redirect(url_for("web.search", q=query, category=name)) return _render_tools(category_override=name) @web_bp.route("/search", endpoint="search") def search(): query = request.args.get("q", "").strip() page = request.args.get("page", 1) category = request.args.get("category") if query: params = {"q": query, "page": page, "per_page": 12} if category: params["category"] = category status, payload = _api_get("/api/v1/tools/search", params=params) if status != 200: return render_template("errors/500.html"), 500 meta = payload.get("meta", {}) return render_template( "pages/search.html", query=query, results=payload.get("data", []), pagination=_build_pagination(meta), popular_categories=[], ) categories = sorted(_load_categories(), key=lambda c: c.count, reverse=True)[:8] return render_template( "pages/search.html", query="", results=[], pagination=None, popular_categories=categories, ) @web_bp.route("/tools//", endpoint="tool_detail") def tool_detail(owner: str, name: str): status, payload = _api_get(f"/api/v1/tools/{owner}/{name}") if status == 404: return render_template("errors/404.html"), 404 if status != 200: return render_template("errors/500.html"), 500 tool = payload.get("data", {}) tool["tags"] = ", ".join(tool.get("tags", [])) if isinstance(tool.get("tags"), list) else tool.get("tags") tool["readme_html"] = _render_readme(tool.get("readme")) tool_id = _load_tool_id(owner, name, tool.get("version", "")) tool["id"] = tool_id publisher = _load_publisher(owner) versions = _load_tool_versions(owner, name) return render_template( "pages/tool_detail.html", tool=tool, publisher=publisher, versions=versions, ) @web_bp.route("/tools///versions/", endpoint="tool_version") def tool_version(owner: str, name: str, version: str): status, payload = _api_get(f"/api/v1/tools/{owner}/{name}", params={"version": version}) if status == 404: return render_template("errors/404.html"), 404 if status != 200: return render_template("errors/500.html"), 500 tool = payload.get("data", {}) tool["tags"] = ", ".join(tool.get("tags", [])) if isinstance(tool.get("tags"), list) else tool.get("tags") tool["readme_html"] = _render_readme(tool.get("readme")) tool["id"] = _load_tool_id(owner, name, tool.get("version", version)) publisher = _load_publisher(owner) versions = _load_tool_versions(owner, name) return render_template( "pages/tool_detail.html", tool=tool, publisher=publisher, versions=versions, ) @web_bp.route("/publishers/", endpoint="publisher") def publisher(slug: str): publisher_row = _load_publisher(slug) if not publisher_row: return render_template("errors/404.html"), 404 tools = _load_publisher_tools(slug) return render_template( "pages/publisher.html", publisher=publisher_row, tools=tools, ) def _publisher_alias(slug: str): return publisher(slug) web_bp.add_url_rule("/publishers/", endpoint="publisher_profile", view_func=_publisher_alias) def _render_dashboard_overview(): redirect_response = _require_login() if redirect_response: return redirect_response token = session.get("auth_token") user = _load_current_publisher() or session.get("user", {}) status, payload = _api_get("/api/v1/me/tools", token=token) tools = payload.get("data", []) if status == 200 else [] token_status, token_payload = _api_get("/api/v1/tokens", token=token) tokens = token_payload.get("data", []) if token_status == 200 else [] stats = { "tools_count": len(tools), "total_downloads": sum(tool.get("downloads", 0) for tool in tools), "tokens_count": len(tokens), } return render_template( "dashboard/index.html", user=user, tools=tools, stats=stats, ) @web_bp.route("/dashboard", endpoint="dashboard") def dashboard(): return _render_dashboard_overview() @web_bp.route("/dashboard/tools", endpoint="dashboard_tools") def dashboard_tools(): redirect_response = _require_login() if redirect_response: return redirect_response token = session.get("auth_token") user = _load_current_publisher() or session.get("user", {}) status, payload = _api_get("/api/v1/me/tools", token=token) tools = payload.get("data", []) if status == 200 else [] token_status, token_payload = _api_get("/api/v1/tokens", token=token) tokens = token_payload.get("data", []) if token_status == 200 else [] stats = { "tools_count": len(tools), "total_downloads": sum(tool.get("downloads", 0) for tool in tools), "tokens_count": len(tokens), } pending_prs = [] if user and user.get("id"): pending_prs = _load_pending_prs(int(user["id"])) return render_template( "dashboard/tools.html", user=user, tools=tools, stats=stats, pending_prs=pending_prs, ) @web_bp.route("/dashboard/tokens", endpoint="dashboard_tokens") def dashboard_tokens(): redirect_response = _require_login() if redirect_response: return redirect_response token = session.get("auth_token") user = _load_current_publisher() or session.get("user", {}) tools_status, tools_payload = _api_get("/api/v1/me/tools", token=token) tools = tools_payload.get("data", []) if tools_status == 200 else [] token_status, token_payload = _api_get("/api/v1/tokens", token=token) tokens = token_payload.get("data", []) if token_status == 200 else [] for item in tokens: token_id = str(item.get("id", "")) item["token_suffix"] = token_id[-6:] if token_id else "" item["revoked_at"] = item.get("revoked_at") stats = { "tools_count": len(tools), "total_downloads": sum(tool.get("downloads", 0) for tool in tools), "tokens_count": len(tokens), } return render_template( "dashboard/tokens.html", user=user, tools=tools, stats=stats, tokens=tokens, ) @web_bp.route("/dashboard/settings", endpoint="dashboard_settings") def dashboard_settings(): redirect_response = _require_login() if redirect_response: return redirect_response token = session.get("auth_token") user = _load_current_publisher() or session.get("user", {}) tools_status, tools_payload = _api_get("/api/v1/me/tools", token=token) tools = tools_payload.get("data", []) if tools_status == 200 else [] token_status, token_payload = _api_get("/api/v1/tokens", token=token) tokens = token_payload.get("data", []) if token_status == 200 else [] stats = { "tools_count": len(tools), "total_downloads": sum(tool.get("downloads", 0) for tool in tools), "tokens_count": len(tokens), } return render_template( "dashboard/settings.html", user=user, tools=tools, stats=stats, tokens=tokens, errors=[], success_message=None, ) @web_bp.route("/docs", defaults={"path": ""}, endpoint="docs") @web_bp.route("/docs/", endpoint="docs") def docs(path: str): from .docs_content import get_doc, get_toc toc = get_toc() current = path or "getting-started" # Get content from docs_content module doc = get_doc(current) if doc: # Convert (id, title) tuples to objects with id, text, level attributes headings = [ SimpleNamespace(id=h[0], text=h[1], level=2) for h in doc.get("headings", []) ] page = SimpleNamespace( title=doc["title"], description=doc["description"], content_html=Markup(doc["content"]), headings=headings, parent=doc.get("parent"), ) else: page = SimpleNamespace( title=_title_case(current), description="SmartTools documentation", content_html=Markup(f"

Documentation for {escape(current)} is coming soon.

"), headings=[], parent=None, ) show_ads = current_app.config.get("SHOW_ADS", False) return render_template( "pages/docs.html", page=page, toc=toc, current_path=current, prev_page=None, next_page=None, show_ads=show_ads, ) @web_bp.route("/tutorials", endpoint="tutorials") def tutorials(): core_tutorials = [] video_tutorials = [] return render_template( "pages/tutorials.html", core_tutorials=core_tutorials, video_tutorials=video_tutorials, ) @web_bp.route("/tutorials/", endpoint="tutorials_path") def tutorials_path(path: str): from .docs_content import get_doc, get_toc doc = get_doc(path) if doc: # Convert (id, title) tuples to objects with id, text, level attributes headings = [ SimpleNamespace(id=h[0], text=h[1], level=2) for h in doc.get("headings", []) ] page = SimpleNamespace( title=doc["title"], description=doc["description"], content_html=Markup(doc["content"]), headings=headings, parent=doc.get("parent"), ) return render_template( "pages/docs.html", page=page, toc=get_toc(), current_path=path, prev_page=None, next_page=None, ) return render_template( "pages/content.html", title=_title_case(path), body="Tutorial content for this topic is coming soon.", ) web_bp.add_url_rule("/tutorials/", endpoint="tutorial", view_func=tutorials_path) @web_bp.route("/community", endpoint="community") def community(): return render_template("pages/community.html") @web_bp.route("/about", endpoint="about") def about(): return render_template("pages/about.html") @web_bp.route("/donate", endpoint="donate") def donate(): return render_template("pages/donate.html") @web_bp.route("/privacy", endpoint="privacy") def privacy(): return render_template("pages/privacy.html") @web_bp.route("/terms", endpoint="terms") def terms(): return render_template("pages/terms.html") @web_bp.route("/forgot-password", endpoint="forgot_password") def forgot_password(): return render_template( "pages/content.html", title="Reset Password", body="Password resets are not available yet. Please contact support if needed.", ) @web_bp.route("/robots.txt", endpoint="robots") def robots(): """Serve robots.txt - we welcome all crawlers including AI.""" from flask import send_from_directory return send_from_directory( web_bp.static_folder, "robots.txt", mimetype="text/plain" ) @web_bp.route("/sitemap.xml", endpoint="sitemap") def sitemap(): """Generate dynamic sitemap.xml with all public pages and tools.""" from flask import Response from datetime import datetime conn = connect_db() try: tools = query_all( conn, """ WITH latest AS ( SELECT owner, name, MAX(id) AS max_id FROM tools WHERE version NOT LIKE '%-%' GROUP BY owner, name ) SELECT t.owner, t.name, t.published_at FROM tools t JOIN latest ON t.owner = latest.owner AND t.name = latest.name AND t.id = latest.max_id ORDER BY t.published_at DESC """, [], ) tools = [dict(row) for row in tools] finally: conn.close() base_url = request.url_root.rstrip("/") today = datetime.utcnow().strftime("%Y-%m-%d") # Static pages static_pages = [ ("/", "1.0", "daily"), ("/tools", "0.9", "daily"), ("/docs", "0.8", "weekly"), ("/docs/getting-started", "0.8", "weekly"), ("/docs/installation", "0.7", "weekly"), ("/docs/first-tool", "0.7", "weekly"), ("/docs/publishing", "0.7", "weekly"), ("/docs/providers", "0.7", "weekly"), ("/tutorials", "0.7", "weekly"), ("/about", "0.5", "monthly"), ("/community", "0.5", "weekly"), ("/donate", "0.4", "monthly"), ("/privacy", "0.3", "monthly"), ("/terms", "0.3", "monthly"), ] xml_parts = [''] xml_parts.append('') # Add static pages for path, priority, freq in static_pages: xml_parts.append(f""" {base_url}{path} {today} {freq} {priority} """) # Add tool pages for tool in tools: pub_date = tool.get("published_at", today) if pub_date and "T" in str(pub_date): pub_date = str(pub_date).split("T")[0] xml_parts.append(f""" {base_url}/tools/{tool['owner']}/{tool['name']} {pub_date or today} weekly 0.6 """) xml_parts.append("") return Response("\n".join(xml_parts), mimetype="application/xml") # ============================================ # Dashboard API Proxy Routes # These proxy browser requests to the API using session auth # ============================================ from flask import jsonify @web_bp.route("/dashboard/api/tokens", methods=["POST"]) def dashboard_create_token(): """Proxy token creation from dashboard to API.""" redirect_response = _require_login() if redirect_response: return jsonify({"error": {"code": "UNAUTHORIZED", "message": "Not logged in"}}), 401 token = session.get("auth_token") data = request.get_json() or {} status, payload = _api_post("/api/v1/tokens", data=data, token=token) return jsonify(payload), status @web_bp.route("/dashboard/api/tokens/", methods=["DELETE"]) def dashboard_revoke_token(token_id: int): """Proxy token revocation from dashboard to API.""" redirect_response = _require_login() if redirect_response: return jsonify({"error": {"code": "UNAUTHORIZED", "message": "Not logged in"}}), 401 token = session.get("auth_token") status, payload = _api_delete(f"/api/v1/tokens/{token_id}", token=token) return jsonify(payload), status @web_bp.route("/dashboard/api/tools///deprecate", methods=["POST"]) def dashboard_deprecate_tool(owner: str, name: str): """Proxy tool deprecation from dashboard to API.""" redirect_response = _require_login() if redirect_response: return jsonify({"error": {"code": "UNAUTHORIZED", "message": "Not logged in"}}), 401 token = session.get("auth_token") data = request.get_json() or {} status, payload = _api_post(f"/api/v1/tools/{owner}/{name}/deprecate", data=data, token=token) return jsonify(payload), status @web_bp.route("/dashboard/api/tools///undeprecate", methods=["POST"]) def dashboard_undeprecate_tool(owner: str, name: str): """Proxy tool undeprecation from dashboard to API.""" redirect_response = _require_login() if redirect_response: return jsonify({"error": {"code": "UNAUTHORIZED", "message": "Not logged in"}}), 401 token = session.get("auth_token") data = request.get_json() or {} status, payload = _api_post(f"/api/v1/tools/{owner}/{name}/undeprecate", data=data, token=token) return jsonify(payload), status