Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| from __future__ import annotations | |
| import base64 | |
| import hashlib | |
| import html | |
| import json | |
| import os | |
| import re | |
| import secrets | |
| import time | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from typing import Any | |
| from urllib.parse import urlencode | |
| import httpx | |
| from huggingface_hub import HfApi | |
| from starlette.applications import Starlette | |
| from starlette.requests import Request | |
| from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse, Response | |
| from starlette.routing import Mount, Route | |
| from starlette.staticfiles import StaticFiles | |
| APP_NAME = "Trackio Laboratory" | |
| SESSION_COOKIE = "trackio_home_session" | |
| SESSION_TTL_SECONDS = 86400 * 30 | |
| STATE_TTL_SECONDS = 600 | |
| DEFAULT_OAUTH_SCOPES = "openid profile read-repos" | |
| CACHE_DIR = Path(os.getenv("TRACKIO_LAB_CACHE_DIR", "/data/trackio-laboratory/cache")) | |
| api = HfApi() | |
| _sessions: dict[str, dict[str, Any]] = {} | |
| _pending_states: dict[str, dict[str, Any]] = {} | |
| def _cache_root() -> Path: | |
| if CACHE_DIR.exists() or CACHE_DIR.parent.exists(): | |
| return CACHE_DIR | |
| return Path(".cache/trackio-laboratory") | |
| def _cache_key(username: str | None) -> str: | |
| raw = (username or "unknown").strip().lower() | |
| return hashlib.sha256(raw.encode("utf-8")).hexdigest() | |
| def _cache_path(username: str | None) -> Path: | |
| return _cache_root() / f"{_cache_key(username)}.json" | |
| def _read_cached_spaces(username: str | None) -> dict[str, Any] | None: | |
| path = _cache_path(username) | |
| try: | |
| if not path.is_file(): | |
| return None | |
| with path.open(encoding="utf-8") as cache_file: | |
| payload = json.load(cache_file) | |
| if not isinstance(payload, dict) or not isinstance(payload.get("data"), dict): | |
| return None | |
| data = payload["data"] | |
| data["cache"] = { | |
| "hit": True, | |
| "saved_at": payload.get("saved_at"), | |
| } | |
| return data | |
| except Exception: | |
| return None | |
| def _write_cached_spaces(username: str | None, data: dict[str, Any]) -> None: | |
| root = _cache_root() | |
| try: | |
| root.mkdir(parents=True, exist_ok=True) | |
| path = _cache_path(username) | |
| tmp_path = path.with_suffix(".json.tmp") | |
| payload = { | |
| "saved_at": datetime.now(timezone.utc).isoformat(), | |
| "data": data, | |
| } | |
| with tmp_path.open("w", encoding="utf-8") as cache_file: | |
| json.dump(payload, cache_file, separators=(",", ":")) | |
| tmp_path.replace(path) | |
| except Exception: | |
| pass | |
| def _current_space_id() -> str: | |
| if os.getenv("SPACE_ID"): | |
| return os.environ["SPACE_ID"] | |
| author = os.getenv("SPACE_AUTHOR_NAME") | |
| repo = os.getenv("SPACE_REPO_NAME") | |
| if author and repo: | |
| return f"{author}/{repo}" | |
| return "trackio/laboratory" | |
| def _now() -> float: | |
| return time.monotonic() | |
| def _evict_expired() -> None: | |
| now = _now() | |
| for state, payload in list(_pending_states.items()): | |
| if now - payload["created_at"] > STATE_TTL_SECONDS: | |
| del _pending_states[state] | |
| for session_id, payload in list(_sessions.items()): | |
| if now - payload["created_at"] > SESSION_TTL_SECONDS: | |
| del _sessions[session_id] | |
| def _safe_next(value: str | None) -> str: | |
| if value and value.startswith("/") and not value.startswith("//"): | |
| return value | |
| return "/" | |
| def _oauth_redirect_uri(request: Request) -> str: | |
| space_host = os.getenv("SPACE_HOST") | |
| if space_host: | |
| return f"https://{space_host.split(',')[0]}/login/callback" | |
| return str(request.base_url).rstrip("/") + "/login/callback" | |
| def _oauth_configured() -> bool: | |
| return bool(os.getenv("OAUTH_CLIENT_ID") and os.getenv("OAUTH_CLIENT_SECRET")) | |
| def _session_id_from_request(request: Request) -> str | None: | |
| session_id = request.cookies.get(SESSION_COOKIE) | |
| if session_id and session_id in _sessions: | |
| payload = _sessions[session_id] | |
| if _now() - payload["created_at"] <= SESSION_TTL_SECONDS: | |
| return session_id | |
| del _sessions[session_id] | |
| return None | |
| def _session_from_request(request: Request) -> dict[str, Any] | None: | |
| _evict_expired() | |
| session_id = _session_id_from_request(request) | |
| if not session_id: | |
| return None | |
| return _sessions.get(session_id) | |
| def _set_session_cookie(resp: Response, session_id: str, request: Request) -> None: | |
| secure = bool(os.getenv("SPACE_HOST")) or request.url.scheme == "https" | |
| same_site = "none" if bool(os.getenv("SPACE_HOST")) else "lax" | |
| resp.set_cookie( | |
| SESSION_COOKIE, | |
| session_id, | |
| max_age=SESSION_TTL_SECONDS, | |
| httponly=True, | |
| samesite=same_site, | |
| secure=secure, | |
| path="/", | |
| ) | |
| def _clear_session_cookie(resp: Response, request: Request) -> None: | |
| secure = bool(os.getenv("SPACE_HOST")) or request.url.scheme == "https" | |
| same_site = "none" if bool(os.getenv("SPACE_HOST")) else "lax" | |
| resp.delete_cookie(SESSION_COOKIE, path="/", samesite=same_site, secure=secure) | |
| def _display_username(whoami: dict[str, Any] | None) -> str | None: | |
| if not whoami: | |
| return None | |
| return whoami.get("fullname") or whoami.get("name") | |
| def _org_names(whoami: dict[str, Any]) -> list[str]: | |
| orgs = [] | |
| for org in whoami.get("orgs") or []: | |
| if isinstance(org, dict) and org.get("name"): | |
| orgs.append(org["name"]) | |
| elif isinstance(org, str): | |
| orgs.append(org) | |
| return sorted(set(orgs)) | |
| def _namespaces_for_user(token: str) -> tuple[list[str], dict[str, Any]]: | |
| whoami = api.whoami(token=token, cache=True) | |
| username = whoami.get("name") | |
| namespaces = [username] if username else [] | |
| namespaces.extend(_org_names(whoami)) | |
| return sorted(set(namespaces)), whoami | |
| def _has_trackio_tag(space: Any) -> bool: | |
| return "trackio" in {str(tag).lower() for tag in (getattr(space, "tags", None) or [])} | |
| def _list_trackio_spaces_for_namespace(namespace: str, token: str) -> list[Any]: | |
| try: | |
| spaces = list( | |
| api.list_spaces( | |
| author=namespace, | |
| filter="trackio", | |
| full=True, | |
| token=token, | |
| ) | |
| ) | |
| except TypeError: | |
| spaces = list(api.list_spaces(author=namespace, full=True, token=token)) | |
| return [space for space in spaces if _has_trackio_tag(space)] | |
| def _iso(value: Any) -> str | None: | |
| if value is None: | |
| return None | |
| if isinstance(value, datetime): | |
| if value.tzinfo is None: | |
| value = value.replace(tzinfo=timezone.utc) | |
| return value.isoformat() | |
| return str(value) | |
| def _space_runtime_stage(space: Any) -> str | None: | |
| runtime = getattr(space, "runtime", None) | |
| stage = getattr(runtime, "stage", None) | |
| return str(stage) if stage is not None else None | |
| def _space_app_url(space: Any) -> str | None: | |
| host = getattr(space, "host", None) | |
| if isinstance(host, str) and host: | |
| return host if host.startswith("http") else f"https://{host}" | |
| subdomain = getattr(space, "subdomain", None) | |
| if isinstance(subdomain, str) and subdomain: | |
| return f"https://{subdomain}.hf.space" | |
| space_id = getattr(space, "id", "") | |
| if isinstance(space_id, str) and "/" in space_id: | |
| namespace, name = space_id.split("/", 1) | |
| subdomain = re.sub(r"[^a-z0-9-]+", "-", f"{namespace}-{name}".lower()).strip( | |
| "-" | |
| ) | |
| if subdomain: | |
| return f"https://{subdomain}.hf.space" | |
| return None | |
| def _serialize_space(space: Any) -> dict[str, Any]: | |
| space_id = getattr(space, "id", "") | |
| namespace, _, name = space_id.partition("/") | |
| last_modified = getattr(space, "last_modified", None) | |
| app_url = _space_app_url(space) | |
| private = bool(getattr(space, "private", False)) | |
| return { | |
| "id": space_id, | |
| "namespace": namespace, | |
| "name": name or space_id, | |
| "private": private, | |
| "sdk": getattr(space, "sdk", None), | |
| "runtime_stage": _space_runtime_stage(space), | |
| "last_modified": _iso(last_modified), | |
| "repo_url": f"https://huggingface.co/spaces/{space_id}", | |
| "app_url": app_url, | |
| "embeddable": bool(app_url) and not private, | |
| } | |
| def discover_trackio_spaces(token: str) -> dict[str, Any]: | |
| namespaces, whoami = _namespaces_for_user(token) | |
| by_id: dict[str, Any] = {} | |
| namespace_errors: dict[str, str] = {} | |
| for namespace in namespaces: | |
| try: | |
| for space in _list_trackio_spaces_for_namespace(namespace, token): | |
| space_id = getattr(space, "id", "") | |
| if not space_id or space_id == _current_space_id(): | |
| continue | |
| by_id[space_id] = space | |
| except Exception as err: | |
| namespace_errors[namespace] = str(err) | |
| spaces = [_serialize_space(space) for space in by_id.values()] | |
| spaces.sort(key=lambda item: item.get("last_modified") or "", reverse=True) | |
| return { | |
| "user": { | |
| "name": whoami.get("name"), | |
| "display_name": _display_username(whoami), | |
| "namespaces": namespaces, | |
| }, | |
| "spaces": spaces, | |
| "errors": namespace_errors, | |
| } | |
| async def homepage(request: Request) -> Response: | |
| session = _session_from_request(request) | |
| logout_name_source = ( | |
| str(session.get("display_name") or session.get("username") or "").strip() | |
| if session | |
| else "" | |
| ) | |
| logout_first_name = html.escape( | |
| logout_name_source.split()[0] if logout_name_source else "" | |
| ) | |
| logout_label = f"Logout, {logout_first_name}" if logout_first_name else "Logout" | |
| oauth_ready = _oauth_configured() | |
| body_class = "is-authed" if session else "is-login" | |
| html_doc = f"""<!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>{APP_NAME}</title> | |
| <style> | |
| :root {{ | |
| color-scheme: light; | |
| --bg: #fbfbff; | |
| --bg-warm: #fff7ed; | |
| --panel: #ffffff; | |
| --text: #211a16; | |
| --muted: #705b4f; | |
| --subtle: #8a7568; | |
| --border: #eadfd7; | |
| --border-strong: #d8c7bb; | |
| --accent: #f97316; | |
| --accent-dark: #c2410c; | |
| --accent-soft: #ffedd5; | |
| --indigo: #4f46e5; | |
| --indigo-soft: #eef2ff; | |
| --green: #047857; | |
| --green-soft: #dcfce7; | |
| --red: #b42318; | |
| --shadow: 0 18px 48px rgba(124, 45, 18, 0.12); | |
| }} | |
| * {{ box-sizing: border-box; }} | |
| body {{ | |
| margin: 0; | |
| min-height: 100vh; | |
| font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", sans-serif; | |
| background: | |
| linear-gradient(180deg, rgba(255, 247, 237, 0.78) 0, rgba(251, 251, 255, 0.96) 310px), | |
| var(--bg); | |
| color: var(--text); | |
| letter-spacing: 0; | |
| }} | |
| a {{ color: inherit; }} | |
| button, input, select {{ font: inherit; }} | |
| .login-shell {{ | |
| min-height: 100vh; | |
| display: grid; | |
| place-items: center; | |
| padding: 24px; | |
| background: | |
| linear-gradient(135deg, rgba(255, 237, 213, 0.64), rgba(238, 242, 255, 0.72)), | |
| linear-gradient(90deg, rgba(249, 115, 22, 0.10) 1px, transparent 1px), | |
| linear-gradient(180deg, rgba(79, 70, 229, 0.08) 1px, transparent 1px); | |
| background-size: auto, 34px 34px, 34px 34px; | |
| }} | |
| .login-panel {{ | |
| width: min(560px, 100%); | |
| padding: 32px; | |
| background: rgba(255, 255, 255, 0.96); | |
| border: 1px solid rgba(216, 199, 187, 0.95); | |
| border-radius: 8px; | |
| box-shadow: var(--shadow); | |
| text-align: center; | |
| }} | |
| .brand-mark {{ | |
| width: 188px; | |
| height: auto; | |
| display: block; | |
| margin: 0 auto 22px; | |
| }} | |
| h1 {{ | |
| margin: 0 0 12px; | |
| font-size: 38px; | |
| line-height: 1.1; | |
| font-weight: 600; | |
| }} | |
| .login-panel p {{ | |
| margin: 0 auto 24px; | |
| max-width: 420px; | |
| color: var(--muted); | |
| font-size: 16px; | |
| line-height: 1.5; | |
| }} | |
| .hf-button, .secondary-button {{ | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| min-height: 38px; | |
| padding: 0 14px; | |
| border-radius: 6px; | |
| border: 1px solid transparent; | |
| text-decoration: none; | |
| cursor: pointer; | |
| font-weight: 600; | |
| white-space: nowrap; | |
| }} | |
| .hf-button {{ | |
| color: white; | |
| background: #111827; | |
| border-color: #111827; | |
| gap: 9px; | |
| box-shadow: 0 8px 18px rgba(17, 24, 39, 0.18); | |
| }} | |
| .hf-button:hover {{ | |
| background: #000000; | |
| border-color: #000000; | |
| }} | |
| .hf-button img {{ | |
| width: 22px; | |
| height: 22px; | |
| flex: 0 0 auto; | |
| }} | |
| .hf-button.disabled {{ | |
| pointer-events: none; | |
| background: #6b7280; | |
| border-color: #6b7280; | |
| box-shadow: none; | |
| }} | |
| .app-shell {{ | |
| height: 100vh; | |
| min-height: 0; | |
| overflow: hidden; | |
| }} | |
| input, select {{ | |
| height: 36px; | |
| width: 100%; | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| background: white; | |
| color: var(--text); | |
| padding: 0 12px; | |
| outline: none; | |
| box-shadow: 0 1px 0 rgba(124, 45, 18, 0.04); | |
| }} | |
| input:focus, select:focus {{ | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 3px rgba(249, 115, 22, 0.14); | |
| }} | |
| .space-sidebar input, .space-sidebar select {{ | |
| background: #ffffff; | |
| border-color: rgba(255, 237, 213, 0.44); | |
| color: #211a16; | |
| box-shadow: 0 1px 0 rgba(124, 45, 18, 0.22); | |
| }} | |
| .space-sidebar input::placeholder {{ | |
| color: #9a7a6a; | |
| }} | |
| .space-sidebar input:focus, .space-sidebar select:focus {{ | |
| border-color: #fed7aa; | |
| box-shadow: | |
| 0 0 0 3px rgba(255, 237, 213, 0.24), | |
| 0 1px 0 rgba(124, 45, 18, 0.22); | |
| }} | |
| .secondary-button {{ | |
| background: white; | |
| border-color: var(--border); | |
| color: var(--text); | |
| }} | |
| .secondary-button:hover {{ | |
| border-color: var(--accent); | |
| color: var(--accent-dark); | |
| }} | |
| button.secondary-button {{ | |
| min-height: 36px; | |
| }} | |
| .cli-button {{ | |
| gap: 8px; | |
| }} | |
| .info-icon {{ | |
| width: 18px; | |
| height: 18px; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex: 0 0 auto; | |
| border: 1.5px solid currentColor; | |
| border-radius: 999px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| line-height: 1; | |
| font-style: normal; | |
| }} | |
| .code-icon {{ | |
| width: 20px; | |
| height: 12px; | |
| flex: 0 0 auto; | |
| }} | |
| .laboratory-shell {{ | |
| display: grid; | |
| grid-template-columns: 320px minmax(0, 1fr); | |
| height: 100%; | |
| min-height: 0; | |
| overflow: hidden; | |
| }} | |
| .space-sidebar {{ | |
| min-height: 0; | |
| height: 100%; | |
| border-right: 1px solid #7c2d12; | |
| background: | |
| linear-gradient(180deg, #9a3412 0%, #7c2d12 100%); | |
| color: #fff7ed; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| }} | |
| .sidebar-controls {{ | |
| padding: 14px 12px 12px; | |
| border-bottom: 1px solid rgba(255, 237, 213, 0.18); | |
| }} | |
| .sidebar-toolbar {{ | |
| display: grid; | |
| grid-template-columns: minmax(0, 1fr) 36px; | |
| gap: 8px; | |
| margin-top: 10px; | |
| align-items: center; | |
| }} | |
| .status-line {{ | |
| display: none; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 10px; | |
| min-height: 34px; | |
| padding: 8px 12px; | |
| border-bottom: 1px solid rgba(255, 237, 213, 0.18); | |
| color: rgba(255, 247, 237, 0.86); | |
| font-size: 13px; | |
| font-weight: 600; | |
| line-height: 1.3; | |
| }} | |
| .status-line.error-line, .status-line.loading-line {{ | |
| display: flex; | |
| }} | |
| .status-text {{ | |
| min-width: 0; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| }} | |
| .refresh-icon {{ | |
| width: 36px; | |
| height: 36px; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex: 0 0 auto; | |
| border: 1px solid rgba(255, 237, 213, 0.24); | |
| border-radius: 6px; | |
| background: rgba(255, 247, 237, 0.10); | |
| color: #fff7ed; | |
| cursor: pointer; | |
| font-size: 16px; | |
| line-height: 1; | |
| }} | |
| .refresh-icon:hover {{ | |
| border-color: rgba(255, 247, 237, 0.62); | |
| background: rgba(255, 247, 237, 0.18); | |
| color: white; | |
| }} | |
| .refresh-icon:disabled {{ | |
| cursor: default; | |
| opacity: 0.55; | |
| }} | |
| .error-line {{ color: #fee2e2; }} | |
| .space-list {{ | |
| min-height: 0; | |
| overflow-y: auto; | |
| padding: 8px; | |
| flex: 1; | |
| }} | |
| .space-list::-webkit-scrollbar {{ | |
| width: 10px; | |
| }} | |
| .space-list::-webkit-scrollbar-track {{ | |
| background: rgba(124, 45, 18, 0.52); | |
| }} | |
| .space-list::-webkit-scrollbar-thumb {{ | |
| background: rgba(255, 237, 213, 0.32); | |
| border: 2px solid rgba(124, 45, 18, 0.52); | |
| border-radius: 999px; | |
| }} | |
| .space-item {{ | |
| width: 100%; | |
| min-height: 54px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 9px 10px; | |
| border: 1px solid transparent; | |
| border-radius: 6px; | |
| background: transparent; | |
| color: #fff7ed; | |
| cursor: pointer; | |
| text-align: left; | |
| }} | |
| .space-item:hover {{ | |
| background: rgba(255, 237, 213, 0.11); | |
| border-color: rgba(255, 237, 213, 0.24); | |
| }} | |
| .space-item.active {{ | |
| background: rgba(255, 247, 237, 0.16); | |
| color: white; | |
| border-color: rgba(255, 237, 213, 0.72); | |
| box-shadow: | |
| inset 3px 0 0 #fdba74, | |
| 0 0 0 3px rgba(255, 237, 213, 0.14); | |
| }} | |
| .space-dot {{ | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 999px; | |
| flex: 0 0 auto; | |
| background: #fdba74; | |
| box-shadow: 0 0 0 4px rgba(255, 237, 213, 0.18); | |
| }} | |
| .space-dot.status-running {{ | |
| background: #22c55e; | |
| box-shadow: 0 0 0 4px rgba(187, 247, 208, 0.24); | |
| }} | |
| .space-dot.status-building {{ | |
| background: #facc15; | |
| box-shadow: 0 0 0 4px rgba(254, 240, 138, 0.24); | |
| }} | |
| .space-dot.status-error {{ | |
| background: #ef4444; | |
| box-shadow: 0 0 0 4px rgba(254, 202, 202, 0.24); | |
| }} | |
| .space-dot.status-paused {{ | |
| background: #cbd5e1; | |
| box-shadow: 0 0 0 4px rgba(226, 232, 240, 0.22); | |
| }} | |
| .space-dot.status-unknown {{ | |
| background: #fdba74; | |
| box-shadow: 0 0 0 4px rgba(255, 237, 213, 0.18); | |
| }} | |
| .space-copy {{ | |
| min-width: 0; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 3px; | |
| }} | |
| .space-copy strong {{ | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| font-size: 14px; | |
| line-height: 1.2; | |
| font-weight: 600; | |
| }} | |
| .space-copy span {{ | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| color: rgba(255, 247, 237, 0.76); | |
| font-size: 12px; | |
| }} | |
| .space-item.active .space-copy span {{ | |
| color: rgba(255, 247, 237, 0.82); | |
| }} | |
| .space-sidebar .empty {{ | |
| color: rgba(255, 247, 237, 0.86); | |
| background: rgba(255, 247, 237, 0.10); | |
| border-color: rgba(255, 237, 213, 0.22); | |
| }} | |
| .sidebar-footer {{ | |
| display: grid; | |
| gap: 8px; | |
| padding: 10px 12px 12px; | |
| border-top: 1px solid rgba(255, 237, 213, 0.18); | |
| background: rgba(124, 45, 18, 0.28); | |
| }} | |
| .sidebar-footer .secondary-button {{ | |
| width: 100%; | |
| min-height: 38px; | |
| background: rgba(255, 247, 237, 0.96); | |
| border-color: rgba(255, 237, 213, 0.48); | |
| color: #211a16; | |
| box-shadow: 0 1px 0 rgba(124, 45, 18, 0.22); | |
| }} | |
| .sidebar-footer .secondary-button:hover {{ | |
| background: white; | |
| border-color: #fed7aa; | |
| color: #7c2d12; | |
| }} | |
| .sidebar-footer .utility-button {{ | |
| background: transparent; | |
| border-color: rgba(255, 237, 213, 0.34); | |
| color: rgba(255, 247, 237, 0.92); | |
| box-shadow: none; | |
| }} | |
| .sidebar-footer .utility-button:hover {{ | |
| background: rgba(255, 247, 237, 0.10); | |
| border-color: rgba(255, 237, 213, 0.64); | |
| color: white; | |
| }} | |
| .dashboard-stage {{ | |
| min-width: 0; | |
| min-height: 0; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| background: #fffaf5; | |
| overflow: hidden; | |
| }} | |
| .dashboard-frame {{ | |
| width: 100%; | |
| flex: 1; | |
| min-height: 0; | |
| border: 0; | |
| background: white; | |
| overflow: auto; | |
| }} | |
| .empty {{ | |
| margin: 16px; | |
| padding: 28px 18px; | |
| color: var(--muted); | |
| text-align: center; | |
| background: white; | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| }} | |
| .stage-empty {{ | |
| flex: 1; | |
| display: grid; | |
| place-items: center; | |
| padding: 24px; | |
| color: var(--muted); | |
| text-align: center; | |
| }} | |
| .stage-empty-inner {{ | |
| max-width: 420px; | |
| padding: 28px; | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| background: white; | |
| box-shadow: 0 10px 30px rgba(124, 45, 18, 0.07); | |
| }} | |
| .stage-empty-inner strong {{ | |
| display: block; | |
| margin-bottom: 8px; | |
| color: var(--text); | |
| font-size: 16px; | |
| }} | |
| .stage-link {{ | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| min-height: 36px; | |
| margin-top: 16px; | |
| padding: 0 12px; | |
| border-radius: 6px; | |
| background: #111827; | |
| color: white; | |
| text-decoration: none; | |
| font-weight: 600; | |
| }} | |
| .stage-link:hover {{ background: #000000; }} | |
| .modal-backdrop {{ | |
| position: fixed; | |
| inset: 0; | |
| z-index: 20; | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 24px; | |
| background: rgba(33, 26, 22, 0.38); | |
| }} | |
| .modal-backdrop.open {{ | |
| display: flex; | |
| }} | |
| .modal-card {{ | |
| width: min(560px, 100%); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| background: white; | |
| box-shadow: var(--shadow); | |
| }} | |
| .modal-head {{ | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 16px; | |
| padding: 16px 18px; | |
| border-bottom: 1px solid var(--border); | |
| }} | |
| .modal-head strong {{ | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 16px; | |
| font-weight: 600; | |
| }} | |
| .modal-actions {{ | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| }} | |
| .copy-button {{ | |
| min-height: 30px; | |
| padding: 0 9px; | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| background: white; | |
| color: var(--text); | |
| cursor: pointer; | |
| font: inherit; | |
| font-size: 12px; | |
| font-weight: 600; | |
| white-space: nowrap; | |
| }} | |
| .copy-button:hover {{ | |
| border-color: var(--accent); | |
| color: var(--accent-dark); | |
| }} | |
| .copy-button.copied {{ | |
| border-color: var(--green); | |
| color: var(--green); | |
| background: var(--green-soft); | |
| }} | |
| .modal-close {{ | |
| width: 34px; | |
| height: 34px; | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| background: white; | |
| color: var(--text); | |
| cursor: pointer; | |
| font-size: 20px; | |
| line-height: 1; | |
| }} | |
| .modal-body {{ | |
| padding: 18px; | |
| color: var(--muted); | |
| font-size: 14px; | |
| line-height: 1.5; | |
| }} | |
| .cli-steps {{ | |
| display: grid; | |
| gap: 14px; | |
| margin: 0; | |
| padding: 0; | |
| list-style: none; | |
| }} | |
| .cli-steps li {{ | |
| display: grid; | |
| gap: 6px; | |
| }} | |
| .cli-steps span {{ | |
| color: var(--text); | |
| font-weight: 500; | |
| }} | |
| .command-row {{ | |
| position: relative; | |
| }} | |
| code.cli-command {{ | |
| display: block; | |
| overflow-x: auto; | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| background: #111827; | |
| color: white; | |
| padding: 10px 46px 10px 12px; | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; | |
| font-size: 13px; | |
| line-height: 1.4; | |
| white-space: nowrap; | |
| }} | |
| .command-row .copy-button {{ | |
| position: absolute; | |
| top: 5px; | |
| right: 5px; | |
| width: 28px; | |
| height: 28px; | |
| min-height: 0; | |
| padding: 0; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: rgba(255, 255, 255, 0.08); | |
| border-color: rgba(255, 255, 255, 0.18); | |
| color: rgba(255, 255, 255, 0.78); | |
| box-shadow: none; | |
| }} | |
| .command-row .copy-button:hover {{ | |
| background: rgba(255, 255, 255, 0.14); | |
| border-color: rgba(255, 255, 255, 0.34); | |
| color: white; | |
| }} | |
| .command-row .copy-button.copied {{ | |
| background: rgba(34, 197, 94, 0.18); | |
| border-color: rgba(187, 247, 208, 0.72); | |
| color: #bbf7d0; | |
| box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.12); | |
| transform: translateY(-1px); | |
| }} | |
| .copy-icon {{ | |
| width: 15px; | |
| height: 15px; | |
| display: block; | |
| }} | |
| .check-icon {{ | |
| width: 14px; | |
| height: 14px; | |
| display: block; | |
| }} | |
| .is-login .app-shell, .is-authed .login-shell {{ display: none; }} | |
| @media (max-width: 760px) {{ | |
| h1 {{ font-size: 32px; }} | |
| .laboratory-shell {{ | |
| grid-template-columns: 1fr; | |
| }} | |
| .space-sidebar {{ | |
| max-height: 46vh; | |
| border-right: 0; | |
| border-bottom: 1px solid #7c2d12; | |
| }} | |
| .sidebar-controls {{ | |
| padding: 10px; | |
| }} | |
| .sidebar-footer {{ | |
| grid-template-columns: 1fr 1fr; | |
| padding: 8px 10px; | |
| }} | |
| .dashboard-stage {{ | |
| min-height: 54vh; | |
| }} | |
| .login-panel {{ padding: 24px; }} | |
| }} | |
| </style> | |
| </head> | |
| <body class="{body_class}"> | |
| <section class="login-shell"> | |
| <div class="login-panel"> | |
| <img class="brand-mark" src="/static/trackio_logo_type_light_transparent.png" alt="Trackio" /> | |
| <p>Open the Trackio dashboards connected to your Hugging Face account and organizations.</p> | |
| <a id="login-link" class="hf-button {'disabled' if not oauth_ready else ''}" href="/login/huggingface?_target_url=/"> | |
| <img src="/static/huggingface-logo.svg" alt="" aria-hidden="true" /> | |
| <span>Sign in with Hugging Face</span> | |
| </a> | |
| </div> | |
| </section> | |
| <section class="app-shell"> | |
| <div class="laboratory-shell"> | |
| <aside class="space-sidebar"> | |
| <div class="sidebar-controls"> | |
| <input id="search" type="search" placeholder="Search Trackio dashboards" autocomplete="off" /> | |
| <div class="sidebar-toolbar"> | |
| <select id="sort" aria-label="Sort dashboards"> | |
| <option value="recent">Recent</option> | |
| <option value="name">Name</option> | |
| <option value="owner">Owner</option> | |
| </select> | |
| <button class="refresh-icon" id="refresh" type="button" aria-label="Refresh dashboards" title="Refresh dashboards">↻</button> | |
| </div> | |
| </div> | |
| <div id="status" class="status-line"> | |
| <span id="status-text" class="status-text">Loading Trackio Spaces...</span> | |
| </div> | |
| <div id="content" class="space-list"></div> | |
| <div class="sidebar-footer"> | |
| <button class="secondary-button utility-button cli-button" id="cli-access" type="button"> | |
| <span class="info-icon" aria-hidden="true">i</span> | |
| <span>CLI Access</span> | |
| </button> | |
| <a class="secondary-button" href="/logout">{logout_label}</a> | |
| </div> | |
| </aside> | |
| <main class="dashboard-stage"> | |
| <iframe id="dashboard-frame" class="dashboard-frame" title="Trackio dashboard" referrerpolicy="strict-origin-when-cross-origin" style="display: none;"></iframe> | |
| <div id="stage-empty" class="stage-empty"> | |
| <div class="stage-empty-inner"> | |
| <strong>No dashboard selected</strong> | |
| <span>Select a Trackio Space from the sidebar.</span> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| </section> | |
| <div class="modal-backdrop" id="cli-modal" role="dialog" aria-modal="true" aria-labelledby="cli-modal-title"> | |
| <div class="modal-card"> | |
| <div class="modal-head"> | |
| <strong id="cli-modal-title"> | |
| <svg class="code-icon" width="20" height="12" viewBox="0 0 20 12" fill="none" aria-hidden="true"> | |
| <path d="M5 2L2 6l3 4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /> | |
| <path d="M12 1L8 11" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" /> | |
| <path d="M15 2l3 4-3 4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /> | |
| </svg> | |
| <span>CLI Access</span> | |
| </strong> | |
| <div class="modal-actions"> | |
| <button class="copy-button" id="copy-cli-markdown" type="button">Copy Markdown</button> | |
| <button class="modal-close" id="cli-modal-close" type="button" aria-label="Close">×</button> | |
| </div> | |
| </div> | |
| <div class="modal-body"> | |
| <ol class="cli-steps"> | |
| <li> | |
| <span>1. First make sure you're using the latest version of Trackio.</span> | |
| <div class="command-row"> | |
| <code class="cli-command">pip install --upgrade trackio</code> | |
| <button class="copy-button copy-command" type="button" data-command="pip install --upgrade trackio" aria-label="Copy command"> | |
| <svg class="copy-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"> | |
| <path d="M6 5.2C6 4.54 6.54 4 7.2 4h4.6c.66 0 1.2.54 1.2 1.2v6.6c0 .66-.54 1.2-1.2 1.2H7.2C6.54 13 6 12.46 6 11.8V5.2Z" stroke="currentColor" stroke-width="1.35" /> | |
| <path d="M3 9.8V3.2C3 2.54 3.54 2 4.2 2h4.6" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </li> | |
| <li> | |
| <span>2. Get all Spaces in your lab.</span> | |
| <div class="command-row"> | |
| <code class="cli-command">trackio list spaces</code> | |
| <button class="copy-button copy-command" type="button" data-command="trackio list spaces" aria-label="Copy command"> | |
| <svg class="copy-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"> | |
| <path d="M6 5.2C6 4.54 6.54 4 7.2 4h4.6c.66 0 1.2.54 1.2 1.2v6.6c0 .66-.54 1.2-1.2 1.2H7.2C6.54 13 6 12.46 6 11.8V5.2Z" stroke="currentColor" stroke-width="1.35" /> | |
| <path d="M3 9.8V3.2C3 2.54 3.54 2 4.2 2h4.6" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </li> | |
| <li> | |
| <span>3. Get info about a Space.</span> | |
| <div class="command-row"> | |
| <code class="cli-command">trackio list project --spaces <space_id></code> | |
| <button class="copy-button copy-command" type="button" data-command="trackio list project --spaces <space_id>" aria-label="Copy command"> | |
| <svg class="copy-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"> | |
| <path d="M6 5.2C6 4.54 6.54 4 7.2 4h4.6c.66 0 1.2.54 1.2 1.2v6.6c0 .66-.54 1.2-1.2 1.2H7.2C6.54 13 6 12.46 6 11.8V5.2Z" stroke="currentColor" stroke-width="1.35" /> | |
| <path d="M3 9.8V3.2C3 2.54 3.54 2 4.2 2h4.6" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </li> | |
| </ol> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const state = {{ spaces: [], user: null, errors: {{}} }}; | |
| const status = document.querySelector("#status"); | |
| const statusText = document.querySelector("#status-text"); | |
| const content = document.querySelector("#content"); | |
| const search = document.querySelector("#search"); | |
| const sort = document.querySelector("#sort"); | |
| const refresh = document.querySelector("#refresh"); | |
| const frame = document.querySelector("#dashboard-frame"); | |
| const stageEmpty = document.querySelector("#stage-empty"); | |
| const cliAccess = document.querySelector("#cli-access"); | |
| const cliModal = document.querySelector("#cli-modal"); | |
| const cliModalClose = document.querySelector("#cli-modal-close"); | |
| const copyCliMarkdown = document.querySelector("#copy-cli-markdown"); | |
| const cliMarkdown = [ | |
| "## Trackio CLI Access", | |
| "", | |
| "1. First make sure you're using the latest version of Trackio.", | |
| "", | |
| "```bash", | |
| "pip install --upgrade trackio", | |
| "```", | |
| "", | |
| "2. Get all Spaces in your lab.", | |
| "", | |
| "```bash", | |
| "trackio list spaces", | |
| "```", | |
| "", | |
| "3. Get info about a Space.", | |
| "", | |
| "```bash", | |
| "trackio list project --spaces <space_id>", | |
| "```", | |
| ].join("\\n"); | |
| async function copyText(text) {{ | |
| if (navigator.clipboard?.writeText) {{ | |
| await navigator.clipboard.writeText(text); | |
| return; | |
| }} | |
| const textarea = document.createElement("textarea"); | |
| textarea.value = text; | |
| textarea.setAttribute("readonly", ""); | |
| textarea.style.position = "fixed"; | |
| textarea.style.top = "-1000px"; | |
| document.body.appendChild(textarea); | |
| textarea.select(); | |
| document.execCommand("copy"); | |
| textarea.remove(); | |
| }} | |
| function markCopied(button) {{ | |
| button.classList.add("copied"); | |
| const isCommandButton = button.classList.contains("copy-command"); | |
| const previousContent = button.innerHTML; | |
| if (isCommandButton) {{ | |
| button.innerHTML = ` | |
| <svg class="check-icon" viewBox="0 0 14 14" fill="none" aria-hidden="true"> | |
| <path d="M3 7.2l2.2 2.2L11 3.6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /> | |
| </svg> | |
| `; | |
| }} else {{ | |
| button.textContent = "Copied"; | |
| }} | |
| setTimeout(() => {{ | |
| button.classList.remove("copied"); | |
| button.innerHTML = previousContent; | |
| }}, 1400); | |
| }} | |
| function formatDate(value) {{ | |
| if (!value) return "Unknown"; | |
| const date = new Date(value); | |
| if (Number.isNaN(date.getTime())) return value; | |
| return date.toLocaleString(undefined, {{ dateStyle: "medium", timeStyle: "short" }}); | |
| }} | |
| function visibleSpaces() {{ | |
| const q = search.value.trim().toLowerCase(); | |
| let rows = state.spaces.filter((space) => {{ | |
| const haystack = `${{space.id}} ${{space.namespace}} ${{space.name}}`.toLowerCase(); | |
| return !q || haystack.includes(q); | |
| }}); | |
| rows = [...rows].sort((a, b) => {{ | |
| if (sort.value === "name") return a.name.localeCompare(b.name); | |
| if (sort.value === "owner") return a.namespace.localeCompare(b.namespace) || a.name.localeCompare(b.name); | |
| return String(b.last_modified || "").localeCompare(String(a.last_modified || "")); | |
| }}); | |
| return rows; | |
| }} | |
| function dashboardUrl(space) {{ | |
| return space?.app_url || space?.repo_url || ""; | |
| }} | |
| function setSelectedSpace(spaceId) {{ | |
| state.selectedId = spaceId; | |
| render(); | |
| }} | |
| function spaceStatus(space) {{ | |
| const stage = String(space.runtime_stage || "").toLowerCase(); | |
| if (["running"].includes(stage)) {{ | |
| return {{ className: "status-running", label: "running" }}; | |
| }} | |
| if (["building", "build_queued", "starting", "pending"].includes(stage)) {{ | |
| return {{ className: "status-building", label: stage.replaceAll("_", " ") }}; | |
| }} | |
| if (stage.includes("error") || stage.includes("failed")) {{ | |
| return {{ className: "status-error", label: stage.replaceAll("_", " ") }}; | |
| }} | |
| if (["paused", "sleeping", "stopped", "stopped_app"].includes(stage)) {{ | |
| return {{ className: "status-paused", label: stage.replaceAll("_", " ") }}; | |
| }} | |
| return {{ className: "status-unknown", label: stage ? stage.replaceAll("_", " ") : "status unknown" }}; | |
| }} | |
| function showStageMessage(title, bodyHtml = "") {{ | |
| frame.removeAttribute("src"); | |
| frame.style.display = "none"; | |
| stageEmpty.style.display = "grid"; | |
| stageEmpty.innerHTML = ` | |
| <div class="stage-empty-inner"> | |
| <strong>${{escapeHtml(title)}}</strong> | |
| ${{bodyHtml}} | |
| </div>`; | |
| }} | |
| function updateDashboard(selected) {{ | |
| if (!selected) {{ | |
| showStageMessage("No dashboard selected", "<span>Select a Trackio Space from the sidebar.</span>"); | |
| return; | |
| }} | |
| if (!selected.embeddable) {{ | |
| const repoUrl = selected.repo_url || dashboardUrl(selected); | |
| showStageMessage( | |
| "This private Space cannot be embedded", | |
| `<span>${{escapeHtml(selected.id)}} is private. Open it on Hugging Face to view it with your account permissions.</span><br /><a class="stage-link" href="${{escapeHtml(repoUrl)}}" target="_blank" rel="noopener">Open on Hugging Face</a>`, | |
| ); | |
| return; | |
| }} | |
| const url = dashboardUrl(selected); | |
| stageEmpty.style.display = "none"; | |
| frame.style.display = ""; | |
| if (frame.getAttribute("src") !== url) {{ | |
| frame.setAttribute("src", url); | |
| }} | |
| }} | |
| function render() {{ | |
| const rows = visibleSpaces(); | |
| const errorCount = Object.keys(state.errors || {{}}).length; | |
| const totalCount = state.spaces.length; | |
| search.placeholder = `Search ${{totalCount}} Trackio dashboard${{totalCount === 1 ? "" : "s"}}`; | |
| status.className = errorCount ? "status-line error-line" : "status-line"; | |
| statusText.textContent = errorCount | |
| ? `${{rows.length}} dashboard${{rows.length === 1 ? "" : "s"}}. Some namespaces could not be loaded.` | |
| : ""; | |
| if (!rows.length) {{ | |
| content.innerHTML = '<div class="empty">No Trackio dashboards found for this account.</div>'; | |
| updateDashboard(null); | |
| return; | |
| }} | |
| if (!state.selectedId || !rows.some((space) => space.id === state.selectedId)) {{ | |
| state.selectedId = rows[0].id; | |
| }} | |
| const selected = rows.find((space) => space.id === state.selectedId) || rows[0]; | |
| updateDashboard(selected); | |
| content.innerHTML = rows.map((space) => {{ | |
| const status = spaceStatus(space); | |
| return ` | |
| <button class="space-item ${{space.id === selected.id ? "active" : ""}}" type="button" data-space-id="${{escapeHtml(space.id)}}" title="${{escapeHtml(space.id)}} · ${{escapeHtml(status.label)}}"> | |
| <span class="space-dot ${{status.className}}" aria-hidden="true"></span> | |
| <span class="space-copy"> | |
| <strong>${{escapeHtml(space.name)}}</strong> | |
| <span>${{escapeHtml(space.namespace)}}</span> | |
| </span> | |
| </button>`; | |
| }}).join(""); | |
| content.querySelectorAll(".space-item").forEach((button) => {{ | |
| button.addEventListener("click", () => setSelectedSpace(button.dataset.spaceId)); | |
| }}); | |
| }} | |
| function escapeHtml(value) {{ | |
| return String(value ?? "") | |
| .replaceAll("&", "&") | |
| .replaceAll("<", "<") | |
| .replaceAll(">", ">") | |
| .replaceAll('"', """) | |
| .replaceAll("'", "'"); | |
| }} | |
| function applySpacesData(data) {{ | |
| state.spaces = data.spaces || []; | |
| state.user = data.user || null; | |
| state.errors = data.errors || {{}}; | |
| state.cache = data.cache || null; | |
| if (!state.selectedId && state.spaces.length) {{ | |
| state.selectedId = state.spaces[0].id; | |
| }} | |
| render(); | |
| }} | |
| async function fetchSpaces(refreshCache = false) {{ | |
| const url = refreshCache ? "/api/spaces?refresh=1" : "/api/spaces"; | |
| const resp = await fetch(url, {{ credentials: "include" }}); | |
| if (resp.status === 401) {{ | |
| window.location.href = "/"; | |
| return null; | |
| }} | |
| if (!resp.ok) throw new Error(`Request failed with ${{resp.status}}`); | |
| return await resp.json(); | |
| }} | |
| async function loadSpaces() {{ | |
| refresh.disabled = true; | |
| status.className = "status-line loading-line"; | |
| statusText.textContent = "Loading Trackio Spaces..."; | |
| try {{ | |
| const data = await fetchSpaces(false); | |
| if (!data) return; | |
| applySpacesData(data); | |
| if (data.cache?.hit) {{ | |
| fetchSpaces(true) | |
| .then((fresh) => {{ if (fresh) applySpacesData(fresh); }}) | |
| .catch((err) => console.warn("Background refresh failed:", err)); | |
| }} | |
| }} catch (err) {{ | |
| status.className = "status-line error-line"; | |
| statusText.textContent = `Could not load Trackio Spaces: ${{err.message}}`; | |
| content.innerHTML = ""; | |
| }} finally {{ | |
| refresh.disabled = false; | |
| }} | |
| }} | |
| search.addEventListener("input", render); | |
| sort.addEventListener("change", render); | |
| refresh.addEventListener("click", async () => {{ | |
| refresh.disabled = true; | |
| try {{ | |
| const data = await fetchSpaces(true); | |
| if (data) applySpacesData(data); | |
| }} catch (err) {{ | |
| status.className = "status-line error-line"; | |
| statusText.textContent = `Could not refresh Trackio Spaces: ${{err.message}}`; | |
| }} finally {{ | |
| refresh.disabled = false; | |
| }} | |
| }}); | |
| function closeCliModal() {{ | |
| cliModal?.classList.remove("open"); | |
| }} | |
| cliAccess?.addEventListener("click", () => {{ | |
| cliModal?.classList.add("open"); | |
| cliModalClose?.focus(); | |
| }}); | |
| cliModalClose?.addEventListener("click", closeCliModal); | |
| cliModal?.addEventListener("click", (event) => {{ | |
| if (event.target === cliModal) closeCliModal(); | |
| }}); | |
| copyCliMarkdown?.addEventListener("click", async () => {{ | |
| try {{ | |
| await copyText(cliMarkdown); | |
| markCopied(copyCliMarkdown); | |
| }} catch (err) {{ | |
| console.warn("Could not copy CLI markdown:", err); | |
| }} | |
| }}); | |
| document.querySelectorAll(".copy-command").forEach((button) => {{ | |
| button.addEventListener("click", async () => {{ | |
| try {{ | |
| await copyText(button.dataset.command || ""); | |
| markCopied(button); | |
| }} catch (err) {{ | |
| console.warn("Could not copy CLI command:", err); | |
| }} | |
| }}); | |
| }}); | |
| window.addEventListener("keydown", (event) => {{ | |
| if (event.key === "Escape") closeCliModal(); | |
| }}); | |
| const loginLink = document.querySelector("#login-link"); | |
| if (loginLink) {{ | |
| loginLink.addEventListener("click", (event) => {{ | |
| if (loginLink.classList.contains("disabled")) return; | |
| event.preventDefault(); | |
| window.parent?.postMessage({{ type: "SET_SCROLLING", enabled: true }}, "*"); | |
| setTimeout(() => {{ | |
| window.location.assign(loginLink.getAttribute("href")); | |
| }}, 500); | |
| }}); | |
| }} | |
| if (document.body.classList.contains("is-authed")) loadSpaces(); | |
| </script> | |
| </body> | |
| </html>""" | |
| return HTMLResponse(html_doc) | |
| async def login(request: Request) -> Response: | |
| client_id = os.getenv("OAUTH_CLIENT_ID") | |
| if not client_id: | |
| return RedirectResponse("/", status_code=302) | |
| _evict_expired() | |
| state = secrets.token_urlsafe(32) | |
| _pending_states[state] = { | |
| "created_at": _now(), | |
| "next": _safe_next( | |
| request.query_params.get("_target_url") or request.query_params.get("next") | |
| ), | |
| } | |
| redirect_uri = _oauth_redirect_uri(request) | |
| scope = os.getenv("OAUTH_SCOPES", DEFAULT_OAUTH_SCOPES).strip() | |
| url = "https://huggingface.co/oauth/authorize?" + urlencode( | |
| { | |
| "client_id": client_id, | |
| "redirect_uri": redirect_uri, | |
| "response_type": "code", | |
| "scope": scope, | |
| "state": state, | |
| } | |
| ) | |
| return RedirectResponse(url, status_code=302) | |
| async def login_callback(request: Request) -> Response: | |
| client_id = os.getenv("OAUTH_CLIENT_ID") | |
| client_secret = os.getenv("OAUTH_CLIENT_SECRET") | |
| code = request.query_params.get("code") | |
| state = request.query_params.get("state") | |
| pending = _pending_states.pop(state, None) if state else None | |
| if not client_id or not client_secret or not code or not pending: | |
| return RedirectResponse("/?oauth_error=1", status_code=302) | |
| if _now() - pending["created_at"] > STATE_TTL_SECONDS: | |
| return RedirectResponse("/?oauth_error=1", status_code=302) | |
| redirect_uri = _oauth_redirect_uri(request) | |
| auth_b64 = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode() | |
| try: | |
| async with httpx.AsyncClient(timeout=30) as client: | |
| token_resp = await client.post( | |
| "https://huggingface.co/oauth/token", | |
| headers={"Authorization": f"Basic {auth_b64}"}, | |
| data={ | |
| "grant_type": "authorization_code", | |
| "code": code, | |
| "redirect_uri": redirect_uri, | |
| "client_id": client_id, | |
| }, | |
| ) | |
| token_resp.raise_for_status() | |
| access_token = token_resp.json()["access_token"] | |
| whoami = api.whoami(token=access_token, cache=True) | |
| except Exception: | |
| return RedirectResponse("/?oauth_error=1", status_code=302) | |
| session_id = secrets.token_urlsafe(32) | |
| _sessions[session_id] = { | |
| "token": access_token, | |
| "created_at": _now(), | |
| "username": whoami.get("name"), | |
| "display_name": _display_username(whoami), | |
| } | |
| resp = RedirectResponse(_safe_next(pending.get("next")), status_code=302) | |
| _set_session_cookie(resp, session_id, request) | |
| return resp | |
| async def logout(request: Request) -> Response: | |
| session_id = _session_id_from_request(request) | |
| if session_id: | |
| _sessions.pop(session_id, None) | |
| resp = RedirectResponse("/", status_code=302) | |
| _clear_session_cookie(resp, request) | |
| return resp | |
| async def spaces_api(request: Request) -> Response: | |
| session = _session_from_request(request) | |
| if not session: | |
| return JSONResponse({"error": "Not authenticated"}, status_code=401) | |
| refresh = request.query_params.get("refresh") in {"1", "true", "yes"} | |
| username = session.get("username") | |
| if not refresh: | |
| cached = _read_cached_spaces(username) | |
| if cached is not None: | |
| return JSONResponse(cached) | |
| try: | |
| data = discover_trackio_spaces(session["token"]) | |
| data["cache"] = {"hit": False, "saved_at": None} | |
| _write_cached_spaces(username, data) | |
| return JSONResponse(data) | |
| except Exception as err: | |
| cached = _read_cached_spaces(username) | |
| if cached is not None: | |
| cached["cache"]["stale_due_to_error"] = str(err) | |
| return JSONResponse(cached) | |
| return JSONResponse({"error": str(err)}, status_code=500) | |
| routes = [ | |
| Route("/", homepage, methods=["GET"]), | |
| Route("/login", login, methods=["GET"]), | |
| Route("/login/huggingface", login, methods=["GET"]), | |
| Route("/login/callback", login_callback, methods=["GET"]), | |
| Route("/logout", logout, methods=["GET"]), | |
| Route("/api/spaces", spaces_api, methods=["GET"]), | |
| Mount("/static", StaticFiles(directory="static"), name="static"), | |
| ] | |
| app = Starlette(routes=routes) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", "7860"))) | |