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 8501Dashboard 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
| Decision | Rationale |
|---|---|
| Direct service calls | No HTTP overhead; shared AppConfig and SQLite DB |
| Separate process from FastAPI | Streamlit has its own server; both can run independently |
| Session-state caching | Service 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:
| Feature | Details |
|---|---|
| List | All instances with state, user, endpoint, ID |
| Search/Filter | By username/group or instance state |
| Create | Group, username, port, password authentication |
| Actions | Pause, resume, kill, restart (per instance) |
| Bulk actions | Multi-select via st.dataframe for pause/resume/kill |
| Detail expanders | Full instance info including URL, password, image |
| Recreate | Re-create a terminated sandbox with the same PVC workspace volume |
Groups
Manage groups and their users:
| Feature | Details |
|---|---|
| List | All groups with user counts |
| Search | Filter by group name or username |
| Create/Rename/Delete | Full group lifecycle |
| Add/Remove/Rename users | Per-group user management |
| Transfer user | Move a user between groups |
| Start user instance | Start a sandbox for an individual user with PVC workspace |
| Start all | Start sandboxes for all users in a group with PVC workspaces |
| User detail table | UUID, 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:
| Feature | Details |
|---|---|
| Status overview | Online/offline, model, context size, users |
| API information | Endpoint, OpenAI compatibility, key status |
| Server health | Version, active model, loaded models, WS port |
| Performance stats | TTFT, tokens/sec, input/output tokens |
| Slots | llama.cpp slot states and cache info |
| System info | OS, CPU, memory, GPU devices |
| Available models | Registered 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:
| Feature | Details |
|---|---|
| Configuration | Enable/disable gateway, mode (per-user/per-group), rate limits, Redis, Lemonade URL |
| Status overview | Running/offline, consumer count, route status, mode |
| Consumer list | Username, API key, rate limit, type (user/group), group name, user count |
| Setup | Create route + consumers from DB groups with one click |
| Cleanup | Remove all consumers and routes |
| Delete consumer | Remove individual consumers by username |
| Settings persistence | Mode, rate limit, time window persisted to SQLite |
Gracefully handles offline gateways — non-critical API calls use _safe_proxy()
which catches GatewayConnectionError and GatewayNotEnabledError.
Settings
| Setting | Description |
|---|---|
| External IP | Persisted to SQLite; used for public URL generation |
| Configuration Files | Upload, 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 Key | Label | Description |
|---|---|---|
config_groups_yaml | Groups YAML | Groups and users definition for main.py --from-db |
config_kilo_json | Kilo Code Config | Kilo Code provider config injected into each sandbox |
config_vscode_settings | VS Code Settings | Code-server settings injected into each sandbox |
Each config file supports:
- Upload File — Upload a
.yaml,.json, or.jsoncfile - 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=mysecretOr in thon.yaml:
auth:
local_password: "mysecret" # not a standard thon.yaml field; uses env varBehavior
| Aspect | Details |
|---|---|
| Scope | Streamlit dashboard only (not the FastAPI REST API) |
| Mechanism | Single password check via Streamlit form |
| Session | Stored in st.session_state.authenticated; lost on browser refresh is handled by re-checking |
| Security | Suitable 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
| Provider | Protocol | Scope | User ID Format |
|---|---|---|---|
| GitHub | OAuth2 | read:user user:email | github:<id> |
| GitLab | OIDC | read_user | gitlab:<id> |
| OIDC | openid profile email | linkedin:<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.mainOr 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
| Variable | Default | Description |
|---|---|---|
AUTH_ENABLED | false | Enable 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> │ │
└──────────┘ └──────────┘- Login —
GET /api/auth/login/{provider}returns an authorization URL - Redirect — Browser redirects user to the provider (GitHub, GitLab, LinkedIn)
- Authorize — User grants access on the provider's site
- Callback — Provider redirects back to
/api/auth/callback/{provider}?code=...&state=... - Exchange — Server exchanges the authorization code for an access token
- Session — Server creates an HMAC-signed session token, sets it as an
HttpOnlycookie with 24-hour expiry andSameSite=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:
| Property | Value |
|---|---|
| Token format | v1:<session_id>:<hmac_signature> |
| Signature | HMAC-SHA256 truncated to 16 hex chars |
| TTL | 24 hours (86400 seconds) |
| Storage | In-memory dictionary (per-process) |
| Cookie | session, 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
- Go to Settings → Developer settings → OAuth Apps → New OAuth App
- Set Homepage URL to
https://your-domain - Set Authorization callback URL to
https://your-domain/api/auth/callback/github - Copy the Client ID and generate a Client Secret
GitLab
- Go to Settings → Applications → New application
- Set Redirect URI to
https://your-domain/api/auth/callback/gitlab - Enable openid, profile, email scopes
- Copy the Application ID and Secret
- Go to My Apps → Create App
- Set Redirect URL to
https://your-domain/api/auth/callback/linkedin - Request Sign In with LinkedIn using OpenID Connect product
- Copy the Client ID and Client Secret
Combining Both Methods
For maximum security, you can enable both authentication methods:
AUTH_LOCAL_PASSWORDprotects the Streamlit dashboardAUTH_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 8501Files
| File | Purpose |
|---|---|
dashboard/streamlit_app.py | Main app with 5 pages and sidebar navigation |
dashboard/streamlit_styles.py | Dark theme CSS injection matching the original JS theme |
dashboard/index.html | Legacy HTML shell (superseded by Streamlit) |
dashboard/static/style.css | Legacy CSS (superseded by Streamlit) |
dashboard/static/app.js | Legacy 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.
| Variable | Default | Description |
|---|---|---|
SANDBOX_DOMAIN | localhost:8080 | Sandbox server address |
SANDBOX_API_KEY | (none) | Sandbox API key |
SANDBOX_IMAGE | waterpistol/thon:latest | Docker image for sandboxes |
LEMONADE_HOST | 0.0.0.0 | Lemonade server bind address |
LEMONADE_PORT | 13305 | Lemonade server port |
LEMONADE_API_KEY | (none) | Lemonade API key |
LEMONADE_ADMIN_API_KEY | (none) | Lemonade admin API key |
GATEWAY_ENABLED | false | Enable APISIX AI Gateway |
GATEWAY_ADMIN_URL | http://127.0.0.1:9180 | APISIX Admin API URL |
GATEWAY_ADMIN_KEY | (default) | APISIX Admin API key |
GATEWAY_PROXY_PORT | 9080 | APISIX proxy port |
GATEWAY_REDIS_HOST | (none) | Redis host for rate limiting |
GATEWAY_RATE_LIMIT_TOKENS | 500 | Token limit per consumer |
GATEWAY_RATE_LIMIT_WINDOW | 60 | Time window in seconds |
GATEWAY_MODE | per-user | Consumer mode: per-user or per-group |
THON_DB_PATH | ~/.thon/thon.db | SQLite database path |
THON_WORKSPACE_DIR | ~/.thon/workspace | Workspace 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/docsBoth processes share the same AppConfig loaded from environment variables and
the same SQLite database.
REST API Endpoints
Instances
| Method | Path | Description |
|---|---|---|
GET | /api/instances | List instances (filter by state, paginate) |
POST | /api/instances | Create new instance |
GET | /api/instances/{id} | Get instance details |
POST | /api/instances/{id}/pause | Pause instance |
POST | /api/instances/{id}/resume | Resume instance |
DELETE | /api/instances/{id} | Terminate instance |
POST | /api/instances/{id}/renew | Extend TTL |
POST | /api/instances/bulk/pause | Bulk pause |
POST | /api/instances/bulk/resume | Bulk resume |
POST | /api/instances/bulk/kill | Bulk terminate |
Groups
| Method | Path | Description |
|---|---|---|
GET | /api/groups | List all groups with users |
POST | /api/groups | Create a new group |
GET | /api/groups/export | Export groups as YAML dict |
GET | /api/groups/events | List 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}/users | Add 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}/transfer | Transfer user to another group |
Config Files
| Method | Path | Description |
|---|---|---|
GET | /api/config-files | List 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}/upload | Upload a config file |
DELETE | /api/config-files/{key} | Delete a config file |
Lemonade
| Method | Path | Description |
|---|---|---|
GET | /api/lemonade/status | Lemonade server status |
GET | /api/lemonade/models | Available models |
GET | /api/lemonade/api-info | API endpoint info |
GET | /api/lemonade/health | Proxy: server health |
GET | /api/lemonade/stats | Proxy: performance statistics |
GET | /api/lemonade/system-info | Proxy: hardware details |
GET | /api/lemonade/live | Proxy: liveness probe |
GET | /api/lemonade/slots | Proxy: llama.cpp slots state |
POST | /api/lemonade/pull | Proxy: pull a model |
POST | /api/lemonade/delete | Proxy: delete a model |
POST | /api/lemonade/load | Proxy: load a model |
POST | /api/lemonade/unload | Proxy: unload a model |
Gateway
| Method | Path | Description |
|---|---|---|
GET | /api/gateway/status | Gateway status |
GET | /api/gateway/consumers | List consumers |
POST | /api/gateway/consumers | Create consumer |
DELETE | /api/gateway/consumers/{username} | Delete consumer |
POST | /api/gateway/setup | Full setup |
POST | /api/gateway/cleanup | Remove all consumers and routes |
POST | /api/gateway/route | Create/update AI proxy route |
DELETE | /api/gateway/route | Delete AI proxy route |
Auth
| Method | Path | Description |
|---|---|---|
GET | /api/auth/providers | List 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/logout | Invalidate session and clear cookie |
GET | /api/auth/me | Get 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():
- If no event loop is running, uses
asyncio.run()directly - If an event loop is already running (rare in Streamlit), runs
asyncio.run()in aThreadPoolExecutorto 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.