Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2f19be2
refactor: collapse to a single command and drop cobra
aeddi Jun 26, 2026
d80d866
refactor(web): remove unused UI blocks and dead server code
aeddi Jun 26, 2026
f3da8fd
docs: drop signal references and update for the slimmed-down CLI
aeddi Jun 26, 2026
c2ec320
feat: source validator/peer/signing data from RPC and on-chain valope…
aeddi Jun 29, 2026
904d16d
docs: update README and AGENT for the slimmed-down design
aeddi Jun 29, 2026
5e28d85
feat: add IP-geolocation network map of peers and validators
aeddi Jun 29, 2026
e838cca
feat(web): full-width addresses and a SPOF column for validators
aeddi Jun 30, 2026
8f710bc
ci: add Dockerfile and GHCR release workflow
aeddi Jun 30, 2026
d48ebd8
feat: add token-gated API to send a test to a configured notify channel
aeddi Jul 1, 2026
7672d09
feat: add header links, BetterStack status links, and a status endpoi…
aeddi Jul 1, 2026
4307f56
feat: add --hide-host flag to hide the monitored node from the peers …
aeddi Jul 2, 2026
94e56c9
feat(web): add per-country peer list and hover linking to the network…
aeddi Jul 2, 2026
72b6410
fix(web): cap the country list to the map height and make it scroll
aeddi Jul 2, 2026
99de302
feat(web): add country column to the peer list with map cross-highlight
aeddi Jul 3, 2026
68434ac
feat(history): record block-signing history and expose per-validator …
aeddi Jul 3, 2026
aa92d27
feat(web): add missed-24h column to the validator list
aeddi Jul 3, 2026
aeaa793
feat(node): detect peer cloud provider from IP via DB-IP ASN Lite
aeddi Jul 3, 2026
cfd42a6
feat(web): show cloud provider + country in peer and validator tables
aeddi Jul 3, 2026
2580db8
feat(history): add multi-window missed-block query
aeddi Jul 3, 2026
96d6e6a
feat(web): add /api/stats aggregate network statistics endpoint
aeddi Jul 3, 2026
289329d
feat(web): extend /api/status with network state, recent blocks, and …
aeddi Jul 3, 2026
feca334
docs: document network map, cloud providers, missed-block history, an…
aeddi Jul 3, 2026
2c8d27c
feat(web): show validators as blue dots on the map with a legend
aeddi Jul 3, 2026
f4edfde
feat(web): add map country/provider toggle and polish validator/peer …
aeddi Jul 3, 2026
d321828
feat(web): responsive address truncation, RPC tooltip cleanup, and pr…
aeddi Jul 3, 2026
2161ac7
feat(web): add provider line to the map dot tooltip
aeddi Jul 3, 2026
787a524
feat: consolidate multiple RPC sources into a network overview
aeddi Jul 3, 2026
9a270c7
docs: document multi-source RPC consolidation
aeddi Jul 3, 2026
9f51d15
feat: add configurable chain display name
aeddi Jul 3, 2026
5b069a1
refactor: remove ineffective ?v= cache-buster
aeddi Jul 3, 2026
f809922
feat: make all validator and peer table columns sortable
aeddi Jul 3, 2026
8e27b23
feat: alert when a validator misses N blocks in a row
aeddi Jul 6, 2026
7a08816
feat(web): streak-alert UI and country-clustered network map
aeddi Jul 6, 2026
10799d5
feat(web): map clusters follow list mode with tree tooltips and hover…
aeddi Jul 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Keep the build context small and free of local-only artifacts.
.git
.github/
.ignore/
.playwright-mcp/
*.png
*.md
Dockerfile
.dockerignore
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version: 2
updates:
# Keep the SHA-pinned actions in release.yml current.
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
63 changes: 63 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: release

# Builds and publishes the container image to GHCR. Triggers on pushes to the
# refactor/slim-down branch and on v* tags, so the branch can be published
# without merging to main (workflow_dispatch is intentionally not used — it
# only appears when the workflow lives on the default branch).
on:
push:
branches:
- refactor/slim-down
tags:
- 'v*'

permissions:
contents: read
packages: write

concurrency:
# Don't cancel an in-flight publish (a multi-tag push must finish).
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
docker:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Log in to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Image metadata (tags + labels)
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ghcr.io/${{ github.repository }}
# latest=false: a feature branch must not claim the :latest tag.
flavor: latest=false
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,format=short

- name: Set up Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0

- name: Build and push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
57 changes: 27 additions & 30 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,41 @@

## What is this?

