The Hackathon Organizer Node

Dashboard

Streamlit dashboard for managing VS Code instances, groups, and Lemonade Server

Dashboard

THON includes a Streamlit-based web dashboard for managing VS Code sandbox instances, groups, and Lemonade Server — replacing the original vanilla JS frontend.

Quick Start

# Install dependencies
pip install streamlit pandas

# Run the dashboard
streamlit run dashboard/streamlit_app.py --server.port 8501

Dashboard available at http://localhost:8501.

Architecture

The Streamlit dashboard calls backend services directly — no HTTP API needed between the frontend and backend. The FastAPI REST API on port 8100 remains available for programmatic/scripted access.

┌──────────────────────────────────────────────────────────┐
│                      Host Machine                         │
│                                                           │
│  ┌────────────────────────────────────────────────────┐   │
│  │          Streamlit Dashboard (:8501)                │   │
│  │                                                    │   │
│  │  Instances │ Groups │ Lemonade │ Gateway │ Settings│   │
│  │       │         │         │         │         │    │   │
│  │       ▼         ▼         ▼         ▼         ▼    │   │
│  │  SandboxService  GroupsService  LemonadeService    │   │
│  │  LemonadeService  app.db         app.db            │   │
│  │  app.db               ApisixService                │   │
│  └────────────────────────────────────────────────────┘   │
│                                                           │
│  ┌────────────────────────────────────────────────────┐   │
│  │          FastAPI REST API (:8100)                   │   │
│  │          /docs — Swagger UI                         │   │
│  │          /api/* — programmatic access               │   │
│  └────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────┘

Key Design Decisions

DecisionRationale
Direct service callsNo HTTP overhead; shared AppConfig and SQLite DB
Separate process from FastAPIStreamlit has its own server; both can run independently
Session-state cachingService instances cached in st.session_state to avoid re-creation
Async bridge via _run_async()SandboxService is async; bridged with asyncio.run() in a thread pool

Pages

Instances

Full CRUD for VS Code sandbox instances:

FeatureDetails
ListAll instances with state, user, endpoint, ID
Search/FilterBy username/group or instance state
CreateGroup, username, port, password authentication
ActionsPause, resume, kill, restart (per instance)
Bulk actionsMulti-select via st.dataframe for pause/resume/kill
Detail expandersFull instance info including URL, password, image
RecreateRe-create a terminated sandbox with the same PVC workspace volume

Groups

Manage groups and their users:

FeatureDetails
ListAll groups with user counts
SearchFilter by group name or username
Create/Rename/DeleteFull group lifecycle
Add/Remove/Rename usersPer-group user management
Transfer userMove a user between groups
Start user instanceStart a sandbox for an individual user with PVC workspace
Start allStart sandboxes for all users in a group with PVC workspaces
User detail tableUUID, username, workspace path

Starting Instances from Groups

The Groups page provides per-user and per-group instance start buttons:

  • ▶ username — Starts a sandbox for a single user. Uses the user's PVC workspace volume (thon-workspace-*) if one exists in the database.
  • ▶ Start All — Starts sandboxes for all users in the group concurrently. Each user gets their own PVC workspace volume.

Both features prevent duplicate instances — if a user already has a running or paused instance, a warning is shown and no new instance is created.

Lemonade Server

Monitor and inspect the Lemonade inference server:

FeatureDetails
Status overviewOnline/offline, model, context size, users
API informationEndpoint, OpenAI compatibility, key status
Server healthVersion, active model, loaded models, WS port
Performance statsTTFT, tokens/sec, input/output tokens
Slotsllama.cpp slot states and cache info
System infoOS, CPU, memory, GPU devices
Available modelsRegistered models from user_models.json

Gracefully handles offline servers — non-critical API calls use _safe_proxy() which catches LemonadeConnectionError so the page still renders.

AI Gateway

Configure and manage the APISIX AI Gateway:

FeatureDetails
ConfigurationEnable/disable gateway, mode (per-user/per-group), rate limits, Redis, Lemonade URL
Status overviewRunning/offline, consumer count, route status, mode
Consumer listUsername, API key, rate limit, type (user/group), group name, user count
SetupCreate route + consumers from DB groups with one click
CleanupRemove all consumers and routes
Delete consumerRemove individual consumers by username
Settings persistenceMode, rate limit, time window persisted to SQLite

Gracefully handles offline gateways — non-critical API calls use _safe_proxy() which catches GatewayConnectionError and GatewayNotEnabledError.

Settings

SettingDescription
External IPPersisted to SQLite; used for public URL generation
Configuration FilesUpload, edit, and delete config files stored in the database

Configuration Files Section

The Settings page includes a Configuration Files section for managing config file contents stored in the SQLite database. When main.py runs without CLI flags, it reads these from the database.

Config KeyLabelDescription
config_groups_yamlGroups YAMLGroups and users definition for main.py --from-db
config_kilo_jsonKilo Code ConfigKilo Code provider config injected into each sandbox
config_vscode_settingsVS Code SettingsCode-server settings injected into each sandbox

Each config file supports:

  • Upload File — Upload a .yaml, .json, or .jsonc file
  • Edit — Inline text editor for modifying content directly
  • Delete — Remove the stored config

Priority: CLI flag > database > none.

Authentication

The dashboard supports two authentication mechanisms that can be used independently or together, depending on your deployment needs.

Local Password (Streamlit Dashboard)

The simplest authentication method — a single shared password that gates access to the Streamlit dashboard. When enabled, visitors see a login form before any dashboard content is rendered.

Enabling

# Set the password via environment variable
AUTH_LOCAL_PASSWORD=mysecret streamlit run dashboard/streamlit_app.py --server.port 8501

# Or in .env / thon.yaml
export AUTH_LOCAL_PASSWORD=mysecret

Or in thon.yaml:

auth:
  local_password: "mysecret"   # not a standard thon.yaml field; uses env var

Behavior

AspectDetails
ScopeStreamlit dashboard only (not the FastAPI REST API)
MechanismSingle password check via Streamlit form
SessionStored in st.session_state.authenticated; lost on browser refresh is handled by re-checking
SecuritySuitable for internal/hackathon use; not a replacement for OIDC in production

When AUTH_LOCAL_PASSWORD is unset (default), no authentication is required to access the dashboard.

OIDC/OAuth2 (FastAPI REST API)

The FastAPI backend supports full OIDC/OAuth2 authentication via external identity providers. This protects the REST API endpoints (/api/*) with session-based authentication.

Supported Providers

ProviderProtocolScopeUser ID Format
GitHubOAuth2read:user user:emailgithub:<id>
GitLabOIDCread_usergitlab:<id>
LinkedInOIDCopenid profile emaillinkedin:<sub>

Enabling

# Enable auth with GitHub
AUTH_ENABLED=true \
AUTH_SESSION_SECRET=$(openssl rand -hex 32) \
AUTH_GITHUB_CLIENT_ID=your-client-id \
AUTH_GITHUB_CLIENT_SECRET=your-client-secret \
python -m app.main

Or in thon.yaml:

auth:
  enabled: true
  session_secret: "a-random-secret-at-least-32-chars"
  github:
    client_id: "your-client-id"
    client_secret: "your-client-secret"

Environment Variables

VariableDefaultDescription
AUTH_ENABLEDfalseEnable OIDC authentication on the REST API
AUTH_SESSION_SECRET(none)HMAC secret for signing session tokens (required when enabled)
AUTH_LOCAL_PASSWORD(none)Single password for Streamlit dashboard access
AUTH_GITHUB_CLIENT_ID(none)GitHub OAuth App client ID
AUTH_GITHUB_CLIENT_SECRET(none)GitHub OAuth App client secret
AUTH_GITLAB_CLIENT_ID(none)GitLab OAuth App client ID
AUTH_GITLAB_CLIENT_SECRET(none)GitLab OAuth App client secret
AUTH_LINKEDIN_CLIENT_ID(none)LinkedIn OIDC client ID
AUTH_LINKEDIN_CLIENT_SECRET(none)LinkedIn OIDC client secret

OAuth Flow

┌──────────┐     1. GET /api/auth/login/github     ┌──────────┐
│  Browser  │ ───────────────────────────────────▶ │  FastAPI  │
│           │ ◀───────────────────────────────────  │  Server   │
│           │     2. { authorization_url }          │           │
│           │                                       │           │
│           │     3. Redirect to GitHub             ┌──────────┐
│           │ ───────────────────────────────────▶ │  GitHub   │
│           │ ◀───────────────────────────────────  │           │
│           │     4. Redirect back with ?code=      └──────────┘
│           │                                       │
│           │     5. GET /api/auth/callback/github  ┌──────────┐
│           │ ───────────────────────────────────▶ │  FastAPI  │
│           │ ◀───────────────────────────────────  │  Server   │
│           │     6. Set-Cookie: session=<token>    │           │
└──────────┘                                       └──────────┘
  1. LoginGET /api/auth/login/{provider} returns an authorization URL
  2. Redirect — Browser redirects user to the provider (GitHub, GitLab, LinkedIn)
  3. Authorize — User grants access on the provider's site
  4. Callback — Provider redirects back to /api/auth/callback/{provider}?code=...&state=...
  5. Exchange — Server exchanges the authorization code for an access token
  6. Session — Server creates an HMAC-signed session token, sets it as an HttpOnly cookie with 24-hour expiry and SameSite=Lax

PKCE Support

The OAuth flow uses PKCE (Proof Key for Code Exchange) with the S256 method for enhanced security. A code_verifier and code_challenge are generated for each authorization request.

Session Management

Sessions are stored in-memory using SessionStore with HMAC-signed tokens:

PropertyValue
Token formatv1:<session_id>:<hmac_signature>
SignatureHMAC-SHA256 truncated to 16 hex chars
TTL24 hours (86400 seconds)
StorageIn-memory dictionary (per-process)
Cookiesession, HttpOnly, SameSite=Lax

::: warning In-memory sessions are lost on server restart. For production deployments, replace SessionStore with Redis or database-backed session storage. :::

Setting Up Provider OAuth Apps

GitHub
  1. Go to Settings → Developer settings → OAuth Apps → New OAuth App
  2. Set Homepage URL to https://your-domain
  3. Set Authorization callback URL to https://your-domain/api/auth/callback/github
  4. Copy the Client ID and generate a Client Secret
GitLab
  1. Go to Settings → Applications → New application
  2. Set Redirect URI to https://your-domain/api/auth/callback/gitlab
  3. Enable openid, profile, email scopes
  4. Copy the Application ID and Secret
LinkedIn
  1. Go to My Apps → Create App
  2. Set Redirect URL to https://your-domain/api/auth/callback/linkedin
  3. Request Sign In with LinkedIn using OpenID Connect product
  4. Copy the Client ID and Client Secret

Combining Both Methods

For maximum security, you can enable both authentication methods:

  • AUTH_LOCAL_PASSWORD protects the Streamlit dashboard
  • AUTH_ENABLED + provider credentials protects the FastAPI REST API
AUTH_LOCAL_PASSWORD=mysecret \
AUTH_ENABLED=true \
AUTH_SESSION_SECRET=$(openssl rand -hex 32) \
AUTH_GITHUB_CLIENT_ID=xxx \
AUTH_GITHUB_CLIENT_SECRET=xxx \
streamlit run dashboard/streamlit_app.py --server.port 8501

Files

FilePurpose
dashboard/streamlit_app.pyMain app with 5 pages and sidebar navigation
dashboard/streamlit_styles.pyDark theme CSS injection matching the original JS theme
dashboard/index.htmlLegacy HTML shell (superseded by Streamlit)
dashboard/static/style.cssLegacy CSS (superseded by Streamlit)
dashboard/static/app.jsLegacy JS (superseded by Streamlit)

Configuration

The dashboard reads configuration from the same environment variables as the backend services. No separate configuration file is needed — or use python -m thon config env to generate the .env file from thon.yaml.

VariableDefaultDescription
SANDBOX_DOMAINlocalhost:8080Sandbox server address
SANDBOX_API_KEY(none)Sandbox API key
SANDBOX_IMAGEwaterpistol/thon:latestDocker image for sandboxes
LEMONADE_HOST0.0.0.0Lemonade server bind address
LEMONADE_PORT13305Lemonade server port
LEMONADE_API_KEY(none)Lemonade API key
LEMONADE_ADMIN_API_KEY(none)Lemonade admin API key
GATEWAY_ENABLEDfalseEnable APISIX AI Gateway
GATEWAY_ADMIN_URLhttp://127.0.0.1:9180APISIX Admin API URL
GATEWAY_ADMIN_KEY(default)APISIX Admin API key
GATEWAY_PROXY_PORT9080APISIX proxy port
GATEWAY_REDIS_HOST(none)Redis host for rate limiting
GATEWAY_RATE_LIMIT_TOKENS500Token limit per consumer
GATEWAY_RATE_LIMIT_WINDOW60Time window in seconds
GATEWAY_MODEper-userConsumer mode: per-user or per-group
THON_DB_PATH~/.thon/thon.dbSQLite database path
THON_WORKSPACE_DIR~/.thon/workspaceWorkspace directory for groups

Running Alongside the REST API

Both the Streamlit dashboard and the FastAPI REST API can run simultaneously:

# Terminal 1: Streamlit dashboard
streamlit run dashboard/streamlit_app.py --server.port 8501

# Terminal 2: FastAPI REST API (optional, for programmatic access)
python -m app.main
# API docs at http://localhost:8100/docs

Both processes share the same AppConfig loaded from environment variables and the same SQLite database.

REST API Endpoints

Instances

MethodPathDescription
GET/api/instancesList instances (filter by state, paginate)
POST/api/instancesCreate new instance
GET/api/instances/{id}Get instance details
POST/api/instances/{id}/pausePause instance
POST/api/instances/{id}/resumeResume instance
DELETE/api/instances/{id}Terminate instance
POST/api/instances/{id}/renewExtend TTL
POST/api/instances/bulk/pauseBulk pause
POST/api/instances/bulk/resumeBulk resume
POST/api/instances/bulk/killBulk terminate

Groups

MethodPathDescription
GET/api/groupsList all groups with users
POST/api/groupsCreate a new group
GET/api/groups/exportExport groups as YAML dict
GET/api/groups/eventsList all events
GET/api/groups/events/{event_id}Get event by ID
GET/api/groups/{group_id}Get single group
PUT/api/groups/{group_id}Rename a group
DELETE/api/groups/{group_id}Delete a group and its users
POST/api/groups/{group_id}/usersAdd a user to a group
PUT/api/groups/{group_id}/users/{user_id}Update a user
DELETE/api/groups/{group_id}/users/{user_id}Delete a user
POST/api/groups/{group_id}/users/{user_id}/transferTransfer user to another group

Config Files

MethodPathDescription
GET/api/config-filesList all config file slots with state
GET/api/config-files/{key}Get config file content
PUT/api/config-files/{key}Update config file content
POST/api/config-files/{key}/uploadUpload a config file
DELETE/api/config-files/{key}Delete a config file

Lemonade

MethodPathDescription
GET/api/lemonade/statusLemonade server status
GET/api/lemonade/modelsAvailable models
GET/api/lemonade/api-infoAPI endpoint info
GET/api/lemonade/healthProxy: server health
GET/api/lemonade/statsProxy: performance statistics
GET/api/lemonade/system-infoProxy: hardware details
GET/api/lemonade/liveProxy: liveness probe
GET/api/lemonade/slotsProxy: llama.cpp slots state
POST/api/lemonade/pullProxy: pull a model
POST/api/lemonade/deleteProxy: delete a model
POST/api/lemonade/loadProxy: load a model
POST/api/lemonade/unloadProxy: unload a model

Gateway

MethodPathDescription
GET/api/gateway/statusGateway status
GET/api/gateway/consumersList consumers
POST/api/gateway/consumersCreate consumer
DELETE/api/gateway/consumers/{username}Delete consumer
POST/api/gateway/setupFull setup
POST/api/gateway/cleanupRemove all consumers and routes
POST/api/gateway/routeCreate/update AI proxy route
DELETE/api/gateway/routeDelete AI proxy route

Auth

MethodPathDescription
GET/api/auth/providersList enabled OIDC/OAuth providers and auth status
GET/api/auth/login/{provider}Start OAuth flow; returns authorization_url and state
GET/api/auth/callback/{provider}Handle OAuth callback; sets session cookie
POST/api/auth/logoutInvalidate session and clear cookie
GET/api/auth/meGet current authenticated user info

Dark Theme

The dashboard uses a dark theme that matches the original vanilla JS dashboard's color scheme. The CSS is injected once at startup via inject_dark_theme() in dashboard/streamlit_styles.py, which writes CSS variables for:

  • Background colors (--bg-primary, --bg-secondary, --bg-card)
  • Border and hover colors
  • Text colors (primary, secondary, muted)
  • Accent color (indigo #6366f1)
  • Status colors (success, warning, danger, info)

The theme targets Streamlit's internal CSS class names (stMetric, stDataFrame, stExpander, stButton, etc.) with !important overrides.

Implementation Notes

Async Bridge

SandboxService methods are async (using opensandbox SDK). The Streamlit app bridges these calls via _run_async():

  1. If no event loop is running, uses asyncio.run() directly
  2. If an event loop is already running (rare in Streamlit), runs asyncio.run() in a ThreadPoolExecutor to avoid blocking

Dialog Pattern

Since st.modal is not available in all Streamlit versions, dialogs use st.container(border=True) with session state flags:

if st.button("Create", type="primary"):
    st.session_state.show_create_instance = True

# Later in the page:
if st.session_state.get("show_create_instance"):
    with st.container(border=True):
        # form fields...

Multi-Instance Selection

st.dataframe with on_select="rerun" and selection_mode="multi-row" enables selecting multiple instances for bulk pause/resume/kill actions. Selected row indices are mapped back to instance IDs via a hidden "Full ID" column.

Lemonade Error Handling

_safe_proxy() wraps Lemonade API calls and returns None on connection errors, allowing the page to render partial data when the server is offline.

PVC Workspace Volumes

When starting instances from the Groups page, the dashboard reads each user's workspace_path from the database. If it starts with thon-, it is treated as a Docker named volume (PVC) and mounted at /workspace in the sandbox. This ensures workspace data persists across instance recreations.

Duplicate Instance Prevention

Before creating a new instance, the dashboard checks if the user already has a running or paused sandbox. If so, a warning is displayed and creation is skipped. This prevents accidental duplicate instances.

On this page