Merge branch 'master' of git.wrede.ca:andreas/heartbeat
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Edit(*)",
|
||||
"Bash(pytest *)",
|
||||
"Bash(python *)",
|
||||
"Bash(python3 *)",
|
||||
"Bash(.venv/bin/pytest *)",
|
||||
"Bash(npm *)",
|
||||
"Bash(git *)",
|
||||
"Bash(ls *)",
|
||||
"Bash(cat *)",
|
||||
"Bash(grep *)",
|
||||
"Bash(find *)",
|
||||
"Bash(mkdir *)",
|
||||
"Bash(touch *)",
|
||||
"Bash(uv *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
run: |
|
||||
@@ -18,22 +20,38 @@ jobs:
|
||||
|
||||
- name: Install build tools
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install build twine
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip install --upgrade pip
|
||||
.venv/bin/pip install build twine
|
||||
|
||||
- name: Build package
|
||||
run: python3 -m build
|
||||
run: .venv/bin/python -m build
|
||||
|
||||
- name: Extract version from tag
|
||||
id: get_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||
- name: Generate changelog
|
||||
id: changelog
|
||||
run: |
|
||||
PREV_TAG=$(git tag --sort=-version:refname | grep -m 1 -v "^${GITHUB_REF#refs/tags/}$")
|
||||
if [ -n "$PREV_TAG" ]; then
|
||||
CHANGELOG=$(git log --pretty=format:"- %s" "${PREV_TAG}..HEAD")
|
||||
else
|
||||
CHANGELOG="Initial release"
|
||||
fi
|
||||
# Write multiline to output
|
||||
{
|
||||
echo "CHANGELOG<<EOF"
|
||||
echo "$CHANGELOG"
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload to Gitea PyPI registry
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
||||
run: |
|
||||
python3 -m twine upload --repository-url https://git.wrede.ca/api/packages/andreas/pypi dist/*
|
||||
.venv/bin/python3 -m twine upload --repository-url https://git.wrede.ca/api/packages/andreas/pypi dist/*
|
||||
|
||||
- name: Create release
|
||||
uses: actions/gitea-release-action@v1
|
||||
@@ -42,4 +60,4 @@ jobs:
|
||||
dist/*.whl
|
||||
dist/*.tar.gz
|
||||
title: "Release ${{ steps.get_version.outputs.VERSION }}"
|
||||
body: "Release version ${{ steps.get_version.outputs.VERSION }}"
|
||||
body: "${{ steps.changelog.outputs.CHANGELOG }}"
|
||||
|
||||
@@ -5,6 +5,7 @@ __pycache__/
|
||||
*.pyo
|
||||
.flake8
|
||||
.venv/
|
||||
.continue/
|
||||
test/
|
||||
build/
|
||||
dist/
|
||||
|
||||
+445
@@ -0,0 +1,445 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project are documented here, organized by release.
|
||||
|
||||
## [5.3.9]
|
||||
|
||||
### Added
|
||||
- auto-update CHANGELOG and README in bumpminor.sh
|
||||
|
||||
---
|
||||
|
||||
## [5.3.8]
|
||||
|
||||
### Added
|
||||
- Wiki home page with overview and getting started guide
|
||||
|
||||
### Fixed
|
||||
- Release workflow: use `GITHUB_REF`/`GITHUB_OUTPUT` (Gitea Actions uses GitHub-compatible variable names)
|
||||
- Release workflow: replace `head -1` with `grep -m 1` to avoid SIGPIPE (exit 141) in changelog step
|
||||
|
||||
---
|
||||
|
||||
## [5.3.7]
|
||||
|
||||
### Added
|
||||
- Dark mode with light/dark/auto theme setting
|
||||
- UNKNOWN level filter in Log of Events
|
||||
- Per-metric grace period input in threshold settings
|
||||
- Replace Dynamic DNS YAML editor with a web form
|
||||
- Sort hosts, thresholds, and channels alphabetically on settings page
|
||||
- Suppress alerts for unwatched hosts
|
||||
|
||||
### Fixed
|
||||
- Preserve log message order when replaying history on connect
|
||||
|
||||
---
|
||||
|
||||
## [5.3.6]
|
||||
|
||||
### Added
|
||||
- MIT license
|
||||
|
||||
### Fixed
|
||||
- Correct ZFS pool status threshold operator and add per-metric grace
|
||||
- Normalize email and domain fields
|
||||
- Move dependencies back under `[project]` in pyproject.toml
|
||||
|
||||
---
|
||||
|
||||
## [5.3.4]
|
||||
|
||||
### Fixed
|
||||
- Run full reload after HTTP config publish, not just `config.reload()`
|
||||
|
||||
---
|
||||
|
||||
## [5.3.3]
|
||||
|
||||
### Added
|
||||
- Replace YAML threshold editor with a form-based UI
|
||||
- Replace multi-select fields with dual-panel picker on settings page
|
||||
- Nav bar button to publish pending config changes
|
||||
- Host, level, and message filters in Log of Events
|
||||
|
||||
### Fixed
|
||||
- Remove container max-width; stop stretching inputs on settings page
|
||||
|
||||
### Removed
|
||||
- Legacy `dyndnshosts`/`drophosts` config keys
|
||||
|
||||
---
|
||||
|
||||
## [5.3.2]
|
||||
|
||||
### Added
|
||||
- Retry DNS resolution indefinitely; add `-4`/`-6` address-family flags to `hbc` and `hbc_mini`
|
||||
- Replace YAML hosts editor with form-based CRUD table
|
||||
- Replace YAML notification channel editor with form-based UI
|
||||
|
||||
### Fixed
|
||||
- Support list-valued `threshold_config` in hosts table
|
||||
- Derive hosts threshold config list from config file keys
|
||||
- Replace channel checkboxes in Users table with multi-select
|
||||
- Support plugin-level `enabled: false` in threshold config
|
||||
- Always populate glance strip for all hosts on page load
|
||||
- Fetch host info on initial page load
|
||||
|
||||
---
|
||||
|
||||
## [5.3.1]
|
||||
|
||||
### Added
|
||||
- Host info section in Host Overview (fetched and rendered on card expand)
|
||||
- `GET /api/0/hosts/{hostname}/info` endpoint
|
||||
- Show suffix-matched metric coverage in host info threshold table
|
||||
- Move `hbc_version` and `hbc_type` out of `os_info` into the host info section
|
||||
|
||||
### Fixed
|
||||
- Correct `THRESHOLD_DEFAULTS` metric keys and add missing defaults
|
||||
|
||||
---
|
||||
|
||||
## [5.3.0]
|
||||
|
||||
### Added
|
||||
- Profile page self-service: change identity, password, and notification channels
|
||||
- Settings page editor with form sections, YAML editors, stage/publish/rollback workflow
|
||||
- Config read API: `GET /api/0/config`, `/section/{name}`, `/backups`
|
||||
- Config write API: `POST /api/0/config`, `POST /api/0/config/rollback`
|
||||
- `configio` module for comment-preserving YAML round-trip writes
|
||||
- Multi-provider OAuth2 login page and generic provider routes
|
||||
- Log login/logout events to the event log with auth source
|
||||
|
||||
### Fixed
|
||||
- ZFS monitor alerts dropped on restart with wildcard pool thresholds
|
||||
- Preserve OAuth users across config reload
|
||||
- Config API error handling, consistent 403 messages, deduplicated key lists
|
||||
- Validate password body type; coerce `notification_channels` to strings in profile API
|
||||
- Preserve OAuth `client_secret` on roundtrip; harden rollback path validation
|
||||
|
||||
---
|
||||
|
||||
## [5.2.6]
|
||||
|
||||
### Added
|
||||
- Alerts host-filter field with URL query parameter and notify URL
|
||||
- Optional logo on Gitea OAuth login button
|
||||
|
||||
### Fixed
|
||||
- Show human-readable duration in re-notification messages
|
||||
|
||||
---
|
||||
|
||||
## [5.2.5]
|
||||
|
||||
### Added
|
||||
- Alert CRITICAL on degraded or suspended ZFS pools (ONLINE=OK, DEGRADED=WARNING, all else=CRITICAL)
|
||||
- Sign in with Gitea button on login page with OAuth2 redirect/callback routes
|
||||
- OAuth2 CSRF state management
|
||||
- Host owner shown in glance strip for admin users
|
||||
- C port of `hbc_mini` (single-file client in `scripts/c/`)
|
||||
|
||||
### Fixed
|
||||
- Use `base_url` config for OAuth redirect URI to handle reverse proxy deployments
|
||||
- Preserve OAuth users across config reload
|
||||
- Escape HTML in login page error display
|
||||
|
||||
---
|
||||
|
||||
## [5.2.4]
|
||||
|
||||
### Added
|
||||
- `hbc`/`hbc_mini`: `owner` config field included in `os_info`; server applies to host record
|
||||
- Server requests InfoPlugin refresh when a host has no plugin data
|
||||
- Event log stores structured dicts; filter by user
|
||||
|
||||
### Fixed
|
||||
- Strip `_status_code` suffix from displayed metric names in threshold alerts
|
||||
- Use plain URL in Mattermost plugin metrics link
|
||||
- Fall back to `default_owner` when `os_info` has no owner
|
||||
|
||||
---
|
||||
|
||||
## [5.2.3]
|
||||
|
||||
### Added
|
||||
- `hbc`/`hbc_mini`: log name and version at startup
|
||||
- Show metric name inline with hostname in alerts and notifications
|
||||
|
||||
### Fixed
|
||||
- Send shutdown message only if a boot message was previously sent; suppress both on restart
|
||||
|
||||
---
|
||||
|
||||
## [5.2.2]
|
||||
|
||||
### Fixed
|
||||
- Retry connection on network error instead of permanently dropping it
|
||||
- Silence `aiohttp.access` log; strip plugin prefix in alerts UI
|
||||
|
||||
---
|
||||
|
||||
## [5.2.1]
|
||||
|
||||
### Fixed
|
||||
- Threshold and logging improvements
|
||||
|
||||
---
|
||||
|
||||
## [5.2.0]
|
||||
|
||||
### Added
|
||||
- `nagios` operator for direct exit-code severity mapping
|
||||
|
||||
### Fixed
|
||||
- Always show `THRESHOLD_DEFAULTS` in Settings threshold config
|
||||
|
||||
---
|
||||
|
||||
## [5.1.21]
|
||||
|
||||
### Added
|
||||
- `nagios_runner` improvements and alerts page fixes
|
||||
|
||||
---
|
||||
|
||||
## [5.1.20]
|
||||
|
||||
### Added
|
||||
- Generic threshold matching for `nagios_runner` with `{check_name}` display support
|
||||
|
||||
### Fixed
|
||||
- Reduce default hysteresis from 10% to 2%
|
||||
- Show recovery threshold in alerts UI
|
||||
|
||||
---
|
||||
|
||||
## [5.1.19]
|
||||
|
||||
### Added
|
||||
- Exclude ZFS ARC from `memory_percent`
|
||||
- Add `uptime_seconds` to `cpu_monitor`
|
||||
|
||||
### Fixed
|
||||
- Send boot/shutdown message on the first open connection, not blindly on the first in list
|
||||
|
||||
---
|
||||
|
||||
## [5.1.18]
|
||||
|
||||
### Added
|
||||
- Fetch-based Update/Delete buttons with toast notifications on Host Overview
|
||||
|
||||
### Fixed
|
||||
- Settings thresholds show correct per-config metrics; miscellaneous `hbc` fixes
|
||||
|
||||
---
|
||||
|
||||
## [5.1.17]
|
||||
|
||||
### Added
|
||||
- Owner Update/Delete buttons on Host Overview; purge stale alerts on reload
|
||||
- Retry `AsyncConnection.open()` indefinitely; drop IPv6 only on early startup failure
|
||||
- Alert pie chart in the nav bar
|
||||
|
||||
### Fixed
|
||||
- Make Alerts page scrollable
|
||||
|
||||
---
|
||||
|
||||
## [5.1.16]
|
||||
|
||||
### Added
|
||||
- Generic `ping_monitor` thresholds; round RTT to nearest ms
|
||||
|
||||
---
|
||||
|
||||
## [5.1.15]
|
||||
|
||||
### Added
|
||||
- Link hostnames in Live Dashboard to Host Overview
|
||||
- Threshold Configurations section on settings page
|
||||
|
||||
### Fixed
|
||||
- Suppress notifications on alert de-escalation (e.g. CRITICAL→WARNING)
|
||||
- Suppress recover messages for down durations under 4 seconds
|
||||
|
||||
---
|
||||
|
||||
## [5.1.14]
|
||||
|
||||
### Added
|
||||
- ZFS pool renderer in Host Overview
|
||||
|
||||
---
|
||||
|
||||
## [5.1.13]
|
||||
|
||||
### Added
|
||||
- ZFS monitor plugin
|
||||
- Host-level watch flag to suppress notifications
|
||||
- Filter Live Dashboard and Host Overview by owner/manager
|
||||
- Composable `threshold_config` list for per-host threshold layering
|
||||
- Restart on SIGHUP in `hbc` and `hbc_mini`
|
||||
|
||||
### Fixed
|
||||
- Mask `api_password` and `access_token` in settings page
|
||||
|
||||
---
|
||||
|
||||
## [5.1.12]
|
||||
|
||||
Internal release — no user-visible changes.
|
||||
|
||||
---
|
||||
|
||||
## [5.1.11]
|
||||
|
||||
### Fixed
|
||||
- Install under Docker
|
||||
- Clean up install script
|
||||
|
||||
---
|
||||
|
||||
## [5.1.10]
|
||||
|
||||
### Fixed
|
||||
- Synchronize version in `hbc_mini`
|
||||
- Install script no longer overwrites itself
|
||||
|
||||
---
|
||||
|
||||
## [5.1.9]
|
||||
|
||||
### Added
|
||||
- Install `hbc_mini` via package or install script
|
||||
|
||||
---
|
||||
|
||||
## [5.1.8]
|
||||
|
||||
### Added
|
||||
- Track `hbc` type and version
|
||||
|
||||
### Fixed
|
||||
- Nav bar position
|
||||
|
||||
---
|
||||
|
||||
## [5.1.7]
|
||||
|
||||
### Added
|
||||
- `hbc_mini`: single-file heartbeat client
|
||||
|
||||
### Fixed
|
||||
- Drop dead connections on protocol error
|
||||
|
||||
---
|
||||
|
||||
## [5.1.6]
|
||||
|
||||
### Fixed
|
||||
- Simplify event log usage; fix argument handling
|
||||
|
||||
---
|
||||
|
||||
## [5.1.5]
|
||||
|
||||
### Added
|
||||
- Update `hbc` via `hb_install.sh` instead of code patching
|
||||
|
||||
---
|
||||
|
||||
## [5.1.4]
|
||||
|
||||
### Added
|
||||
- Redesign Plugin Metrics page as Host Overview
|
||||
|
||||
---
|
||||
|
||||
## [5.1.3]
|
||||
|
||||
### Added
|
||||
- Validate absolute command paths at `nagios_runner` init
|
||||
- Async subprocess in `nagios_runner` with stderr capture and signal handling
|
||||
- `skip_reason` field on `Plugin`; surface in `PluginLoader` init messaging
|
||||
|
||||
### Fixed
|
||||
- Use `shlex.split()` for `nagios_runner` path validation to handle quoted paths
|
||||
- Reconfigure logging to syslog after `daemonize()`
|
||||
|
||||
---
|
||||
|
||||
## [5.1.2]
|
||||
|
||||
### Fixed
|
||||
- Plugin config lookup shadowed by `CLIENT_DEFAULTS` plugins key
|
||||
- Apply grace period to all threshold alerts before logging/notifying
|
||||
- RECOVER routing: use consistent level name and route via alerted channel
|
||||
- Early reminder notifications and lost recovery notifications
|
||||
- Non-alerting of overdue hosts
|
||||
|
||||
### Added
|
||||
- Swiss clock widget in the UI
|
||||
|
||||
---
|
||||
|
||||
## [5.1.1]
|
||||
|
||||
### Added
|
||||
- SMS and Matrix notification channels
|
||||
- CLI commands `stop`, `restart`, and `reload` for `hbd`
|
||||
- WebSocket endpoint at `http://.../ws`
|
||||
- Mobile HTML pages
|
||||
|
||||
### Fixed
|
||||
- Profile not updating
|
||||
- Sortable columns in tables
|
||||
|
||||
---
|
||||
|
||||
## [5.1.0]
|
||||
|
||||
### Added
|
||||
- Ping monitor plugin
|
||||
- Persist state to pickle file; restart timers on server restart
|
||||
- SIGHUP config reload for `hbd`
|
||||
- Renotify on CRITICAL only; persistent user sessions
|
||||
- RTT count threshold
|
||||
|
||||
### Fixed
|
||||
- Bogus notification on new clients
|
||||
- Show "overdue" in alerts instead of null
|
||||
|
||||
---
|
||||
|
||||
## [5.0.12]
|
||||
|
||||
### Added
|
||||
- User management and settings page
|
||||
|
||||
---
|
||||
|
||||
## [5.0.10]
|
||||
|
||||
### Added
|
||||
- Publish package to Gitea PyPI registry
|
||||
|
||||
---
|
||||
|
||||
## [5.0.9]
|
||||
|
||||
### Added
|
||||
- Use `SO_TIMESTAMP` for RTT measurement (Linux, FreeBSD, macOS)
|
||||
- Persist state to pickle file; restart timers on restart
|
||||
|
||||
---
|
||||
|
||||
## [5.0.6]
|
||||
|
||||
### Added
|
||||
- Major codebase refactoring: restructured into client/server components
|
||||
- Per-client threshold configuration
|
||||
- Display and acknowledge alerts in the UI
|
||||
- Proper `hbc` termination; `hbd` config reloadable at runtime
|
||||
@@ -0,0 +1,210 @@
|
||||
# Heartbeat
|
||||
|
||||
Heartbeat is a lightweight host monitoring system built around a simple idea: each machine you want to monitor runs a small client (`hbc`) that sends a UDP "heartbeat" packet to a central server (`hbd`) on a regular interval. If a heartbeat stops arriving, you get notified. Alongside reachability, clients can ship system metrics — CPU, memory, disk, network — and the server will alert you when any of those cross a threshold.
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
[ monitored host ] [ your server ]
|
||||
┌─────────────┐ UDP 50003 ┌────────────────────────┐
|
||||
│ hbc │ ────────────> │ hbd │
|
||||
│ │ │ host state tracking │
|
||||
│ plugins: │ <──────────── │ threshold alerting │
|
||||
│ cpu, mem, │ ACK / CMD │ notifications │
|
||||
│ disk, ... │ │ web dashboard + API │
|
||||
└─────────────┘ └────────────────────────┘
|
||||
```
|
||||
|
||||
- **hbd** — the server daemon. Tracks which hosts are alive, evaluates metric thresholds, fires notifications, serves the web dashboard and REST API.
|
||||
- **hbc** — the client. Sends heartbeats and plugin data over UDP. Runs on any Linux/BSD/macOS host.
|
||||
- **hbc_mini** — a zero-dependency single-file alternative (`hbc_mini.py` or `hbc_mini.c`) for hosts where you can't install Python packages.
|
||||
|
||||
Notifications can go to Pushover, email, Mattermost, Matrix, Signal, or VoIP.ms SMS. The dashboard shows host connectivity, RTT graphs, active alerts, and per-host plugin metrics in real time via WebSocket.
|
||||
|
||||
---
|
||||
|
||||
## Getting started
|
||||
|
||||
This tutorial sets up a server on one machine and a client on a second machine. You'll end up with a working dashboard and your first host being monitored.
|
||||
|
||||
### 1. Install the server
|
||||
|
||||
On the machine that will run `hbd`:
|
||||
|
||||
```bash
|
||||
git clone https://git.wrede.ca/andreas/heartbeat.git
|
||||
cd heartbeat
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install .
|
||||
```
|
||||
|
||||
Verify the install:
|
||||
|
||||
```bash
|
||||
hbd --help
|
||||
```
|
||||
|
||||
### 2. Create a server config
|
||||
|
||||
Create `~/.hb.yaml`:
|
||||
|
||||
```yaml
|
||||
hb_port: 50003 # UDP port — clients send heartbeats here
|
||||
hbd_port: 50004 # HTTP port — web dashboard and API
|
||||
ws_port: 50005 # WebSocket port — live dashboard updates
|
||||
|
||||
interval: 20 # Expected heartbeat interval (seconds)
|
||||
grace: 2 # Seconds of slack before a host is considered overdue
|
||||
|
||||
pickfile: ~/.hb.pick
|
||||
pidfile: ~/.hb.pid
|
||||
logfile: ~/.hb.log
|
||||
```
|
||||
|
||||
That's enough to get started. No hosts, no users, no notifications needed yet — the server will accept any client that connects.
|
||||
|
||||
### 3. Start the server
|
||||
|
||||
```bash
|
||||
hbd serve -c ~/.hb.yaml -f -v
|
||||
```
|
||||
|
||||
`-f` keeps it in the foreground so you can watch the log. You should see:
|
||||
|
||||
```
|
||||
Heartbeat daemon starting on UDP :50003, HTTP :50004, WS :50005
|
||||
```
|
||||
|
||||
Open `http://your-server:50004/live` in a browser. The dashboard is empty for now.
|
||||
|
||||
### 4. Install the client on a host to monitor
|
||||
|
||||
On the machine you want to monitor (must be able to reach the server on UDP 50003):
|
||||
|
||||
```bash
|
||||
pip install hbd # or: copy scripts/hbc_mini.py if you can't install packages
|
||||
```
|
||||
|
||||
#### Quick start — no config file
|
||||
|
||||
```bash
|
||||
hbc your-server.example.com
|
||||
```
|
||||
|
||||
Within a few seconds the server log will show the host checking in, and it will appear on the dashboard.
|
||||
|
||||
#### With a config file
|
||||
|
||||
Create `~/.hbc.yaml` on the client host:
|
||||
|
||||
```yaml
|
||||
hb_port: 50003
|
||||
interval: 10 # Send a heartbeat every 10 seconds
|
||||
|
||||
plugins:
|
||||
cpu_monitor:
|
||||
interval: 60
|
||||
memory_monitor:
|
||||
interval: 60
|
||||
disk_monitor:
|
||||
interval: 60
|
||||
```
|
||||
|
||||
Then start the client:
|
||||
|
||||
```bash
|
||||
hbc -c ~/.hbc.yaml your-server.example.com
|
||||
```
|
||||
|
||||
Send a boot message at startup so the server logs when the host came up:
|
||||
|
||||
```bash
|
||||
hbc -b -c ~/.hbc.yaml your-server.example.com
|
||||
```
|
||||
|
||||
Run as a daemon (logs go to syslog):
|
||||
|
||||
```bash
|
||||
hbc -d -b -c ~/.hbc.yaml your-server.example.com
|
||||
```
|
||||
|
||||
### 5. View the dashboard
|
||||
|
||||
Open `http://your-server:50004/live`. You'll see the monitored host, its last heartbeat time, and RTT. Click the host name to see plugin metrics.
|
||||
|
||||
Navigate to `/plugins/<hostname>` for CPU, memory, and disk graphs.
|
||||
|
||||
### 6. Add a notification channel (optional)
|
||||
|
||||
Edit `~/.hb.yaml` on the server:
|
||||
|
||||
```yaml
|
||||
notification_channels:
|
||||
pushover_ops:
|
||||
type: pushover
|
||||
token: YOUR_APP_TOKEN
|
||||
user: YOUR_USER_KEY
|
||||
|
||||
users:
|
||||
alice:
|
||||
password: pbkdf2:sha256:... # generate: hbd passwd alice
|
||||
admin: true
|
||||
notification_channels: [pushover_ops]
|
||||
|
||||
default_owner: alice
|
||||
```
|
||||
|
||||
Generate the password hash:
|
||||
|
||||
```bash
|
||||
hbd passwd alice
|
||||
```
|
||||
|
||||
Paste the output into the config, then reload:
|
||||
|
||||
```bash
|
||||
hbd reload
|
||||
```
|
||||
|
||||
Test the channel:
|
||||
|
||||
```bash
|
||||
hbd notify
|
||||
```
|
||||
|
||||
### 7. Set a threshold alert (optional)
|
||||
|
||||
Add to `~/.hb.yaml`:
|
||||
|
||||
```yaml
|
||||
thresholds:
|
||||
cpu_monitor:
|
||||
cpu_percent:
|
||||
warning: 80.0
|
||||
critical: 90.0
|
||||
disk_monitor:
|
||||
partitions:
|
||||
/:
|
||||
percent:
|
||||
warning: 80.0
|
||||
critical: 90.0
|
||||
```
|
||||
|
||||
Reload: `hbd reload`. The server will now alert when a monitored host crosses these values.
|
||||
|
||||
---
|
||||
|
||||
## What's next
|
||||
|
||||
| Topic | Where to look |
|
||||
|---|---|
|
||||
| Full server config reference | [README — Server](https://git.wrede.ca/andreas/heartbeat/src/branch/master/README.md#server-hbd) |
|
||||
| Client options and all plugins | [README — Client](https://git.wrede.ca/andreas/heartbeat/src/branch/master/README.md#client-hbc) |
|
||||
| Threshold alerting details | [THRESHOLD_ALERTING.md](https://git.wrede.ca/andreas/heartbeat/src/branch/master/docs/THRESHOLD_ALERTING.md) |
|
||||
| Notification channels | [NOTIFICATIONS.md](https://git.wrede.ca/andreas/heartbeat/src/branch/master/docs/NOTIFICATIONS.md) |
|
||||
| User accounts and roles | [USERS.md](https://git.wrede.ca/andreas/heartbeat/src/branch/master/docs/USERS.md) |
|
||||
| Writing a custom plugin | [PLUGIN_DEVELOPMENT.md](https://git.wrede.ca/andreas/heartbeat/src/branch/master/docs/PLUGIN_DEVELOPMENT.md) |
|
||||
| Nagios check integration | [NAGIOS_INTEGRATION.md](https://git.wrede.ca/andreas/heartbeat/src/branch/master/docs/NAGIOS_INTEGRATION.md) |
|
||||
| REST API | [HTTP_API.md](https://git.wrede.ca/andreas/heartbeat/src/branch/master/docs/HTTP_API.md) |
|
||||
| Zero-dependency client | [README — hbc_mini](https://git.wrede.ca/andreas/heartbeat/src/branch/master/README.md#hbc_mini--zero-dependency-client) |
|
||||
@@ -20,7 +20,7 @@ A lightweight UDP-based host monitoring system. Monitored hosts run a client (`h
|
||||
└────────────────────┘ └────────────────────────────┘
|
||||
```
|
||||
|
||||
**Package:** `hbd` v5.3.4
|
||||
**Package:** `hbd` v5.3.9
|
||||
**Python:** 3.11+
|
||||
|
||||
### Subpackages
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Dark Mode
|
||||
|
||||
Every page in the Heartbeat web UI supports light mode, dark mode, and automatic (follows the OS/browser setting). Each user picks their preference independently; it is stored in the browser and takes effect immediately without a page reload.
|
||||
|
||||
---
|
||||
|
||||
## Choosing a theme
|
||||
|
||||
Open your profile page (`/profile`) and scroll to the **Appearance** section. Click one of the three buttons:
|
||||
|
||||
| Button | Behaviour |
|
||||
|--------|-----------|
|
||||
| **Auto** | Follows the OS or browser dark-mode preference. Updates live if the system setting changes. |
|
||||
| **Light** | Always light, regardless of system setting. |
|
||||
| **Dark** | Always dark, regardless of system setting. |
|
||||
|
||||
The preference is stored in `localStorage` under the key `hbd_theme` and applies to the current browser only. Clearing browser storage resets it to **Auto**.
|
||||
|
||||
---
|
||||
|
||||
## Implementation notes
|
||||
|
||||
### No flash of unstyled content
|
||||
|
||||
A small synchronous `<script>` runs at the very top of `<head>`, before any CSS is parsed, and sets `data-theme="dark"` on `<html>` when the stored preference (or the system setting in auto mode) calls for dark. Because it runs before paint, there is no visible flicker on page load.
|
||||
|
||||
### CSS custom properties
|
||||
|
||||
All colours are expressed as CSS custom properties defined in `head.html`:
|
||||
|
||||
```
|
||||
:root — light-mode values (default)
|
||||
html[data-theme="dark"] — dark-mode overrides
|
||||
```
|
||||
|
||||
Key variables:
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `--bg` | Page background |
|
||||
| `--surface` | Card / panel background |
|
||||
| `--surface-2` / `--surface-3` | Slightly lighter/darker surfaces (table rows, hover states) |
|
||||
| `--text` / `--text-sec` / `--text-muted` | Primary, secondary, muted text |
|
||||
| `--border` / `--border-2`…`4` | Border shades from prominent to faint |
|
||||
| `--link` | Hyperlink and interactive-element colour |
|
||||
| `--nav-bg` | Navigation bar background |
|
||||
| `--input-bg` / `--input-border` | Form control colours |
|
||||
| `--shadow` / `--shadow-sm` | Box-shadow alphas |
|
||||
|
||||
A single global rule in `head.html` themes all `<input>`, `<select>`, and `<textarea>` elements across every page at once:
|
||||
|
||||
```css
|
||||
html[data-theme="dark"] input:not([type=checkbox]):not([type=radio]),
|
||||
html[data-theme="dark"] select,
|
||||
html[data-theme="dark"] textarea { … }
|
||||
```
|
||||
|
||||
Each page template adds its own `html[data-theme="dark"]` block for page-specific elements (cards, tables, badges, etc.).
|
||||
|
||||
### Auto-mode live updates
|
||||
|
||||
A `matchMedia` change listener in `head.html` updates `data-theme` whenever the OS preference changes, so users in **Auto** mode see the theme switch without reloading.
|
||||
|
||||
### Semantic colours are unchanged
|
||||
|
||||
Alert colours (red for critical, orange for warning, green for ok) and status indicators are intentionally left as fixed values — they are semantic signals, not surface colours, and look correct on both light and dark backgrounds.
|
||||
+1
-1
@@ -14,4 +14,4 @@ Install options:
|
||||
"""
|
||||
|
||||
__all__ = ["__version__"]
|
||||
__version__ = "5.3.6"
|
||||
__version__ = "5.3.9"
|
||||
|
||||
@@ -197,7 +197,7 @@ def get_settings_sections(config: dict, threshold_checker=None) -> list:
|
||||
# ---- Notification channels (complex, built separately) ----------------
|
||||
_METADATA_KEYS = {"type", "owner", "private", "min_level"}
|
||||
notif_channels = []
|
||||
for ch_name, ch_cfg in (config.get("notification_channels") or {}).items():
|
||||
for ch_name, ch_cfg in sorted((config.get("notification_channels") or {}).items()):
|
||||
if not isinstance(ch_cfg, dict):
|
||||
continue
|
||||
ch_type = ch_cfg.get("type", "")
|
||||
@@ -276,7 +276,7 @@ def get_settings_sections(config: dict, threshold_checker=None) -> list:
|
||||
|
||||
# ---- Hosts summary ----------------------------------------------------
|
||||
hosts_list = []
|
||||
for hname, hcfg in (config.get("hosts") or {}).items():
|
||||
for hname, hcfg in sorted((config.get("hosts") or {}).items()):
|
||||
if not isinstance(hcfg, dict):
|
||||
continue
|
||||
hosts_list.append({
|
||||
|
||||
@@ -100,6 +100,19 @@
|
||||
}
|
||||
|
||||
.logo-text { flex: 1; }
|
||||
|
||||
/* ── Dark mode ── */
|
||||
html[data-theme="dark"] h1 { color: var(--text); }
|
||||
html[data-theme="dark"] .subtitle { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .section { background: var(--surface); box-shadow: 0 1px 6px var(--shadow); }
|
||||
html[data-theme="dark"] .section h2 { color: var(--text); border-bottom-color: var(--border); }
|
||||
html[data-theme="dark"] .info-row { border-bottom-color: var(--border-4); }
|
||||
html[data-theme="dark"] .info-label { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .info-value { color: var(--text); }
|
||||
html[data-theme="dark"] .info-value a { color: var(--link); }
|
||||
html[data-theme="dark"] .hb-logo { color: var(--link); }
|
||||
html[data-theme="dark"] .hb-tagline { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .version-badge { background: #1a3255; color: #60a5fa; }
|
||||
</style>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -305,6 +305,31 @@
|
||||
text-align: right;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* ── Dark mode ── */
|
||||
html[data-theme="dark"] h1 { color: var(--text); }
|
||||
html[data-theme="dark"] .subtitle { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .summary-card { background: var(--surface); }
|
||||
html[data-theme="dark"] .summary-label { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .filters { background: var(--surface); }
|
||||
html[data-theme="dark"] .filter-label { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .filter-button { background: var(--surface-2); border-color: var(--border); color: var(--text); }
|
||||
html[data-theme="dark"] .filter-button.active { background: #2196f3; color: #fff; border-color: #2196f3; }
|
||||
html[data-theme="dark"] .filter-input { background: var(--input-bg); border-color: var(--input-border); color: var(--text); }
|
||||
html[data-theme="dark"] .alerts-container { background: var(--surface); }
|
||||
html[data-theme="dark"] .alert-item { background: var(--surface-2); }
|
||||
html[data-theme="dark"] .alert-item.acknowledged { background: var(--surface-3); }
|
||||
html[data-theme="dark"] .alert-item.critical { background: #2e0a0a; border-left-color: #f44336; }
|
||||
html[data-theme="dark"] .alert-item.warning { background: #2e1a00; border-left-color: #ff9800; }
|
||||
html[data-theme="dark"] .alert-item.unknown { background: var(--surface-2); }
|
||||
html[data-theme="dark"] .alert-hostname { color: var(--link); }
|
||||
html[data-theme="dark"] .alert-details { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .alert-value { color: var(--text); }
|
||||
html[data-theme="dark"] .alert-duration { color: var(--text-muted); }
|
||||
html[data-theme="dark"] .last-update { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .refresh-info { color: var(--text-muted); border-top-color: var(--border); }
|
||||
html[data-theme="dark"] .no-alerts,
|
||||
html[data-theme="dark"] .loading { color: var(--text-muted); }
|
||||
</style>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -5,7 +5,68 @@
|
||||
<link rel="icon" href="/static/images/favicon.ico" sizes="32x32" />
|
||||
<title>{{ title }}</title>
|
||||
{% if extra_scripts %}<script src="{{ extra_scripts }}"></script>{% endif %}
|
||||
<script>
|
||||
/* Apply saved theme before first paint to avoid flash */
|
||||
(function() {
|
||||
try {
|
||||
var p = localStorage.getItem('hbd_theme') || 'auto';
|
||||
var dark = p === 'dark' || (p === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
if (dark) document.documentElement.setAttribute('data-theme', 'dark');
|
||||
} catch(e) {}
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
/* ── Theme variables ── */
|
||||
:root {
|
||||
--bg: #f5f5f5;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f8f8f8;
|
||||
--surface-3: #f5f5f5;
|
||||
--text: #222222;
|
||||
--text-2: #333333;
|
||||
--text-3: #555555;
|
||||
--text-sec: #666666;
|
||||
--text-muted: #888888;
|
||||
--text-dim: #aaaaaa;
|
||||
--text-ghost: #cccccc;
|
||||
--border: #e0e0e0;
|
||||
--border-2: #eeeeee;
|
||||
--border-3: #f0f0f0;
|
||||
--border-4: #f5f5f5;
|
||||
--link: #0066cc;
|
||||
--nav-bg: #ffffff;
|
||||
--input-bg: #ffffff;
|
||||
--input-border: #cccccc;
|
||||
--shadow-sm: rgba(0,0,0,.08);
|
||||
--shadow: rgba(0,0,0,.10);
|
||||
--shadow-nav: rgba(0,0,0,.10);
|
||||
}
|
||||
html[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--bg: #111827;
|
||||
--surface: #1f2937;
|
||||
--surface-2: #283447;
|
||||
--surface-3: #374151;
|
||||
--text: #e5e7eb;
|
||||
--text-2: #d1d5db;
|
||||
--text-3: #9ca3af;
|
||||
--text-sec: #9ca3af;
|
||||
--text-muted: #6b7280;
|
||||
--text-dim: #4b5563;
|
||||
--text-ghost: #374151;
|
||||
--border: #374151;
|
||||
--border-2: #2d3748;
|
||||
--border-3: #253040;
|
||||
--border-4: #1e2a38;
|
||||
--link: #60a5fa;
|
||||
--nav-bg: #1f2937;
|
||||
--input-bg: #283447;
|
||||
--input-border: #4b5563;
|
||||
--shadow-sm: rgba(0,0,0,.30);
|
||||
--shadow: rgba(0,0,0,.40);
|
||||
--shadow-nav: rgba(0,0,0,.40);
|
||||
}
|
||||
|
||||
/* ── Reset / shared baseline ── */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html {
|
||||
@@ -16,10 +77,11 @@
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
padding-top: 60px;
|
||||
background: #f5f5f5;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
h1 { font-size: 1.5em; color: #333; margin: 0 0 5px; }
|
||||
h2 { font-size: 1.1em; color: #333; margin: 0 0 8px; }
|
||||
h1 { font-size: 1.5em; color: var(--text-2); margin: 0 0 5px; }
|
||||
h2 { font-size: 1.1em; color: var(--text-2); margin: 0 0 8px; }
|
||||
p { margin: 0; }
|
||||
|
||||
/* Navigation bar — shared across all pages */
|
||||
@@ -29,9 +91,9 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 200;
|
||||
background: #fff;
|
||||
background: var(--nav-bg);
|
||||
padding: 6px 12px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,.1);
|
||||
box-shadow: 0 2px 4px var(--shadow-nav);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -42,25 +104,25 @@
|
||||
.nav a {
|
||||
margin-right: 20px;
|
||||
text-decoration: none;
|
||||
color: #0066cc;
|
||||
color: var(--link);
|
||||
font-weight: 500;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.nav a:hover { text-decoration: underline; }
|
||||
.nav a.active { color: #333; font-weight: bold; }
|
||||
.nav a.active { color: var(--text-2); font-weight: bold; }
|
||||
.nav-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
color: var(--text-2);
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
border-radius: 20px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.nav-user:hover { background: #f0f4ff; text-decoration: none; }
|
||||
.nav-user:hover { background: var(--surface-2); text-decoration: none; }
|
||||
.nav-username {
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
@@ -81,7 +143,7 @@
|
||||
.nav-initials {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 50%;
|
||||
background: #0066cc;
|
||||
background: var(--link);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -106,7 +168,7 @@
|
||||
.nav-hamburger span {
|
||||
display: block;
|
||||
height: 3px;
|
||||
background: #555;
|
||||
background: var(--text-muted);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@@ -118,13 +180,22 @@
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #eee;
|
||||
border-top: 1px solid var(--border-2);
|
||||
order: 3;
|
||||
}
|
||||
.nav-links.nav-open { display: flex; }
|
||||
.nav-links a { margin-right: 0; padding: 6px 0; font-size: 1em; }
|
||||
}
|
||||
|
||||
/* ── Global dark-mode: inputs ── */
|
||||
html[data-theme="dark"] input:not([type=checkbox]):not([type=radio]),
|
||||
html[data-theme="dark"] select,
|
||||
html[data-theme="dark"] textarea {
|
||||
background-color: var(--input-bg);
|
||||
border-color: var(--input-border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Pending config publish button */
|
||||
.nav-publish-btn {
|
||||
background: #e65100;
|
||||
@@ -279,6 +350,17 @@
|
||||
setTimeout(clockTick, delay);
|
||||
}
|
||||
|
||||
/* Keep auto-theme in sync with system setting changes */
|
||||
try {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
|
||||
var pref = localStorage.getItem('hbd_theme') || 'auto';
|
||||
if (pref === 'auto') {
|
||||
if (e.matches) { document.documentElement.setAttribute('data-theme', 'dark'); }
|
||||
else { document.documentElement.removeAttribute('data-theme'); }
|
||||
}
|
||||
});
|
||||
} catch(e) {}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
/* Start the shared tick loop */
|
||||
clockTick();
|
||||
|
||||
@@ -288,6 +288,31 @@
|
||||
}
|
||||
#ntable a.host-link { color: inherit; text-decoration: none; }
|
||||
#ntable a.host-link:hover { text-decoration: underline; }
|
||||
|
||||
/* ── Dark mode ── */
|
||||
html[data-theme="dark"] h1,
|
||||
html[data-theme="dark"] h2 { color: var(--text); }
|
||||
html[data-theme="dark"] .subtitle { color: var(--text-sec); }
|
||||
html[data-theme="dark"] h2,
|
||||
html[data-theme="dark"] .table-section,
|
||||
html[data-theme="dark"] .log-section,
|
||||
html[data-theme="dark"] .log-section-header { background: var(--surface); }
|
||||
html[data-theme="dark"] .log-section-title { color: var(--text); }
|
||||
html[data-theme="dark"] #ntable td,
|
||||
html[data-theme="dark"] #ntable th { border-color: var(--border); }
|
||||
html[data-theme="dark"] #ntable tr:nth-child(even) { background: var(--surface-2); }
|
||||
html[data-theme="dark"] #ntable tr:hover { background: #1e3a5f; }
|
||||
html[data-theme="dark"] #ntable tbody tr.row-warning { background: #3a2800; }
|
||||
html[data-theme="dark"] #ntable tbody tr.row-critical { background: #3a0a0a; }
|
||||
html[data-theme="dark"] #ntable tbody tr.row-warning:hover { background: #4a3200; }
|
||||
html[data-theme="dark"] #ntable tbody tr.row-critical:hover { background: #4a1010; }
|
||||
html[data-theme="dark"] #messages .log-entry { border-bottom-color: var(--border-3); }
|
||||
html[data-theme="dark"] .log-ts,
|
||||
html[data-theme="dark"] .log-service { color: var(--text-muted); }
|
||||
html[data-theme="dark"] .log-info .log-level { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .log-filter-bar input,
|
||||
html[data-theme="dark"] .log-filter-bar select { color: var(--text); }
|
||||
html[data-theme="dark"] .connection-modal-content { background: var(--surface); color: var(--text); }
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
var cnt = 0;
|
||||
@@ -540,7 +565,7 @@
|
||||
if (msg.service) html += '<span class="log-service">' + msg.service + '</span>';
|
||||
html += '<span class="log-msg">' + msg.message + '</span>';
|
||||
html += '</div>';
|
||||
msgs.insertAdjacentHTML("afterbegin", html);
|
||||
msgs.insertAdjacentHTML(state.history ? "beforeend" : "afterbegin", html);
|
||||
applyLogFilters();
|
||||
}
|
||||
cnt++;
|
||||
|
||||
@@ -412,6 +412,47 @@
|
||||
.info-note { color: #888; font-style: italic; }
|
||||
.info-loading { color: #bbb; font-style: italic; }
|
||||
.threshold-covers { font-size: 1.00em; color: #777; font-style: italic; }
|
||||
|
||||
/* ── Dark mode ── */
|
||||
html[data-theme="dark"] h1 { color: var(--text); }
|
||||
html[data-theme="dark"] .subtitle { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .host-card { background: var(--surface); }
|
||||
html[data-theme="dark"] .host-header:hover { background: var(--surface-2); }
|
||||
html[data-theme="dark"] .host-name { color: var(--text); }
|
||||
html[data-theme="dark"] .collapse-icon,
|
||||
html[data-theme="dark"] .acc-icon { color: var(--text-muted); }
|
||||
html[data-theme="dark"] .host-body { border-top-color: var(--border-3); }
|
||||
html[data-theme="dark"] .plugin-accordion { border-color: var(--border); }
|
||||
html[data-theme="dark"] .plugin-acc-header { background: var(--surface-2); }
|
||||
html[data-theme="dark"] .plugin-acc-header:hover { background: var(--surface-3); }
|
||||
html[data-theme="dark"] .plugin-label { color: var(--text-2); }
|
||||
html[data-theme="dark"] .plugin-summary { color: var(--text-muted); }
|
||||
html[data-theme="dark"] .data-table { background: var(--surface); }
|
||||
html[data-theme="dark"] .data-table td { border-top-color: var(--border); color: var(--text); }
|
||||
html[data-theme="dark"] .data-table td.key { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .data-table tbody tr:nth-child(even) { background: var(--surface-2); }
|
||||
html[data-theme="dark"] .data-table tbody tr:hover { background: #1e3a5f; }
|
||||
html[data-theme="dark"] .bar-track { background: var(--border); }
|
||||
html[data-theme="dark"] .table-section-label { color: var(--text-muted); }
|
||||
html[data-theme="dark"] .no-data,
|
||||
html[data-theme="dark"] .loading { color: var(--text-dim); }
|
||||
html[data-theme="dark"] .timestamp { color: var(--text-dim); border-top-color: var(--border-3); }
|
||||
html[data-theme="dark"] .glance-chip.neutral { background: var(--surface-3); color: var(--text-sec); }
|
||||
html[data-theme="dark"] .os-label { color: var(--text-muted); }
|
||||
html[data-theme="dark"] .host-info-section { background: var(--surface-2); border-bottom-color: var(--border); }
|
||||
html[data-theme="dark"] .info-label { color: var(--text-3); }
|
||||
html[data-theme="dark"] .info-value { color: var(--text); }
|
||||
html[data-theme="dark"] .info-thresholds-title { color: var(--text-3); }
|
||||
html[data-theme="dark"] .info-note,
|
||||
html[data-theme="dark"] .info-loading,
|
||||
html[data-theme="dark"] .threshold-covers { color: var(--text-muted); }
|
||||
html[data-theme="dark"] .check-ok { background: #0d2e17; }
|
||||
html[data-theme="dark"] .check-warning { background: #2e1a00; }
|
||||
html[data-theme="dark"] .check-critical { background: #2e0a0a; }
|
||||
html[data-theme="dark"] .check-unknown { background: var(--surface-2); }
|
||||
html[data-theme="dark"] .check-output { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .container::-webkit-scrollbar-track { background: var(--surface-2); }
|
||||
html[data-theme="dark"] .container::-webkit-scrollbar-thumb { background: var(--border); }
|
||||
</style>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -247,6 +247,56 @@
|
||||
.btn-sm-del { background: transparent; color: #c62828; border: 1px solid #e0e0e0; border-radius: 4px; padding: 2px 7px; font-size: .78em; cursor: pointer; }
|
||||
.btn-sm-del:hover { background: #fce4ec; }
|
||||
|
||||
/* ---- Theme picker ---- */
|
||||
.theme-btns { display: flex; gap: 6px; }
|
||||
.theme-btn {
|
||||
padding: 5px 14px;
|
||||
border: 1px solid var(--border, #e0e0e0);
|
||||
border-radius: 4px;
|
||||
background: var(--surface-3, #f5f5f5);
|
||||
color: var(--text-sec, #666);
|
||||
cursor: pointer;
|
||||
font-size: .88em;
|
||||
font-family: inherit;
|
||||
}
|
||||
.theme-btn:hover { border-color: var(--link, #0066cc); color: var(--link, #0066cc); }
|
||||
.theme-btn.active { background: var(--link, #0066cc); color: #fff; border-color: var(--link, #0066cc); }
|
||||
|
||||
/* ── Dark mode ── */
|
||||
html[data-theme="dark"] h1 { color: var(--text); }
|
||||
html[data-theme="dark"] .subtitle { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .profile-card { background: var(--surface); box-shadow: 0 1px 6px var(--shadow); }
|
||||
html[data-theme="dark"] .profile-name { color: var(--text); }
|
||||
html[data-theme="dark"] .profile-username { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .badge-admin { background: #1a3255; color: #7aa8f0; }
|
||||
html[data-theme="dark"] .badge-user { background: var(--surface-3); color: var(--text-sec); }
|
||||
html[data-theme="dark"] .section { background: var(--surface); box-shadow: 0 1px 6px var(--shadow); }
|
||||
html[data-theme="dark"] .section h2 { color: var(--text); border-bottom-color: var(--border); }
|
||||
html[data-theme="dark"] .settings-row { border-bottom-color: var(--border-4); }
|
||||
html[data-theme="dark"] .settings-label { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .settings-value { color: var(--text); }
|
||||
html[data-theme="dark"] .settings-empty { color: var(--text-dim); }
|
||||
html[data-theme="dark"] .edit-section h4 { color: var(--text); border-bottom-color: var(--border); }
|
||||
html[data-theme="dark"] .edit-field label { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .edit-input { background: var(--input-bg); border-color: var(--input-border); color: var(--text); }
|
||||
html[data-theme="dark"] .channel-row { border-bottom-color: var(--border-4); }
|
||||
html[data-theme="dark"] .channel-name { color: var(--text); }
|
||||
html[data-theme="dark"] .ch-picker-label { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .ch-chip.selected { background: #1a3255; color: #60a5fa; }
|
||||
html[data-theme="dark"] .ch-chip.available { background: var(--surface-3); color: var(--text-sec); }
|
||||
html[data-theme="dark"] .ch-chip.available:hover { background: var(--border); color: var(--link); }
|
||||
html[data-theme="dark"] .my-ch-card { border-color: var(--border); }
|
||||
html[data-theme="dark"] .my-ch-header { background: var(--surface-2); border-bottom-color: var(--border); }
|
||||
html[data-theme="dark"] .my-ch-name { color: var(--text); }
|
||||
html[data-theme="dark"] .host-chip.owner { background: #0d2e17; color: #66bb6a; }
|
||||
html[data-theme="dark"] .host-chip.manager { background: #0d1f40; color: #64b5f6; }
|
||||
html[data-theme="dark"] .host-chip.monitor { background: #1e0d30; color: #ba68c8; }
|
||||
html[data-theme="dark"] .no-hosts { color: var(--text-dim); }
|
||||
html[data-theme="dark"] .ch-modal-box { background: var(--surface); color: var(--text); }
|
||||
html[data-theme="dark"] .ch-modal-box h3 { color: var(--text); }
|
||||
html[data-theme="dark"] .ch-form-row label { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .ch-form-divider { color: var(--text-muted); border-top-color: var(--border); }
|
||||
|
||||
/* ---- Channel modal (for My Channels CRUD) ---- */
|
||||
.ch-modal-overlay {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,.4);
|
||||
@@ -477,6 +527,19 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Appearance -->
|
||||
<div class="section">
|
||||
<h2>Appearance</h2>
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Theme</span>
|
||||
<div class="theme-btns">
|
||||
<button class="theme-btn" data-theme-val="auto" onclick="setTheme('auto')">Auto</button>
|
||||
<button class="theme-btn" data-theme-val="light" onclick="setTheme('light')">Light</button>
|
||||
<button class="theme-btn" data-theme-val="dark" onclick="setTheme('dark')">Dark</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Host access -->
|
||||
<div class="section">
|
||||
<h2>Host Access</h2>
|
||||
@@ -523,6 +586,28 @@
|
||||
|
||||
</div>
|
||||
<script>
|
||||
// ---- Theme ----
|
||||
function applyTheme(pref) {
|
||||
var dark = pref === 'dark' ||
|
||||
(pref === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
if (dark) { document.documentElement.setAttribute('data-theme', 'dark'); }
|
||||
else { document.documentElement.removeAttribute('data-theme'); }
|
||||
}
|
||||
function setTheme(pref) {
|
||||
try { localStorage.setItem('hbd_theme', pref); } catch(e) {}
|
||||
applyTheme(pref);
|
||||
document.querySelectorAll('.theme-btn').forEach(function(b) {
|
||||
b.classList.toggle('active', b.dataset.themeVal === pref);
|
||||
});
|
||||
}
|
||||
(function() {
|
||||
var pref = 'auto';
|
||||
try { pref = localStorage.getItem('hbd_theme') || 'auto'; } catch(e) {}
|
||||
document.querySelectorAll('.theme-btn').forEach(function(b) {
|
||||
b.classList.toggle('active', b.dataset.themeVal === pref);
|
||||
});
|
||||
})();
|
||||
|
||||
// ---- Identity ----
|
||||
async function saveIdentity() {
|
||||
const full_name = document.getElementById('profile-fullname').value;
|
||||
|
||||
@@ -456,6 +456,67 @@
|
||||
display: flex; justify-content: flex-end; background: #f8f8f8;
|
||||
}
|
||||
.mpick-none { padding: 10px; font-size: .82em; color: #aaa; text-align: center; }
|
||||
|
||||
/* ── Dark mode ── */
|
||||
html[data-theme="dark"] h1 { color: var(--text); }
|
||||
html[data-theme="dark"] .subtitle { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .sidebar-nav a { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .sidebar-nav a:hover { background: var(--surface-3); color: var(--link); }
|
||||
html[data-theme="dark"] .sidebar-nav a.active { background: #1a3255; color: #60a5fa; }
|
||||
html[data-theme="dark"] .sidebar-toggle { background: var(--surface-3); color: var(--text-sec); }
|
||||
html[data-theme="dark"] .sidebar-nav { background: var(--surface); }
|
||||
html[data-theme="dark"] .section { background: var(--surface); box-shadow: 0 1px 4px var(--shadow); }
|
||||
html[data-theme="dark"] .section-header { border-bottom-color: var(--border); }
|
||||
html[data-theme="dark"] .section-title { color: var(--text-2); }
|
||||
html[data-theme="dark"] .section-desc { color: var(--text-muted); }
|
||||
html[data-theme="dark"] .section-footer { border-top-color: var(--border-3); }
|
||||
html[data-theme="dark"] .field-row { border-bottom-color: var(--border-4); }
|
||||
html[data-theme="dark"] .field-label { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .field-value { color: var(--text); }
|
||||
html[data-theme="dark"] .field-desc { color: var(--text-muted); }
|
||||
html[data-theme="dark"] .val-boolean.on { background: #0d2e17; color: #66bb6a; }
|
||||
html[data-theme="dark"] .val-boolean.off { background: #2e0d0d; color: #ef9a9a; }
|
||||
html[data-theme="dark"] .val-tag { background: #1a2d5a; color: #7aa8f0; }
|
||||
html[data-theme="dark"] .val-empty { color: var(--text-dim); }
|
||||
html[data-theme="dark"] .val-masked { color: var(--text-muted); }
|
||||
html[data-theme="dark"] .mini-table th { background: var(--surface-3); color: var(--text-sec); border-bottom-color: var(--border); }
|
||||
html[data-theme="dark"] .mini-table td { border-bottom-color: var(--border-3); color: var(--text); }
|
||||
html[data-theme="dark"] .mini-table tbody tr:hover { background: var(--surface-2); }
|
||||
html[data-theme="dark"] .badge-admin { background: #1a3255; color: #7aa8f0; }
|
||||
html[data-theme="dark"] .badge-user { background: var(--surface-3); color: var(--text-sec); }
|
||||
html[data-theme="dark"] .channel-card { border-color: var(--border); }
|
||||
html[data-theme="dark"] .channel-header { background: var(--surface-2); border-bottom-color: var(--border); }
|
||||
html[data-theme="dark"] .channel-name-text { color: var(--text); }
|
||||
html[data-theme="dark"] .channel-field { border-bottom-color: var(--border-4); }
|
||||
html[data-theme="dark"] .channel-field-label { color: var(--text-muted); }
|
||||
html[data-theme="dark"] .channel-field-value { color: var(--text); }
|
||||
html[data-theme="dark"] .thresh-cfg-card { border-color: var(--border); }
|
||||
html[data-theme="dark"] .thresh-cfg-header { background: var(--surface-2); border-bottom-color: var(--border); }
|
||||
html[data-theme="dark"] .thresh-cfg-name-label { color: #60a5fa; }
|
||||
html[data-theme="dark"] .crud-table th { background: var(--surface-3); color: var(--text-sec); border-bottom-color: var(--border); }
|
||||
html[data-theme="dark"] .crud-table td { border-bottom-color: var(--border-3); color: var(--text); }
|
||||
html[data-theme="dark"] .yaml-editor { background: var(--input-bg); border-color: var(--input-border); color: var(--text); }
|
||||
html[data-theme="dark"] .pending-banner { background: #2d2400; border-color: #a08020; }
|
||||
html[data-theme="dark"] .pending-banner .pending-msg { color: #e8c840; }
|
||||
html[data-theme="dark"] .modal-box,
|
||||
html[data-theme="dark"] .ch-modal-box { background: var(--surface); color: var(--text); }
|
||||
html[data-theme="dark"] .modal-box h3,
|
||||
html[data-theme="dark"] .ch-modal-box h3 { color: var(--text); }
|
||||
html[data-theme="dark"] .ch-form-row label { color: var(--text-sec); }
|
||||
html[data-theme="dark"] .ch-form-divider { color: var(--text-muted); border-top-color: var(--border); }
|
||||
html[data-theme="dark"] .backup-row { border-bottom-color: var(--border-3); }
|
||||
html[data-theme="dark"] .mpick-display { background: var(--input-bg); border-color: var(--input-border); }
|
||||
html[data-theme="dark"] .mpick-display:hover { border-color: var(--link); background: var(--surface-2); }
|
||||
html[data-theme="dark"] .mpick-tag { background: #1a2d5a; color: #7aa8f0; }
|
||||
html[data-theme="dark"] .mpick-more,
|
||||
html[data-theme="dark"] .mpick-empty { color: var(--text-muted); }
|
||||
html[data-theme="dark"] .mpick-panel { background: var(--surface); border-color: var(--border); }
|
||||
html[data-theme="dark"] .mpick-panel-header { background: var(--surface-3); color: var(--text-sec); border-bottom-color: var(--border); }
|
||||
html[data-theme="dark"] .mpick-item { border-bottom-color: var(--border-4); color: var(--text); }
|
||||
html[data-theme="dark"] .mpick-item-avail:hover { background: #0d2e17; }
|
||||
html[data-theme="dark"] .mpick-item-sel:hover { background: #2e0d0d; }
|
||||
html[data-theme="dark"] .mpick-panel-footer { background: var(--surface-2); border-top-color: var(--border); }
|
||||
html[data-theme="dark"] .mpick-none { color: var(--text-dim); }
|
||||
</style>
|
||||
|
||||
<body>
|
||||
|
||||
+5
-3
@@ -85,13 +85,15 @@ async def handler(request):
|
||||
except Exception as e:
|
||||
logger.error("Error sending initial hosts: %s", e)
|
||||
|
||||
# Send recent messages, filtered to hosts this user may see
|
||||
# Send recent messages newest-first so the client can append them in
|
||||
# display order without reordering on arrival (tagged history=True so
|
||||
# the client knows to append rather than prepend).
|
||||
if data.msgs:
|
||||
try:
|
||||
for m in data.msgs:
|
||||
for m in reversed(data.msgs):
|
||||
host_name = m.get("host") if isinstance(m, dict) else None
|
||||
if not host_name or _user_can_see_host(user, host_name):
|
||||
await ws.send_str(json.dumps({"type": "message", "data": m}))
|
||||
await ws.send_str(json.dumps({"type": "message", "data": m, "history": True}))
|
||||
except Exception as e:
|
||||
logger.error("Error sending initial messages: %s", e)
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "hbd"
|
||||
version = "5.3.6"
|
||||
version = "5.3.9"
|
||||
description = "Heartbeat monitoring system — client (hbc) and server (hbd)"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
+15
-1
@@ -5,9 +5,23 @@ uv version --bump patch
|
||||
VER=$(uv version --short)
|
||||
sed -i".bak" "s/__version__ = \"[0-9.]*\"\(.*\)$/__version__ = \"$VER\"\1/" hbd/__init__.py
|
||||
sed -i".bak" "s/__version__ = \"[0-9.]*\"\(.*\)$/__version__ = \"$VER\"\1/" scripts/hbc_mini.py
|
||||
sed -i".bak" "s/\*\*Package:\*\* \`hbd\` v[0-9.]*/\*\*Package:\*\* \`hbd\` v$VER/" README.md
|
||||
|
||||
# Update CHANGELOG.md with commits since last tag
|
||||
LASTTAG=$(git describe --tags --abbrev=0 2>/dev/null || true)
|
||||
ADDED=$(git log "${LASTTAG:+$LASTTAG..}HEAD" --pretty="%s" | grep "^feat:" | sed 's/^feat: /- /')
|
||||
FIXED=$(git log "${LASTTAG:+$LASTTAG..}HEAD" --pretty="%s" | grep "^fix:" | sed 's/^fix: /- /')
|
||||
{
|
||||
printf "## [%s]\n" "$VER"
|
||||
[ -n "$ADDED" ] && printf "\n### Added\n%s\n" "$ADDED"
|
||||
[ -n "$FIXED" ] && printf "\n### Fixed\n%s\n" "$FIXED"
|
||||
printf "\n---\n\n"
|
||||
} > /tmp/changelog_entry.txt
|
||||
sed -i".bak" "4r /tmp/changelog_entry.txt" CHANGELOG.md
|
||||
rm /tmp/changelog_entry.txt CHANGELOG.md.bak
|
||||
|
||||
# commit pyproject.toml
|
||||
git commit -m "version $VER" pyproject.toml hbd/__init__.py scripts/hbc_mini.py
|
||||
git commit -m "version $VER" pyproject.toml hbd/__init__.py scripts/hbc_mini.py README.md CHANGELOG.md
|
||||
git push
|
||||
# tag version
|
||||
git tag -a v$VER -m "Version $VER"
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
# updated by scripts/bumpminor.sh
|
||||
__version__ = "5.3.6"
|
||||
__version__ = "5.3.9"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Protocol (mirrors hbd/common/proto.py)
|
||||
|
||||
Reference in New Issue
Block a user