Metadata-Version: 2.4
Name: pingmonitor
Version: 1.1.0
Summary: TUI monitor of latency and availability to servers by country
Project-URL: Homepage, https://github.com/kottot13/pingmon
Project-URL: Repository, https://github.com/kottot13/pingmon
Author: pingmon contributors
License: MIT
License-File: LICENSE
Keywords: geoip,latency,monitoring,network,ping,textual,tui
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: System :: Monitoring
Classifier: Topic :: System :: Networking :: Monitoring
Requires-Python: >=3.11
Requires-Dist: textual>=0.80
Description-Content-Type: text/markdown

# pingmon

A full-featured terminal UI for monitoring **latency and availability** to
servers in different countries. Built with [Textual](https://textual.textualize.io/).

It continuously TCP-pings a set of targets (one or more reachable hosts per
country), and shows a live, colour-coded dashboard with per-target latency,
average, jitter, packet loss, a status indicator, and an animated trend graph.

![pingmon screenshot](docs/screenshot.svg)

## Features

- **Live dashboard** — sortable table of targets with status dot, latency, average, loss and an inline coloured sparkline trend.
- **Detail panel** — for the selected target: live latency graph, quality gauge, min / max / avg / jitter / loss, **MOS** call-quality score, resolved IP, **GeoIP city / region and the hosting network (ASN + ISP)**, sample count.
- **★ Region Advisor** *(unique)* — ranks regions by a composite 0–100 score under a chosen **use-case profile** (VoIP / Gaming / Web / Bulk) and highlights the best region to pick right now. Press `g`. See [below](#region-advisor).
- **MOS / R-factor** — turns latency + jitter + loss into the single VoIP call-quality number (ITU-T E-model), the same metric paid monitoring suites charge for.
- **Threshold alerts** — fire when a target stays slow or lossy for N samples: terminal bell, an in-app toast, an OS desktop notification, and a blinking row marker. Auto-clears on recovery.
- **Traceroute drill-down** — press `Enter` (or click) on a row for an mtr-style hop-by-hop path with per-hop loss and per-probe timings, plus **GeoIP per hop** (country code, city and ASN) so you can see which countries and networks the traffic crosses.
- **Per-country set, ready to go** — the Netherlands, Germany, United Kingdom, France, Cyprus, Italy, Spain, Greece, Sweden, Ireland and the United States are pre-configured with reachable hosts.
- **Editable targets** — add (`a`), edit (`E`) and delete (`d`) targets right inside the TUI, or edit the TOML config by hand; reset stats with `r`. In-app changes are saved to the config immediately.
- **GeoIP auto-detect & enrichment** — every target is looked up via ip-api.com: the detail panel shows its **city, region and hosting network (ASN + ISP)**, and when you add a target with country/code left blank they are filled in automatically.
- **Works in every terminal** — each country is shown as its **two-letter code** (`NL`, `DE`, `US`), not a flag emoji. Many terminals (iTerm2 among them) can't render regional-indicator flags and fall back to two boxed letters; the code is always legible.
- **Show filter** — flip the table between **all**, **mine only** (targets you added) and **others only** (the built-in set) with `f`.
- **No root required** — uses TCP connect timing (port 443/80), so it works without raw-socket / ICMP privileges and measures real service latency.
- **Modern terminal UX** — truecolor, mouse support, zebra-striped table, a live "heartbeat" spinner, keyboard and mouse navigation, sort modes and modal dialogs.

| Region Advisor | Traceroute drill-down |
| -------------- | --------------------- |
| ![advisor](docs/advisor.svg) | ![traceroute](docs/traceroute.svg) |

## Requirements

- Python 3.11+ (not needed for the standalone binary below)
- `textual >= 0.80` (installed automatically)

## Install as a system command

Use it like `htop` — install once, run `pingmon` from anywhere.

### pipx / uv (recommended, Linux + macOS)

```bash
pipx install pingmonitor                 # from PyPI
pipx install .                           # or from a checkout of this repo
pipx install git+https://github.com/kottot13/pingmon
# uv works the same:  uv tool install pingmonitor   /   uvx --from pingmonitor pingmon
```

The PyPI package is **`pingmonitor`**; it installs the **`pingmon`** command.
`pipx` keeps it in its own isolated environment and puts `pingmon` on your
`PATH`. Then just run `pingmon`.

### Homebrew (macOS / Linuxbrew)

A formula skeleton lives in [`packaging/pingmon.rb`](packaging/pingmon.rb).
Publish to PyPI, fill in the sdist URL + `brew update-python-resources`, then:

```bash
brew install kottot13/tap/pingmon
```

### Standalone binary (no Python on the target)

The most htop-like option — a single executable built with PyInstaller
([`packaging/pingmon.spec`](packaging/pingmon.spec)):

```bash
pip install pyinstaller
pyinstaller packaging/pingmon.spec
sudo cp dist/pingmon /usr/local/bin/     # macOS
cp dist/pingmon ~/.local/bin/            # Linux
```

Build once per OS/architecture (PyInstaller does not cross-compile).

### From source (development)

```bash
./run.sh                # makes a local venv, installs Textual, launches
# or
python3 -m venv .venv && .venv/bin/pip install -e . && .venv/bin/pingmon
```

Config lives at `~/.config/pingmon/config.toml` by default (or a local
`./config.toml` if present, or `$PINGMON_CONFIG`). Run `pingmon --help` for the
CLI flags (`-V/--version`, `-c/--config PATH`).

## Keys

| Key            | Action                          |
| -------------- | ------------------------------- |
| `↑ / ↓`, `j/k` | Move selection                  |
| `Enter`        | Traceroute drill-down for the selected target |
| `g`            | Open the Region Advisor (`[` `]` / `p` switch profile inside) |
| `p`            | Cycle the Advisor profile (VoIP / Gaming / Web / Bulk) |
| `f`            | Cycle the show filter (all / mine only / others only) |
| `space`        | Pause / resume probing          |
| `m` / `M`      | Sort by latency (ms); press again to flip fastest ⇄ slowest first |
| `s`            | Cycle sort (country / latency / loss / jitter) |
| `a`            | Add a target                    |
| `E`            | Edit the selected target (form pre-filled) |
| `A`            | Toggle the alert system on / off |
| `d`            | Delete the selected target      |
| `r`            | Reset all statistics            |
| `e`            | Show the config file path       |
| `q`            | Quit                            |

## Region Advisor

The Advisor (`g`) answers the question the tool was born from — *which region
should I actually pick right now?* It computes a **0–100 score** per region from
latency, jitter and loss, under a selectable **profile**:

| Profile | What it optimises for |
| ------- | --------------------- |
| **VoIP / Video call** | jitter & loss dominate; driven by the MOS / E-model score |
| **Gaming** | raw latency + jitter, loss punished hard |
| **Web / API** | latency-led, mild loss penalty |
| **Bulk / Backup** | loss-led, tolerant of high latency |

Regions are ranked best-first with the #1 pick highlighted, each with a score
bar, a letter grade (A–F) and a one-line reason. Switch profile with `[` / `]`,
`p` or `Tab`; close with `Esc`.

## Alerts

A target enters **alert** state when, for `alert_window` consecutive samples, it
is unreachable, slower than `alert_latency` ms, or its recent loss exceeds
`alert_loss` %. On entry pingmon rings the terminal bell, shows a toast, raises
an OS desktop notification (macOS `osascript` / Linux `notify-send`, toggle with
`desktop_notify`) and blinks the row's status marker; it auto-clears with a
"Recovered" toast. Press `A` to switch the whole alert system off or on at any
time (the banner shows `⚲ alerts off` while disabled); tune or permanently
disable the triggers in `config.toml`.

## Status colours

| Status      | Meaning (last sample)         |
| ----------- | ----------------------------- |
| `EXCELLENT` | < 40 ms                       |
| `GOOD`      | < 90 ms                       |
| `FAIR`      | < 180 ms                      |
| `POOR`      | < 350 ms / ≥ 350 ms           |
| `UNSTABLE`  | loss ≥ 20% or a recent drop   |
| `DOWN`      | 3+ consecutive failures       |
| `PENDING`   | no samples yet                |

## Configuration

On first run a `config.toml` is created at `~/.config/pingmon/config.toml` (or
wherever `$PINGMON_CONFIG` / `--config` points). The location does **not** depend
on your current directory, so targets you add in-app are always reloaded from the
same file no matter where you launch `pingmon`. It is plain TOML and meant to be
hand-edited:

```toml
interval = 2.0        # poll period per target, seconds
timeout  = 2.0        # TCP connect timeout, seconds
history  = 90         # samples kept in memory for the graph

alert_latency = 300.0 # alert if latency stays above this (ms); 0 disables
alert_loss    = 20.0  # alert if recent loss exceeds this (%); 0 disables
alert_window  = 3     # consecutive bad samples before an alert fires
desktop_notify = true # also raise an OS desktop notification on alert

[[targets]]
country = "Netherlands"
flag = "🇳🇱"
host = "speedtest.ams1.nl.leaseweb.net"
port = 80
source = "builtin"   # "builtin" (shipped) or "user" (added in-app) — drives the `f` filter

[[targets]]
country = "United States"
flag = "🇺🇸"
host = "speedtest.newark.linode.com"
port = 443
source = "builtin"
```

Add as many `[[targets]]` blocks as you like; any host or IP works, and the
port is per-target. `source` is optional — if omitted it is inferred (hosts in
the built-in set are `builtin`, everything else `user`). `flag` is optional too:
the UI shows the two-letter country code derived from it, so you can leave it out
and let GeoIP fill it in. When adding a target in-app, the country field accepts a
plain code like `PL`.

## How latency is measured

`pingmon` opens a TCP connection to `host:port` and times the round-trip of the
connection handshake (SYN → SYN/ACK). That is close to true network RTT and,
unlike ICMP, needs no elevated privileges and reflects whether the service port
is actually answering. Failed or timed-out connects count as packet loss.

## Project layout

```
pingmon/
  app.py      # Textual app: table, detail panel, graphs, advisor, alerts, actions
  pinger.py   # async TCP ping + DNS resolve
  stats.py    # rolling per-target stats (latency, jitter, loss, MOS, status)
  scoring.py  # Region Advisor: composite score + use-case profiles
  netutil.py  # async traceroute + OS desktop notifications
  render.py   # colours, status meta, text sparklines
  config.py   # TOML load/save + built-in per-country target set
  app.tcss    # dark theme / layout
```