gnockpit is a real-time monitoring dashboard for gno.land validator nodes. It's a single Go binary that connects to a gnoland node's Tendermint RPC and provides both a web UI and CLI tools.
gnockpit is a real-time monitoring dashboard for gno.land validator nodes. It's a single Go binary (`gnockpit [flags]`, no subcommands) that connects to one or more gnoland RPC endpoints (`-rpc`, repeatable), consolidates their views, and serves a live web dashboard, with web-push/PWA alerts and external (Shoutrrr) notifications.

## Architecture

```
main.go — CLI entry point (cobra commands: status, peers, web, etc.)
main.go — CLI entry point (single command: starts the web dashboard)
node/ — RPC client + types (pure data layer, no UI)
client.go — HTTP client for Tendermint RPC (/status, /net_info, /validators, etc.)
sources.go — Sources: an ordered set of RPC endpoints polled together; best-source pick for global data + peer union/dedup (MergePeers)
ip.go — public-IP validation (IsPublicIP): only public IPs are ever geolocated or displayed
types.go — All data types (Status, Peer, Validator, Snapshot, SigningStats, etc.)
validators.go — NameRegistry: maps validator addresses ↔ monikers, persists to JSON
web/ — Web dashboard
geoip.go — DB-IP City Lite: IP → coordinates + country (auto-downloaded, monthly)
asn.go — DB-IP ASN Lite: IP → ASN + cloud provider (auto-downloaded, monthly)
history/ — SQLite store of per-block validator signing (shares the push DB handle)
store.go — RecordBlocks / MissedInWindow / MissedByWindows / Prune; missed-block windows
web/ — Web dashboard + HTTP API
server.go — HTTP/WebSocket server, background data fetcher, system info collector
status.go — /api/status: health + curated network state / recent blocks / peer + validator columns
stats.go — /api/stats: missed-block windows, provider/country aggregates, set health, Nakamoto
index.html — Single-page dashboard (embedded via go:embed, vanilla JS, no framework)
```

## Key Concepts

### Data Flow
1. `server.go` runs a **publish loop** every N seconds (default 5s)
2. Each cycle calls `fetchSnapshot()` which queries the local RPC for status, validators, consensus, peers, block signing stats
2. Each cycle calls `fetchSnapshot()` which polls every configured source (`node/sources.go: Poll`): global chain data (status, validators, consensus, signing) comes from the freshest reachable source; peers are unioned across all reachable sources and deduped by node ID (`MergePeers`) before their RPCs are probed. All sources down → `snap.Error` drives the "connecting to sources" banner.
3. The snapshot is broadcast to all connected WebSocket clients as typed messages: `status`, `peers`, `votes`, `checks`, `signing`
4. `index.html` receives these messages and updates the DOM in-place
5. Log lines are streamed from `journalctl` via a separate goroutine
5. Each cycle also geolocates peers (`geoip`) and resolves their cloud provider (`asn`) — using each peer's resolved public IP only (peers with no usable public IP are geolocated to nothing and bucketed "Unknown" on the map) — and records the recent blocks' missing-validator sets into the `history` store (forward-only, deduped by height). This powers the network map, the Country/Provider columns, and per-validator missed-block windows (1h/24h/7d/30d/total). Validators inherit the country/provider of their correlated peer (matched via `ValAddress`), so validators no source is peered with have none.

### NameRegistry (validators.go)
Maps validator addresses to human-readable monikers. This is critical because Tendermint RPC only returns addresses, not names. Discovery happens through:
- **Genesis file** — validator names from genesis.json at startup
- **Genesis (RPC)** — validator names and the genesis time are seeded at startup by streaming the node's `/genesis` endpoint, parsing only the head (the large `app_state` is never downloaded)
- **Peer RPC queries** — when we query a peer's `/status`, we get their validator_info.address + moniker
- **Node-ID verification** — on shared IPs (multiple nodes same IP), the RPC response's node_info.id is checked against the peer's P2P node-id before trusting the mapping
- **Correlation** — unmatched validators are matched to unmatched peers heuristically
Expand All @@ -42,33 +50,30 @@ Fetches the last N blocks (default 100), extracts:
- Block timestamps → compute per-proposer average block time
- Sign rate per validator (signed/total as percentage)

