This directory contains the observability UI for RunnerQ.
The runnerq-console.html is a self-contained, single-page HTML file that provides a complete observability dashboard for RunnerQ. It's designed to be embedded in your Rust application, similar to how Swagger UI works.
- Real-time Updates: Server-Sent Events (SSE) for instant activity updates
- Live Statistics: Queue stats with processing, pending, scheduled, and dead-letter counts
- Priority Distribution: Real-time breakdown of activities by priority level
- Activity Management: Browse pending, processing, scheduled, completed, and dead-letter activities
- Activity Results: View execution results and outputs for completed activities
- Event Timeline: Detailed activity lifecycle events with multiple view modes
- Search & Filter: Filter activities by type, status, and ID
- Modern UI: Temporal-inspired design with dark theme and responsive layout
- Zero Build Step: No npm, webpack, or build process required
- Self-Contained: All HTML, CSS, and JavaScript in a single file
- 7-Day History: Completed activities queryable for 7 days
Simply use the provided Rust helper functions to serve the UI:
use runner_q::{runnerq_ui, storage::PostgresBackend, WorkerEngine};
use axum::{serve, Router};
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let backend = PostgresBackend::new("postgres://localhost/mydb", "my_app").await?;
let engine = WorkerEngine::builder()
.backend(Arc::new(backend))
.queue_name("my_app")
.build()
.await?;
let inspector = engine.inspector();
let app = Router::new()
.nest("/console", runnerq_ui(inspector));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8081").await?;
println!("✨ RunnerQ Console: http://localhost:8081/console");
serve(listener, app).await?;
Ok(())
}That's it! The UI will be available at http://localhost:8081/console with the API automatically mounted at http://localhost:8081/console/api/observability.
If you want to serve just the API (e.g., for a custom UI):
use runner_q::{observability_api, QueueInspector};
let app = Router::new()
.nest("/api/observability", observability_api(inspector));The UI expects these endpoints to be available:
GET /api/observability/stats- Queue statistics (including priority distribution)GET /api/observability/stream- Server-Sent Events stream for real-time updatesGET /api/observability/activities/pending- List pending activitiesGET /api/observability/activities/processing- List processing activitiesGET /api/observability/activities/scheduled- List scheduled activitiesGET /api/observability/activities/completed- List completed activities (7-day history)GET /api/observability/activities/dead_letter- List dead-letter activitiesGET /api/observability/activities/{id}- Get activity detailsGET /api/observability/activities/{id}/events- Get activity eventsGET /api/observability/activities/{id}/result- Get activity execution resultGET /api/observability/dead-letter- Get dead-letter records
The UI automatically detects the correct API path based on where it's served from:
- Served at
/console→ API at/console/api/observability - Served at
/ui→ API at/ui/api/observability - Served at
/→ API at/api/observability
To override the auto-detection and use a custom API base URL, set window.RUNNERQ_API_BASE before the page loads:
<script>
window.RUNNERQ_API_BASE = '/custom/api/path';
</script>The UI uses Server-Sent Events (SSE) for instant real-time updates with zero configuration.
- When you call
runnerq_ui(inspector)orengine.inspector(), event streaming is automatically enabled - The UI connects via SSE to
/api/observability/stream - Every activity lifecycle event triggers instant UI updates
- Connection automatically reconnects if interrupted
- Works in modern browsers only (no polling fallback)
The UI receives real-time events for:
Enqueued- Activity added to queueStarted- Worker begins processingCompleted- Activity finished successfullyFailed- Activity failed (will retry)DeadLetter- Activity moved to dead-letter queueHeartbeat- Worker is still processing
Get an inspector from the engine (built with any backend that implements InspectionStorage, e.g. Postgres or Redis via runner_q_redis):
let backend = PostgresBackend::new("postgres://localhost/mydb", "my_app").await?;
let engine = WorkerEngine::builder()
.backend(Arc::new(backend))
.queue_name("my_app")
.build()
.await?;
let inspector = engine.inspector();
let app = Router::new().nest("/console", runnerq_ui(inspector));# With PostgreSQL: set DATABASE_URL and run the console example
DATABASE_URL=postgres://localhost/mydb cargo run --example console_ui
# With Redis: set REDIS_URL and run the Redis example
REDIS_URL=redis://127.0.0.1:6379 cargo run --example redis_console_ui
# Open http://localhost:8081/console (or :8082 for redis_console_ui)The console is a single HTML file (runnerq-console.html) with embedded CSS and JavaScript. To modify:
- Edit
ui/runnerq-console.htmldirectly - Test by running
cargo run --example console_ui - No build step required - changes are immediately visible on page refresh
The UI is designed to work in modern browsers and uses:
- Vanilla JavaScript (no framework)
- CSS Grid and Flexbox for layout
- Server-Sent Events for real-time updates
- Fetch API for HTTP requests