Adding the new rewrite

This commit is contained in:
Ketan Patel committed 2026-04-04 13:53:57 -04:00
1 parent 4aca9f08d1
commit 9b48b01d1d
62 files changed
+4510 -1406

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block title %}Alerts — UPS{% endblock %}
{% block content %}
<div class="page-header">
<h1>Alerts</h1>
<div class="filters">
<select id="filter-severity"><option value="">All Severities</option><option value="CRITICAL">CRITICAL</option><option value="WARNING">WARNING</option><option value="INFO">INFO</option></select>
<select id="filter-ups"><option value="">All UPS</option></select>
<select id="filter-view"><option value="active">Active</option><option value="history">History (30d)</option></select>
<button id="refresh-btn" class="btn btn-secondary">Refresh</button>
</div>
</div>
<div class="main-content">
<table class="data-table">
<thead><tr><th>Time</th><th>Severity</th><th>UPS</th><th>Code</th><th>Message</th><th>Acked</th><th></th></tr></thead>
<tbody id="alerts-tbody"><tr><td colspan="7">Loading...</td></tr></tbody>
</table>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/alerts.js"></script>
{% endblock %}
+35
View File
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}UPS Dashboard{% endblock %}</title>
<meta name="csrf-token" content="{{ request.cookies.get('csrf_token', '') }}" />
<link rel="stylesheet" href="/static/css/base.css" />
<link rel="stylesheet" href="/static/css/style.css" />
{% block head_extra %}{% endblock %}
</head>
<body>
<header class="site-header">
<div class="brand"><a href="/">⚡ APC UPS Dashboard</a></div>
<nav class="main-nav">
<a href="/" class="{% if active_nav == 'dashboard' %}active{% endif %}">Dashboard</a>
<a href="/events" class="{% if active_nav == 'events' %}active{% endif %}">Events</a>
<a href="/alerts" class="{% if active_nav == 'alerts' %}active{% endif %}">Alerts</a>
<a href="/config" class="{% if active_nav == 'config' %}active{% endif %}">UPS Config</a>
<a href="/settings" class="{% if active_nav == 'settings' %}active{% endif %}">Settings</a>
</nav>
<div class="user-menu">
{% if current_user %}
<span class="username">{{ current_user }}</span>
<button type="button" id="logout-btn" class="btn-logout">Logout</button>
{% endif %}
</div>
</header>
<main class="site-main">
{% block content %}{% endblock %}
</main>
<script src="/static/js/app.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
+79 -103
View File
@@ -1,112 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>UPS Configuration</title>
<link rel="stylesheet" href="/static/css/style.css" />
<link rel="stylesheet" href="/static/css/config.css" />
</head>
<body>
<div class="header">
<h1>UPS Configuration</h1>
<nav>
<a href="/">← Back to Dashboard</a>
<button id="add-ups-btn" class="btn btn-primary">Add UPS</button>
</nav>
</div>
{% extends "base.html" %}
{% block title %}UPS Configuration{% endblock %}
{% block head_extra %}
<link rel="stylesheet" href="/static/css/config.css" />
{% endblock %}
{% block content %}
<div class="page-header">
<h1>UPS Configuration</h1>
<button id="add-ups-btn" class="btn btn-primary">Add UPS</button>
</div>
<div class="main-content">
<div id="ups-list" class="ups-list">
<!-- UPS configurations will be loaded here -->
<div class="main-content">
<div id="ups-list" class="ups-list"></div>
</div>
<div id="ups-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">Add UPS</h2>
<span class="close">&times;</span>
</div>
</div>
<!-- Add/Edit UPS Modal -->
<div id="ups-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">Add UPS</h2>
<span class="close">&times;</span>
<form id="ups-form">
<div class="form-group">
<label for="ups-name">Name *</label>
<input type="text" id="ups-name" name="name" required pattern="[A-Za-z0-9_-]{1,32}" maxlength="32">
<small>1-32 alphanumeric, underscore, dash</small>
</div>
<form id="ups-form">
<div class="form-group">
<label for="ups-name">Name *</label>
<input type="text" id="ups-name" name="name" required>
<small>Friendly name for the UPS</small>
</div>
<div class="form-group">
<label for="ups-host">Host/IP *</label>
<input type="text" id="ups-host" name="host" required>
<small>IP address or hostname of the apcupsd server</small>
</div>
<div class="form-group">
<label for="ups-port">Port</label>
<input type="number" id="ups-port" name="port" value="3551">
<small>apcupsd NIS port (default: 3551)</small>
</div>
<div class="form-group">
<label for="ups-interval">Polling Interval (seconds)</label>
<input type="number" id="ups-interval" name="interval_seconds" value="30" min="1">
<small>How often to poll the UPS for data</small>
</div>
<h3>Alert Thresholds</h3>
<div class="form-group">
<label for="ups-loadpct">High Load Alert (%)</label>
<input type="number" id="ups-loadpct" name="alert_loadpct_high" min="0" max="100" step="0.1">
<small>Trigger alert when load percentage reaches this value</small>
</div>
<div class="form-group">
<label for="ups-bcharge">Low Battery Alert (%)</label>
<input type="number" id="ups-bcharge" name="alert_bcharge_low" min="0" max="100" step="0.1">
<small>Trigger alert when battery charge drops to this value</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="ups-onbattery" name="alert_on_battery">
Alert when on battery power
</label>
</div>
<div class="form-group">
<label for="ups-runtime">Low Runtime Alert (minutes)</label>
<input type="number" id="ups-runtime" name="alert_runtime_low_minutes" min="0" step="0.1">
<small>Trigger alert when remaining runtime drops to this value</small>
</div>
<div class="form-actions">
<button type="button" id="test-connection-btn" class="btn btn-secondary">Test Connection</button>
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" id="cancel-btn" class="btn btn-secondary">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Confirmation Dialog -->
<div id="confirm-modal" class="modal">
<div class="modal-content small">
<div class="modal-header">
<h2>Confirm Action</h2>
<span class="close">&times;</span>
<div class="form-group">
<label for="ups-host">Host/IP *</label>
<input type="text" id="ups-host" name="host" required>
<small>IP or hostname of the apcupsd server</small>
</div>
<div class="form-group">
<label for="ups-port">Port</label>
<input type="number" id="ups-port" name="port" value="3551" min="1" max="65535">
</div>
<div class="form-group">
<label for="ups-interval">Polling Interval (seconds)</label>
<input type="number" id="ups-interval" name="interval_seconds" value="30" min="5" max="3600">
</div>
<h3>Alert Thresholds</h3>
<div class="form-group">
<label for="ups-loadpct">High Load Alert (%)</label>
<input type="number" id="ups-loadpct" name="alert_loadpct_high" min="0" max="100" step="0.1">
</div>
<div class="form-group">
<label for="ups-bcharge">Low Battery Alert (%)</label>
<input type="number" id="ups-bcharge" name="alert_bcharge_low" min="0" max="100" step="0.1">
</div>
<div class="form-group">
<label><input type="checkbox" id="ups-onbattery" name="alert_on_battery"> Alert when on battery power</label>
</div>
<div class="form-group">
<label for="ups-runtime">Low Runtime Alert (minutes)</label>
<input type="number" id="ups-runtime" name="alert_runtime_low_minutes" min="0" step="0.1">
</div>
<div class="form-group">
<label for="ups-itemp">High Temperature Alert (°C)</label>
<input type="number" id="ups-itemp" name="alert_itemp_high" min="0" max="120" step="0.5">
</div>
<p id="confirm-message"></p>
<div class="form-actions">
<button id="confirm-yes" class="btn btn-danger">Yes</button>
<button id="confirm-no" class="btn btn-secondary">No</button>
<button type="button" id="test-connection-btn" class="btn btn-secondary">Test Connection</button>
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" id="cancel-btn" class="btn btn-secondary">Cancel</button>
</div>
</form>
</div>
</div>
<div id="confirm-modal" class="modal">
<div class="modal-content small">
<div class="modal-header">
<h2>Confirm Action</h2>
<span class="close">&times;</span>
</div>
<p id="confirm-message"></p>
<div class="form-actions">
<button id="confirm-yes" class="btn btn-danger">Yes</button>
<button id="confirm-no" class="btn btn-secondary">No</button>
</div>
</div>
</div>
<!-- Toast notifications -->
<div id="toast-container"></div>
<script src="/static/js/config.js"></script>
</body>
</html>
<div id="toast-container"></div>
{% endblock %}
{% block scripts %}
<script src="/static/js/config.js"></script>
{% endblock %}
+51 -43
View File
@@ -1,46 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>UPS Dashboard</title>
<link rel="stylesheet" href="/static/css/style.css" />
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<div class="header">
<h1>APC UPS Dashboard</h1>
<nav>
<a href="/config" class="config-link">⚙️ Configuration</a>
</nav>
{% extends "base.html" %}
{% block title %}Dashboard — UPS{% endblock %}
{% block head_extra %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% endblock %}
{% block content %}
<section class="fleet-overview" id="fleet-overview">
<div class="fleet-card">
<div class="fleet-stat">
<span class="fleet-stat-label">Total UPS</span>
<span class="fleet-stat-value" id="fleet-total">-</span>
</div>
<div class="fleet-stat"><span class="fleet-stat-label">Online</span><span class="fleet-stat-value online" id="fleet-online">-</span></div>
<div class="fleet-stat"><span class="fleet-stat-label">On Battery</span><span class="fleet-stat-value onbatt" id="fleet-onbatt">-</span></div>
<div class="fleet-stat"><span class="fleet-stat-label">Offline</span><span class="fleet-stat-value offline" id="fleet-offline">-</span></div>
<div class="fleet-stat"><span class="fleet-stat-label">Total Load (W)</span><span class="fleet-stat-value" id="fleet-watts">-</span></div>
<div class="fleet-stat"><span class="fleet-stat-label">Min Runtime</span><span class="fleet-stat-value" id="fleet-min-runtime">-</span></div>
</div>
<div id="ups-cards" data-ui='{{ ui_cfg | tojson }}'>
{% for ups in ups_list %}
<div class="card" id="card-{{ ups.name }}">
<div class="card-header">
<h2>{{ ups.name }}</h2>
<div class="ups-connection" data-field-conn>{{ ups.host }}:{{ ups.port }}</div>
<button class="add-metric-btn" data-add-metric title="Add metric tile">+</button>
<button class="reset-layout-btn" data-reset-layout title="Reset tile layout">⟳</button>
</div>
<div class="metrics">
<div class="highline">
<span class="ups-status unknown" data-field="STATUS">-</span>
<span class="badge" data-field="RUNTIME_MINUTES" data-badge="runtime">- m</span>
<span class="badge" data-field="DERIVED_WATTS" data-badge="watts">- W</span>
<span class="badge" data-field="HEADROOM_PCT" data-badge="headroom">- headroom</span>
</div>
<div class="tile-grid" data-tile-grid></div>
</div>
<div class="ups-footer" data-ups-footer></div>
</section>
<div id="ups-cards" data-ui='{{ ui_cfg | tojson }}'>
{% for ups in ups_list %}
<div class="card" id="card-{{ ups.name }}">
<div class="card-header">
<h2>
<span class="state-dot" data-state-dot></span>
{{ ups.name }}
</h2>
<div class="ups-connection" data-field-conn>{{ ups.host }}:{{ ups.port }}</div>
<button class="reset-layout-btn" data-reset-layout title="Reset tile layout">⟳</button>
</div>
{% endfor %}
{% if not ups_list %}
<div class="empty-state">
<h3>No UPS Configured</h3>
<p>Get started by <a href="/config">adding a UPS configuration</a>.</p>
<div class="metrics">
<div class="highline">
<span class="ups-status unknown" data-field="STATUS">-</span>
<span class="badge" data-field="RUNTIME_MINUTES" data-badge="runtime">- m</span>
<span class="badge" data-field="DERIVED_WATTS" data-badge="watts">- W</span>
<span class="badge" data-field="HEADROOM_PCT" data-badge="headroom">- headroom</span>
</div>
<div class="tile-grid" data-tile-grid></div>
</div>
{% endif %}
</div>
<script src="/static/js/dashboard.js?v=1"></script>
</body>
</html>
<div class="ups-footer" data-ups-footer></div>
</div>
{% endfor %}
{% if not ups_list %}
<div class="empty-state">
<h3>No UPS Configured</h3>
<p>Get started by <a href="/config">adding a UPS configuration</a>.</p>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/dashboard.js?v=2"></script>
{% endblock %}
+31
View File
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body style="font-family: -apple-system, Segoe UI, Arial, sans-serif; margin: 0; padding: 0; background: #f4f4f4;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background: #f4f4f4; padding: 24px 0;">
<tr>
<td align="center">
<table role="presentation" width="560" cellpadding="0" cellspacing="0" style="background: #ffffff; border-radius: 6px; overflow: hidden; box-shadow: 0 2px 6px rgba(0,0,0,.08);">
<tr>
<td style="background: {{ color }}; color: #ffffff; padding: 16px 20px; font-size: 18px; font-weight: 600;">
[{{ top_severity }}] {{ ups_name }}
</td>
</tr>
<tr>
<td style="padding: 20px;">
<p style="margin: 0 0 12px; color: #333;">The following alerts were triggered:</p>
<ul style="margin: 0 0 16px; padding-left: 20px; color: #333;">
{% for a in alerts %}
<li style="margin-bottom: 6px;"><strong>[{{ a.severity }}]</strong> {{ a.message }}</li>
{% endfor %}
</ul>
<p style="margin: 0 0 8px;"><a href="{{ dashboard_url }}" style="color: #2980b9;">Open dashboard</a></p>
<p style="margin: 0; color: #888; font-size: 12px;">Sent at {{ sent_at }}</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body style="font-family: -apple-system, Segoe UI, Arial, sans-serif;">
<h2>UPS Daily Summary</h2>
<p>Sent at: {{ sent_at }}</p>
<table cellpadding="6" cellspacing="0" border="1" style="border-collapse: collapse;">
<tr><th>UPS</th><th>Status</th><th>Load %</th><th>Battery %</th><th>kWh today</th><th>Alerts (24h)</th></tr>
{% for row in summary.ups_rows %}
<tr>
<td>{{ row.name }}</td>
<td>{{ row.status }}</td>
<td>{{ row.loadpct }}</td>
<td>{{ row.bcharge }}</td>
<td>{{ row.kwh }}</td>
<td>{{ row.alerts_count }}</td>
</tr>
{% endfor %}
</table>
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block title %}Events — UPS{% endblock %}
{% block content %}
<div class="page-header">
<h1>Event Log</h1>
<div class="filters">
<select id="filter-ups"><option value="">All UPS</option></select>
<select id="filter-kind"><option value="">All Events</option><option value="STATUS">STATUS</option><option value="XFER">XFER</option></select>
<button id="refresh-btn" class="btn btn-secondary">Refresh</button>
</div>
</div>
<div class="main-content">
<table class="data-table">
<thead><tr><th>Time</th><th>UPS</th><th>Type</th><th>Detail</th></tr></thead>
<tbody id="events-tbody"><tr><td colspan="4">Loading...</td></tr></tbody>
</table>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/events.js"></script>
{% endblock %}
+49
View File
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Login — UPS Dashboard</title>
<link rel="stylesheet" href="/static/css/base.css" />
<link rel="stylesheet" href="/static/css/login.css" />
</head>
<body class="auth-body">
<div class="auth-card">
<h1>UPS Dashboard</h1>
<form id="login-form">
<div class="form-field">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autocomplete="username" />
</div>
<div class="form-field">
<label for="password">Password</label>
<input type="password" id="password" name="password" required autocomplete="current-password" />
</div>
<button type="submit" class="btn-primary btn-block">Sign in</button>
<div id="login-error" class="login-error" hidden></div>
</form>
</div>
<script>
document.getElementById('login-form').addEventListener('submit', async (e) => {
e.preventDefault();
const errBox = document.getElementById('login-error');
errBox.hidden = true;
const resp = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: document.getElementById('username').value,
password: document.getElementById('password').value,
}),
});
if (resp.ok) {
window.location.href = '/';
} else {
const err = await resp.json().catch(() => ({}));
errBox.textContent = err.detail || 'Login failed';
errBox.hidden = false;
}
});
</script>
</body>
</html>
+44
View File
@@ -0,0 +1,44 @@
{% extends "base.html" %}
{% block title %}Settings — UPS{% endblock %}
{% block content %}
<div class="page-header">
<h1>Settings</h1>
</div>
<div class="main-content">
<section class="settings-section">
<h2>UI / Feature Flags</h2>
<form id="ui-form">
<label><input type="checkbox" name="show_events"> Show events tile</label>
<label><input type="checkbox" name="show_energy"> Show energy</label>
<label><input type="checkbox" name="color_badges"> Colorize badges</label>
<label><input type="checkbox" name="enable_transfer_burst_alert"> Transfer-burst alerts</label>
<label><input type="checkbox" name="enable_voltage_deviation_alert"> Voltage-deviation alerts</label>
<label>Energy cost per kWh ($): <input type="number" step="0.001" min="0" name="energy_cost_per_kwh"></label>
<button type="submit" class="btn btn-primary">Save UI Settings</button>
</form>
</section>
<section class="settings-section">
<h2>SMTP / Email Alerts</h2>
<form id="smtp-form">
<label>Host <input type="text" name="host" required></label>
<label>Port <input type="number" name="port" min="1" max="65535" required></label>
<label>Username <input type="text" name="username"></label>
<label><input type="checkbox" name="use_tls"> Use STARTTLS</label>
<label><input type="checkbox" name="use_ssl"> Use SSL</label>
<label>From <input type="email" name="from_addr"></label>
<label>To (comma-separated) <input type="text" name="to_addrs"></label>
<label>Subject Prefix <input type="text" name="subject_prefix" value="[UPS]"></label>
<label>Silent hours start (0-23, optional) <input type="number" name="silent_hours_start" min="0" max="23"></label>
<label>Silent hours end (0-23, optional) <input type="number" name="silent_hours_end" min="0" max="23"></label>
<label>Daily summary hour (0-23, optional) <input type="number" name="daily_summary_hour" min="0" max="23"></label>
<p class="muted">Password is read from the <code>SMTP_PASSWORD</code> environment variable and never stored in Redis.</p>
<button type="submit" class="btn btn-primary">Save SMTP</button>
<button type="button" id="test-smtp-btn" class="btn btn-secondary">Send Test Email</button>
</form>
</section>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/settings.js"></script>
{% endblock %}
+62
View File
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Setup — UPS Dashboard</title>
<link rel="stylesheet" href="/static/css/base.css" />
<link rel="stylesheet" href="/static/css/login.css" />
</head>
<body class="auth-body">
<div class="auth-card wide">
<h1>First-run Setup</h1>
<p class="muted">Create the admin account. Passwords must be at least 8 characters.</p>
<form id="setup-form">
<div class="form-field">
<label for="username">Admin username</label>
<input type="text" id="username" name="username" required minlength="3" autocomplete="username" />
</div>
<div class="form-field">
<label for="password">Password</label>
<input type="password" id="password" name="password" required minlength="8" autocomplete="new-password" />
</div>
<div class="form-field">
<label for="password2">Confirm password</label>
<input type="password" id="password2" name="password2" required minlength="8" />
</div>
<button type="submit" class="btn-primary btn-block">Create admin account</button>
<div id="setup-error" class="login-error" hidden></div>
</form>
<p class="muted" style="margin-top:16px;">After setup you'll be redirected to the dashboard. Configure UPS devices and SMTP from the nav.</p>
</div>
<script>
document.getElementById('setup-form').addEventListener('submit', async (e) => {
e.preventDefault();
const errBox = document.getElementById('setup-error');
errBox.hidden = true;
const pw = document.getElementById('password').value;
const pw2 = document.getElementById('password2').value;
if (pw !== pw2) {
errBox.textContent = 'Passwords do not match';
errBox.hidden = false;
return;
}
const resp = await fetch('/api/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: document.getElementById('username').value,
password: pw,
}),
});
if (resp.ok) {
window.location.href = '/';
} else {
const err = await resp.json().catch(() => ({}));
errBox.textContent = err.detail || 'Setup failed';
errBox.hidden = false;
}
});
</script>
</body>
</html>