### Doctor (index.html: runDoctor)
Client-side diagnostic checks that run on every data update:
- Prevote/precommit threshold not reached
- Vote gossip fragmentation (peers see different vote counts)
- Split-height deadlock (validators at different consensus heights)
- Consensus frozen (stuck for >5min or >1h)
- Old-chain peers (peer height way higher = different genesis)
- Validators not voting when network is stuck
This is a live 100-block window. Longer-term **missed-block counts** come from the
`history` store instead (Data Flow #5): it records each block's missing set forward
and aggregates over 1h/24h/7d/30d/total via `MissedByWindows`. History is
forward-only (never backfilled) and pruned past 31 days.

### Diagnose (server.go: handleDiagnose)
Per-node diagnostics triggered by clicking the 💡 button. Queries the target node's RPC and checks: reachability, height, sync, block time, validator status, chain-id match, version, peer count, consensus state, round age.
### HTTP API (CORS-open JSON)
- `/api/status` (status.go) — health summary (retrocompat `status`/`chain`/`height`/`reason`/`time`) plus `network`, `recent_blocks` (last 100), and per-`peers`/`validators` column data.
- `/api/stats` (stats.go) — per-validator missed-block windows, provider + country aggregates, validator-set health, Nakamoto coefficient.
- `/api` (server.go) — raw `Snapshot` dump (unstable shape; debugging).
- Also: `/badge.svg`, `/ws`, `/api/boot`, `/api/push/*`, `/api/notify/*`.

## Important Patterns

### No hardcoded values
Everything is auto-detected or configurable via flags. The tool works on any gno.land chain without code changes:
- Chain ID → from RPC `/status`
- Service name → from chain ID + `.service`
- Genesis path → from `--data-dir` + `/config/genesis.json`
- Validator names → discovered dynamically from peers
- Gno source path → from `GNOROOT` env
- Genesis time + validator names → streamed from RPC `/genesis` (head only)
- Validator names → also discovered dynamically from peers

### Single HTML file
`web/index.html` is a complete SPA with no build step. Vanilla JS, CSS variables for dark theme, no external dependencies. It's embedded in the binary via `go:embed`.

### WebSocket protocol
Messages are JSON with `{type: string, data: any}`. Types: `status`, `peers`, `votes`, `checks`, `signing`, `time`, `log`, `diagnose`.
Messages are JSON with `{type: string, data: any}`. Types: `snapshot` (full state on connect), `update` (batched periodic), and individual `status`, `peers`, `votes`, `checks`, `signing`, `time`.

### Scroll preservation
`renderNetwork()` saves `window.scrollY` before DOM updates and restores it after, preventing the page from jumping during live updates.
Expand All @@ -85,12 +90,6 @@ Each card has `<h2 data-section="name">` that toggles `.collapsed` class. State
4. Broadcast it in the publish loop
5. Handle the WebSocket message in `web/index.html`

### Adding a new doctor check
Add to the `runDoctor()` function in `index.html`. Push to the `items` array with `{level: 'crit'|'warn'|'ok', title: string, detail: string, action: string}`.

### Adding a new diagnose check
Add to `handleDiagnose()` in `server.go`. Append to `report.Checks` with `DiagCheck{Name, Status, Detail}`.

### Adding a new peer table column
1. Add `<th>` in the header row
2. Add the cell creation in the validator loop (after `// Section 1: Validators`)
Expand All @@ -110,10 +109,8 @@ Tests are minimal — focused on JSON parsing and name registry logic. The web U
Typical systemd service:
```ini
[Service]
ExecStart=/usr/local/bin/gnockpit web \
ExecStart=/usr/local/bin/gnockpit \
--rpc http://127.0.0.1:26657 \
--data-dir /path/to/gnoland-data \
--service chainname.service \
--port 8080 --addr 127.0.0.1 --interval 5s
```

Expand Down
31 changes: 31 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# syntax=docker/dockerfile:1

# ---- Build (fully static, pure Go: modernc sqlite + maxminddb, no cgo) ----
FROM golang:1.25-alpine AS build
WORKDIR /src

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN CGO_ENABLED=0 go build -trimpath \
-ldflags "-s -w" \
-o /out/gnockpit .

# ---- Runtime ----
FROM alpine:3.21
# ca-certificates: gnockpit downloads the DB-IP geo database over HTTPS.
RUN apk add --no-cache ca-certificates \
&& adduser -D -u 10001 gnockpit
COPY --from=build /out/gnockpit /usr/local/bin/gnockpit
USER gnockpit
EXPOSE 8080

# Point gnockpit at your node's RPC and (optionally) a mounted volume for
# persistence, e.g.:
# docker run -p 8080:8080 -v gnockpit-data:/data ghcr.io/gnoverse/gnockpit \
# --rpc http://your-node:26657 \
# --db-path /data/gnockpit.db --names /data/gnockpit-names.json \
# --geoip-db /data/gnockpit-geoip.mmdb
ENTRYPOINT ["gnockpit"]
Loading
Loading