Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
84 changes: 79 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

> **Ionity (Pty) Ltd** — [www.ionity.today](https://www.ionity.today)

A cross-platform, Python-based network monitoring tool with a full **GUI** and a rich **CLI**.
Monitor pings, DNS, active ports, interface traffic and Pi-hole statistics — all from one place.
A cross-platform, Python-based network monitoring tool with a full **GUI**, a rich **CLI**,
and an installable **mobile app (PWA)**. Monitor pings, DNS, active ports, interface traffic
and Pi-hole statistics — plus the *full network stack* from physical interfaces all the way up
to **cellular (2G–5G)**, **Wi-Fi**, **satellite internet (Starlink)** and **GNSS / satellite
positioning** — all from one place, including from your phone.

---

Expand All @@ -21,6 +24,25 @@ Monitor pings, DNS, active ports, interface traffic and Pi-hole statistics — a
| Pi-hole status / summary / top blocked | ✓ | ✓ |
| Live continuous monitoring | ✓ | ✓ |

### 📱 Mobile app — extended network stack

A separate **mobile PWA** (served by the built-in API server) adds full-stack
visibility you can open on any phone:

| Layer | Source | Notes |
|---|---|---|
| Host / interfaces / IP / traffic | psutil | Same data as desktop |
| Internet connectivity & quality | ping | Live dashboard |
| **Cellular (2G/3G/4G/5G)** | ModemManager (`mmcli`) | Operator, generation, signal |
| **Wi-Fi link** | `nmcli` / `iw` | SSID, signal, band, rate |
| **Satellite internet (Starlink)** | Dish gRPC @ `192.168.100.1` | Latency, throughput, obstruction |
| **GNSS / satellite positioning** | `gpsd` | Fix, lat/lon, satellites in view (GPS/Galileo/GLONASS/BeiDou) |
| Ping / DNS / port tools | core engine | Interactive, on-device |

Each extended layer degrades gracefully: when the hardware or helper tool
isn't present, the app shows a clear "unavailable" state with the reason
instead of failing.

---

## 🚀 Quick Start
Expand Down Expand Up @@ -65,6 +87,49 @@ run_gui.bat # Windows
run_cli.bat --help # Windows
```

**📱 Mobile app (PWA):**
```bash
./run_mobile.sh # Unix
run_mobile.bat # Windows
```
Then, on your phone (connected to the same network), open the printed URL
(e.g. `http://192.168.1.50:8088`) in your browser and choose **"Add to Home
Screen"** to install it as an app. The server binds to `0.0.0.0:8088` by
default; override with `NZM_HOST` / `NZM_PORT` or the `[Mobile]` section of
`config.ini`.

---

## 📡 Mobile REST API

The mobile app is backed by a small Flask API (`api_server.py`). The same
endpoints can be consumed by any client:

| Endpoint | Description |
|---|---|
| `GET /api/overview` | Everything for the dashboard in one call |
| `GET /api/network` | Hostname, IPs, interfaces |
| `GET /api/connectivity` | Internet reachability & quality |
| `GET /api/traffic` | Per-interface traffic counters |
| `GET /api/ping?host=&count=` | Ping a host |
| `GET /api/dns?domain=&server=` | DNS lookup |
| `GET /api/ports?host=&ports=` | Port scan |
| `GET /api/pihole/<action>` | Pi-hole status / summary / blocked |
| `GET /api/extended/cellular` | Mobile broadband (2G–5G) |
| `GET /api/extended/wifi` | Wi-Fi link details |
| `GET /api/extended/satellite` | Satellite internet (Starlink) |
| `GET /api/extended/gnss` | GNSS / satellite positioning |
| `GET /api/extended/all` | Consolidated multi-layer snapshot |

### Optional helper tools (for full extended data)

These are auto-detected; install only the layers you need:

- **Cellular:** `ModemManager` (provides `mmcli`)
- **Wi-Fi:** `network-manager` (provides `nmcli`)
- **Satellite internet:** `grpcurl` (for live Starlink telemetry)
- **GNSS:** `gpsd` + a connected GNSS/GPS receiver

---

## 📟 CLI Reference
Expand Down Expand Up @@ -181,15 +246,24 @@ python test_networkzero.py --demo
```
NetworkzeroMonitor/
├── network_monitor.py # Core monitoring engine
├── network_extended.py # Extended layers: cellular, wifi, satellite, GNSS
├── networkzero_cli.py # Command-line interface
├── networkzero_gui.py # Graphical user interface (tkinter)
├── api_server.py # Mobile REST API + PWA server (Flask)
├── mobile/ # Installable mobile PWA front-end
│ ├── index.html
│ ├── styles.css
│ ├── app.js
│ ├── manifest.webmanifest
│ ├── service-worker.js
│ └── icon.svg
├── test_networkzero.py # Unit + integration tests
├── config.ini # Application configuration
├── requirements.txt # Python dependencies
├── setup.sh # Unix setup script
├── setup.bat # Windows setup script
├── setup.sh / setup.bat # Setup scripts
├── run_cli.sh / run_cli.bat
└── run_gui.sh / run_gui.bat
├── run_gui.sh / run_gui.bat
└── run_mobile.sh / run_mobile.bat
```

---
Expand Down
199 changes: 199 additions & 0 deletions api_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
"""
NetworkzeroMonitor - Mobile API Server
Ionity (Pty) Ltd - www.ionity.today

A lightweight Flask REST API that exposes the full NetworkzeroMonitor
backend over HTTP and serves the installable mobile PWA. Point any
phone's browser at this server (http://<host>:8088) to monitor the
network from a handset, or "Add to Home Screen" to install it as an app.

Endpoints (all return JSON unless noted):
GET / -> mobile PWA (HTML)
GET /api/health -> server liveness + version
GET /api/network -> hostname, IPs, interfaces
GET /api/connectivity -> internet reachability + quality
GET /api/traffic -> per-interface traffic counters
GET /api/ping?host=&count= -> ping a host
GET /api/dns?domain=&server= -> DNS lookup
GET /api/ports?host=&ports= -> port scan
GET /api/pihole/<action> -> pihole status|summary|blocked (query: url, api_key)
GET /api/extended/cellular -> mobile broadband (2G-5G)
GET /api/extended/wifi -> Wi-Fi link details
GET /api/extended/satellite -> satellite internet (Starlink)
GET /api/extended/gnss -> GNSS / satellite positioning
GET /api/extended/all -> consolidated multi-layer snapshot
GET /api/overview -> everything in one call (mobile dashboard)
"""

import os
import configparser

from flask import Flask, jsonify, request, send_from_directory

from network_monitor import NetworkMonitor, PiHoleMonitor
from network_extended import ExtendedNetworkMonitor

APP_VERSION = '1.1.0'
MOBILE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'mobile')

