initial commit

This commit is contained in:
Jack
2026-08-30 04:48:55 +00:00
commit 295410075f
9 changed files with 966 additions and 0 deletions
Binary file not shown.
+310
View File
@@ -0,0 +1,310 @@
from flask import Flask, render_template, request, jsonify, redirect, url_for
from apscheduler.schedulers.background import BackgroundScheduler
from datetime import datetime
import subprocess
import re
import database as db
app = Flask(__name__)
# Modem configuration profiles
MODEM_PROFILES = {
'rv50': {
'oid_sent': '.1.3.6.1.4.1.20542.9.1.1.2.284.0',
'oid_recv': '.1.3.6.1.4.1.20542.9.1.1.2.283.0',
'type': 'counter64'
},
'bullet-lte': {
'oid_sent': '.1.3.6.1.4.1.21703.6030.9.4.3.0',
'oid_recv': '.1.3.6.1.4.1.21703.6030.9.4.1.0',
'type': 'displaystring'
}
}
# Store last readings for rate calculation
last_readings = {}
scheduler = BackgroundScheduler()
scheduler.start()
def format_bytes(bytes_value):
"""Convert bytes to human-readable format"""
if bytes_value is None or bytes_value == 0:
return "N/A", 0
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if abs(bytes_value) < 1024.0:
return f"{bytes_value:.2f} {unit}", bytes_value
bytes_value /= 1024.0
return f"{bytes_value:.2f} PB", bytes_value
def parse_snmp_value(value, data_type):
"""Parse SNMP response based on data type"""
if value is None:
return None
if data_type == 'counter64':
# Counter64: "Counter64: 1234567890"
match = re.search(r'Counter64:\s*(\d+)', value)
if match:
return int(match.group(1))
elif data_type == 'displaystring':
# DisplayString: "STRING: 1234567890" or just the number
match = re.search(r'STRING:\s*"?([^"]+)"?', value)
if match:
str_value = match.group(1).strip()
# Remove any non-digit characters (commas, spaces, etc.)
digits = re.sub(r'[^\d]', '', str_value)
if digits:
return int(digits)
# Fallback: try to find any number in the output
digits = re.sub(r'[^\d]', '', value)
if digits:
return int(digits)
return None
def snmp_get(ip, port, community, oid):
"""Execute snmpget command and return raw output"""
try:
cmd = [
'snmpget', '-v', '2c', '-c', community,
'-t', '5', '-r', '2',
f'{ip}:{port}', oid
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
return result.stdout.strip()
return None
except Exception as e:
print(f"SNMP error: {e}")
return None
def poll_target(target_id, ip, port, community, modem_type):
"""Poll a target and store the reading"""
global last_readings
profile = MODEM_PROFILES.get(modem_type, MODEM_PROFILES['rv50'])
data_type = profile['type']
# Get raw SNMP responses
raw_sent = snmp_get(ip, port, community, profile['oid_sent'])
raw_recv = snmp_get(ip, port, community, profile['oid_recv'])
# Parse values based on data type
bytes_sent = parse_snmp_value(raw_sent, data_type)
bytes_recv = parse_snmp_value(raw_recv, data_type)
if bytes_sent is not None and bytes_recv is not None:
db.add_reading(target_id, bytes_sent, bytes_recv)
# Calculate rates (bytes per second)
rate_sent = 0
rate_recv = 0
if target_id in last_readings:
last_sent, last_recv, last_time = last_readings[target_id]
time_diff = (datetime.now() - last_time).total_seconds()
if time_diff > 0 and bytes_sent >= last_sent and bytes_recv >= last_recv:
rate_sent = (bytes_sent - last_sent) / time_diff
rate_recv = (bytes_recv - last_recv) / time_diff
last_readings[target_id] = (bytes_sent, bytes_recv, datetime.now())
return {
'bytes_sent': bytes_sent,
'bytes_recv': bytes_recv,
'rate_sent': rate_sent,
'rate_recv': rate_recv,
'success': True
}
return {'success': False, 'error': 'Failed to parse SNMP response'}
def scheduled_poll():
"""Background job to poll all enabled targets"""
targets = db.get_all_targets()
for target in targets:
if target['enabled']:
poll_target(
target['id'],
target['ip_address'],
target['port'],
target['community'],
target['modem_type']
)
@app.route('/')
def index():
targets = db.get_all_targets()
target_data = []
for target in targets:
latest = db.get_latest_reading(target['id'])
if latest:
sent_formatted, _ = format_bytes(latest['bytes_sent'])
recv_formatted, _ = format_bytes(latest['bytes_recv'])
# Calculate rate if we have previous reading
rate_sent = 0
rate_recv = 0
if target['id'] in last_readings:
last_sent, last_recv, last_time = last_readings[target['id']]
time_diff = (datetime.now() - last_time).total_seconds()
if time_diff > 0 and latest['bytes_sent'] >= last_sent:
rate_sent = (latest['bytes_sent'] - last_sent) / time_diff
rate_recv = (latest['bytes_recv'] - last_recv) / time_diff
rate_sent_fmt, _ = format_bytes(rate_sent)
rate_recv_fmt, _ = format_bytes(rate_recv)
else:
sent_formatted = recv_formatted = rate_sent_fmt = rate_recv_fmt = "N/A"
target_data.append({
**target,
'bytes_sent_fmt': sent_formatted,
'bytes_recv_fmt': recv_formatted,
'rate_sent_fmt': f"{rate_sent_fmt}/s",
'rate_recv_fmt': f"{rate_recv_fmt}/s",
'last_poll': latest['timestamp'] if latest else None
})
return render_template('index.html', targets=target_data)
@app.route('/add', methods=['POST'])
def add_target():
name = request.form.get('name', 'Unnamed')
ip_address = request.form.get('ip_address')
port = int(request.form.get('port', 161))
community = request.form.get('community', 'public')
period_minutes = int(request.form.get('period_minutes', 5))
modem_type = request.form.get('modem_type', 'rv50')
if ip_address:
target_id = db.add_target(name, ip_address, port, community, period_minutes, modem_type)
scheduler.add_job(
scheduled_poll,
'interval',
minutes=period_minutes,
id=f'target_{target_id}',
replace_existing=True
)
poll_target(target_id, ip_address, port, community, modem_type)
return redirect(url_for('index'))
@app.route('/edit/<int:target_id>', methods=['POST'])
def edit_target(target_id):
name = request.form.get('name', 'Unnamed')
ip_address = request.form.get('ip_address')
port = int(request.form.get('port', 161))
community = request.form.get('community', 'public')
period_minutes = int(request.form.get('period_minutes', 5))
enabled = 1 if request.form.get('enabled') else 0
modem_type = request.form.get('modem_type', 'rv50')
db.update_target(target_id, name, ip_address, port, community, period_minutes, enabled, modem_type)
if enabled:
scheduler.add_job(
scheduled_poll,
'interval',
minutes=period_minutes,
id=f'target_{target_id}',
replace_existing=True
)
else:
try:
scheduler.remove_job(f'target_{target_id}')
except:
pass
return redirect(url_for('index'))
@app.route('/delete/<int:target_id>', methods=['POST'])
def delete_target(target_id):
try:
scheduler.remove_job(f'target_{target_id}')
except:
pass
db.delete_target(target_id)
return redirect(url_for('index'))
@app.route('/poll/<int:target_id>', methods=['POST'])
def manual_poll(target_id):
target = db.get_target(target_id)
if target:
result = poll_target(
target['id'],
target['ip_address'],
target['port'],
target['community'],
target['modem_type']
)
return jsonify(result)
return jsonify({'success': False, 'error': 'Target not found'}), 404
@app.route('/graph/<int:target_id>')
def graph(target_id):
target = db.get_target(target_id)
hours = request.args.get('hours', 24, type=int)
readings = db.get_readings(target_id, hours)
labels = []
sent_data = []
recv_data = []
for r in readings:
labels.append(r['timestamp'])
sent_data.append(r['bytes_sent'])
recv_data.append(r['bytes_recv'])
return render_template('graph.html',
target=target,
labels=labels,
sent_data=sent_data,
recv_data=recv_data,
hours=hours)
@app.route('/api/readings/<int:target_id>')
def api_readings(target_id):
hours = request.args.get('hours', 24, type=int)
readings = db.get_readings(target_id, hours)
data = []
for r in readings:
data.append({
'timestamp': r['timestamp'],
'bytes_sent': r['bytes_sent'],
'bytes_recv': r['bytes_recv']
})
return jsonify(data)
if __name__ == '__main__':
db.init_db()
targets = db.get_all_targets()
for target in targets:
if target['enabled']:
scheduler.add_job(
scheduled_poll,
'interval',
minutes=target['period_minutes'],
id=f'target_{target["id"]}',
replace_existing=True
)
poll_target(
target['id'],
target['ip_address'],
target['port'],
target['community'],
target['modem_type']
)
app.run(debug=True, host='0.0.0.0', port=5500)
+132
View File
@@ -0,0 +1,132 @@
import sqlite3
from datetime import datetime
DB_PATH = 'snmp_monitor.db'
def init_db():
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
# Targets table (devices to monitor)
c.execute('''
CREATE TABLE IF NOT EXISTS targets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
ip_address TEXT NOT NULL,
port INTEGER DEFAULT 161,
community TEXT NOT NULL,
period_minutes INTEGER DEFAULT 5,
modem_type TEXT DEFAULT 'rv50',
enabled INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Readings table (stored SNMP data)
c.execute('''
CREATE TABLE IF NOT EXISTS readings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
target_id INTEGER NOT NULL,
bytes_sent INTEGER,
bytes_recv INTEGER,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (target_id) REFERENCES targets(id)
)
''')
# Check if modem_type column exists, add it if not (for existing databases)
try:
c.execute('SELECT modem_type FROM targets LIMIT 1')
except sqlite3.OperationalError:
c.execute('ALTER TABLE targets ADD COLUMN modem_type TEXT DEFAULT "rv50"')
conn.commit()
conn.commit()
conn.close()
def add_target(name, ip_address, port, community, period_minutes, modem_type='rv50'):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('''
INSERT INTO targets (name, ip_address, port, community, period_minutes, modem_type)
VALUES (?, ?, ?, ?, ?, ?)
''', (name, ip_address, port, community, period_minutes, modem_type))
conn.commit()
target_id = c.lastrowid
conn.close()
return target_id
def get_all_targets():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute('SELECT * FROM targets ORDER BY id')
targets = c.fetchall()
conn.close()
return targets
def get_target(target_id):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute('SELECT * FROM targets WHERE id = ?', (target_id,))
target = c.fetchone()
conn.close()
return target
def update_target(target_id, name, ip_address, port, community, period_minutes, enabled, modem_type='rv50'):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('''
UPDATE targets
SET name=?, ip_address=?, port=?, community=?, period_minutes=?, enabled=?, modem_type=?
WHERE id=?
''', (name, ip_address, port, community, period_minutes, enabled, modem_type, target_id))
conn.commit()
conn.close()
def delete_target(target_id):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('DELETE FROM readings WHERE target_id = ?', (target_id,))
c.execute('DELETE FROM targets WHERE id = ?', (target_id,))
conn.commit()
conn.close()
def add_reading(target_id, bytes_sent, bytes_recv):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('''
INSERT INTO readings (target_id, bytes_sent, bytes_recv)
VALUES (?, ?, ?)
''', (target_id, bytes_sent, bytes_recv))
conn.commit()
conn.close()
def get_readings(target_id, hours=24):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute('''
SELECT * FROM readings
WHERE target_id = ?
AND timestamp >= datetime('now', '-{} hours')
ORDER BY timestamp
'''.format(hours), (target_id,))
readings = c.fetchall()
conn.close()
return readings
def get_latest_reading(target_id):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute('''
SELECT * FROM readings
WHERE target_id = ?
ORDER BY timestamp DESC
LIMIT 1
''', (target_id,))
reading = c.fetchone()
conn.close()
return reading
+255
View File
@@ -0,0 +1,255 @@
from flask import Flask, render_template, request, jsonify, redirect, url_for
from apscheduler.schedulers.background import BackgroundScheduler
from datetime import datetime
import subprocess
import re
import database as db
app = Flask(__name__)
# OIDs for Sierra Wireless RV50
OID_BYTES_SENT = '.1.3.6.1.4.1.20542.9.1.1.2.284.0'
OID_BYTES_RECV = '.1.3.6.1.4.1.20542.9.1.1.2.283.0'
# Store last readings for rate calculation
last_readings = {}
scheduler = BackgroundScheduler()
scheduler.start()
def format_bytes(bytes_value):
"""Convert bytes to human-readable format"""
if bytes_value is None:
return "N/A", 0
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if abs(bytes_value) < 1024.0:
return f"{bytes_value:.2f} {unit}", bytes_value
bytes_value /= 1024.0
return f"{bytes_value:.2f} PB", bytes_value
def snmp_get(ip, port, community, oid):
"""Execute snmpget command and return value"""
try:
cmd = [
'snmpget', '-v', '2c', '-c', community,
'-t', '5', '-r', '2',
f'{ip}:{port}', oid
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
# Parse output: "SNMPv2-SMI::enterprises.20542.9.1.1.2.284.0 = Counter64: 123456"
match = re.search(r'Counter64:\s*(\d+)', result.stdout)
if match:
return int(match.group(1))
return None
except Exception as e:
print(f"SNMP error: {e}")
return None
def poll_target(target_id, ip, port, community):
"""Poll a target and store the reading"""
global last_readings
bytes_sent = snmp_get(ip, port, community, OID_BYTES_SENT)
bytes_recv = snmp_get(ip, port, community, OID_BYTES_RECV)
if bytes_sent is not None and bytes_recv is not None:
db.add_reading(target_id, bytes_sent, bytes_recv)
# Calculate rates (bytes per second)
rate_sent = 0
rate_recv = 0
if target_id in last_readings:
last_sent, last_recv, last_time = last_readings[target_id]
time_diff = (datetime.now() - last_time).total_seconds()
if time_diff > 0:
rate_sent = (bytes_sent - last_sent) / time_diff
rate_recv = (bytes_recv - last_recv) / time_diff
last_readings[target_id] = (bytes_sent, bytes_recv, datetime.now())
return {
'bytes_sent': bytes_sent,
'bytes_recv': bytes_recv,
'rate_sent': rate_sent,
'rate_recv': rate_recv,
'success': True
}
return {'success': False}
def scheduled_poll():
"""Background job to poll all enabled targets"""
targets = db.get_all_targets()
for target in targets:
if target['enabled']:
poll_target(target['id'], target['ip_address'],
target['port'], target['community'])
@app.route('/')
def index():
targets = db.get_all_targets()
target_data = []
for target in targets:
latest = db.get_latest_reading(target['id'])
if latest:
sent_formatted, _ = format_bytes(latest['bytes_sent'])
recv_formatted, _ = format_bytes(latest['bytes_recv'])
# Calculate rate if we have previous reading
rate_sent = 0
rate_recv = 0
if target['id'] in last_readings:
last_sent, last_recv, last_time = last_readings[target['id']]
time_diff = (datetime.now() - last_time).total_seconds()
if time_diff > 0 and latest['bytes_sent'] >= last_sent:
rate_sent = (latest['bytes_sent'] - last_sent) / time_diff
rate_recv = (latest['bytes_recv'] - last_recv) / time_diff
rate_sent_fmt, _ = format_bytes(rate_sent)
rate_recv_fmt, _ = format_bytes(rate_recv)
else:
sent_formatted = recv_formatted = rate_sent_fmt = rate_recv_fmt = "N/A"
target_data.append({
**target,
'bytes_sent_fmt': sent_formatted,
'bytes_recv_fmt': recv_formatted,
'rate_sent_fmt': f"{rate_sent_fmt}/s",
'rate_recv_fmt': f"{rate_recv_fmt}/s",
'last_poll': latest['timestamp'] if latest else None
})
return render_template('index.html', targets=target_data)
@app.route('/add', methods=['POST'])
def add_target():
name = request.form.get('name', 'Unnamed')
ip_address = request.form.get('ip_address')
port = int(request.form.get('port', 161))
community = request.form.get('community', 'public')
period_minutes = int(request.form.get('period_minutes', 5))
if ip_address:
target_id = db.add_target(name, ip_address, port, community, period_minutes)
# Schedule polling for this target
scheduler.add_job(
scheduled_poll,
'interval',
minutes=period_minutes,
id=f'target_{target_id}',
replace_existing=True
)
# Do initial poll
poll_target(target_id, ip_address, port, community)
return redirect(url_for('index'))
@app.route('/edit/<int:target_id>', methods=['POST'])
def edit_target(target_id):
name = request.form.get('name', 'Unnamed')
ip_address = request.form.get('ip_address')
port = int(request.form.get('port', 161))
community = request.form.get('community', 'public')
period_minutes = int(request.form.get('period_minutes', 5))
enabled = 1 if request.form.get('enabled') else 0
db.update_target(target_id, name, ip_address, port, community, period_minutes, enabled)
# Update scheduler
if enabled:
scheduler.add_job(
scheduled_poll,
'interval',
minutes=period_minutes,
id=f'target_{target_id}',
replace_existing=True
)
else:
try:
scheduler.remove_job(f'target_{target_id}')
except:
pass
return redirect(url_for('index'))
@app.route('/delete/<int:target_id>', methods=['POST'])
def delete_target(target_id):
try:
scheduler.remove_job(f'target_{target_id}')
except:
pass
db.delete_target(target_id)
return redirect(url_for('index'))
@app.route('/poll/<int:target_id>', methods=['POST'])
def manual_poll(target_id):
target = db.get_target(target_id)
if target:
result = poll_target(target['id'], target['ip_address'],
target['port'], target['community'])
return jsonify(result)
return jsonify({'success': False, 'error': 'Target not found'}), 404
@app.route('/graph/<int:target_id>')
def graph(target_id):
target = db.get_target(target_id)
hours = request.args.get('hours', 24, type=int)
readings = db.get_readings(target_id, hours)
labels = []
sent_data = []
recv_data = []
for r in readings:
labels.append(r['timestamp'])
sent_data.append(r['bytes_sent'])
recv_data.append(r['bytes_recv'])
return render_template('graph.html',
target=target,
labels=labels,
sent_data=sent_data,
recv_data=recv_data,
hours=hours)
@app.route('/api/readings/<int:target_id>')
def api_readings(target_id):
hours = request.args.get('hours', 24, type=int)
readings = db.get_readings(target_id, hours)
data = []
for r in readings:
data.append({
'timestamp': r['timestamp'],
'bytes_sent': r['bytes_sent'],
'bytes_recv': r['bytes_recv']
})
return jsonify(data)
if __name__ == '__main__':
db.init_db()
# Start with initial poll for any existing targets
targets = db.get_all_targets()
for target in targets:
if target['enabled']:
scheduler.add_job(
scheduled_poll,
'interval',
minutes=target['period_minutes'],
id=f'target_{target["id"]}',
replace_existing=True
)
poll_target(target['id'], target['ip_address'],
target['port'], target['community'])
app.run(debug=True, host='0.0.0.0', port=5500)
+2
View File
@@ -0,0 +1,2 @@
flask==3.0.0
apscheduler==3.10.4
BIN
View File
Binary file not shown.
+41
View File
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}SNMP Monitor{% endblock %}</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e; color: #eee; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { margin-bottom: 20px; color: #00d9ff; }
.card { background: #16213e; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
.btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;
font-size: 14px; margin-right: 5px; }
.btn-primary { background: #00d9ff; color: #000; }
.btn-danger { background: #e94560; color: #fff; }
.btn-success { background: #0f3460; color: #00d9ff; border: 1px solid #00d9ff; }
input, select { padding: 8px; border-radius: 4px; border: 1px solid #0f3460;
background: #1a1a2e; color: #eee; margin-right: 10px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #0f3460; }
th { color: #00d9ff; }
.status-ok { color: #4ade80; }
.status-error { color: #e94560; }
.form-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.7); justify-content: center; align-items: center; }
.modal.active { display: flex; }
.modal-content { background: #16213e; padding: 30px; border-radius: 8px;
max-width: 500px; width: 90%; }
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<div class="container">
{% block content %}{% endblock %}
</div>
{% block extra_js %}{% endblock %}
</body>
</html>
+79
View File
@@ -0,0 +1,79 @@
{% extends 'base.html' %}
{% block title %}Graph - {{ target.name }}{% endblock %}
{% block extra_css %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% endblock %}
{% block content %}
<h1>📊 {{ target.name }} - Data Usage</h1>
<div class="card">
<div style="margin-bottom: 15px;">
<a href="/graph/{{ target.id }}?hours=6" class="btn btn-success">6h</a>
<a href="/graph/{{ target.id }}?hours=24" class="btn btn-success">24h</a>
<a href="/graph/{{ target.id }}?hours=168" class="btn btn-success">7d</a>
<a href="/" class="btn btn-primary">← Back</a>
</div>
<canvas id="usageChart" style="max-height: 400px;"></canvas>
</div>
<div class="card">
<h3 style="margin-bottom: 15px; color: #00d9ff;">Device Info</h3>
<p><strong>Name:</strong> {{ target.name }}</p>
<p><strong>Modem Type:</strong> {{ target.modem_type or 'rv50' }}</p>
<p><strong>IP:</strong> {{ target.ip_address }}:{{ target.port }}</p>
<p><strong>Community:</strong> {{ target.community }}</p>
<p><strong>Poll Period:</strong> {{ target.period_minutes }} minutes</p>
</div>
{% endblock %}
{% block extra_js %}
<script>
const labels = {{ labels | tojson }};
const sentData = {{ sent_data | tojson }};
const recvData = {{ recv_data | tojson }};
// Convert to KB for readability
const sentKB = sentData.map(v => v ? (v / 1024).toFixed(2) : 0);
const recvKB = recvData.map(v => v ? (v / 1024).toFixed(2) : 0);
new Chart(document.getElementById('usageChart'), {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Bytes Sent (KB)',
data: sentKB,
borderColor: '#00d9ff',
backgroundColor: 'rgba(0, 217, 255, 0.1)',
fill: true,
tension: 0.4
},
{
label: 'Bytes Received (KB)',
data: recvKB,
borderColor: '#4ade80',
backgroundColor: 'rgba(74, 222, 128, 0.1)',
fill: true,
tension: 0.4
}
]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { labels: { color: '#eee' } }
},
scales: {
x: { ticks: { color: '#888' }, grid: { color: '#0f3460' } },
y: { ticks: { color: '#888' }, grid: { color: '#0f3460' } }
}
}
});
</script>
{% endblock %}
+147
View File
@@ -0,0 +1,147 @@
{% extends 'base.html' %}
{% block title %}SNMP Monitor - Dashboard{% endblock %}
{% block content %}
<h1>📡 Cellular Data Usage Monitor</h1>
<!-- Add Target Form -->
<div class="card">
<h3 style="margin-bottom: 15px; color: #00d9ff;">Add New Target</h3>
<form action="/add" method="POST">
<div class="form-row">
<input type="text" name="name" placeholder="Device Name" required style="flex: 1;">
<select name="modem_type" style="width: 160px;">
<option value="rv50">Sierra RV50</option>
<option value="bullet-lte">Microhard Bullet-LTE</option>
</select>
<input type="text" name="ip_address" placeholder="IP Address" required style="flex: 1;">
<input type="number" name="port" value="161" placeholder="Port" style="width: 80px;">
<input type="text" name="community" value="public" placeholder="Community" style="flex: 1;">
<input type="number" name="period_minutes" value="5" placeholder="Minutes" style="width: 80px;">
<button type="submit" class="btn btn-primary">Add Target</button>
</div>
</form>
</div>
<!-- Targets Table -->
<div class="card">
<h3 style="margin-bottom: 15px; color: #00d9ff;">Monitored Devices</h3>
{% if targets %}
<table>
<thead>
<tr>
<th>Name</th>
<th>Modem</th>
<th>IP Address</th>
<th>Bytes Sent</th>
<th>Bytes Received</th>
<th>Rate Sent</th>
<th>Rate Recv</th>
<th>Period</th>
<th>Last Poll</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for target in targets %}
<tr>
<td>{{ target.name }}</td>
<td>{{ target.modem_type or 'rv50' }}</td>
<td>{{ target.ip_address }}:{{ target.port }}</td>
<td>{{ target.bytes_sent_fmt }}</td>
<td>{{ target.bytes_recv_fmt }}</td>
<td>{{ target.rate_sent_fmt }}</td>
<td>{{ target.rate_recv_fmt }}</td>
<td>{{ target.period_minutes }} min</td>
<td>{{ target.last_poll or 'Never' }}</td>
<td class="{{ 'status-ok' if target.enabled else 'status-error' }}">
{{ 'Active' if target.enabled else 'Paused' }}
</td>
<td>
<a href="/graph/{{ target.id }}?hours=24" class="btn btn-success">📊 Graph</a>
<button onclick="manualPoll({{ target.id }})" class="btn btn-primary">🔄 Poll</button>
<button onclick="editTarget({{ target.id }}, '{{ target.name }}', '{{ target.ip_address }}', {{ target.port }}, '{{ target.community }}', {{ target.period_minutes }}, {{ target.enabled }}, '{{ target.modem_type or 'rv50' }}')" class="btn btn-success">✏️ Edit</button>
<form action="/delete/{{ target.id }}" method="POST" style="display: inline;"
onsubmit="return confirm('Delete this target?')">
<button type="submit" class="btn btn-danger">🗑️</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p style="color: #888;">No targets configured. Add one above!</p>
{% endif %}
</div>
<!-- Edit Modal -->
<div id="editModal" class="modal">
<div class="modal-content">
<h3 style="margin-bottom: 15px; color: #00d9ff;">Edit Target</h3>
<form id="editForm" method="POST">
<div class="form-row">
<input type="text" name="name" id="edit_name" placeholder="Device Name" required style="flex: 1;">
</div>
<div class="form-row">
<select name="modem_type" id="edit_modem_type" style="width: 160px;">
<option value="rv50">Sierra RV50</option>
<option value="bullet-lte">Microhard Bullet-LTE</option>
</select>
</div>
<div class="form-row">
<input type="text" name="ip_address" id="edit_ip" placeholder="IP Address" required style="flex: 1;">
<input type="number" name="port" id="edit_port" value="161" style="width: 80px;">
</div>
<div class="form-row">
<input type="text" name="community" id="edit_community" placeholder="Community" style="flex: 1;">
<input type="number" name="period_minutes" id="edit_period" value="5" style="width: 80px;">
</div>
<div class="form-row">
<label><input type="checkbox" name="enabled" id="edit_enabled" checked> Enabled</label>
</div>
<div class="form-row">
<button type="submit" class="btn btn-primary">Save Changes</button>
<button type="button" onclick="closeEditModal()" class="btn btn-danger">Cancel</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function manualPoll(targetId) {
fetch(`/poll/${targetId}`, { method: 'POST' })
.then(r => r.json())
.then(data => {
if (data.success) {
alert('Poll successful!');
location.reload();
} else {
alert('Poll failed: ' + (data.error || 'Unknown error'));
}
})
.catch(err => alert('Error: ' + err));
}
function editTarget(id, name, ip, port, community, period, enabled, modemType) {
document.getElementById('edit_name').value = name;
document.getElementById('edit_ip').value = ip;
document.getElementById('edit_port').value = port;
document.getElementById('edit_community').value = community;
document.getElementById('edit_period').value = period;
document.getElementById('edit_enabled').checked = enabled == 1;
document.getElementById('edit_modem_type').value = modemType;
document.getElementById('editForm').action = `/edit/${id}`;
document.getElementById('editModal').classList.add('active');
}
function closeEditModal() {
document.getElementById('editModal').classList.remove('active');
}
</script>
{% endblock %}