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:
@@ -12,12 +12,25 @@ tags:
|
||||
- nut
|
||||
|
||||
config:
|
||||
# ── Managed NUT (run NUT inside this container) ────────────────────────────
|
||||
# Enable to have FabledScryer manage the NUT daemon for you — no separate NUT
|
||||
# install required. Connects to your UPS over the network (SNMP or XML/HTTP).
|
||||
# Changes take effect on the next container restart.
|
||||
nut_managed: false
|
||||
nut_ups_host: "" # IP/hostname of UPS management card (required when nut_managed=true)
|
||||
nut_driver: "snmp-ups" # snmp-ups (most UPS brands) or netxml-ups (Eaton XML/HTTP)
|
||||
nut_snmp_community: "public" # SNMP community string (snmp-ups only)
|
||||
nut_snmp_version: "v1" # v1 or v2c (snmp-ups only)
|
||||
# ── NUT connection ─────────────────────────────────────────────────────────
|
||||
# When nut_managed=true these default to localhost (the managed instance).
|
||||
# Point at an external NUT server if nut_managed=false.
|
||||
nut_host: "localhost"
|
||||
nut_port: 3493
|
||||
ups_name: "ups"
|
||||
nut_username: "" # leave blank if NUT is configured without auth
|
||||
nut_password: ""
|
||||
poll_interval_seconds: 30
|
||||
# ── Ansible shutdown automation ────────────────────────────────────────────
|
||||
shutdown_after_seconds: 300 # seconds on battery before triggering shutdown (0 = disabled)
|
||||
shutdown_source: "" # Ansible source name containing the shutdown playbook
|
||||
shutdown_playbook: "" # relative path within source, e.g. "shutdown.yml"
|
||||
|
||||
@@ -123,3 +123,50 @@ async def widget():
|
||||
on_battery_since=_on_battery_since,
|
||||
shutdown_triggered=_shutdown_triggered,
|
||||
)
|
||||
|
||||
|
||||
@ups_bp.get("/widget/history")
|
||||
@require_role(UserRole.viewer)
|
||||
async def widget_history():
|
||||
"""HTMX dashboard widget: Chart.js multi-line history (battery%, load%, input voltage)."""
|
||||
from datetime import timedelta
|
||||
import json as _json
|
||||
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.strftime('%s', UpsStatus.scraped_at), Integer) / b_secs
|
||||
).label("bucket")
|
||||
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
result = await db.execute(
|
||||
select(
|
||||
func.avg(UpsStatus.battery_charge_pct).label("battery"),
|
||||
func.avg(UpsStatus.load_pct).label("load"),
|
||||
func.avg(UpsStatus.input_voltage).label("voltage"),
|
||||
func.min(UpsStatus.scraped_at).label("scraped_at"),
|
||||
bucket_col,
|
||||
)
|
||||
.where(UpsStatus.scraped_at >= since)
|
||||
.group_by(bucket_col)
|
||||
.order_by(bucket_col)
|
||||
)
|
||||
history = result.all()
|
||||
|
||||
labels = list(range(len(history)))
|
||||
battery = [round(r.battery or 0, 1) for r in history]
|
||||
load = [round(r.load or 0, 1) for r in history]
|
||||
voltage = [round(r.voltage or 0, 1) for r in history]
|
||||
|
||||
return await render_template(
|
||||
"ups/widget_history.html",
|
||||
labels_json=_json.dumps(labels),
|
||||
battery_json=_json.dumps(battery),
|
||||
load_json=_json.dumps(load),
|
||||
voltage_json=_json.dumps(voltage),
|
||||
widget_id=widget_id,
|
||||
hours=hours,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
{# ups/widget_history.html — Chart.js multi-line: battery%, load%, input voltage #}
|
||||
{% if not labels_json or labels_json == '[]' %}
|
||||
<div style="color:var(--text-muted);font-size:0.85rem;padding:0.5rem 0;">No UPS history for last {{ hours }}h.</div>
|
||||
{% else %}
|
||||
<div style="position:relative;height:160px;">
|
||||
<canvas id="chart-ups-hist-{{ widget_id }}"></canvas>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
var ctx = document.getElementById("chart-ups-hist-{{ widget_id }}");
|
||||
if (!ctx || !window.Chart) return;
|
||||
var existing = Chart.getChart(ctx);
|
||||
if (existing) existing.destroy();
|
||||
new Chart(ctx.getContext("2d"), {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: {{ labels_json | safe }},
|
||||
datasets: [
|
||||
{
|
||||
label: "Battery %",
|
||||
data: {{ battery_json | safe }},
|
||||
borderColor: "#26d96b",
|
||||
fill: false,
|
||||
tension: 0.3,
|
||||
pointRadius: 0,
|
||||
borderWidth: 1.5,
|
||||
yAxisID: "y",
|
||||
},
|
||||
{
|
||||
label: "Load %",
|
||||
data: {{ load_json | safe }},
|
||||
borderColor: "#e07828",
|
||||
fill: false,
|
||||
tension: 0.3,
|
||||
pointRadius: 0,
|
||||
borderWidth: 1.5,
|
||||
yAxisID: "y",
|
||||
},
|
||||
{
|
||||
label: "Input V",
|
||||
data: {{ voltage_json | safe }},
|
||||
borderColor: "#7c5ef4",
|
||||
fill: false,
|
||||
tension: 0.3,
|
||||
pointRadius: 0,
|
||||
borderWidth: 1.5,
|
||||
yAxisID: "y2",
|
||||
},
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: { color: "#6858a0", boxWidth: 10, font: { size: 10 } }
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: { display: false },
|
||||
y: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: { color: "rgba(30,30,62,0.8)" },
|
||||
ticks: { color: "#6858a0", font: { size: 10 } },
|
||||
title: { display: true, text: "%", color: "#6858a0", font: { size: 9 } },
|
||||
},
|
||||
y2: {
|
||||
position: "right",
|
||||
grid: { drawOnChartArea: false },
|
||||
ticks: { color: "#7c5ef4", font: { size: 10 } },
|
||||
title: { display: true, text: "V", color: "#7c5ef4", font: { size: 9 } },
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<div style="font-size:0.72rem;color:var(--text-muted);margin-top:0.35rem;text-align:right;">
|
||||
last {{ hours }}h
|
||||
</div>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user