Files
heartbeat/hbd/server/templates/settings.html
T
2026-04-13 08:45:50 -04:00

491 lines
15 KiB
HTML

<!DOCTYPE html>
<html>
{% include 'head.html' %}
<style>
html, body { overflow: visible; }
.container {
max-width: 960px;
}
h1 { color: #333; margin-bottom: 4px; font-size: 1.5em; }
.subtitle { color: #666; margin-bottom: 24px; font-size: 0.9em; }
/* ---- Sidebar + content layout ---- */
.settings-layout {
display: flex;
gap: 24px;
align-items: flex-start;
}
.settings-sidebar {
width: 180px;
flex-shrink: 0;
position: sticky;
top: 20px;
}
.sidebar-nav a {
display: block;
padding: 6px 10px;
border-radius: 4px;
text-decoration: none;
font-size: 0.85em;
color: #444;
margin-bottom: 2px;
transition: background 0.1s, color 0.1s;
}
.sidebar-nav a:hover { background: #e8eaf6; color: #1a237e; }
.sidebar-nav a.active { background: #e3f2fd; color: #0066cc; font-weight: 600; }
.settings-main { flex: 1; min-width: 0; }
/* ---- Section card ---- */
.section {
background: #fff;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0,0,0,.08);
margin-bottom: 24px;
overflow: hidden;
}
.section-header {
padding: 14px 20px 12px;
border-bottom: 1px solid #eee;
}
.section-title {
font-size: 0.95em;
font-weight: 700;
color: #222;
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 0 0 3px;
}
.section-desc {
font-size: 0.82em;
color: #888;
margin: 0;
}
/* ---- Field rows ---- */
.field-row {
display: flex;
align-items: baseline;
padding: 10px 20px;
border-bottom: 1px solid #f5f5f5;
gap: 16px;
}
.field-row:last-child { border-bottom: none; }
.field-label {
width: 200px;
flex-shrink: 0;
font-size: 0.88em;
font-weight: 500;
color: #444;
}
.field-body { flex: 1; min-width: 0; }
.field-value {
font-size: 0.9em;
color: #222;
word-break: break-all;
}
.field-desc {
font-size: 0.78em;
color: #999;
margin-top: 2px;
}
/* ---- Value type renderers ---- */
.val-boolean {
display: inline-block;
padding: 2px 9px;
border-radius: 10px;
font-size: 0.8em;
font-weight: 600;
}
.val-boolean.on { background: #e8f5e9; color: #2e7d32; }
.val-boolean.off { background: #fce4ec; color: #c62828; }
.val-masked {
font-family: monospace;
color: #bbb;
letter-spacing: 2px;
}
.val-list { display: flex; flex-wrap: wrap; gap: 5px; }
.val-tag {
display: inline-block;
padding: 2px 9px;
background: #e8eaf6;
color: #283593;
border-radius: 10px;
font-size: 0.8em;
}
.val-empty { color: #ccc; font-style: italic; font-size: 0.88em; }
/* ---- Users table ---- */
.mini-table {
width: 100%;
border-collapse: collapse;
font-size: 0.875em;
}
.mini-table th {
background: #f5f5f5;
padding: 7px 12px;
text-align: left;
font-weight: 600;
color: #555;
font-size: 0.82em;
text-transform: uppercase;
letter-spacing: 0.4px;
border-bottom: 1px solid #e0e0e0;
}
.mini-table td {
padding: 7px 12px;
border-bottom: 1px solid #f0f0f0;
color: #333;
vertical-align: middle;
}
.mini-table tbody tr:last-child td { border-bottom: none; }
.mini-table tbody tr:hover { background: #fafafa; }
.badge {
display: inline-block;
padding: 1px 8px;
border-radius: 10px;
font-size: 0.75em;
font-weight: 600;
}
.badge-admin { background: #e8f0fe; color: #1a73e8; }
.badge-user { background: #f1f3f4; color: #666; }
/* ---- Notification channels ---- */
.channel-card {
border: 1px solid #e8eaf6;
border-radius: 6px;
margin: 12px 20px;
overflow: hidden;
}
.channel-header {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 14px;
background: #f8f9ff;
border-bottom: 1px solid #e8eaf6;
}
.channel-name-text { font-weight: 600; font-size: 0.9em; color: #222; }
.ch-type-badge {
padding: 2px 8px;
border-radius: 8px;
font-size: 0.75em;
font-weight: 600;
background: #e8eaf6;
color: #3949ab;
}
.channel-fields { padding: 6px 0; }
.channel-field {
display: flex;
padding: 5px 14px;
font-size: 0.85em;
border-bottom: 1px solid #f5f5f5;
gap: 12px;
}
.channel-field:last-child { border-bottom: none; }
.channel-field-label { width: 130px; flex-shrink: 0; color: #777; }
.channel-field-value { color: #333; word-break: break-all; }
/* ---- Hosts table ---- */
/* ---- Mobile: collapsible sidebar ---- */
.sidebar-toggle {
display: none;
width: 100%;
padding: 8px 12px;
background: #e8eaf6;
border: none;
border-radius: 6px;
font-size: 0.9em;
font-weight: 600;
color: #283593;
cursor: pointer;
text-align: left;
margin-bottom: 16px;
}
.sidebar-toggle::after { content: ' ▾'; float: right; }
.sidebar-toggle.open::after { content: ' ▴'; }
@media (max-width: 640px) {
.sidebar-toggle { display: block; }
.settings-layout { flex-direction: column; gap: 0; }
.settings-sidebar {
width: 100%;
position: static;
margin-bottom: 0;
}
.sidebar-nav {
display: none;
background: white;
border-radius: 6px;
box-shadow: 0 1px 4px rgba(0,0,0,.1);
margin-bottom: 16px;
padding: 4px 0;
}
.sidebar-nav.open { display: block; }
.sidebar-nav a { padding: 10px 16px; font-size: 1em; }
.field-row { flex-direction: column; gap: 4px; }
.field-label { width: 100%; font-size: 0.82em; color: #888; }
}
.host-bool { text-align: center; }
.dot-yes { color: #2e7d32; font-size: 1.1em; }
.dot-no { color: #ddd; font-size: 1.1em; }
</style>
<body>
{% include 'nav.html' %}
<div class="container">
<h1>Settings</h1>
<p class="subtitle">Current server configuration — read from the config file at startup.</p>
<div class="settings-layout">
<!-- Sidebar navigation -->
<nav class="settings-sidebar">
<button class="sidebar-toggle" id="sidebar-toggle" aria-expanded="false">Sections</button>
<div class="sidebar-nav" id="sidebar-nav">
{% for section in sections %}
<a href="#{{ section.id }}" onclick="closeSidebar()">{{ section.title }}</a>
{% endfor %}
</div>
</nav>
<!-- Main content -->
<div class="settings-main">
{% for section in sections %}
<div class="section" id="{{ section.id }}">
<div class="section-header">
<p class="section-title">{{ section.title }}</p>
{% if section.description %}<p class="section-desc">{{ section.description }}</p>{% endif %}
</div>
{# ---- Standard field rows ---- #}
{% for f in section.fields %}
<div class="field-row">
<div class="field-label">{{ f.label }}</div>
<div class="field-body">
{% if f.sensitive %}
<div class="field-value"><span class="val-masked">••••••••</span></div>
{% elif f.type == "boolean" %}
<div class="field-value">
<span class="val-boolean {{ 'on' if f.value else 'off' }}">
{{ 'Enabled' if f.value else 'Disabled' }}
</span>
</div>
{% elif f.type == "list" %}
<div class="field-value">
{% if f.value %}
<span class="val-list">
{% for item in f.value %}<span class="val-tag">{{ item }}</span>{% endfor %}
</span>
{% else %}
<span class="val-empty">None</span>
{% endif %}
</div>
{% elif f.value is none or f.value == "" %}
<div class="field-value"><span class="val-empty">Not set</span></div>
{% else %}
<div class="field-value">{{ f.value }}</div>
{% endif %}
{% if f.description %}
<div class="field-desc">{{ f.description }}</div>
{% endif %}
</div>
</div>
{% endfor %}
{# ---- Users section ---- #}
{% if section.id == "users" and section.users %}
<div style="padding: 0 0 4px;">
<table class="mini-table">
<thead>
<tr>
<th>Username</th>
<th>Full Name</th>
<th>Role</th>
<th>Avatar</th>
<th>Channels</th>
</tr>
</thead>
<tbody>
{% for u in section.users %}
<tr>
<td><strong>{{ u.username }}</strong></td>
<td>{{ u.full_name or '—' }}</td>
<td>
{% if u.admin %}
<span class="badge badge-admin">Admin</span>
{% else %}
<span class="badge badge-user">User</span>
{% endif %}
</td>
<td style="font-size:0.8em; color:#888;">
{% if u.avatar %}{{ u.avatar }}{% else %}—{% endif %}
</td>
<td>
{% if u.notification_channels %}
<span class="val-list">
{% for ch in u.notification_channels %}
<span class="val-tag">{{ ch }}</span>
{% endfor %}
</span>
{% else %}—{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{# ---- Notification channels section ---- #}
{% if section.id == "channels" %}
{% for ch in section.channels %}
<div class="channel-card">
<div class="channel-header">
<span class="channel-name-text">{{ ch.name }}</span>
<span class="ch-type-badge">{{ ch.type_label }}</span>
</div>
<div class="channel-fields">
{% for cf in ch.fields %}
<div class="channel-field">
<span class="channel-field-label">{{ cf.label }}</span>
<span class="channel-field-value">
{% if cf.sensitive %}
<span class="val-masked">••••••••</span>
{% elif cf.value is iterable and cf.value is not string %}
{{ cf.value | join(', ') }}
{% else %}
{{ cf.value }}
{% endif %}
</span>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% if not section.channels %}
<div class="field-row"><span class="val-empty">No notification channels configured.</span></div>
{% endif %}
{% endif %}
{# ---- Hosts section ---- #}
{% if section.id == "hosts" %}
{% if section.hosts %}
<div style="overflow-x: auto;">
<table class="mini-table">
<thead>
<tr>
<th>Host</th>
<th>Watch</th>
<th>DynDNS</th>
<th>Owner</th>
<th>Threshold config</th>
<th>Channels</th>
</tr>
</thead>
<tbody>
{% for h in section.hosts %}
<tr>
<td><strong>{{ h.name }}</strong></td>
<td class="host-bool">
<span class="{{ 'dot-yes' if h.watch else 'dot-no' }}"></span>
</td>
<td class="host-bool">
<span class="{{ 'dot-yes' if h.dyndns else 'dot-no' }}"></span>
</td>
<td>{{ h.owner or '—' }}</td>
<td>{{ h.threshold_config or '—' }}</td>
<td>
{% if h.notification_channels %}
<span class="val-list">
{% for ch in h.notification_channels %}
<span class="val-tag">{{ ch }}</span>
{% endfor %}
</span>
{% else %}—{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="field-row"><span class="val-empty">No hosts defined in config.</span></div>
{% endif %}
{% endif %}
</div>{# /section #}
{% endfor %}
</div>{# /settings-main #}
</div>{# /settings-layout #}
</div>{# /container #}
<script>
// Highlight sidebar link for the section currently in view
const sections = document.querySelectorAll('.section');
const navLinks = document.querySelectorAll('.sidebar-nav a');
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const id = entry.target.id;
navLinks.forEach(a => {
a.classList.toggle('active', a.getAttribute('href') === '#' + id);
});
}
});
}, { threshold: 0.25 });
sections.forEach(s => observer.observe(s));
// Collapsible sidebar on mobile
var sidebarToggle = document.getElementById('sidebar-toggle');
var sidebarNav = document.getElementById('sidebar-nav');
if (sidebarToggle && sidebarNav) {
sidebarToggle.addEventListener('click', function() {
var open = sidebarNav.classList.toggle('open');
sidebarToggle.classList.toggle('open', open);
sidebarToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
});
}
</script>
<script>
function closeSidebar() {
var sidebarNav = document.getElementById('sidebar-nav');
var sidebarToggle = document.getElementById('sidebar-toggle');
if (sidebarNav) { sidebarNav.classList.remove('open'); }
if (sidebarToggle) {
sidebarToggle.classList.remove('open');
sidebarToggle.setAttribute('aria-expanded', 'false');
}
}
</script>
</body>
</html>