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.4", = ["postgres"] }
# or for MySQL:
# hammerwork-web = { version = "2.4", 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 the last?hours=Nhours, up to 31 days)GET /api/stats/health- Health assessment
/api/stats/detailed and /api/stats/trends take these query parameters:
hours=N- the last N hours (1 to 744).time_range=START,END- two RFC 3339 timestamps separated by a comma, e.g.time_range=2024-01-01T00:00:00Z,2024-01-02T00:00:00Z(at most 31 days;hourswins if both are given).queues=a,b- only these queues (comma-separated, like the other list filters); stats, trends and error patterns cover just them.granularity=hour|day|week- trend bucket size;houris the default, days are UTC days and weeks start on Monday (UTC).
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 filters.statusis any ofpending(including jobs scheduled for later),running,completed,failed(Failed, Dead or TimedOut),dead,timedout,retrying, orrecurring; the newest 100 jobs per status are listed. Withoutstatus, ready pending jobs, dead jobs and recurring jobs are listedGET /api/jobs/{id}- Get job detailsPOST /api/jobs- Create new jobPOST /api/jobs/{id}/actions-{"action": "retry" | "cancel" | "delete"};retryre-queues the job,cancelanddeletedelete itPOST /api/jobs/bulk-{"job_ids": [...], "action": ...}, the same actions for several jobsPOST /api/jobs/search- Search jobs ({"query": ..., "queues": [...], "statuses": [...], ...})
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 days (age is measured fromfailed_at, elsetimed_out_at, elsecreated_at; the dry run counts exactly what the real run deletes)vacuum: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.