network-check-tool 0.0.1__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,6 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ ...
Binary file
Binary file
@@ -0,0 +1,3 @@
1
+ """Network Checker - A network monitoring tool with ping tracking and dashboard visualization."""
2
+
3
+ __version__ = "0.0.1"
@@ -0,0 +1,28 @@
1
+ from flask import Flask
2
+ from flask_cors import CORS
3
+ from .models import db
4
+ from .config.config import config
5
+ from .views.dashboard import dashboard_bp
6
+ import os
7
+
8
+
9
+ def create_app(config_name=None):
10
+ """Application factory pattern"""
11
+ if config_name is None:
12
+ config_name = os.environ.get('FLASK_ENV', 'development')
13
+
14
+ app = Flask(__name__)
15
+ app.config.from_object(config[config_name])
16
+
17
+ # Initialize extensions
18
+ db.init_app(app)
19
+ CORS(app)
20
+
21
+ # Register blueprints
22
+ app.register_blueprint(dashboard_bp)
23
+
24
+ # Create database tables
25
+ with app.app_context():
26
+ db.create_all()
27
+
28
+ return app
@@ -0,0 +1,145 @@
1
+ import click
2
+ import os
3
+ from .app import create_app
4
+ from .services import SchedulerService
5
+ from .config.config import Config
6
+
7
+
8
+ @click.group()
9
+ def cli():
10
+ """Network Checker CLI - Monitor network hosts with ping"""
11
+ pass
12
+
13
+
14
+ @cli.command('run-server')
15
+ @click.option('--host', '-h', default='0.0.0.0', help='Host to bind to')
16
+ @click.option('--port', '-p', default=9991, help='Port to bind to')
17
+ @click.option('--debug', is_flag=True, help='Enable debug mode')
18
+ def run_server(host, port, debug):
19
+ """Run the Flask web application"""
20
+ app = create_app('development' if debug else 'production')
21
+ app.run(host=host, port=port, debug=debug)
22
+
23
+
24
+ @cli.command()
25
+ @click.argument('hosts', nargs=-1, required=True)
26
+ @click.option('--interval', '-i', default=60, help='Ping interval in seconds')
27
+ def start_monitoring(hosts, interval):
28
+ """Start monitoring specified hosts"""
29
+ app = create_app()
30
+
31
+ with app.app_context():
32
+ scheduler = SchedulerService()
33
+
34
+ try:
35
+ scheduler.start()
36
+ click.echo(f"Starting ping monitoring for: {', '.join(hosts)}")
37
+ click.echo(f"Ping interval: {interval} seconds")
38
+
39
+ for host in hosts:
40
+ scheduler.add_ping_job(host, interval)
41
+ click.echo(f"Added ping job for {host}")
42
+
43
+ click.echo("Monitoring started. Press Ctrl+C to stop.")
44
+
45
+ # Keep the process running
46
+ import time
47
+ while True:
48
+ time.sleep(1)
49
+
50
+ except KeyboardInterrupt:
51
+ click.echo("\nStopping monitoring...")
52
+ scheduler.shutdown()
53
+ click.echo("Monitoring stopped.")
54
+ except Exception as e:
55
+ click.echo(f"Error: {e}")
56
+ scheduler.shutdown()
57
+
58
+
59
+ @cli.command()
60
+ @click.argument('host')
61
+ @click.option('--count', '-c', default=4, help='Number of pings to send')
62
+ def ping(host, count):
63
+ """Send ping to a specific host"""
64
+ app = create_app()
65
+
66
+ with app.app_context():
67
+ from .services import PingService
68
+ ping_service = PingService()
69
+
70
+ click.echo(f"Pinging {host} {count} times...")
71
+
72
+ for i in range(count):
73
+ result = ping_service.ping_and_save(host)
74
+
75
+ if result.packet_loss:
76
+ click.echo(f"Ping {i+1}: Request timeout ({result.error_message})")
77
+ else:
78
+ click.echo(f"Ping {i+1}: {result.response_time:.2f}ms")
79
+
80
+ if i < count - 1:
81
+ import time
82
+ time.sleep(1)
83
+
84
+
85
+ @cli.command()
86
+ @click.option('--host', help='Filter by host')
87
+ @click.option('--limit', '-l', default=10, help='Number of recent results to show')
88
+ def status(host, limit):
89
+ """Show recent ping status"""
90
+ app = create_app()
91
+
92
+ with app.app_context():
93
+ from .models import PingResult
94
+ from sqlalchemy import desc
95
+
96
+ query = PingResult.query
97
+
98
+ if host:
99
+ query = query.filter(PingResult.target_host == host)
100
+
101
+ results = query.order_by(desc(PingResult.timestamp)).limit(limit).all()
102
+
103
+ if not results:
104
+ click.echo("No ping results found.")
105
+ return
106
+
107
+ click.echo(f"Recent ping results{'for ' + host if host else ''}:")
108
+ click.echo("-" * 70)
109
+ click.echo(f"{'Host':<20} {'Time':<20} {'Response':<12} {'Status'}")
110
+ click.echo("-" * 70)
111
+
112
+ for result in results:
113
+ status_str = "FAIL" if result.packet_loss else "OK"
114
+ response_str = f"{result.response_time:.2f}ms" if result.response_time else "timeout"
115
+ time_str = result.timestamp.strftime("%Y-%m-%d %H:%M:%S")
116
+
117
+ click.echo(f"{result.target_host:<20} {time_str:<20} {response_str:<12} {status_str}")
118
+
119
+
120
+ @cli.command()
121
+ def init():
122
+ """Initialize the database"""
123
+ app = create_app()
124
+
125
+ with app.app_context():
126
+ from .models import db
127
+ db.create_all()
128
+ click.echo("Database initialized successfully.")
129
+
130
+
131
+ @cli.command()
132
+ @click.confirmation_option(prompt='Are you sure you want to clear all ping data?')
133
+ def clear():
134
+ """Clear all ping data from the database"""
135
+ app = create_app()
136
+
137
+ with app.app_context():
138
+ from .models import db, PingResult
139
+ db.session.query(PingResult).delete()
140
+ db.session.commit()
141
+ click.echo("All ping data cleared.")
142
+
143
+
144
+ if __name__ == '__main__':
145
+ cli()
@@ -0,0 +1,42 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ class Config:
5
+ SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
6
+
7
+ # Database configuration
8
+ SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///network_checker.db'
9
+ SQLALCHEMY_TRACK_MODIFICATIONS = False
10
+
11
+ # Ping configuration
12
+ PING_TIMEOUT = float(os.environ.get('PING_TIMEOUT', '4.0'))
13
+ DEFAULT_PING_INTERVAL = int(os.environ.get('DEFAULT_PING_INTERVAL', '60'))
14
+
15
+ # Default hosts to monitor
16
+ DEFAULT_HOSTS = os.environ.get('DEFAULT_HOSTS', 'google.com,cloudflare.com').split(',')
17
+
18
+ # Application directory
19
+ APP_DIR = Path(__file__).parent.parent
20
+ INSTANCE_PATH = APP_DIR.parent / 'instance'
21
+
22
+ @staticmethod
23
+ def init_app(app):
24
+ """Initialize application with configuration"""
25
+ # Ensure instance directory exists
26
+ Config.INSTANCE_PATH.mkdir(exist_ok=True)
27
+
28
+
29
+ class DevelopmentConfig(Config):
30
+ DEBUG = True
31
+ SQLALCHEMY_ECHO = True
32
+
33
+
34
+ class ProductionConfig(Config):
35
+ DEBUG = False
36
+
37
+
38
+ config = {
39
+ 'development': DevelopmentConfig,
40
+ 'production': ProductionConfig,
41
+ 'default': DevelopmentConfig
42
+ }
@@ -0,0 +1,3 @@
1
+ from .ping_result import db, PingResult
2
+
3
+ __all__ = ['db', 'PingResult']
@@ -0,0 +1,28 @@
1
+ from datetime import datetime
2
+ from flask_sqlalchemy import SQLAlchemy
3
+
4
+ db = SQLAlchemy()
5
+
6
+
7
+ class PingResult(db.Model):
8
+ __tablename__ = 'ping_results'
9
+
10
+ id = db.Column(db.Integer, primary_key=True)
11
+ target_host = db.Column(db.String(255), nullable=False, index=True)
12
+ timestamp = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, index=True)
13
+ response_time = db.Column(db.Float, nullable=True)
14
+ packet_loss = db.Column(db.Boolean, default=False, nullable=False)
15
+ error_message = db.Column(db.Text, nullable=True)
16
+
17
+ def __repr__(self):
18
+ return f'<PingResult {self.target_host} at {self.timestamp}>'
19
+
20
+ def to_dict(self):
21
+ return {
22
+ 'id': self.id,
23
+ 'target_host': self.target_host,
24
+ 'timestamp': self.timestamp.isoformat(),
25
+ 'response_time': self.response_time,
26
+ 'packet_loss': self.packet_loss,
27
+ 'error_message': self.error_message
28
+ }
@@ -0,0 +1,4 @@
1
+ from .ping_service import PingService
2
+ from .scheduler_service import SchedulerService
3
+
4
+ __all__ = ['PingService', 'SchedulerService']
@@ -0,0 +1,57 @@
1
+ import ping3
2
+ from datetime import datetime
3
+ from typing import Dict, Optional
4
+ from ..models import db, PingResult
5
+
6
+
7
+ class PingService:
8
+ def __init__(self, timeout: float = 4.0):
9
+ self.timeout = timeout
10
+ ping3.EXCEPTIONS = True
11
+
12
+ def ping_host(self, host: str) -> Dict:
13
+ try:
14
+ response_time = ping3.ping(host, timeout=self.timeout)
15
+
16
+ if response_time is None:
17
+ return {
18
+ 'host': host,
19
+ 'timestamp': datetime.utcnow(),
20
+ 'response_time': None,
21
+ 'packet_loss': True,
22
+ 'error_message': 'Request timeout'
23
+ }
24
+ else:
25
+ return {
26
+ 'host': host,
27
+ 'timestamp': datetime.utcnow(),
28
+ 'response_time': response_time * 1000, # Convert to milliseconds
29
+ 'packet_loss': False,
30
+ 'error_message': None
31
+ }
32
+
33
+ except Exception as e:
34
+ return {
35
+ 'host': host,
36
+ 'timestamp': datetime.utcnow(),
37
+ 'response_time': None,
38
+ 'packet_loss': True,
39
+ 'error_message': str(e)
40
+ }
41
+
42
+ def save_ping_result(self, result: Dict) -> PingResult:
43
+ ping_result = PingResult(
44
+ target_host=result['host'],
45
+ timestamp=result['timestamp'],
46
+ response_time=result['response_time'],
47
+ packet_loss=result['packet_loss'],
48
+ error_message=result['error_message']
49
+ )
50
+
51
+ db.session.add(ping_result)
52
+ db.session.commit()
53
+ return ping_result
54
+
55
+ def ping_and_save(self, host: str) -> PingResult:
56
+ result = self.ping_host(host)
57
+ return self.save_ping_result(result)
@@ -0,0 +1,94 @@
1
+ from apscheduler.schedulers.background import BackgroundScheduler
2
+ from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
3
+ from apscheduler.executors.pool import ThreadPoolExecutor
4
+ from typing import List, Optional
5
+ import logging
6
+ from .ping_service import PingService
7
+
8
+
9
+ class SchedulerService:
10
+ def __init__(self, database_url: str = 'sqlite:///network_checker.db'):
11
+ jobstores = {
12
+ 'default': SQLAlchemyJobStore(url=database_url)
13
+ }
14
+
15
+ executors = {
16
+ 'default': ThreadPoolExecutor(20)
17
+ }
18
+
19
+ job_defaults = {
20
+ 'coalesce': False,
21
+ 'max_instances': 3
22
+ }
23
+
24
+ self.scheduler = BackgroundScheduler(
25
+ jobstores=jobstores,
26
+ executors=executors,
27
+ job_defaults=job_defaults
28
+ )
29
+
30
+ self.ping_service = PingService()
31
+ self.is_running = False
32
+
33
+ # Configure logging
34
+ logging.basicConfig(level=logging.INFO)
35
+ self.logger = logging.getLogger(__name__)
36
+
37
+ def start(self):
38
+ if not self.is_running:
39
+ self.scheduler.start()
40
+ self.is_running = True
41
+ self.logger.info("Scheduler started")
42
+
43
+ def shutdown(self):
44
+ if self.is_running:
45
+ self.scheduler.shutdown()
46
+ self.is_running = False
47
+ self.logger.info("Scheduler stopped")
48
+
49
+ def add_ping_job(self, host: str, interval: int = 60, job_id: Optional[str] = None):
50
+ if job_id is None:
51
+ job_id = f"ping_{host}"
52
+
53
+ self.scheduler.add_job(
54
+ func=self._ping_job,
55
+ trigger='interval',
56
+ seconds=interval,
57
+ id=job_id,
58
+ args=[host],
59
+ replace_existing=True
60
+ )
61
+
62
+ self.logger.info(f"Added ping job for {host} with {interval}s interval")
63
+
64
+ def remove_ping_job(self, job_id: str):
65
+ try:
66
+ self.scheduler.remove_job(job_id)
67
+ self.logger.info(f"Removed ping job: {job_id}")
68
+ except Exception as e:
69
+ self.logger.error(f"Failed to remove job {job_id}: {e}")
70
+
71
+ def add_multiple_ping_jobs(self, hosts: List[str], interval: int = 60):
72
+ for host in hosts:
73
+ self.add_ping_job(host, interval)
74
+
75
+ def list_jobs(self):
76
+ return self.scheduler.get_jobs()
77
+
78
+ def _ping_job(self, host: str):
79
+ try:
80
+ result = self.ping_service.ping_and_save(host)
81
+ self.logger.debug(f"Ping result for {host}: {result.response_time}ms, packet_loss: {result.packet_loss}")
82
+ except Exception as e:
83
+ self.logger.error(f"Error pinging {host}: {e}")
84
+
85
+ def get_job_status(self, job_id: str):
86
+ job = self.scheduler.get_job(job_id)
87
+ if job:
88
+ return {
89
+ 'id': job.id,
90
+ 'name': str(job.func),
91
+ 'next_run_time': job.next_run_time.isoformat() if job.next_run_time else None,
92
+ 'trigger': str(job.trigger)
93
+ }
94
+ return None
@@ -0,0 +1,276 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Network Checker Dashboard</title>
7
+ <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
8
+ <style>
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11
+ margin: 0;
12
+ padding: 20px;
13
+ background-color: #f5f5f5;
14
+ }
15
+
16
+ .container {
17
+ max-width: 1200px;
18
+ margin: 0 auto;
19
+ }
20
+
21
+ .header {
22
+ background: white;
23
+ padding: 20px;
24
+ border-radius: 8px;
25
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
26
+ margin-bottom: 20px;
27
+ }
28
+
29
+ .controls {
30
+ display: flex;
31
+ gap: 15px;
32
+ margin-bottom: 20px;
33
+ align-items: center;
34
+ }
35
+
36
+ select, button {
37
+ padding: 8px 12px;
38
+ border: 1px solid #ddd;
39
+ border-radius: 4px;
40
+ font-size: 14px;
41
+ }
42
+
43
+ button {
44
+ background-color: #007bff;
45
+ color: white;
46
+ border: none;
47
+ cursor: pointer;
48
+ }
49
+
50
+ button:hover {
51
+ background-color: #0056b3;
52
+ }
53
+
54
+ .stats-grid {
55
+ display: grid;
56
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
57
+ gap: 15px;
58
+ margin-bottom: 20px;
59
+ }
60
+
61
+ .stat-card {
62
+ background: white;
63
+ padding: 20px;
64
+ border-radius: 8px;
65
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
66
+ text-align: center;
67
+ }
68
+
69
+ .stat-value {
70
+ font-size: 2em;
71
+ font-weight: bold;
72
+ color: #007bff;
73
+ }
74
+
75
+ .stat-label {
76
+ color: #666;
77
+ margin-top: 5px;
78
+ }
79
+
80
+ .chart-container {
81
+ background: white;
82
+ padding: 20px;
83
+ border-radius: 8px;
84
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
85
+ margin-bottom: 20px;
86
+ }
87
+
88
+ .loading {
89
+ text-align: center;
90
+ padding: 50px;
91
+ color: #666;
92
+ }
93
+
94
+ .error {
95
+ background-color: #f8d7da;
96
+ color: #721c24;
97
+ padding: 15px;
98
+ border-radius: 4px;
99
+ margin-bottom: 20px;
100
+ }
101
+
102
+ .success {
103
+ background-color: #d4edda;
104
+ color: #155724;
105
+ padding: 15px;
106
+ border-radius: 4px;
107
+ margin-bottom: 20px;
108
+ }
109
+ </style>
110
+ </head>
111
+ <body>
112
+ <div class="container">
113
+ <div class="header">
114
+ <h1>Network Checker Dashboard</h1>
115
+ <p>Monitor ping responses and packet loss for your network hosts</p>
116
+ </div>
117
+
118
+ <div class="controls">
119
+ <label for="hostSelect">Host:</label>
120
+ <select id="hostSelect">
121
+ <option value="">Select a host...</option>
122
+ </select>
123
+
124
+ <label for="timeRange">Time Range:</label>
125
+ <select id="timeRange">
126
+ <option value="1">Last 1 hour</option>
127
+ <option value="6">Last 6 hours</option>
128
+ <option value="24" selected>Last 24 hours</option>
129
+ <option value="72">Last 3 days</option>
130
+ <option value="168">Last week</option>
131
+ </select>
132
+
133
+ <button onclick="refreshData()">Refresh</button>
134
+ </div>
135
+
136
+ <div id="statsContainer" class="stats-grid" style="display: none;">
137
+ <div class="stat-card">
138
+ <div class="stat-value" id="totalPings">-</div>
139
+ <div class="stat-label">Total Pings</div>
140
+ </div>
141
+ <div class="stat-card">
142
+ <div class="stat-value" id="successRate">-</div>
143
+ <div class="stat-label">Success Rate</div>
144
+ </div>
145
+ <div class="stat-card">
146
+ <div class="stat-value" id="avgResponseTime">-</div>
147
+ <div class="stat-label">Avg Response Time</div>
148
+ </div>
149
+ <div class="stat-card">
150
+ <div class="stat-value" id="packetLossRate">-</div>
151
+ <div class="stat-label">Packet Loss Rate</div>
152
+ </div>
153
+ </div>
154
+
155
+ <div class="chart-container">
156
+ <div id="chartContainer">
157
+ <div class="loading">
158
+ Select a host to view ping data
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </div>
163
+
164
+ <script>
165
+ let currentHost = '';
166
+ let currentTimeRange = 24;
167
+
168
+ // Load hosts on page load
169
+ loadHosts();
170
+
171
+ // Event listeners
172
+ document.getElementById('hostSelect').addEventListener('change', function() {
173
+ currentHost = this.value;
174
+ if (currentHost) {
175
+ loadData();
176
+ } else {
177
+ document.getElementById('statsContainer').style.display = 'none';
178
+ document.getElementById('chartContainer').innerHTML = '<div class="loading">Select a host to view ping data</div>';
179
+ }
180
+ });
181
+
182
+ document.getElementById('timeRange').addEventListener('change', function() {
183
+ currentTimeRange = parseInt(this.value);
184
+ if (currentHost) {
185
+ loadData();
186
+ }
187
+ });
188
+
189
+ function loadHosts() {
190
+ fetch('/api/hosts')
191
+ .then(response => response.json())
192
+ .then(data => {
193
+ const select = document.getElementById('hostSelect');
194
+ select.innerHTML = '<option value="">Select a host...</option>';
195
+
196
+ data.forEach(host => {
197
+ const option = document.createElement('option');
198
+ option.value = host;
199
+ option.textContent = host;
200
+ select.appendChild(option);
201
+ });
202
+ })
203
+ .catch(error => {
204
+ console.error('Error loading hosts:', error);
205
+ showError('Failed to load hosts');
206
+ });
207
+ }
208
+
209
+ function loadData() {
210
+ if (!currentHost) return;
211
+
212
+ document.getElementById('chartContainer').innerHTML = '<div class="loading">Loading...</div>';
213
+
214
+ // Load statistics
215
+ fetch(`/api/stats?host=${currentHost}&hours=${currentTimeRange}`)
216
+ .then(response => response.json())
217
+ .then(data => {
218
+ updateStats(data);
219
+ })
220
+ .catch(error => {
221
+ console.error('Error loading stats:', error);
222
+ });
223
+
224
+ // Load chart
225
+ fetch(`/api/ping_chart?host=${currentHost}&hours=${currentTimeRange}`)
226
+ .then(response => response.json())
227
+ .then(data => {
228
+ if (data.error) {
229
+ document.getElementById('chartContainer').innerHTML = `<div class="error">${data.error}</div>`;
230
+ } else {
231
+ Plotly.newPlot('chartContainer', data.data, data.layout, {responsive: true});
232
+ }
233
+ })
234
+ .catch(error => {
235
+ console.error('Error loading chart:', error);
236
+ document.getElementById('chartContainer').innerHTML = '<div class="error">Failed to load chart data</div>';
237
+ });
238
+ }
239
+
240
+ function updateStats(data) {
241
+ document.getElementById('totalPings').textContent = data.total_pings;
242
+ document.getElementById('successRate').textContent = `${(100 - data.packet_loss_rate).toFixed(1)}%`;
243
+ document.getElementById('avgResponseTime').textContent = `${data.avg_response_time.toFixed(1)}ms`;
244
+ document.getElementById('packetLossRate').textContent = `${data.packet_loss_rate.toFixed(1)}%`;
245
+
246
+ document.getElementById('statsContainer').style.display = 'grid';
247
+ }
248
+
249
+ function refreshData() {
250
+ if (currentHost) {
251
+ loadData();
252
+ }
253
+ loadHosts();
254
+ }
255
+
256
+ function showError(message) {
257
+ const container = document.querySelector('.container');
258
+ const errorDiv = document.createElement('div');
259
+ errorDiv.className = 'error';
260
+ errorDiv.textContent = message;
261
+ container.insertBefore(errorDiv, container.firstChild.nextSibling);
262
+
263
+ setTimeout(() => {
264
+ errorDiv.remove();
265
+ }, 5000);
266
+ }
267
+
268
+ // Auto-refresh every 30 seconds if a host is selected
269
+ setInterval(() => {
270
+ if (currentHost) {
271
+ loadData();
272
+ }
273
+ }, 30000);
274
+ </script>
275
+ </body>
276
+ </html>
@@ -0,0 +1,3 @@
1
+ from .dashboard import dashboard_bp
2
+
3
+ __all__ = ['dashboard_bp']
@@ -0,0 +1,174 @@
1
+ from flask import Blueprint, render_template, jsonify, request
2
+ from datetime import datetime, timedelta
3
+ from sqlalchemy import desc, func
4
+ import plotly.graph_objects as go
5
+ import plotly.utils
6
+ import json
7
+ from ..models import db, PingResult
8
+
9
+ dashboard_bp = Blueprint('dashboard', __name__, url_prefix='/')
10
+
11
+
12
+ @dashboard_bp.route('/')
13
+ def index():
14
+ """Main dashboard page"""
15
+ return render_template('dashboard.html')
16
+
17
+
18
+ @dashboard_bp.route('/api/hosts')
19
+ def get_hosts():
20
+ """Get list of monitored hosts"""
21
+ hosts = db.session.query(PingResult.target_host).distinct().all()
22
+ return jsonify([host[0] for host in hosts])
23
+
24
+
25
+ @dashboard_bp.route('/api/ping_data')
26
+ def get_ping_data():
27
+ """Get ping data for visualization"""
28
+ host = request.args.get('host')
29
+ hours = int(request.args.get('hours', 24))
30
+
31
+ if not host:
32
+ return jsonify({'error': 'Host parameter is required'}), 400
33
+
34
+ # Get data for the specified time range
35
+ since = datetime.utcnow() - timedelta(hours=hours)
36
+
37
+ results = db.session.query(PingResult).filter(
38
+ PingResult.target_host == host,
39
+ PingResult.timestamp >= since
40
+ ).order_by(PingResult.timestamp).all()
41
+
42
+ if not results:
43
+ return jsonify({'error': 'No data found for the specified host'}), 404
44
+
45
+ # Prepare data for plotting
46
+ timestamps = []
47
+ response_times = []
48
+ packet_losses = []
49
+
50
+ for result in results:
51
+ timestamps.append(result.timestamp.isoformat())
52
+ response_times.append(result.response_time if not result.packet_loss else None)
53
+ packet_losses.append(1 if result.packet_loss else 0)
54
+
55
+ return jsonify({
56
+ 'host': host,
57
+ 'timestamps': timestamps,
58
+ 'response_times': response_times,
59
+ 'packet_losses': packet_losses,
60
+ 'total_pings': len(results),
61
+ 'successful_pings': sum(1 for r in results if not r.packet_loss),
62
+ 'packet_loss_rate': (sum(packet_losses) / len(packet_losses)) * 100 if packet_losses else 0
63
+ })
64
+
65
+
66
+ @dashboard_bp.route('/api/ping_chart')
67
+ def get_ping_chart():
68
+ """Generate Plotly chart for ping data"""
69
+ host = request.args.get('host')
70
+ hours = int(request.args.get('hours', 24))
71
+
72
+ if not host:
73
+ return jsonify({'error': 'Host parameter is required'}), 400
74
+
75
+ since = datetime.utcnow() - timedelta(hours=hours)
76
+
77
+ results = db.session.query(PingResult).filter(
78
+ PingResult.target_host == host,
79
+ PingResult.timestamp >= since
80
+ ).order_by(PingResult.timestamp).all()
81
+
82
+ if not results:
83
+ return jsonify({'error': 'No data found'}), 404
84
+
85
+ # Separate successful pings and packet losses
86
+ successful_times = []
87
+ successful_response_times = []
88
+ failed_times = []
89
+
90
+ for result in results:
91
+ if result.packet_loss:
92
+ failed_times.append(result.timestamp)
93
+ else:
94
+ successful_times.append(result.timestamp)
95
+ successful_response_times.append(result.response_time)
96
+
97
+ # Create plotly figure
98
+ fig = go.Figure()
99
+
100
+ # Add successful pings
101
+ if successful_times:
102
+ fig.add_trace(go.Scatter(
103
+ x=successful_times,
104
+ y=successful_response_times,
105
+ mode='lines+markers',
106
+ name='Response Time (ms)',
107
+ line=dict(color='green'),
108
+ marker=dict(size=4)
109
+ ))
110
+
111
+ # Add packet loss markers
112
+ if failed_times:
113
+ fig.add_trace(go.Scatter(
114
+ x=failed_times,
115
+ y=[max(successful_response_times) * 1.1] * len(failed_times) if successful_response_times else [100] * len(failed_times),
116
+ mode='markers',
117
+ name='Packet Loss',
118
+ marker=dict(color='red', symbol='x', size=8)
119
+ ))
120
+
121
+ fig.update_layout(
122
+ title=f'Ping Results for {host} (Last {hours} hours)',
123
+ xaxis_title='Time',
124
+ yaxis_title='Response Time (ms)',
125
+ template='plotly_white',
126
+ hovermode='x unified'
127
+ )
128
+
129
+ return jsonify(json.loads(plotly.utils.PlotlyJSONEncoder().encode(fig)))
130
+
131
+
132
+ @dashboard_bp.route('/api/stats')
133
+ def get_stats():
134
+ """Get overall statistics"""
135
+ host = request.args.get('host')
136
+ hours = int(request.args.get('hours', 24))
137
+
138
+ since = datetime.utcnow() - timedelta(hours=hours)
139
+
140
+ if host:
141
+ query = db.session.query(PingResult).filter(
142
+ PingResult.target_host == host,
143
+ PingResult.timestamp >= since
144
+ )
145
+ else:
146
+ query = db.session.query(PingResult).filter(
147
+ PingResult.timestamp >= since
148
+ )
149
+
150
+ results = query.all()
151
+
152
+ if not results:
153
+ return jsonify({
154
+ 'total_pings': 0,
155
+ 'successful_pings': 0,
156
+ 'failed_pings': 0,
157
+ 'packet_loss_rate': 0,
158
+ 'avg_response_time': 0,
159
+ 'min_response_time': 0,
160
+ 'max_response_time': 0
161
+ })
162
+
163
+ successful = [r for r in results if not r.packet_loss and r.response_time is not None]
164
+ successful_response_times = [r.response_time for r in successful]
165
+
166
+ return jsonify({
167
+ 'total_pings': len(results),
168
+ 'successful_pings': len(successful),
169
+ 'failed_pings': len(results) - len(successful),
170
+ 'packet_loss_rate': ((len(results) - len(successful)) / len(results)) * 100,
171
+ 'avg_response_time': sum(successful_response_times) / len(successful_response_times) if successful_response_times else 0,
172
+ 'min_response_time': min(successful_response_times) if successful_response_times else 0,
173
+ 'max_response_time': max(successful_response_times) if successful_response_times else 0
174
+ })
@@ -0,0 +1,47 @@
1
+ [tool.poetry]
2
+ name = "network-check-tool"
3
+ version = "0.0.1"
4
+ description = "A network monitoring tool with ping tracking and dashboard visualization"
5
+ authors = ["hardwork9047 <tomatosimple1104@gmail.com>"]
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ homepage = "https://github.com/hardwork9047/network-checker"
9
+ repository = "https://github.com/hardwork9047/network-checker"
10
+ documentation = "https://github.com/hardwork9047/network-checker"
11
+ packages = [{ include = "network_checker" }]
12
+
13
+ include = [
14
+ "network_checker/config/config.py",
15
+ "network_checker/templates/*",
16
+ "network_checker/static/*",
17
+ "network_checker/cli.py"
18
+ ]
19
+
20
+ [tool.poetry.dependencies]
21
+ python = "^3.10"
22
+ flask = "^3.1.0"
23
+ flask-sqlalchemy = "^3.1.1"
24
+ pandas = "^2.2.3"
25
+ flask-cors = "^4.0.0"
26
+ click = "^8.1.3"
27
+ ping3 = "^4.0.4"
28
+ apscheduler = "^3.10.4"
29
+ plotly = "^5.17.0"
30
+ toml = "^0.10.2"
31
+
32
+ [tool.poetry.scripts]
33
+ network-checker = "network_checker.cli:cli"
34
+
35
+ [tool.poetry.group.dev.dependencies]
36
+ pytest = "^8.3.4"
37
+ black = "^24.0.0"
38
+
39
+ [tool.black]
40
+ line-length = 88
41
+ target-version = ['py310']
42
+ include = '\.pyi?$'
43
+ skip-string-normalization = false
44
+
45
+ [build-system]
46
+ requires = ["poetry-core"]
47
+ build-backend = "poetry.core.masonry.api"