Hammerwork Web Dashboard
A modern, real-time web-based admin dashboard for monitoring and managing Hammerwork job queues. Built with Rust, Warp, and WebSockets for high-performance job queue administration.
Features
- Real-time Monitoring: Live updates via WebSockets for queue statistics, job status, and system health
- Job Management: View, retry, cancel, and inspect jobs with detailed payload and error information
- Job Archive Management: Archive, restore, and purge jobs with configurable retention policies
- Queue Administration: Monitor queue performance, clear queues, and manage queue priorities
- Archive Statistics: Track storage savings, compression ratios, and archival operations
- Multi-Database Support: Works with both PostgreSQL and MySQL backends
- Security: Built-in authentication with bcrypt password hashing and rate limiting
- Modern UI: Responsive dashboard with charts, tables, and real-time indicators
- REST API: Complete RESTful API for programmatic access
- High Performance: Async/await throughout with efficient database pooling
Screenshots
The dashboard provides:
- Overview Cards: Total jobs, pending/running counts, error rates, and throughput
- Queue Table: Real-time queue statistics with actions (clear, pause, resume)
- Jobs Table: Filterable job listing with status, priority, and actions
- Archive Section: Archived jobs management with statistics and restoration capabilities
- Charts: Throughput over time and job status distribution
- Real-time Updates: WebSocket connections for live data updates
Quick Start
Installation
# Install from crates.io
# Or build from source
Basic Usage
# Start the dashboard locally without authentication (PostgreSQL). Without --no-auth,
# the dashboard refuses to start until a password is configured.
# Start with authentication enabled
# Start with custom port, and let pages on https://ops.example.com call the API (CORS)
The password file holds a bcrypt hash of the password, never the password itself
(see Generate Password Hash). --password hashes a
password given on the command line for you.
Configuration File
Create a dashboard.toml configuration file:
= "0.0.0.0"
= 8080
= "postgresql://localhost/hammerwork"
= 10
= "./assets"
= false
# Other origins whose pages may change data / open WebSockets (and, with enable_cors, read the API)
= []
[]
= true
= "admin"
= "$2b$12$..." # bcrypt hash, never the password itself
= "8h" # caps how long a verified login is remembered (at most 60 s)
= 5 # per client address
= "15m"
[]
= "30s"
= 100
= 1024 # outgoing messages queued per connection
= 65536 # largest message a client may send
# Live updates: poll the database for job/queue changes while clients are connected
= "2s" # zero disables live updates
= 100 # changed jobs read and pushed per poll
Then start with:
Encrypted Queues
Jobs created from the dashboard (POST /api/jobs) are encrypted like your application's.
Set HAMMERWORK_ENCRYPTION_CONFIG to the application's hammerwork.toml (only its
[encryption] section is read; the HAMMERWORK_ENCRYPTION_* variables apply on top) and
make its key available, e.g. HAMMERWORK_ENCRYPTION_KEY. Jobs on its encrypted_queues are
then encrypted with its key. If encryption is enabled but the key cannot be loaded, the
dashboard does not start. Without these settings it still refuses to create a plaintext job
on a queue that already holds encrypted jobs. Viewing jobs never needs the key: encrypted
payloads are shown redacted.
Library Usage
Add to your Cargo.toml:
[]
= { = "2.1", = ["postgres"] }
# or for MySQL:
# hammerwork-web = { version = "2.1", features = ["mysql"] }
Programmatic Usage
use ;
async
With Authentication
use ;
let config = new
.with_database_url
.with_bind_address
.with_auth // bcrypt hash
.with_cors
.with_allowed_origin;
let dashboard = new.await?;
dashboard.start.await?;
API Reference
The dashboard exposes a complete REST API:
Authentication
All API endpoints (except /health) require authentication when enabled:
Endpoints
System
GET /health- Health check (no auth required)GET /api/stats/overview- System overview statisticsGET /api/stats/detailed- Per-queue statistics, hourly trends, error patterns and performance metricsGET /api/stats/trends- Completed/failed jobs per hour from the database (last 24 hours, or atime_rangeof up to 31 days)GET /api/stats/health- Health assessment
Values the dashboard cannot measure are reported as null (for example memory usage on macOS, worker counts, CPU, and the metrics count), never as made-up numbers. "Failed" in trends and error patterns means jobs currently in Failed, Dead or TimedOut, counted at the hour they failed.
Queues
GET /api/queues- List all queues with statisticsGET /api/queues/{name}- Queue details with hourly throughput and recent errorsGET /api/queues/{name}/jobs- Jobs in the queue (same filters as/api/jobs)POST /api/queues/{name}/actions-{"action": "pause" | "resume" | "clear_completed" | "clear_dead"};clear_completeddeletes the queue's completed jobs,clear_deadits dead jobs older than 7 days
Jobs
GET /api/jobs?status=failed&limit=50- List jobs with filtersGET /api/jobs/{id}- Get job detailsPOST /api/jobs- Create new jobPOST /api/jobs/{id}/retry- Retry failed jobDELETE /api/jobs/{id}- Delete job
Archive Management
GET /api/archive/jobs?queue=email&limit=50- List archived jobs with filtersPOST /api/archive/jobs- Archive jobs based on configurable policyPOST /api/archive/jobs/{id}/restore- Restore an archived job to pending statusDELETE /api/archive/purge- Permanently purge old archived jobsGET /api/archive/stats?queue=email- Get archive statistics and metrics
System
-
GET /api/system/info,GET /api/system/config,GET /api/system/metrics,GET /api/version -
POST /api/system/maintenance-{"operation": ..., "target": ..., "dry_run": ...}:cleanupdeletes dead jobs older than 7 daysvacuum:VACUUM (ANALYZE)on PostgreSQL,OPTIMIZE TABLEon MySQLreindex:REINDEX TABLEon PostgreSQL,OPTIMIZE TABLEon MySQL (InnoDB rebuilds the indexes)optimize:ANALYZEon PostgreSQL,OPTIMIZE TABLEon MySQL
Table operations run on every Hammerwork table that exists (
hammerwork_jobs,hammerwork_jobs_archive, ...), or only ontargetwhen it names one of them; other names are rejected. The response lists each statement that ran, or withdry_runwould run.
WebSocket API
Connect to ws://localhost:8080/ws for real-time updates:
const ws = ;
// Subscribe to events
ws.;
// Handle updates
ws ;
A new connection receives every event type until it sends its first Subscribe or
Unsubscribe. Each message has a type:
type |
Event type | Contents |
|---|---|---|
JobUpdate |
job_updates |
job: id, queue_name, status, priority, attempts, updated_at |
QueueUpdate |
queue_updates |
queue_name, stats (pending, running, completed, failed and dead counts, throughput, error rate) |
SystemAlert |
system_alerts |
message, severity |
JobArchived, JobRestored, BulkArchive*, JobsPurged |
archive_events |
archive operation details |
Pong |
the answer to a client {"type": "Ping"} |
Live updates
The dashboard usually runs in its own process, apart from the workers, so it cannot see
the job events a worker publishes in its own process. Instead, while at least one client
is connected, it polls the database every websocket.live_update_interval (default 2
seconds) and pushes:
- a
JobUpdatefor each job created, started, completed, failed or timed out since the previous poll (at mostwebsocket.live_update_max_jobsper poll, newest first); - a
QueueUpdatefor each queue whose counts changed.
Each poll runs two small queries plus the queue statistics query. Nothing is polled
while no client is connected; set live_update_interval to zero to turn polling off.
Changes that leave no timestamp (a retry back to Pending, a deleted job) show up only
in the queue statistics.
An application that embeds the dashboard in the same process as its workers can also forward their events as they happen:
// `events` is the Arc<EventManager> the workers were given with `with_event_manager`.
let dashboard = new
.await?
.with_event_manager;
dashboard.start.await?;
Development
Prerequisites
- Rust 1.70+
- PostgreSQL or MySQL database
- Node.js (for frontend development)
Database Setup
Use the provided scripts to set up test databases:
# Set up both PostgreSQL and MySQL test databases
# PostgreSQL only
# MySQL only
Default test database connections:
- PostgreSQL:
postgresql://postgres:hammerwork@localhost:5433/hammerwork - MySQL:
mysql://root:hammerwork@localhost:3307/hammerwork
Building
# Build with PostgreSQL support
# Build with MySQL support
# Authentication (bcrypt) is a default feature. A build without it
# (--no-default-features) refuses to start with authentication enabled.
# Build everything
Testing
# Unit tests
# Integration tests (requires database)
# Test with both databases
Development Commands
# Format code
# Lint code
# Run with hot reload (requires cargo-watch)
# Generate documentation
Frontend Development
The dashboard frontend is built with vanilla JavaScript, HTML, and CSS for minimal dependencies and fast loading.
Asset Structure
assets/
├── index.html # Main dashboard page
├── dashboard.css # Styles and responsive design
├── dashboard.js # WebSocket client and UI logic
└── chart.min.js # Chart.js for data visualization
Adding Features
- New API Endpoint: Add to appropriate module in
src/api/ - WebSocket Message: Update
ServerMessageenum insrc/websocket.rs - Frontend Component: Add to
assets/dashboard.jsand update UI inindex.html - Tests: Add unit tests in module and integration tests in
tests/
Archive API Examples
# List archived jobs
# Archive completed jobs older than 7 days
# Restore an archived job
# Get archive statistics
Security
Authentication
- Basic Authentication: RFC 7617 compliant with base64 encoding
- Password Hashing: the configured
password_hashis a bcrypt hash and is only ever verified with bcrypt, never compared to the password as text. bcrypt comes with theauthfeature, which is on by default; a build without it refuses to start with authentication enabled. - No blocking: bcrypt runs on Tokio's blocking pool, one verification per CPU at a
time. A successful login is remembered for 60 seconds (or
session_timeout, if shorter;0turns this off), so a dashboard polling several endpoints does not pay for bcrypt on every request. - Constant-time checks: usernames are compared in constant time, and bcrypt runs whether or not the username matched, so response times do not reveal the username.
- Lockout: failures are counted per client address (the TCP peer address, never a
header) and username. After
max_failed_attemptsfailures that client is refused with429forlockout_duration; afterwards the count starts over, and a successful login resets it. An attacker therefore cannot lock the administrator out from other addresses. Behind a reverse proxy every client shares the proxy's address. At most 10,000 clients are tracked, and made-up usernames share one record per address.
Cross-site requests (CSRF)
A page the operator visits in the same browser could make it send requests to the dashboard, with any cached Basic credentials attached. The dashboard therefore:
- requires
Content-Type: application/jsonon every request with a body (415otherwise), which a page on another origin cannot send without a CORS preflight; - refuses state-changing requests (
POST,PUT,DELETE, ...) and WebSocket handshakes that a browser sent from another origin (403), judged bySec-Fetch-Siteor, without it, by comparingOriginwithHost. Requests without either header (curl, scripts) are not from a browser page and pass. Origins inallowed_origins(--allowed-origin) pass too. - grants CORS (
enable_cors/--cors) only to the origins inallowed_origins, never to every origin; enabling CORS without any is a startup error.
Request limits
- JSON request bodies are limited to 1 MiB and need a
Content-Lengthheader (413/411). - A bulk job action takes at most 1,000 job IDs.
- Pages hold at most 1,000 items (
limitis clamped before the offset is computed). Archive listings are filtered, counted and paged in the database. - WebSocket messages from clients are limited to
websocket.max_message_size; each connection queues at mostwebsocket.message_buffer_sizeoutgoing messages, and messages for a client that does not read are dropped. Subscriptions accept only the known event types.
Best Practices
- Change default credentials immediately
- Use strong passwords (12+ characters)
- Enable authentication in production
- Use HTTPS in production
- Configure firewall rules appropriately
- Store password hashes securely
- Regular security updates
Example: Generate Password Hash
# Using the htpasswd tool (Apache utils); strip the leading "user:"
|
# Or in Rust
;
Deployment
Docker
FROM rust:1.70 as builder
WORKDIR /app
COPY . .
RUN cargo build --release --features postgres
FROM debian:booksworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/hammerwork-web /usr/local/bin/
COPY --from=builder /app/hammerwork-web/assets /app/assets
WORKDIR /app
EXPOSE 8080
CMD ["hammerwork-web", "--bind", "0.0.0.0", "--port", "8080", "--auth", "--password-file", "/run/secrets/dashboard_password_hash"]
systemd Service
[Unit]
Description=Hammerwork Web Dashboard
After=network.target postgresql.service
[Service]
Type=simple
User=hammerwork
WorkingDirectory=/opt/hammerwork
ExecStart=/opt/hammerwork/hammerwork-web --config /etc/hammerwork/dashboard.toml
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Nginx Reverse Proxy
server {
listen 80;
server_name hammerwork.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
Performance
Optimization Tips
- Connection Pooling: Tune
pool_sizebased on concurrent users - WebSocket Limits: Configure
max_connectionsfor your use case - Request Timeouts: The dashboard applies none of its own (
request_timeoutwas removed in 2.0; files that still set it load); set them on a reverse proxy in front of it - Database Indexes: Ensure proper indexes on hammerwork_jobs table
- Static Assets: Use a CDN for production deployments
- Monitoring: Enable structured logging and metrics collection
Monitoring
# Enable debug logging
RUST_LOG=hammerwork_web=debug
# Monitor WebSocket connections
|
# Database connection health
Troubleshooting
Common Issues
Database Connection Failed
Error: Database connection failed
- Verify database URL format and credentials
- Check database server is running and accessible
- Ensure database exists and migrations are applied
Static Assets Not Found
Error: Static file not found
- Check
static_dirpath in configuration - Ensure assets directory contains
index.html - Verify file permissions
Authentication Issues
401 Unauthorized
- Verify username and password
- Check password hash format (bcrypt)
- Review rate limiting settings
WebSocket Connection Failed
WebSocket connection closed unexpectedly
- Check firewall settings for WebSocket traffic
- Verify proxy configuration for Upgrade headers
- Review browser console for detailed errors
Debug Mode
# Enable verbose logging
RUST_LOG=debug
# Enable specific module logging
RUST_LOG=hammerwork_web::websocket=trace
Contributing
We welcome contributions! Please see the main CONTRIBUTING.md for guidelines.
Areas for Contribution
- Additional database backends (SQLite, CockroachDB)
- Enhanced security features (OAuth, JWT)
- More visualization options (metrics dashboards)
- Archive management enhancements (scheduled policies, bulk operations)
- Real-time WebSocket events for archive operations
- Mobile-responsive improvements
- Internationalization (i18n)
- Plugin system for custom integrations
License
This project is licensed under the MIT License - see the LICENSE file for details.
Changelog
See CHANGELOG.md for version history and changes.