laboratory / app.py
abidlabs's picture
abidlabs HF Staff
Refine Laboratory sidebar actions
4c96c12 verified
Raw
History Blame Contribute Delete
47.4 kB
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 &lt;space_id&gt;</code>
<button class="copy-button copy-command" type="button" data-command="trackio list project --spaces &lt;space_id&gt;" 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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}}
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")))