netdoctor 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ venv/
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+ __pycache__/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .git/
10
+ .github/
11
+ .DS_Store
@@ -0,0 +1,28 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.9", "3.11", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - name: Set up Python ${{ matrix.python-version }}
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+ - name: Install
22
+ run: pip install -e ".[dev]"
23
+ - name: Unit tests
24
+ run: pytest -q
25
+ - name: CLI smoke test
26
+ run: |
27
+ netdoctor --version
28
+ netdoctor --help
@@ -0,0 +1,90 @@
1
+ name: Release
2
+
3
+ # Cut a release by pushing a tag: git tag v0.1.0 && git push --tags
4
+ on:
5
+ push:
6
+ tags: ["v*"]
7
+
8
+ jobs:
9
+ # 1) Publish to PyPI via Trusted Publishing (OIDC) — no tokens/secrets needed.
10
+ # One-time: add this repo as a Trusted Publisher on pypi.org (see RELEASING.md).
11
+ pypi:
12
+ name: Publish to PyPI
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ id-token: write
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.12"
21
+ - run: pip install build
22
+ - run: python -m build
23
+ - uses: pypa/gh-action-pypi-publish@release/v1
24
+
25
+ # 2) Build a standalone single-file binary for each OS and attach to the Release.
26
+ binaries:
27
+ name: Binary (${{ matrix.asset }})
28
+ permissions:
29
+ contents: write
30
+ strategy:
31
+ fail-fast: false
32
+ matrix:
33
+ include:
34
+ - os: ubuntu-latest
35
+ asset: netdoctor-linux-x86_64
36
+ - os: macos-latest
37
+ asset: netdoctor-macos-arm64
38
+ - os: macos-13
39
+ asset: netdoctor-macos-x86_64
40
+ - os: windows-latest
41
+ asset: netdoctor-windows-x86_64.exe
42
+ runs-on: ${{ matrix.os }}
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+ - uses: actions/setup-python@v5
46
+ with:
47
+ python-version: "3.12"
48
+ - run: pip install pyinstaller .
49
+ - name: Build binary
50
+ run: pyinstaller --onefile --name netdoctor --collect-submodules rich packaging/entry.py
51
+ - name: Stage asset
52
+ shell: bash
53
+ run: |
54
+ mkdir -p out
55
+ if [ "${{ runner.os }}" = "Windows" ]; then
56
+ cp dist/netdoctor.exe "out/${{ matrix.asset }}"
57
+ else
58
+ cp dist/netdoctor "out/${{ matrix.asset }}"
59
+ fi
60
+ - uses: softprops/action-gh-release@v2
61
+ with:
62
+ files: out/${{ matrix.asset }}
63
+
64
+ # 3) Build and push the container image to GitHub Container Registry.
65
+ docker:
66
+ name: Docker image (GHCR)
67
+ runs-on: ubuntu-latest
68
+ permissions:
69
+ contents: read
70
+ packages: write
71
+ steps:
72
+ - uses: actions/checkout@v4
73
+ - uses: docker/login-action@v3
74
+ with:
75
+ registry: ghcr.io
76
+ username: ${{ github.actor }}
77
+ password: ${{ secrets.GITHUB_TOKEN }}
78
+ - id: meta
79
+ uses: docker/metadata-action@v5
80
+ with:
81
+ images: ghcr.io/sahilll15/netdoctor
82
+ tags: |
83
+ type=semver,pattern={{version}}
84
+ type=raw,value=latest
85
+ - uses: docker/build-push-action@v6
86
+ with:
87
+ context: .
88
+ push: true
89
+ tags: ${{ steps.meta.outputs.tags }}
90
+ labels: ${{ steps.meta.outputs.labels }}
@@ -0,0 +1,22 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+
9
+ # Virtual envs
10
+ .venv/
11
+ venv/
12
+ env/
13
+
14
+ # Tooling
15
+ .pytest_cache/
16
+ .ruff_cache/
17
+ .mypy_cache/
18
+
19
+ # OS / editor
20
+ .DS_Store
21
+ .idea/
22
+ .vscode/
@@ -0,0 +1,22 @@
1
+ # ---- build the wheel ----
2
+ FROM python:3.12-slim AS build
3
+ WORKDIR /app
4
+ COPY . .
5
+ RUN pip install --no-cache-dir build && python -m build --wheel
6
+
7
+ # ---- runtime ----
8
+ FROM python:3.12-slim
9
+ LABEL org.opencontainers.image.source="https://github.com/Sahilll15/netdoctor"
10
+ LABEL org.opencontainers.image.description="A network health-check CLI that walks the DevOps debugging ladder."
11
+ LABEL org.opencontainers.image.licenses="MIT"
12
+
13
+ # ping + traceroute power two of the rungs (need NET_RAW at run time to actually send)
14
+ RUN apt-get update \
15
+ && apt-get install -y --no-install-recommends iputils-ping traceroute \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ COPY --from=build /app/dist/*.whl /tmp/
19
+ RUN pip install --no-cache-dir /tmp/*.whl && rm -f /tmp/*.whl
20
+
21
+ ENTRYPOINT ["netdoctor"]
22
+ CMD ["--help"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sahil Chalke (github.com/Sahilll15)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,371 @@
1
+ Metadata-Version: 2.4
2
+ Name: netdoctor
3
+ Version: 0.1.0
4
+ Summary: A network health-check CLI that walks the DevOps debugging ladder and tells you which rung broke.
5
+ Project-URL: Homepage, https://github.com/Sahilll15/netdoctor
6
+ Project-URL: Repository, https://github.com/Sahilll15/netdoctor
7
+ Project-URL: Issues, https://github.com/Sahilll15/netdoctor/issues
8
+ Author: Sahil Chalke
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: cli,devops,diagnostics,dns,http,networking,tcp,tls
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: System :: Networking :: Monitoring
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: rich>=13.0
22
+ Provides-Extra: build
23
+ Requires-Dist: build; extra == 'build'
24
+ Requires-Dist: pyinstaller>=6.0; extra == 'build'
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # netdoctor
30
+
31
+ > Point it at a host. It walks the network debugging ladder rung by rung and tells you **exactly which one broke** — and what to do about it.
32
+
33
+ <p align="center">
34
+ <img src="assets/hero.gif" alt="netdoctor walking the full network ladder against github.com: an animated ECG header, the signal-path rail filling in rung by rung, a plain-English diagnosis, a quality scorecard, and a hop-by-hop traceroute" width="760">
35
+ </p>
36
+
37
+ `netdoctor` automates the sequence every engineer runs by hand when "the service is down": resolve the name, ping it, check the port, hit the app. Instead of five commands and five mental models, you get one live report, a plain-English diagnosis, and a **quality scorecard** that grades the host on reachability, performance, and latency.
38
+
39
+ ```
40
+ netdoctor github.com
41
+ ```
42
+
43
+ ```text
44
+ ╭──────────────────────────────────────────────────────────────────────────╮
45
+ │ ◉ netdoctor network vitals monitor api.example.com:443 │
46
+ │ ⎯⎯⎯⎯⎯╱╲⎯⎯⎯⎯⎯⎯⎯⎯⎯╱╲⎯⎯⎯⎯⎯⎯⎯⎯⎯╱╲⎯⎯⎯⎯⎯⎯⎯ 2026-06-28 12:54:32 │
47
+ ╰──────────────────────────────────────────────────────────────────────────╯
48
+ ╭─ signal path · resolve → connect → respond ──────────────────────────────╮
49
+ │ │
50
+ │ ● 1 ✓ DNS resolution L7 api.example.com → 93.184.216.34 █······ 12 ms │
51
+ │ │ │
52
+ │ ● 2 ⚠ Ping (ICMP) L3 no reply (ICMP likely blocked) — │
53
+ │ │ │
54
+ │ ● 3 ✓ TCP port 443 L4 open █······ 41 ms │
55
+ │ │ │
56
+ │ ● 4 ✓ HTTPS / L7 200 OK · TLSv1.3 · cert 67d left ███████ 136 ms │
57
+ │ │
58
+ ╰────────────────────────────────────────────────────────────────────────────╯
59
+ ╭─ diagnosis ────────────────────────────────────────────────────────────────╮
60
+ │ │
61
+ │ ● HEALTHY api.example.com is reachable and responding. │
62
+ │ │
63
+ │ vitals ▰ ▰ ▰ ▰ 4 ok · 189 ms total │
64
+ │ │
65
+ │ → No ICMP reply — many hosts and firewalls block ping, so this alone does │
66
+ │ NOT mean the host is down. The TCP port and HTTP checks are authoritative.│
67
+ │ │
68
+ ╰────────────────────────────────────────────────────────────────────────────╯
69
+ ```
70
+
71
+ > The header is a **live ECG line** that scrolls while checks run and freezes in
72
+ > the verdict colour when done. The **signal-path rail** fills in rung by rung as
73
+ > the packet descends the stack (each step tagged with its OSI layer). And note
74
+ > the diagnosis: the host **ignores ping** (normal for production hosts), yet it's
75
+ > still **healthy** — because the port and HTTP checks, the ones that actually
76
+ > matter, both passed. That judgement is the whole reason the tool exists.
77
+
78
+ ---
79
+
80
+ ## Why this exists
81
+
82
+ When a service is unreachable, the fix is almost always finding **which layer** is broken. The classic move is to walk a ladder, top to bottom, until something fails:
83
+
84
+ | Question | The manual command | netdoctor's rung |
85
+ |---|---|---|
86
+ | Does the name resolve to an IP? | `dig` / `nslookup` | **DNS resolution** |
87
+ | Is the host reachable at all? | `ping` | **Ping (ICMP)** — *advisory* |
88
+ | Is the specific port open? | `nc -zv host 443` | **TCP port** |
89
+ | Does the app actually respond? | `curl -v https://host` | **HTTP / TLS** |
90
+ | Where does the path break? | `traceroute` | **Traceroute** (`--trace`) |
91
+
92
+ netdoctor runs the whole ladder for you, stops reasoning about ICMP the way a human does (a blocked ping is **not** an outage), and points at the first rung that genuinely failed.
93
+
94
+ ---
95
+
96
+ ## Install
97
+
98
+ **pipx** (recommended — isolated, always on your PATH):
99
+
100
+ ```bash
101
+ pipx install netdoctor
102
+ ```
103
+
104
+ …or with the modern, faster installer: `uv tool install netdoctor` — or plain `pip install netdoctor`.
105
+
106
+ **Homebrew** (macOS / Linux):
107
+
108
+ ```bash
109
+ brew install Sahilll15/tap/netdoctor
110
+ ```
111
+
112
+ **Docker** (no local Python at all):
113
+
114
+ ```bash
115
+ docker run --rm ghcr.io/sahilll15/netdoctor github.com
116
+ # ping & traceroute need raw sockets — grant them when you want those rungs:
117
+ docker run --rm --cap-add=NET_RAW ghcr.io/sahilll15/netdoctor github.com
118
+ ```
119
+
120
+ **Standalone binary** — grab the file for your OS from the [latest release](https://github.com/Sahilll15/netdoctor/releases), `chmod +x`, and run it. No Python required.
121
+
122
+ **From source** (for hacking on it):
123
+
124
+ ```bash
125
+ git clone https://github.com/Sahilll15/netdoctor.git
126
+ cd netdoctor && python3 -m venv .venv && source .venv/bin/activate
127
+ pip install -e ".[dev]"
128
+ ```
129
+
130
+ > Pre-built packages (PyPI, Homebrew, Docker, binaries) ship automatically with each tagged release — see [RELEASING.md](RELEASING.md).
131
+
132
+ ---
133
+
134
+ ## Usage
135
+
136
+ ```bash
137
+ netdoctor github.com # full picture: ladder + traceroute + scorecard
138
+ netdoctor github.com --quick # fast: skip traceroute & extra probes
139
+ netdoctor db.internal:5432 # is the Postgres port reachable?
140
+ netdoctor github.com 1.1.1.1 db:5432 # a whole fleet at once (concurrent)
141
+ netdoctor github.com --watch # live vitals dashboard (ctrl-c to stop)
142
+ netdoctor github.com --mtr # live mtr-style path monitor
143
+ netdoctor github.com --json # machine-readable output for CI / cron
144
+ ```
145
+
146
+ The target accepts a **bare host**, **host:port**, or a **full URL** — the scheme, port, and path are parsed out automatically. Explicit flags (`--port`, `--scheme`, `--path`) always win. Pass **several targets** to check them concurrently as a fleet.
147
+
148
+ > **Full by default.** A single-host run does the whole picture — the ladder, a traceroute, and a latency scorecard — with no flags to remember. Use `--quick` (or `--no-trace`) when you just want a fast up/down answer.
149
+
150
+ ### Traceroute (default) & `--mtr`
151
+
152
+ Every single-host run includes a **traceroute** that shows the path hop by hop, with per-hop latency, reverse-DNS names, and a marker for **exactly where the path goes dark**:
153
+
154
+ ```text
155
+ ╭─ route → 140.82.112.3 ───────────────────────────────────────────────╮
156
+ │ ● 1 router.local 192.168.1.1 1 ms │
157
+ │ ● 2 isp-gateway.net 10.20.0.1 6 ms │
158
+ │ ○ 3 * * * — │
159
+ │ ● 4 140.82.112.3 140.82.112.3 11 ms ◀ destination │
160
+ │ │
161
+ │ ✓ destination reached │
162
+ ╰───────────────────────────────────────────────────────────────────────╯
163
+ ```
164
+
165
+ `--mtr` turns that into a live, **mtr-style** monitor that re-probes on an interval and tracks rolling **loss %**, last/avg/best/worst latency, and a sparkline **per hop**:
166
+
167
+ <p align="center">
168
+ <img src="assets/mtr.gif" alt="netdoctor --mtr live path monitor: per-hop loss %, last/avg/best/worst latency, sparklines, and the point where the route to github.com goes dark" width="820">
169
+ </p>
170
+
171
+ ```text
172
+ ╭─────┬─────────────────┬──────┬──────┬─────┬──────┬───────┬────────────╮
173
+ │ HOP │ HOST │ LOSS │ LAST │ AVG │ BEST │ WORST │ TREND │
174
+ ├─────┼─────────────────┼──────┼──────┼─────┼──────┼───────┼────────────┤
175
+ │ 1 │ router.local │ 0% │ 1 │ 1 │ 1 │ 2 │ ▁▂▁▁▂▁ │
176
+ │ 2 │ isp-gateway.net │ 0% │ 6 │ 7 │ 5 │ 12 │ ▂▃▂▅▂▃ │
177
+ │ 3 │ * * * │ 100% │ — │ — │ — │ — │ ×××××× │
178
+ ╰─────┴─────────────────┴──────┴──────┴─────┴──────┴───────┴────────────╯
179
+ ⟳ next probe in 3s · run #6 · ctrl-c to stop
180
+ ```
181
+
182
+ ### Live monitor (`--watch`)
183
+
184
+ ```bash
185
+ netdoctor github.com api.internal:8080 --watch --interval 10
186
+ ```
187
+
188
+ Turns netdoctor into a persistent dashboard that re-checks on an interval and keeps a **latency-trend sparkline** and a rolling **uptime %** per host:
189
+
190
+ <p align="center">
191
+ <img src="assets/watch.gif" alt="netdoctor --watch live dashboard monitoring github.com and 1.1.1.1: per-host DNS/port/HTTP, latency-trend sparkline, grade, uptime %, and status" width="880">
192
+ </p>
193
+
194
+ ```text
195
+ ╭────┬──────────────────┬────────┬─────────┬──────────┬───────────────┬──────┬───┬──────────╮
196
+ │ │ HOST │ DNS │ PORT │ HTTP │ LATENCY TREND │ UP │ # │ STATUS │
197
+ ├────┼──────────────────┼────────┼─────────┼──────────┼───────────────┼──────┼───┼──────────┤
198
+ │ ● │ github.com:443 │ ✓ 2ms │ ✓ 41ms │ ✓ 151ms │ ▁▂▂▁▃▂▁▂ │ 100% │ 8 │ HEALTHY │
199
+ │ ● │ api.internal:8080│ ✓ 1ms │ ✗ 0ms │ · skip │ ▅▆█▅▄▅██ │ 62% │ 8 │ UNHEALTHY│
200
+ ╰────┴──────────────────┴────────┴─────────┴──────────┴───────────────┴──────┴───┴──────────╯
201
+ ⟳ next refresh in 7s · every 10s · run #8 · ctrl-c to stop
202
+ ```
203
+
204
+ ### Scorecard — *how good, how fast, how reliable*
205
+
206
+ Every run ends in a **scorecard** that grades the host **A+ → F**. It probes the
207
+ app a few times by default (`--samples 5`) for a real latency distribution, jitter,
208
+ and an estimated request rate — bump `--samples` higher for a fuller picture:
209
+
210
+ ```bash
211
+ netdoctor api.example.com --samples 30
212
+ ```
213
+
214
+ <p align="center">
215
+ <img src="assets/scorecard.gif" alt="netdoctor scorecard from a 12-probe benchmark of github.com: letter grade, reachability/performance/stability gauges, latency min/avg/p95/max, jitter, throughput, and a per-sample distribution sparkline" width="620">
216
+ </p>
217
+
218
+ ```text
219
+ ╭─ scorecard ────────────────────────────────────────────────────────────────╮
220
+ │ │
221
+ │ A- 92 / 100 api.example.com:443 │
222
+ │ │
223
+ │ reachability ██████████████████████ 100 30/30 probes ok │
224
+ │ performance ██████████████████░░░░ 83 │
225
+ │ stability ███████████████████░░░ 88 ±50 ms jitter │
226
+ │ │
227
+ │ latency min 38 · avg 112 · p95 240 · max 410 ms │
228
+ │ rate 100% reachable · ≈ 8.9 req/s (single connection) │
229
+ │ samples ▄▃▄▁▆█▁▃▁▂▅▁▃▂▄▁▂▅▃▁▂▄▆▃▁▂▄▁▃▂ (30) │
230
+ │ │
231
+ ╰──────────────────────────────────────────────────────────────────────────────╯
232
+ ```
233
+
234
+ | Metric | What it measures |
235
+ |---|---|
236
+ | **Reachability** | % of probes that succeeded |
237
+ | **Performance** | 0–100 score derived from average latency |
238
+ | **Stability** | consistency — high when jitter is low (needs `--samples ≥ 2`) |
239
+ | **Latency** | min / avg / p95 / max, plus jitter (std-dev) |
240
+ | **Rate** | success rate + estimated throughput (`≈ req/s`, single connection) |
241
+ | **Grade** | a weighted A+→F summary of all of the above |
242
+
243
+ In fleet and `--watch` views, the grade shows as a compact **GRADE** column per host.
244
+
245
+ ### Options
246
+
247
+ | Flag | Purpose |
248
+ |---|---|
249
+ | `-p, --port` | TCP port to test (default: 443 for https, 80 for http) |
250
+ | `--scheme {http,https}` | Force the scheme for the app check |
251
+ | `--path` | HTTP path to request (default: `/`) |
252
+ | `-t, --timeout SECONDS` | Per-check timeout (default: 4.0) |
253
+ | `-w, --watch` | Live vitals dashboard, re-checking on an interval |
254
+ | `--mtr` | Live mtr-style path monitor (single host) |
255
+ | `-n, --interval SECONDS` | `--watch` / `--mtr` refresh interval (default: 5.0) |
256
+ | `--max-runs N` | With `--watch` / `--mtr`, stop after N refreshes (0 = until ctrl-c) |
257
+ | `-s, --samples N` | App probes for the latency scorecard (default: 5) |
258
+ | `--no-trace` | Skip the traceroute step (faster) |
259
+ | `--quick` | Fastest run: skip traceroute and extra probes |
260
+ | `--json` | Emit JSON instead of the live report |
261
+ | `--no-color` | Disable colour / styling |
262
+
263
+ ### Exit codes
264
+
265
+ `netdoctor` is built to drop into scripts, cron jobs, and CI:
266
+
267
+ | Code | Verdict | Meaning |
268
+ |---|---|---|
269
+ | `0` | healthy | all core checks passed |
270
+ | `1` | degraded | reachable, but a check returned a warning (e.g. HTTP 5xx, cert expiring) |
271
+ | `2` | unhealthy | a core check failed (the diagnosis names which rung) |
272
+ | `64` | — | usage error |
273
+
274
+ ```bash
275
+ netdoctor api.example.com/health || echo "page someone!"
276
+ ```
277
+
278
+ ### JSON output
279
+
280
+ ```bash
281
+ netdoctor github.com --json
282
+ ```
283
+
284
+ ```json
285
+ {
286
+ "target": "github.com",
287
+ "port": 443,
288
+ "scheme": "https",
289
+ "elapsed_ms": 191.4,
290
+ "diagnosis": { "verdict": "healthy", "broken_rung": null, "...": "..." },
291
+ "scorecard": {
292
+ "grade": "A-", "overall": 92.0, "reachability": 100.0, "performance": 83.0,
293
+ "stability": 88.0, "throughput_rps": 8.9,
294
+ "latency_ms": { "min": 38, "avg": 112, "p95": 240, "max": 410, "jitter": 50 }
295
+ },
296
+ "rungs": [
297
+ { "key": "dns", "status": "ok", "detail": "github.com → 140.82.112.3", "latency_ms": 11.4 },
298
+ { "key": "ping", "status": "warn", "detail": "no reply (ICMP likely blocked)", "core": false },
299
+ { "key": "port", "status": "ok", "detail": "open", "latency_ms": 41.0 },
300
+ { "key": "http", "status": "ok", "detail": "200 OK · TLSv1.3 · cert 67d left",
301
+ "data": { "tls": "TLSv1.3", "cert_days_left": 67,
302
+ "timing": { "connect_ms": 38.0, "ttfb_ms": 97.2 }, "status_code": 200 } }
303
+ ]
304
+ }
305
+ ```
306
+
307
+ Pass several targets and you get a JSON **array** instead — ideal for piping into `jq`, dashboards, or a cron alert.
308
+
309
+ ---
310
+
311
+ ## How it works
312
+
313
+ A clean separation of concerns — the whole point is that the logic is testable without touching the network:
314
+
315
+ ```
316
+ src/netdoctor/
317
+ ├── model.py # Rung / Status / Verdict + diagnose() — pure logic, no I/O
318
+ ├── checks.py # one function per rung; returns a completed Rung, never prints
319
+ ├── score.py # the grading model (reachability/performance/latency) — pure logic
320
+ ├── render.py # all the Rich presentation (ECG header, signal rail, dashboard)
321
+ └── cli.py # arg parsing, run loop, watch/fleet orchestration, JSON, exit codes
322
+ ```
323
+
324
+ The **diagnosis engine** (`diagnose()`) is deliberately I/O-free, so the decision rules are unit-tested in isolation:
325
+
326
+ - any **core** rung that fails → **unhealthy** (named by the first failure)
327
+ - else any core rung warning → **degraded**
328
+ - else → **healthy**
329
+ - advisory rungs (ping, traceroute) never change the verdict — a blocked ping just adds a note.
330
+
331
+ ## Tests
332
+
333
+ ```bash
334
+ pip install -e ".[dev]"
335
+ pytest
336
+ ```
337
+
338
+ Tests are deterministic and need no internet: the verdict engine is pure, target
339
+ parsing is table-driven, and the TCP check runs against a socket the test opens
340
+ and closes itself. CI runs them on Python 3.9 / 3.11 / 3.13 via GitHub Actions.
341
+
342
+ ---
343
+
344
+ ## Roadmap
345
+
346
+ - [x] `--watch` — live monitoring dashboard with latency-trend sparklines
347
+ - [x] Multiple targets in one run (concurrent fleet view)
348
+ - [x] HTTP timing breakdown (connect / TTFB)
349
+ - [x] Quality scorecard — reachability / performance / latency / grade (`--samples`)
350
+ - [x] Hop-by-hop traceroute with break detection + an `mtr`-style live monitor (`--mtr`)
351
+ - [ ] Read targets from a file / stdin
352
+ - [ ] Ship as a single-file binary (PyInstaller) and a container image
353
+
354
+ ---
355
+
356
+ ## Demos
357
+
358
+ The animated demos in this README are real `netdoctor` runs, recorded with
359
+ [vhs](https://github.com/charmbracelet/vhs). The scripts live in
360
+ [`assets/`](assets/) as `*.tape` files, so any demo is reproducible:
361
+
362
+ ```bash
363
+ brew install vhs
364
+ vhs assets/hero.tape # → assets/hero.gif
365
+ ```
366
+
367
+ ---
368
+
369
+ ## License
370
+
371
+ MIT © Sahil Chalke