134 lines
6.7 KiB
Markdown
134 lines
6.7 KiB
Markdown
---
|
|
name: hermes-dashboard
|
|
description: "Deploy and extend the Hermes Dashboard — a browser control panel for managing config, models, tools, skills, cron jobs, and logs without the CLI."
|
|
version: 1.0.0
|
|
author: Hermes Agent
|
|
license: MIT
|
|
platforms: [linux]
|
|
metadata:
|
|
hermes:
|
|
tags: [hermes, dashboard, monitoring, control-panel, docker]
|
|
related_skills: [hermes-webui, hermes-agent, server-health-check]
|
|
---
|
|
|
|
# Hermes Dashboard
|
|
|
|
A Flask-based control panel that complements the Hermes Web UI (chat interface). While the Web UI provides chat + session browsing, the Dashboard provides **management** — config editing, model switching, tool toggling, skill browsing, cron job control, and log viewing.
|
|
|
|
## Architecture Decision: No CLI Dependency
|
|
|
|
The dashboard reads Hermes state **directly from the filesystem** rather than shelling out to the `hermes` CLI. This avoids the venv shebang problem (host Python paths break in Docker containers) and keeps the image lightweight.
|
|
|
|
| Data | Source |
|
|
|------|--------|
|
|
| Config | `config.yaml` (read/write via PyYAML) |
|
|
| Cron jobs | `cron/jobs.json` (read/write) |
|
|
| Skills | Filesystem scan of `skills/` for `SKILL.md` |
|
|
| Logs | `logs/gateway.log`, `logs/error.log` (tail) |
|
|
| System status | `systemctl`, `curl` health checks |
|
|
|
|
## Deployment
|
|
|
|
### Directory Layout
|
|
```
|
|
~/docker/hermes-dashboard/
|
|
├── app.py # Flask backend (all routes)
|
|
├── templates/ # Jinja2 HTML (Bootstrap 5 dark theme)
|
|
│ ├── base.html # Nav + layout shell
|
|
│ ├── login.html # Password auth
|
|
│ ├── index.html # Overview with live status
|
|
│ ├── config.html # YAML editor + quick setter
|
|
│ ├── models.html # Model/provider switcher
|
|
│ ├── tools.html # Toolset enable/disable
|
|
│ ├── skills.html # Skill browser + installer
|
|
│ ├── cron.html # Cron job list + toggle/trigger
|
|
│ └── logs.html # Log viewer with auto-refresh
|
|
├── Dockerfile # python:3.11-slim + Flask + PyYAML
|
|
├── docker-compose.yml # Volume mounts ~/.hermes, port 5000, env_file
|
|
├── requirements.txt # flask, pyyaml, werkzeug
|
|
└── .env # DASHBOARD_PASSWORD, DASHBOARD_SECRET_KEY
|
|
```
|
|
|
|
### Quick Deploy
|
|
```bash
|
|
# 1. Create the directory and files (see references/dashboard-app.py for full source)
|
|
mkdir -p ~/docker/hermes-dashboard/{templates,static}
|
|
|
|
# 2. Set password (generate hash with werkzeug)
|
|
python3 -c "from werkzeug.security import generate_password_hash; print(generate_password_hash('your-password'))"
|
|
|
|
# 3. Launch
|
|
cd ~/docker/hermes-dashboard
|
|
docker compose up -d --build
|
|
|
|
# 4. Add nginx reverse proxy (reuse existing certbot SSL cert)
|
|
# See existing nginx configs under /etc/nginx/sites-enabled/ for pattern
|
|
```
|
|
|
|
### Nginx Config Pattern
|
|
```nginx
|
|
server {
|
|
listen 3445 ssl; # Pick next available port
|
|
server_name your-domain.duckdns.org;
|
|
ssl_certificate /etc/letsencrypt/live/your-domain/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/your-domain/privkey.pem;
|
|
location / {
|
|
proxy_pass http://127.0.0.1:8788;
|
|
proxy_set_header Host $host;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Cron Job Management
|
|
|
|
Cron jobs are read from `~/.hermes/cron/jobs.json`. The JSON structure uses `id` (not `job_id`) as the primary key, with `schedule_display` for human-readable schedules and `enabled` as a boolean.
|
|
|
|
Toggle pause/resume by flipping `enabled` in jobs.json and writing it back. The scheduler picks up changes on next tick.
|
|
|
|
For "Run Now", write a trigger flag at `~/.hermes/cron/.trigger_<id>` with a timestamp. The scheduler polls for these.
|
|
|
|
## Design System
|
|
|
|
The UI uses a Linear/Vercel-inspired dark theme with these characteristics:
|
|
|
|
- **Sidebar navigation** (240px fixed, icons + labels, active state highlight)
|
|
- **Glass-morphism cards** with `#18181b` surface, `#27272a` borders, 12px radius
|
|
- **Stat cards** with gradient top border accent (`#6366f1 → #a855f7`) and pulse-animated status dots
|
|
- **Typography**: Inter for UI, JetBrains Mono for code/logs
|
|
- **No Bootstrap JS/CSS dependency** — pure custom CSS with Bootstrap Icons
|
|
- **Color tokens**: `--accent: #6366f1` (indigo), `--green: #22c55e`, `--red: #ef4444`, `--amber: #f59e0b`
|
|
|
|
## Pitfalls
|
|
|
|
- **Password hash with `$` signs in Docker env_file**: Docker Compose interprets `$` as variable substitution. Use `env_file` (not inline `environment:`) and ensure the hash value is quoted or escaped. The dashboard reads the raw password from `DASHBOARD_PASSWORD` and hashes it at startup. If login silently fails (returns 200 with login page, no redirect), the password hash is wrong. Verify with:
|
|
```bash
|
|
docker exec hermes-dashboard python3 -c "
|
|
from werkzeug.security import check_password_hash, generate_password_hash
|
|
import os
|
|
pw = os.environ['DASHBOARD_PASSWORD']
|
|
print('Password:', pw)
|
|
print('Verify with fresh hash:', check_password_hash(generate_password_hash(pw), pw))
|
|
"
|
|
```
|
|
Then test login directly:
|
|
```bash
|
|
curl -sk -c /tmp/cookies.txt -X POST https://localhost:3445/login \
|
|
-d "password=YOUR_PASSWORD" -w "%{http_code}"
|
|
# Should return 302 (redirect to /), not 200 (login page)
|
|
```
|
|
|
|
- **Session cookies break on restart**: Flask `secret_key` must be stable. Set `DASHBOARD_SECRET_KEY` in `.env` (not auto-generated) so sessions survive container restarts. Without it, `os.urandom(24).hex()` generates a new secret each restart, invalidating all cookies.
|
|
|
|
- **`hermes` CLI doesn't work in Docker**: The venv shebang points to host Python paths. Don't try to run `hermes` CLI inside the container — read files directly instead.
|
|
|
|
- **Toolsets may be list or dict in config.yaml**: The `toolsets` key can be either format. When it's a list (e.g. `['hermes-cli']`), it's a per-platform override — NOT an exhaustive enabled-set. **Treat all known tools as enabled by default when toolsets is a list.** Only explicit dict entries with `enabled: false` should show as disabled. The view function must handle both formats, and the toggle endpoint must convert list→dict before writing. Always define `all_known` BEFORE the conversion block (Python NameError otherwise).
|
|
|
|
- **Docker Compose `env_file` vs inline `environment:`**: When using `env_file: .env`, Docker Compose reads the file directly without `$` substitution. Inline `environment:` treats `$` as variable references. For bcrypt hashes (which contain `$2b$...`), always use `env_file`.
|
|
|
|
- **Container recreate needed for image changes**: `docker restart` keeps the old image. Use `docker compose up -d --build --force-recreate` to pick up code changes.
|
|
|
|
## References
|
|
|
|
- `references/firecrawl-reddit-limitations.md` — Firecrawl blocks Reddit; RSS workaround for WSB
|
|
- `references/dashboard-app.py` — Full app.py source (or use skill_view to read it)
|