Jarl keeps a BIND 9.20 server's whole configuration (views, ACLs, TSIG keys, zones of every type, records, named.conf options) in SQLite and renders named.conf, keys.conf and zone files from it. Every change goes through one changeset pipeline: whole-configuration validation, named-checkconf -z and named-checkzone in a staging directory, atomic install, rndc reconfig/reload, serial verification with rndc zonestatus, and rollback if named does not load the change. Changes apply one at a time by default or are bundled and applied together. Applied changesets can be reverted. - jarld: the daemon with the REST API and an embedded web UI, plus import, adopt, check, resync, backup and local user recovery commands - jarl: the CLI, including ACME DNS-01 hooks for certbot and acme.sh - users with argon2id passwords and CSRF-protected sessions; hashed API tokens with granular grants and roles; an audit log - systemd unit, sysusers.d/tmpfiles.d files, Makefile - unit tests, integration tests against a real named, scripts/e2e.sh - docs/ guides, README and CHANGELOG Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
235 lines
12 KiB
CSS
235 lines
12 KiB
CSS
/* Jarl web UI. Plain CSS, light and dark themes. */
|
|
:root {
|
|
--bg: #f6f7f9;
|
|
--panel: #ffffff;
|
|
--text: #1d2330;
|
|
--muted: #5f6b7d;
|
|
--border: #d9dee6;
|
|
--accent: #2f5fb3;
|
|
--accent-text: #ffffff;
|
|
--accent-soft: #e5edfa;
|
|
--ok: #1e7a46;
|
|
--ok-soft: #e3f4ea;
|
|
--warn: #8a5a00;
|
|
--warn-soft: #fdf2d9;
|
|
--err: #b3261e;
|
|
--err-soft: #fbe7e5;
|
|
--info-soft: #eef1f6;
|
|
--code-bg: #f0f2f5;
|
|
--add: #e6f6ea;
|
|
--add-text: #135c2c;
|
|
--del: #fde8e7;
|
|
--del-text: #8c1d18;
|
|
--hunk: #e8eefb;
|
|
--shadow: 0 1px 2px rgba(20, 30, 50, 0.08);
|
|
--radius: 6px;
|
|
--mono: ui-monospace, "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
|
|
color-scheme: light;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) {
|
|
--bg: #14171c;
|
|
--panel: #1c2027;
|
|
--text: #e3e7ee;
|
|
--muted: #9aa5b5;
|
|
--border: #2f3540;
|
|
--accent: #6d9be8;
|
|
--accent-text: #0f1520;
|
|
--accent-soft: #23314a;
|
|
--ok: #59c287;
|
|
--ok-soft: #173326;
|
|
--warn: #e5b450;
|
|
--warn-soft: #3a2f15;
|
|
--err: #f08077;
|
|
--err-soft: #3c1f1d;
|
|
--info-soft: #232833;
|
|
--code-bg: #242933;
|
|
--add: #183a25;
|
|
--add-text: #9fe3b5;
|
|
--del: #43201e;
|
|
--del-text: #f7b2ac;
|
|
--hunk: #23314a;
|
|
--shadow: none;
|
|
color-scheme: dark;
|
|
}
|
|
}
|
|
:root[data-theme="dark"] {
|
|
--bg: #14171c; --panel: #1c2027; --text: #e3e7ee; --muted: #9aa5b5; --border: #2f3540;
|
|
--accent: #6d9be8; --accent-text: #0f1520; --accent-soft: #23314a; --ok: #59c287; --ok-soft: #173326;
|
|
--warn: #e5b450; --warn-soft: #3a2f15; --err: #f08077; --err-soft: #3c1f1d; --info-soft: #232833;
|
|
--code-bg: #242933; --add: #183a25; --add-text: #9fe3b5; --del: #43201e; --del-text: #f7b2ac; --hunk: #23314a;
|
|
--shadow: none; color-scheme: dark;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; padding: 0; }
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font: 14px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, Ubuntu, sans-serif;
|
|
}
|
|
a { color: var(--accent); text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
code, pre, .mono, textarea.mono, input.mono { font-family: var(--mono); font-size: 13px; }
|
|
pre { background: var(--code-bg); padding: 10px 12px; border-radius: var(--radius); overflow: auto; margin: 0; white-space: pre; }
|
|
h1 { font-size: 20px; margin: 0 0 4px; }
|
|
h2 { font-size: 16px; margin: 18px 0 8px; }
|
|
h3 { font-size: 14px; margin: 14px 0 6px; }
|
|
.muted { color: var(--muted); }
|
|
.small { font-size: 12px; }
|
|
.nowrap { white-space: nowrap; }
|
|
.right { text-align: right; }
|
|
.hidden { display: none !important; }
|
|
|
|
/* Layout */
|
|
header.top {
|
|
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
|
|
background: var(--panel); border-bottom: 1px solid var(--border);
|
|
padding: 8px 16px; position: sticky; top: 0; z-index: 10;
|
|
}
|
|
header.top .brand { font-weight: 700; font-size: 17px; letter-spacing: 0.5px; color: var(--text); }
|
|
header.top .brand span { color: var(--muted); font-weight: 400; font-size: 12px; margin-left: 6px; }
|
|
nav.main { display: flex; gap: 2px; flex-wrap: wrap; }
|
|
nav.main a { padding: 6px 10px; border-radius: var(--radius); color: var(--text); }
|
|
nav.main a:hover { background: var(--info-soft); text-decoration: none; }
|
|
nav.main a.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
header.top .spacer { flex: 1; }
|
|
header.top form.search input { width: 220px; }
|
|
header.top .user { color: var(--muted); }
|
|
main { padding: 16px; max-width: 1400px; margin: 0 auto; }
|
|
.pending-bar {
|
|
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
|
background: var(--warn-soft); color: var(--text); border-bottom: 1px solid var(--border);
|
|
padding: 8px 16px;
|
|
}
|
|
.pending-bar strong { color: var(--warn); }
|
|
.page-head { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
.page-head .grow { flex: 1; min-width: 200px; }
|
|
.actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
|
|
|
/* Panels and cards */
|
|
.panel { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; box-shadow: var(--shadow); margin-bottom: 14px; }
|
|
.panel > h2:first-child, .panel > h3:first-child { margin-top: 0; }
|
|
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; margin-bottom: 14px; }
|
|
.card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; box-shadow: var(--shadow); }
|
|
.card .label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.4px; }
|
|
.card .value { font-size: 18px; font-weight: 600; margin-top: 2px; }
|
|
.card .sub { color: var(--muted); font-size: 12px; margin-top: 4px; }
|
|
|
|
/* Tables */
|
|
.table-wrap { overflow-x: auto; }
|
|
table { border-collapse: collapse; width: 100%; background: var(--panel); }
|
|
th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
th { font-size: 12px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; white-space: nowrap; }
|
|
tr:hover td { background: var(--info-soft); }
|
|
td.data { font-family: var(--mono); font-size: 13px; word-break: break-all; }
|
|
td.name { font-family: var(--mono); font-size: 13px; white-space: nowrap; }
|
|
td.name.repeat { color: var(--muted); }
|
|
td.actions-cell { white-space: nowrap; text-align: right; width: 1%; }
|
|
tr.editing td { background: var(--accent-soft); }
|
|
tr.pending-add td { background: var(--add); }
|
|
tr.pending-change td { background: var(--warn-soft); }
|
|
tr.pending-delete td { background: var(--del); text-decoration: line-through; color: var(--muted); }
|
|
.empty { color: var(--muted); padding: 18px; text-align: center; }
|
|
|
|
/* Forms */
|
|
input, select, textarea, button {
|
|
font: inherit; color: var(--text);
|
|
}
|
|
input[type=text], input[type=password], input[type=number], input[type=search], input:not([type]), select, textarea {
|
|
background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 5px 8px; min-width: 0;
|
|
}
|
|
input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent-soft); border-color: var(--accent); }
|
|
textarea { width: 100%; min-height: 80px; resize: vertical; }
|
|
.form-grid { display: grid; grid-template-columns: minmax(140px, 200px) 1fr; gap: 8px 14px; align-items: start; }
|
|
.form-grid > label { padding-top: 6px; color: var(--muted); }
|
|
.form-grid .help { grid-column: 2; margin-top: -4px; color: var(--muted); font-size: 12px; }
|
|
.form-grid input, .form-grid select { width: 100%; max-width: 560px; }
|
|
.inline-form { display: flex; gap: 6px; flex-wrap: wrap; align-items: flex-end; }
|
|
.inline-form .f { display: flex; flex-direction: column; gap: 2px; }
|
|
.inline-form .f label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.3px; }
|
|
.inline-form .grow { flex: 1; min-width: 220px; }
|
|
.inline-form .grow input { width: 100%; }
|
|
.hint { color: var(--muted); font-size: 12px; margin-top: 4px; }
|
|
.check { display: inline-flex; gap: 6px; align-items: center; cursor: pointer; }
|
|
|
|
button, .button {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
|
|
padding: 5px 12px; cursor: pointer; white-space: nowrap; color: var(--text);
|
|
}
|
|
button:hover, .button:hover { background: var(--info-soft); text-decoration: none; }
|
|
button.primary, .button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); font-weight: 600; }
|
|
button.primary:hover { filter: brightness(1.08); background: var(--accent); }
|
|
button.danger { color: var(--err); border-color: var(--err); }
|
|
button.danger:hover { background: var(--err-soft); }
|
|
button.small, .button.small { padding: 2px 8px; font-size: 12px; }
|
|
button.link { border: none; background: none; color: var(--accent); padding: 0 4px; }
|
|
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
/* Badges and messages */
|
|
.badge { display: inline-block; padding: 1px 7px; border-radius: 10px; font-size: 11px; font-weight: 600; background: var(--info-soft); color: var(--muted); white-space: nowrap; }
|
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
.badge.err { background: var(--err-soft); color: var(--err); }
|
|
.badge.accent { background: var(--accent-soft); color: var(--accent); }
|
|
.msg { border-radius: var(--radius); padding: 8px 12px; margin: 8px 0; border: 1px solid transparent; }
|
|
.msg.err { background: var(--err-soft); color: var(--err); border-color: var(--err); }
|
|
.msg.warn { background: var(--warn-soft); color: var(--warn); }
|
|
.msg.ok { background: var(--ok-soft); color: var(--ok); }
|
|
.msg.info { background: var(--info-soft); }
|
|
.msg ul { margin: 4px 0 0; padding-left: 18px; }
|
|
.problems { list-style: none; padding: 0; margin: 6px 0; }
|
|
.problems li { padding: 4px 8px; border-left: 3px solid var(--border); margin-bottom: 3px; background: var(--info-soft); border-radius: 0 var(--radius) var(--radius) 0; }
|
|
.problems li.error { border-color: var(--err); background: var(--err-soft); }
|
|
.problems li.warning { border-color: var(--warn); background: var(--warn-soft); }
|
|
.problems li .where { color: var(--muted); font-size: 12px; margin-left: 6px; }
|
|
|
|
/* Diff */
|
|
.diff { background: var(--code-bg); border-radius: var(--radius); overflow: auto; font-family: var(--mono); font-size: 12.5px; line-height: 1.4; }
|
|
.diff div { white-space: pre; padding: 0 10px; }
|
|
.diff .add { background: var(--add); color: var(--add-text); }
|
|
.diff .del { background: var(--del); color: var(--del-text); }
|
|
.diff .hunk { background: var(--hunk); color: var(--accent); }
|
|
.diff .file { font-weight: 700; padding-top: 6px; }
|
|
|
|
/* Tabs */
|
|
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin-bottom: 12px; flex-wrap: wrap; }
|
|
.tabs a { padding: 7px 12px; border: 1px solid transparent; border-bottom: none; border-radius: var(--radius) var(--radius) 0 0; color: var(--muted); }
|
|
.tabs a.active { background: var(--panel); border-color: var(--border); color: var(--text); font-weight: 600; margin-bottom: -1px; }
|
|
|
|
/* Modal */
|
|
.modal-back { position: fixed; inset: 0; background: rgba(10, 15, 25, 0.45); display: flex; align-items: flex-start; justify-content: center; padding: 60px 16px; z-index: 50; overflow: auto; }
|
|
.modal { background: var(--panel); border-radius: 8px; border: 1px solid var(--border); width: min(760px, 100%); padding: 18px 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.25); }
|
|
.modal.wide { width: min(1100px, 100%); }
|
|
.modal h2 { margin-top: 0; }
|
|
.modal .buttons { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
|
|
|
|
/* Toasts */
|
|
#toasts { position: fixed; bottom: 16px; right: 16px; display: flex; flex-direction: column; gap: 8px; z-index: 100; max-width: min(480px, calc(100% - 32px)); }
|
|
.toast { background: var(--panel); border: 1px solid var(--border); border-left: 4px solid var(--accent); padding: 10px 14px; border-radius: var(--radius); box-shadow: 0 4px 16px rgba(0,0,0,0.18); }
|
|
.toast.ok { border-left-color: var(--ok); }
|
|
.toast.err { border-left-color: var(--err); }
|
|
.toast.warn { border-left-color: var(--warn); }
|
|
|
|
/* Login */
|
|
.login { max-width: 360px; margin: 10vh auto; }
|
|
.login h1 { font-size: 26px; text-align: center; margin-bottom: 4px; }
|
|
.login .tag { text-align: center; color: var(--muted); margin-bottom: 18px; }
|
|
.login form { display: flex; flex-direction: column; gap: 10px; }
|
|
.login input { width: 100%; padding: 8px 10px; }
|
|
.login button { justify-content: center; padding: 8px; }
|
|
|
|
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; }
|
|
.kv dt { color: var(--muted); }
|
|
.kv dd { margin: 0; }
|
|
.grants { font-family: var(--mono); font-size: 12px; }
|
|
.copy-box { display: flex; gap: 6px; align-items: center; }
|
|
.copy-box input { flex: 1; font-family: var(--mono); }
|
|
.sortable-row { display: flex; align-items: center; gap: 8px; }
|
|
@media (max-width: 700px) {
|
|
.form-grid { grid-template-columns: 1fr; }
|
|
.form-grid .help { grid-column: 1; }
|
|
header.top form.search input { width: 140px; }
|
|
}
|