SupraDashboard / src /__main__.py
Tianyi-Billy-Ma
Deploy: simplify codebase (dead-code removal, behavior-preserving)
1727091
Raw
History Blame Contribute Delete
7.24 kB
"""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 = """
<style>
/* The built-in login page renders with `body.dark` (it follows the browser's OS
color-scheme, BEFORE our app theme can flip it). Gradio paints the login card
(.block), inputs and labels from CSS custom properties that, INSIDE the `.dark`
scope, resolve to dark stone values (e.g. --block-background-fill: #292524) — so
hardcoding bg on body/input/button alone loses to those vars and the card stays
black. The robust fix is to REDEFINE the custom properties themselves (under both
:root and .dark, with !important) to bone-light values, then belt-and-suspenders
paint the concrete elements. This is CSS-only (a <script> wouldn't run via
innerHTML), and it makes the card + username/password labels readable in dark OS. */
:root, .dark, body.dark {
color-scheme: light !important;
--body-background-fill: #F4EFE6 !important;
--body-text-color: #2A2622 !important;
--body-text-color-subdued: #6B6258 !important;
--background-fill-primary: #FBF8F1 !important;
--background-fill-secondary: #F4EFE6 !important;
--block-background-fill: #FBF8F1 !important;
--block-label-background-fill: #FBF8F1 !important;
--panel-background-fill: #FBF8F1 !important;
--block-border-color: #2A262220 !important;
--border-color-primary: #2A262222 !important;
--block-label-text-color: #2A2622 !important;
--block-title-text-color: #2A2622 !important;
--block-info-text-color: #6B6258 !important;
--input-background-fill: #FBF8F1 !important;
--input-border-color: #2A262233 !important;
--input-placeholder-color: #6B6258 !important;
--button-primary-background-fill: #1E7A74 !important;
--button-primary-background-fill-hover: #155954 !important;
--button-primary-text-color: #FBF8F1 !important;
--bg: #F4EFE6 !important; --bg-dark: #F4EFE6 !important;
--col: #2A2622 !important; --col-dark: #2A2622 !important;
}
html, body, .gradio-container, gradio-app, .dark, .app, .main, .wrap, .panel,
.column, .form {
background: #F4EFE6 !important; color: #2A2622 !important;
}
/* The login card itself — a Gradio .block — must be bone, not dark stone. */
.block, .block.padded {
background: #FBF8F1 !important;
border: 1px solid #2A262220 !important; border-radius: 12px !important;
}
input, textarea {
background: #FBF8F1 !important; color: #2A2622 !important;
-webkit-text-fill-color: #2A2622 !important;
border: 1px solid #2A262233 !important; border-radius: 8px !important;
}
input::placeholder { color: #6B6258 !important; -webkit-text-fill-color: #6B6258 !important; }
h1, h2, h3, p, label, label span, [data-testid="block-info"],
.label, .form span, .container span {
color: #2A2622 !important;
}
button, [type="submit"], button.primary {
background: #1E7A74 !important; color: #FBF8F1 !important; border: none !important;
border-radius: 10px !important; font-weight: 600 !important;
}
button:hover, button.primary:hover { background: #155954 !important; }
</style>
<div style="font-family:'Hanken Grotesk',ui-sans-serif,sans-serif;color:#2A2622;
text-align:center;font-size:14px;line-height:1.5;margin:2px 0 8px">
<strong>SupraDashboard</strong> &middot; Host-Guest Review
</div>
"""
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)))