Interactive Labs
Lab system architecture — LabLauncher provisioning, container/VM sessions, terminal and VNC access, challenge tracking, and dual-node failover
Interactive Labs
The lab system lets users spin up ephemeral Linux environments for hands-on learning. It supports real LXC containers, QEMU virtual machines, and a client-side simulation fallback. Sessions last 60 minutes with optional extensions.
Architecture
LabLauncher (health check + provisioning)
│
├── POST /api/labs/create → Proxmox provisions container/VM
│
├── Poll /api/labs/{id} until status: "running"
│
└── Dispatch "lab-ready" event
│
├── SessionBar (countdown + resource monitoring)
├── TerminalEmbed (xterm.js via WebSocket)
├── VNCEmbed (noVNC via WebSocket, QEMU only)
├── ChallengeTracker (task progression)
├── DocumentationPanel (learning objectives)
└── LabBubble (floating session indicator)
LabLauncher
File: src/components/labs/LabLauncher.astro
Entry point for all lab provisioning. Manages the full lifecycle:
- Session check: Looks in localStorage for an existing session for this template. If found, validates with the API and reconnects without re-provisioning.
- Health check:
GET /api/labs/healthwith 2 retries and 12-second timeout. If the backend is unavailable, offers simulation mode. - Provisioning:
POST /api/labs/createwithtemplate_id. Receivessession_id,session_token,expires_at,container_ips, andcontainer_password. - Polling: Hits
GET /api/labs/{sessionId}every second untilstatus: "running". Timeout: 30 seconds for LXC containers, 90 seconds for QEMU VMs. - Ready: Dispatches
lab-readycustom event with session details. All other components activate on this event.
Simulation mode: When the backend is unreachable, dispatches lab-simulation-mode and the page falls back to a client-side sandbox environment.
SessionBar
File: src/components/labs/SessionBar.astro
Fixed-position bar at the bottom of the screen showing:
| Element | Behavior |
|---|---|
| Session ID | Short identifier |
| Countdown | MM:SS format, updates every second |
| CPU/MEM bars | Polled every 10 seconds from the API |
| +15m button | POST /api/labs/{id}/extend — extends session |
| End Lab button | DELETE /api/labs/{id} with confirmation |
Timer color states: green (>10 min), yellow (5–10 min), red with blink (<5 min). Auto-destroys the session at 0:00. Registers a beforeunload handler to send a keepalive DELETE if the user closes the browser.
TerminalEmbed
File: src/components/labs/TerminalEmbed.astro
Browser-based terminal using xterm.js with FitAddon and WebLinksAddon.
Connection: WebSocket to wss://labs.Arcturus-Prime.com/ws/terminal/{sessionId}?token={token}&container_id={n}. Multi-container labs get separate embeds with different container_id values (0-indexed).
Data flow:
- User types →
terminal.onData()→ WebSocket send - Backend sends →
onmessage→terminal.write() - Terminal resize →
onResize()→ JSON resize payload to WebSocket
Reconnection: Up to 5 attempts with exponential backoff (2s, 4s, 6s, 8s, 10s). A connection is considered stable after 10 seconds without disconnect, which resets the attempt counter.
Theme: Dark background (#0a0e17) with a cyan cursor (#22d3ee) and the Arcturus-Prime color palette for ANSI colors.
VNCEmbed
File: src/components/labs/VNCEmbed.astro
Graphical console for QEMU VMs using noVNC (self-hosted ES modules in /public/novnc/).
Connection flow:
- Browser creates WebSocket to
wss://labs.Arcturus-Prime.com/ws/vnc/{sessionId}?token={token} - Server sends VNC ticket as the first text message (Proxmox session token)
- noVNC
RFBinstance is created with the ticket as credentials - Display scales to container with quality level 6, compression level 2
Fullscreen: F11 key or button. Chrome bar auto-hides on hover. ResizeObserver re-triggers scaling.
Reconnection: Up to 8 attempts. Server error close codes (4xxx+) display human-readable reasons.
ChallengeTracker
File: src/components/labs/ChallengeTracker.astro
Interactive sidebar with lesson-based task progression.
Challenge structure:
interface Challenge {
id: string;
name: string;
description: string;
difficulty?: 'beginner' | 'intermediate' | 'advanced' | 'expert';
tasks: Array<string | { label: string; steps?: string[]; hint?: string; command?: string; expected?: string }>;
}
Features:
- Difficulty badges (B/I/A/E) with color coding and filter buttons
- Expandable task lists with per-challenge progress (
3/5) - Click tasks to toggle completion
- Expand task details to see step-by-step instructions, copyable commands, hints, and expected output
- Overall progress bar across all challenges
- Progress saved to localStorage:
Arcturus-Prime-challenges-{templateId}
DocumentationPanel
File: src/components/labs/DocumentationPanel.astro
Collapsible reference panel with three sections:
- What You Can Do — Bulleted capabilities list
- Learning Objectives — Goals grouped by difficulty tier
- Resources — External documentation links
Toggle via the book icon header. Slides open with smooth animation.
LabBubble
File: src/components/labs/LabBubble.astro
Floating indicator (bottom-left, z-index 9998) showing all active lab sessions site-wide.
- Collapsed: Green pulsing dot + “X Labs” count
- Expanded: List of sessions with name, countdown, Open/Stop buttons
- Validates sessions every 30 seconds, removes expired ones
- Hidden when SessionBar is visible (SessionBar takes precedence)
Lab API Client
File: src/lib/lab-api.ts
All API requests are HMAC-SHA256 signed:
Signature = HMAC(timestamp:method:path, LAB_API_SECRET)
Headers: X-Lab-Token, X-Lab-Timestamp, X-Session-Token
Key functions: labFetch() (signed requests), labWebSocketUrl() (terminal WS URL), labVNCWebSocketUrl() (VNC WS URL), destroyLab() (DELETE with keepalive), and session storage helpers (storeSession, getStoredSessions, removeStoredSession).
Base URL: https://labs.Arcturus-Prime.com in production (Cloudflare Tunnel), relative URLs in dev (Vite proxy).
Event System
| Event | Dispatched By | Listeners | Detail |
|---|---|---|---|
lab-ready | LabLauncher | SessionBar, TerminalEmbed, VNCEmbed, LabBubble | sessionId, token, expiresAt, templateId, containerIps, containerPassword, containerCount |
lab-ended | SessionBar, TerminalEmbed, LabBubble | LabLauncher, SessionBar, LabBubble | sessionId |
lab-simulation-mode | LabLauncher | Page scripts | (no detail) |
Session Lifecycle
| Duration | Default | Extendable |
|---|---|---|
| Session TTL | 60 minutes | +15 min per click (unlimited) |
| Auto-destroy | On expiration or user “End Lab” | — |
| Browser close | beforeunload sends DELETE with keepalive: true | — |
Dual-Node Infrastructure
Labs provision across two Proxmox hypervisors connected via Tailscale subnet routing:
| Node | Role | Types |
|---|---|---|
| Primary (Izar) | Main hypervisor | LXC containers |
| Secondary (Tarn) | Remote hypervisor | QEMU VMs |
The backend orchestrator handles placement decisions. Multi-container labs can receive container_ips arrays spanning both nodes. The 90-second QEMU timeout accounts for cross-site placement.