"""Module entrypoint for SupraDashboard."""
from __future__ import annotations
import os
# gradio launch() self-checks reachability with `networking.url_ok(self.local_url)`,
# where local_url is http://0.0.0.0:7860/ because we bind server_name="0.0.0.0" (required
# so HF's router can reach the container). httpx.head() to 0.0.0.0 is not connectable from
# inside the container, so url_ok returns False and launch() raises "When localhost is not
# accessible…", crashing startup. The server itself binds and serves fine — only this
# non-essential pre-flight fails — so we neutralize it (and exempt loopback from the proxy).
for _k in ("NO_PROXY", "no_proxy"):
_cur = os.environ.get(_k, "")
_need = "localhost,127.0.0.1,0.0.0.0"
os.environ[_k] = f"{_cur},{_need}" if _cur else _need
import gradio.networking # noqa: E402
gradio.networking.url_ok = lambda url: True # HF binds 0.0.0.0; the self-check is moot here
from ui.app import _auth, build_ui # noqa: E402 — after the launch self-check patch
# Gradio's built-in login page is served BEFORE our app theme/CSS load, so it falls back to
# the browser's dark mode (black card, invisible username/password text). `auth_message` is
# injected as raw HTML on that page — use it to force a light color-scheme and readable inputs
# matching the bone-white app theme.
_LOGIN_MESSAGE = """
SupraDashboard · Host-Guest Review
"""
def _build_app():
"""Mount the Gradio app on a FastAPI app fronted by a brute-force guard:
a per-IP failed-login lockout on the /login endpoint (Gradio's built-in auth
has no rate limiting, so a public Space is otherwise brute-forceable)."""
import time
from collections import defaultdict
import gradio as gr
from fastapi import FastAPI, Request
from starlette.responses import PlainTextResponse
# Tunable via Space variables; defaults: 5 fails / 5 min window -> 15 min lock.
max_fails = int(os.environ.get("LOGIN_MAX_FAILS", "5"))
window = int(os.environ.get("LOGIN_FAIL_WINDOW", "300"))
lockout = int(os.environ.get("LOGIN_LOCKOUT", "900"))
fails: dict[str, list[float]] = defaultdict(list)
locked: dict[str, float] = {}
def client_ip(request: Request) -> str:
# HF's router proxies the container, so the real client IP is the first
# entry of X-Forwarded-For; fall back to the socket peer locally.
xff = request.headers.get("x-forwarded-for")
if xff:
return xff.split(",")[0].strip()
return request.client.host if request.client else "unknown"
app = FastAPI()
@app.middleware("http")
async def login_guard(request: Request, call_next):
is_login = request.method == "POST" and request.url.path.rstrip("/").endswith("/login")
if not is_login:
return await call_next(request)
ip = client_ip(request)
now = time.time()
until = locked.get(ip, 0.0)
if until > now:
return PlainTextResponse(
f"Too many failed login attempts. Try again in {int(until - now)}s.",
status_code=429,
)
resp = await call_next(request)
if resp.status_code >= 400: # Gradio returns 400 on bad credentials
recent = [t for t in fails[ip] if now - t < window]
recent.append(now)
fails[ip] = recent
if len(recent) >= max_fails:
locked[ip] = now + lockout
fails[ip] = []
else: # successful login clears the IP's record
fails.pop(ip, None)
locked.pop(ip, None)
return resp
demo = build_ui().queue(default_concurrency_limit=1, api_open=False)
return gr.mount_gradio_app(
app, demo, path="/", auth=_auth(), auth_message=_LOGIN_MESSAGE,
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(_build_app(), host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))