app = Flask(__name__, static_folder=None)

_monitor = NetworkMonitor()
_extended = ExtendedNetworkMonitor()

# Load config for sensible server + pihole defaults.
_config = configparser.ConfigParser()
_config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.ini')
if os.path.exists(_config_path):
_config.read(_config_path)


def _cfg(section: str, option: str, fallback=None):
if _config.has_option(section, option):
return _config.get(section, option)
return fallback


# --------------------------------------------------------------------------- #
# Static / PWA routes
# --------------------------------------------------------------------------- #
@app.route('/')
def index():
return send_from_directory(MOBILE_DIR, 'index.html')


@app.route('/<path:filename>')
def static_files(filename):
# Serve the PWA assets (manifest, service worker, icons, css, js).
return send_from_directory(MOBILE_DIR, filename)


# --------------------------------------------------------------------------- #
# Core API
# --------------------------------------------------------------------------- #
@app.route('/api/health')
def health():
return jsonify({
'status': 'ok',
'app': 'NetworkzeroMonitor',
'version': APP_VERSION,
'company': 'Ionity (Pty) Ltd',
'website': 'www.ionity.today',
})


@app.route('/api/network')
def network():
return jsonify(_monitor.get_network_info())


@app.route('/api/connectivity')
def connectivity():
return jsonify(_monitor.check_internet_connectivity())


