Add a web UI for settings-configured routers

router-webui (Go, standard library only) edits settings.json: host
name, WAN (DHCP or PPPoE), LANs with DHCP, and the raw JSON for the
rest; keys the forms do not know are kept. It applies and confirms by
starting router-apply.service and router-confirm.service, which a
switch never restarts, and shows the build log and the rollback
countdown. HTTPS with a self-signed certificate, a PBKDF2 password
hash, CSRF tokens and an Origin check; reachable from management LANs
only (new router.managementPorts).

router-apply records the settings it built and router-confirm keeps
them as settings.confirmed.json (new rollback.onConfirm hook), so the
UI can tell unapplied changes and discard them.

checks.apply now also logs in from a LAN client, discards the change
left by the rollback, edits the DHCP pool, applies and confirms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 22:43:58 +01:00
co-authored by Claude Opus 5.5
parent 2c8d5562f9
commit 753459f5d7
20 changed files with 1685 additions and 6 deletions
+37
View File
@@ -0,0 +1,37 @@
:root { --fg: #1d2125; --muted: #5b636b; --bg: #fff; --line: #d6dade; --accent: #2457c5; --warn: #fff4d6; --bad: #b3261e; --ok: #e6f2e9; }
@media (prefers-color-scheme: dark) {
:root { --fg: #e4e7ea; --muted: #9aa2aa; --bg: #15181b; --line: #30363c; --accent: #7aa2ff; --warn: #3a3218; --bad: #ff8a80; --ok: #1d3324; }
}
* { box-sizing: border-box; }
body { margin: 0; font: 15px/1.5 system-ui, sans-serif; color: var(--fg); background: var(--bg); }
header { display: flex; gap: 1.5rem; align-items: center; padding: .75rem 1rem; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
nav { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }
nav form { display: inline; }
a { color: var(--accent); }
main { max-width: 72rem; margin: 0 auto; padding: 1rem; }
h1 { font-size: 1.4rem; margin: .5rem 0 1rem; }
h2 { font-size: 1.1rem; margin: 0 0 .5rem; }
section { margin: 0 0 1.25rem; }
section.busy, section.pending { padding: .75rem 1rem; border-radius: 6px; background: var(--warn); }
.flash { padding: .5rem .75rem; background: var(--ok); border-radius: 6px; }
.error, .field-error { color: var(--bad); }
.field-error { font-size: .9em; }
.hint { color: var(--muted); font-size: .9em; margin: .25rem 0; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: .25rem 1rem; }
dt { color: var(--muted); }
dd { margin: 0; overflow-wrap: anywhere; }
fieldset { border: 1px solid var(--line); border-radius: 6px; margin: 0 0 1rem; padding: .75rem 1rem; overflow-x: auto; }
label { display: inline-flex; flex-direction: column; margin: 0 1rem .5rem 0; font-size: .9em; color: var(--muted); }
input, select, textarea, button { font: inherit; color: var(--fg); background: var(--bg); border: 1px solid var(--line); border-radius: 4px; padding: .3rem .5rem; }
textarea { width: 100%; font-family: ui-monospace, monospace; font-size: 13px; }
table { border-collapse: collapse; }
th { text-align: left; font-weight: 500; color: var(--muted); font-size: .85em; padding: 0 .3rem; }
td { padding: .15rem .3rem; }
button { background: var(--accent); color: var(--bg); border-color: var(--accent); cursor: pointer; padding: .4rem 1rem; }
button.secondary { background: transparent; color: var(--accent); }
button.link { background: none; border: none; color: var(--accent); padding: 0; text-decoration: underline; }
.actions { display: flex; gap: .75rem; flex-wrap: wrap; }
.narrow { max-width: 22rem; display: flex; flex-direction: column; gap: .75rem; }
.narrow label { margin: 0; }
pre.log { background: color-mix(in srgb, var(--line) 35%, transparent); padding: .75rem; border-radius: 6px; overflow-x: auto; font-size: 12.5px; max-height: 30rem; }
code { font-size: .9em; overflow-wrap: anywhere; }