434 lines
13 KiB
HTML
434 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
{% include 'head.html' %}
|
|
|
|
<style>
|
|
html, body {
|
|
overflow: visible;
|
|
}
|
|
|
|
body {
|
|
margin: 20px;
|
|
background: #f5f5f5;
|
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
}
|
|
|
|
.container {
|
|
max-width: 960px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
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 ---- */
|
|
.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">
|
|
<div class="sidebar-nav" id="sidebar-nav">
|
|
{% for section in sections %}
|
|
<a href="#{{ section.id }}">{{ 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));
|
|
</script>
|
|
</body>
|
|
</html>
|