@app.route('/api/traffic')
def traffic():
return jsonify({'interfaces': _monitor.get_interface_traffic()})


@app.route('/api/ping')
def ping():
host = request.args.get('host', _cfg('Network', 'default_ping_host', '8.8.8.8'))
count = request.args.get('count', default=4, type=int)
timeout = request.args.get('timeout', default=2, type=int)
return jsonify(_monitor.ping_host(host, count=count, timeout=timeout))


@app.route('/api/dns')
def dns():
domain = request.args.get('domain')
if not domain:
return jsonify({'success': False, 'error': 'domain query parameter required'}), 400
server = request.args.get('server')
return jsonify(_monitor.check_dns_resolution(domain, server))


@app.route('/api/ports')
def ports():
host = request.args.get('host', '127.0.0.1')
ports_arg = request.args.get('ports')
port_list = None
if ports_arg:
try:
port_list = [int(p) for p in ports_arg.replace(',', ' ').split()]
except ValueError:
return jsonify({'error': 'ports must be integers'}), 400
return jsonify(_monitor.get_open_ports(host=host, ports=port_list))


@app.route('/api/pihole/<action>')
def pihole(action):
if action not in ('status', 'summary', 'blocked'):
return jsonify({'success': False, 'error': 'unknown action'}), 404
url = request.args.get('url', _cfg('PiHole', 'url'))
if not url:
return jsonify({'success': False, 'error': 'pihole url not configured'}), 400
api_key = request.args.get('api_key', _cfg('PiHole', 'api_key'))
pi = PiHoleMonitor(url, api_key)
if action == 'status':
return jsonify(pi.check_status())
if action == 'summary':
return jsonify(pi.get_summary())
count = request.args.get('count', default=10, type=int)
return jsonify(pi.get_top_blocked(count=count))


# --------------------------------------------------------------------------- #
# Extended network layers (cellular, wifi, satellite internet, GNSS)
# --------------------------------------------------------------------------- #
@app.route('/api/extended/cellular')
def ext_cellular():
return jsonify(_extended.get_cellular_info())


@app.route('/api/extended/wifi')
def ext_wifi():
return jsonify(_extended.get_wifi_info())


@app.route('/api/extended/satellite')
def ext_satellite():
return jsonify(_extended.get_satellite_internet())


@app.route('/api/extended/gnss')
def ext_gnss():
return jsonify(_extended.get_gnss_info())


@app.route('/api/extended/all')
def ext_all():
return jsonify(_extended.get_all_layers())


# --------------------------------------------------------------------------- #
# Aggregated dashboard payload (single round-trip for the mobile home screen)
# --------------------------------------------------------------------------- #
@app.route('/api/overview')
def overview():
return jsonify({
'network': _monitor.get_network_info(),
'connectivity': _monitor.check_internet_connectivity(),
'extended': _extended.get_all_layers(),
'version': APP_VERSION,
})


def main():
host = os.environ.get('NZM_HOST', '0.0.0.0')
port = int(os.environ.get('NZM_PORT', '8088'))
print("NetworkzeroMonitor - Mobile API Server")
print("Ionity (Pty) Ltd - www.ionity.today")
print(f"Serving on http://{host}:{port} (open this on your phone)")
app.run(host=host, port=port, threaded=True)


if __name__ == '__main__':
main()
6 changes: 6 additions & 0 deletions config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ ping_timeout = 2
# url = http://192.168.1.1
# api_key = your_api_key_here

[Mobile]
# Mobile API server bind address and port.
# Use 0.0.0.0 so phones on the same network can reach it.
host = 0.0.0.0
port = 8088

[UI]
# GUI window size
window_width = 1024
Expand Down
Loading