truenas-burnin/app/templates/settings.html
Brandon Walter b73b5251ae Initial commit — TrueNAS Burn-In Dashboard v0.5.0
Full-stack burn-in orchestration dashboard (Stages 1–6d complete):
FastAPI backend, SQLite/WAL, SSE live dashboard, mock TrueNAS server,
SMTP/webhook notifications, batch burn-in, settings UI, audit log,
stats page, cancel SMART/burn-in, drag-to-reorder stages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 00:08:29 -05:00

303 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "layout.html" %}
{% block title %}TrueNAS Burn-In — Settings{% endblock %}
{% block content %}
<div class="page-toolbar">
<h1 class="page-title">Settings</h1>
<div class="toolbar-right">
<a class="btn-export" href="/docs" target="_blank" rel="noopener">API Docs</a>
</div>
</div>
<p class="page-subtitle">
Changes take effect immediately. Settings marked
<span class="badge-restart">restart required</span> must be changed in <code>.env</code>.
</p>
<form id="settings-form" autocomplete="off">
<div class="settings-two-col">
<!-- LEFT: Email / SMTP + Webhook stacked -->
<div class="settings-left-col">
<div class="settings-card">
<div class="settings-card-header">
<span class="settings-card-title">Email (SMTP)</span>
{% if smtp_enabled %}
<span class="chip chip-passed" style="font-size:10px">Enabled</span>
{% else %}
<span class="chip chip-unknown" style="font-size:10px">Disabled — set Host to enable</span>
{% endif %}
</div>
<!-- Compact horizontal field grid -->
<div class="sf-fields">
<!-- Test connection row — full width -->
<div class="sf-full sf-row-test" style="margin-bottom:4px">
<button type="button" id="test-smtp-btn" class="btn-secondary">Test Connection</button>
<span id="smtp-test-result" class="settings-test-result" style="display:none"></span>
</div>
<label for="smtp_host">Host</label>
<input class="sf-input" id="smtp_host" name="smtp_host" type="text"
value="{{ editable.smtp_host }}" placeholder="smtp.example.com">
<label for="smtp_ssl_mode">Mode</label>
<div class="sf-inline-group">
<select class="sf-select" id="smtp_ssl_mode" name="smtp_ssl_mode">
<option value="starttls" {% if editable.smtp_ssl_mode == 'starttls' %}selected{% endif %}>STARTTLS (587)</option>
<option value="ssl" {% if editable.smtp_ssl_mode == 'ssl' %}selected{% endif %}>SSL / TLS (465)</option>
<option value="plain" {% if editable.smtp_ssl_mode == 'plain' %}selected{% endif %}>Plain (25)</option>
</select>
<span class="sf-label-sm">Timeout</span>
<input class="sf-input sf-input-xs" id="smtp_timeout" name="smtp_timeout"
type="number" min="5" max="300" value="{{ editable.smtp_timeout }}" style="width:52px">
</div>
<label for="smtp_user">Username</label>
<input class="sf-input" id="smtp_user" name="smtp_user" type="text"
value="{{ editable.smtp_user }}" autocomplete="off">
<label for="smtp_password">Password</label>
<input class="sf-input" id="smtp_password" name="smtp_password" type="password"
placeholder="leave blank to keep existing" autocomplete="new-password">
<label for="smtp_from">From</label>
<input class="sf-input" id="smtp_from" name="smtp_from" type="text"
value="{{ editable.smtp_from }}" placeholder="burnin@example.com">
<label for="smtp_to">To</label>
<input class="sf-input" id="smtp_to" name="smtp_to" type="text"
value="{{ editable.smtp_to }}" placeholder="you@example.com">
</div>
</div>
<!-- Webhook -->
<div class="settings-card">
<div class="settings-card-header">
<span class="settings-card-title">Webhook</span>
</div>
<div class="sf-fields">
<label for="webhook_url">URL</label>
<div>
<input class="sf-input" id="webhook_url" name="webhook_url" type="text"
value="{{ editable.webhook_url }}" placeholder="https://ntfy.sh/your-topic">
<span class="sf-hint" style="margin-top:3px">POST JSON on burnin_passed / burnin_failed. ntfy.sh, Slack, Discord, n8n. Leave blank to disable.</span>
</div>
</div>
</div>
</div><!-- /left col -->
<!-- RIGHT column: Notifications + Behavior -->
<div class="settings-right-col">
<!-- Notifications -->
<div class="settings-card">
<div class="settings-card-header">
<span class="settings-card-title">Notifications</span>
</div>
<div class="sf-toggle-row">
<div>
<div class="sf-label">Daily Report</div>
<div class="sf-hint">Full drive status email each day</div>
</div>
<label class="toggle">
<input type="checkbox" name="smtp_daily_report_enabled" id="smtp_daily_report_enabled"
{% if editable.smtp_daily_report_enabled %}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
<div class="sf-row sf-row-inline" id="report-hour-wrap"
{% if not editable.smtp_daily_report_enabled %}style="opacity:.4;pointer-events:none"{% endif %}>
<div>
<label class="sf-label" for="smtp_report_hour">Report Hour (023 local)</label>
<input class="sf-input sf-input-xs" id="smtp_report_hour" name="smtp_report_hour"
type="number" min="0" max="23" value="{{ editable.smtp_report_hour }}">
</div>
</div>
<div class="sf-divider"></div>
<div class="sf-toggle-row">
<div>
<div class="sf-label">Alert on Failure</div>
<div class="sf-hint">Immediate email when a burn-in fails</div>
</div>
<label class="toggle">
<input type="checkbox" name="smtp_alert_on_fail" id="smtp_alert_on_fail"
{% if editable.smtp_alert_on_fail %}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
<div class="sf-toggle-row">
<div>
<div class="sf-label">Alert on Pass</div>
<div class="sf-hint">Immediate email when a burn-in passes</div>
</div>
<label class="toggle">
<input type="checkbox" name="smtp_alert_on_pass" id="smtp_alert_on_pass"
{% if editable.smtp_alert_on_pass %}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
</div>
<!-- Behavior -->
<div class="settings-card">
<div class="settings-card-header">
<span class="settings-card-title">Burn-In Behavior</span>
</div>
<div class="sf-row">
<label class="sf-label" for="max_parallel_burnins">Max Parallel Burn-Ins</label>
<input class="sf-input sf-input-xs" id="max_parallel_burnins" name="max_parallel_burnins"
type="number" min="1" max="16" value="{{ editable.max_parallel_burnins }}">
<span class="sf-hint">How many jobs can run at the same time</span>
</div>
<div class="sf-row">
<label class="sf-label" for="stuck_job_hours">Stuck Job Threshold (hours)</label>
<input class="sf-input sf-input-xs" id="stuck_job_hours" name="stuck_job_hours"
type="number" min="1" max="168" value="{{ editable.stuck_job_hours }}">
<span class="sf-hint">Jobs running longer than this → auto-marked unknown</span>
</div>
</div>
</div><!-- /right col -->
</div><!-- /two-col -->
<!-- Save row -->
<div class="settings-save-bar">
<button type="submit" class="btn-primary" id="save-btn">Save Settings</button>
<button type="button" class="btn-secondary" id="cancel-settings-btn">Cancel</button>
<span id="save-result" class="settings-test-result" style="display:none"></span>
</div>
</form>
<!-- System (read-only) -->
<div class="settings-card settings-card-readonly">
<div class="settings-card-header">
<span class="settings-card-title">System</span>
<span class="badge-restart">restart required to change</span>
</div>
<div class="sf-readonly-grid">
<div class="sf-ro-row">
<span class="sf-ro-label">TrueNAS URL</span>
<span class="sf-ro-value mono">{{ readonly.truenas_base_url }}</span>
</div>
<div class="sf-ro-row">
<span class="sf-ro-label">Verify TLS</span>
<span class="sf-ro-value">{{ 'Yes' if readonly.truenas_verify_tls else 'No' }}</span>
</div>
<div class="sf-ro-row">
<span class="sf-ro-label">Poll Interval</span>
<span class="sf-ro-value mono">{{ readonly.poll_interval_seconds }}s</span>
</div>
<div class="sf-ro-row">
<span class="sf-ro-label">Stale Threshold</span>
<span class="sf-ro-value mono">{{ readonly.stale_threshold_seconds }}s</span>
</div>
<div class="sf-ro-row">
<span class="sf-ro-label">IP Allowlist</span>
<span class="sf-ro-value mono">{{ readonly.allowed_ips }}</span>
</div>
<div class="sf-ro-row">
<span class="sf-ro-label">Log Level</span>
<span class="sf-ro-value mono">{{ readonly.log_level }}</span>
</div>
</div>
</div>
<script>
(function () {
// Dim report-hour when daily report disabled
var dailyCb = document.getElementById('smtp_daily_report_enabled');
var hourWrap = document.getElementById('report-hour-wrap');
if (dailyCb && hourWrap) {
dailyCb.addEventListener('change', function () {
hourWrap.style.opacity = dailyCb.checked ? '' : '0.4';
hourWrap.style.pointerEvents = dailyCb.checked ? '' : 'none';
});
}
function showResult(el, ok, msg) {
el.style.display = 'inline-flex';
el.className = 'settings-test-result ' + (ok ? 'result-ok' : 'result-err');
el.textContent = (ok ? '✓ ' : '✕ ') + msg;
}
function collectForm() {
var form = document.getElementById('settings-form');
var data = {};
for (var i = 0; i < form.elements.length; i++) {
var el = form.elements[i];
if (!el.name || el.type === 'submit' || el.type === 'button') continue;
data[el.name] = el.type === 'checkbox' ? el.checked : el.value;
}
return data;
}
// Save
var form = document.getElementById('settings-form');
var saveBtn = document.getElementById('save-btn');
var saveResult = document.getElementById('save-result');
form.addEventListener('submit', async function (e) {
e.preventDefault();
saveBtn.disabled = true;
saveBtn.textContent = 'Saving…';
saveResult.style.display = 'none';
try {
var resp = await fetch('/api/v1/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(collectForm()),
});
var data = await resp.json();
if (resp.ok) {
showResult(saveResult, true, 'Saved');
} else {
showResult(saveResult, false, data.detail || 'Save failed');
}
} catch (e) {
showResult(saveResult, false, 'Network error');
} finally {
saveBtn.disabled = false;
saveBtn.textContent = 'Save Settings';
}
});
// Cancel — reload page to restore saved values
var cancelBtn = document.getElementById('cancel-settings-btn');
if (cancelBtn) {
cancelBtn.addEventListener('click', function () {
window.location.reload();
});
}
// Test SMTP
var testBtn = document.getElementById('test-smtp-btn');
var testResult = document.getElementById('smtp-test-result');
testBtn.addEventListener('click', async function () {
testBtn.disabled = true;
testBtn.textContent = 'Testing…';
testResult.style.display = 'none';
try {
var resp = await fetch('/api/v1/settings/test-smtp', { method: 'POST' });
var data = await resp.json();
showResult(testResult, resp.ok, resp.ok ? 'Connection OK' : (data.detail || 'Failed'));
} catch (e) {
showResult(testResult, false, 'Network error');
} finally {
testBtn.disabled = false;
testBtn.textContent = 'Test Connection';
}
});
}());
</script>
{% endblock %}