feat: add http, snmp, docker plugins; new widgets and routes for traefik/unifi/ups
- Add HTTP monitor plugin: endpoint checking with status history, latency tracking, time-range views - Add SNMP plugin: OID polling, device management, metric recording - Add Docker plugin: container status, resource usage, widget - Traefik: access log widget, request chart widget, expanded routes - UniFi: clients widget, devices widget, expanded poll routes - UPS: history widget, additional routes - Update plugin index with new entries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+97
-1
@@ -71,7 +71,7 @@ async def rows():
|
||||
|
||||
b_secs = bucket_seconds(since)
|
||||
bucket_col = (
|
||||
cast(func.strftime('%s', TraefikMetric.scraped_at), Integer) / b_secs
|
||||
cast(func.extract('epoch', TraefikMetric.scraped_at), Integer) / b_secs
|
||||
).label("bucket")
|
||||
|
||||
router_data = []
|
||||
@@ -195,6 +195,102 @@ async def widget():
|
||||
)
|
||||
|
||||
|
||||
@traefik_bp.get("/widget/access_log")
|
||||
@require_role(UserRole.viewer)
|
||||
async def widget_access_log():
|
||||
"""HTMX dashboard widget: traffic origin summary with IP filter."""
|
||||
from collections import defaultdict
|
||||
ip_filter = request.args.get("ip_filter", "all")
|
||||
limit = max(1, min(50, int(request.args.get("limit", 10) or 10)))
|
||||
widget_id = request.args.get("wid", "0")
|
||||
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
result = await db.execute(
|
||||
select(TraefikAccessSummary)
|
||||
.order_by(TraefikAccessSummary.period_end.desc())
|
||||
.limit(12) # last ~1 hour of 5-min windows
|
||||
)
|
||||
rows = result.scalars().all()
|
||||
|
||||
if not rows:
|
||||
return await render_template("traefik/widget_access_log.html",
|
||||
summary=None, widget_id=widget_id)
|
||||
|
||||
total = sum(r.total_requests for r in rows)
|
||||
internal = sum(r.internal_requests for r in rows)
|
||||
external = sum(r.external_requests for r in rows)
|
||||
|
||||
ip_counts: dict = defaultdict(lambda: {"count": 0, "internal": True})
|
||||
for row in rows:
|
||||
for entry in json.loads(row.top_ips_json):
|
||||
k = entry["ip"]
|
||||
ip_counts[k]["count"] += entry["count"]
|
||||
ip_counts[k]["internal"] = entry.get("internal", True)
|
||||
|
||||
all_ips = sorted(
|
||||
[{"ip": k, **v} for k, v in ip_counts.items()],
|
||||
key=lambda x: x["count"], reverse=True,
|
||||
)
|
||||
if ip_filter == "internal":
|
||||
all_ips = [e for e in all_ips if e["internal"]]
|
||||
elif ip_filter == "external":
|
||||
all_ips = [e for e in all_ips if not e["internal"]]
|
||||
|
||||
summary = {
|
||||
"total": total,
|
||||
"internal": internal,
|
||||
"external": external,
|
||||
"external_pct": (external / total * 100.0) if total else 0.0,
|
||||
"top_ips": all_ips[:limit],
|
||||
"ip_filter": ip_filter,
|
||||
}
|
||||
return await render_template("traefik/widget_access_log.html",
|
||||
summary=summary, widget_id=widget_id)
|
||||
|
||||
|
||||
@traefik_bp.get("/widget/request_chart")
|
||||
@require_role(UserRole.viewer)
|
||||
async def widget_request_chart():
|
||||
"""HTMX dashboard widget: Chart.js request rate + error % over time."""
|
||||
from datetime import timedelta
|
||||
hours = max(1, min(24, int(request.args.get("hours", 6) or 6)))
|
||||
widget_id = request.args.get("wid", "0")
|
||||
|
||||
since = datetime.now(timezone.utc) - timedelta(hours=hours)
|
||||
b_secs = max(60, (hours * 3600) // 80) # ~80 buckets
|
||||
|
||||
bucket_col = (
|
||||
cast(func.extract('epoch', TraefikMetric.scraped_at), Integer) / b_secs
|
||||
).label("bucket")
|
||||
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
result = await db.execute(
|
||||
select(
|
||||
func.avg(TraefikMetric.request_rate).label("req_rate"),
|
||||
func.avg(TraefikMetric.error_rate_5xx_pct).label("err_pct"),
|
||||
func.min(TraefikMetric.scraped_at).label("scraped_at"),
|
||||
bucket_col,
|
||||
)
|
||||
.where(TraefikMetric.scraped_at >= since)
|
||||
.group_by(bucket_col)
|
||||
.order_by(bucket_col)
|
||||
)
|
||||
history = result.all()
|
||||
|
||||
labels = list(range(len(history)))
|
||||
req_rates = [round(r.req_rate or 0, 3) for r in history]
|
||||
err_pcts = [round(r.err_pct or 0, 2) for r in history]
|
||||
|
||||
return await render_template(
|
||||
"traefik/widget_request_chart.html",
|
||||
labels_json=json.dumps(labels),
|
||||
req_rate_json=json.dumps(req_rates),
|
||||
error_rate_json=json.dumps(err_pcts),
|
||||
widget_id=widget_id,
|
||||
hours=hours,
|
||||
)
|
||||
|
||||
|
||||
@traefik_bp.get("/traffic")
|
||||
@require_role(UserRole.viewer)
|
||||
async def traffic():
|
||||
|
||||
Reference in New Issue
Block a user