Add token expiry, edit notice, log allowlist, local-zone test; merge PLAN.md into SPECS.md

get_logs needs a unit from an allowlist (router-*, kea, unbound, networkd,
nftables, ddclient, acme, sshd, kernel). API tokens are valid for 1, 7, 30
(default) or 90 days, or without limit. The overview and the Changes page
say which token made the unapplied changes.

options.json (router://options) no longer holds the options' source paths:
they differ between builds of a release, so a router rebuilding itself
offline had to build the file and failed (found by the update test).

Web UI: panels flow into columns on wide screens and the width is no longer
capped; on phones the navigation is a menu button; the Advanced page uses
panels; VLAN dropdowns show their chosen names; the duplicate WireGuard
heading is gone.

basic: a subtest for wan -> local rules.

SPECS.md is now the only document (decisions, how each part works, and what
is not done yet); PLAN.md is gone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
2026-11-01 19:06:27 +01:00
co-authored by Claude Sonnet 5.5
parent 586274af4e
commit b1fa7b1396
25 changed files with 536 additions and 294 deletions
+23 -9
View File
@@ -47,7 +47,13 @@ nav .group > span { display: block; padding: 0 .7rem .25rem; font-size: .7rem; t
nav form { margin-top: 1rem; }
nav button.link { background: none; border: 0; padding: .38rem .7rem; font-weight: 400; cursor: pointer; }
main { max-width: 74rem; padding: 1.5rem 2rem 3rem; }
main { padding: 1.5rem clamp(1rem, 3vw, 3rem) 3rem; }
/* Pages of independent panels flow into up to three columns on wide screens */
.cols { columns: 38rem 3; column-gap: 1.5rem; }
.cols > section, .cols > fieldset, .cols > form > fieldset { break-inside: avoid; }
.cols > section.busy, .cols > section.pending { column-span: all; }
.menu-toggle { position: absolute; opacity: 0; pointer-events: none; }
.menu-button { display: none; }
h1 { font-size: 1.45rem; font-weight: 600; letter-spacing: -.01em; margin: .25rem 0 1.25rem; }
h2 { font-size: .95rem; font-weight: 600; margin: 0 0 .6rem; }
section { margin: 0 0 1.5rem; }
@@ -66,6 +72,10 @@ dd { margin: 0; overflow-wrap: anywhere; }
fieldset { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); margin: 0 0 1rem; padding: 0 0 1rem; min-width: 0; overflow: hidden; }
legend { float: left; width: 100%; margin: 0 0 1rem; padding: .6rem 1rem; font-size: .95rem; font-weight: 600; background: var(--head); border-bottom: 1px solid var(--line); }
legend ~ * { clear: both; }
/* A section as a panel (its h2 is the header bar), for pages made of sections */
section.panel { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; padding: 0 0 1rem; }
section.panel > h2 { margin: 0 0 1rem; padding: .6rem 1rem; font-size: .95rem; font-weight: 600; background: var(--head); border-bottom: 1px solid var(--line); }
section.panel > :not(h2) { margin-left: 1rem; margin-right: 1rem; }
fieldset > :not(legend):not(input[type=hidden]) { margin-left: 1rem; margin-right: 1rem; }
fieldset > .scroll { margin-left: 0; margin-right: 0; padding: 0 1rem; overflow-x: auto; }
@@ -75,6 +85,7 @@ input, select, textarea, button { font: inherit; color: var(--fg); background: v
input:hover, select:hover, textarea:hover { border-color: color-mix(in srgb, var(--fg) 30%, var(--line)); }
input:focus-visible, select:focus-visible, textarea:focus-visible, button:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 28%, transparent); }
input::placeholder, textarea::placeholder { color: color-mix(in srgb, var(--muted) 65%, transparent); }
textarea[rows] { height: min(calc(1.45em * var(--rows, 30)), 65vh); }
textarea { width: 100%; font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 13px; }
.narrow { max-width: 24rem; display: flex; flex-direction: column; gap: .8rem; }
.narrow label { margin: 0; }
@@ -116,17 +127,20 @@ pre code { background: none; padding: 0; }
body.bare { padding-left: 0; }
.bare header { position: static; width: auto; flex-direction: row; background: none; padding: 1rem 1.25rem; }
.bare header > strong { color: var(--fg); }
.bare main { margin: 0 auto; }
.bare main { margin: 0 auto; max-width: 30rem; }
@media (max-width: 50rem) {
body { padding-left: 0; }
header { position: static; width: auto; flex-direction: row; flex-wrap: wrap; align-items: center; gap: .5rem 1rem; padding: .6rem 1rem; }
nav { flex-direction: row; flex-wrap: wrap; align-items: center; gap: .1rem .25rem; }
nav > a, nav .group a, nav button.link { width: auto; padding: .3rem .55rem; }
nav .group { display: contents; }
nav .group > span, nav form { display: none; }
nav form:has(button) { display: block; margin: 0; }
main { padding: 1rem; }
header { position: sticky; top: 0; bottom: auto; width: auto; flex-direction: row; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .25rem 1rem; padding: .5rem .75rem; max-height: 100vh; }
.menu-button { display: inline-block; margin: 0; padding: .3rem .85rem; border: 1px solid var(--side-muted); border-radius: var(--radius); color: var(--side-fg); font-size: .9rem; font-weight: 500; cursor: pointer; }
header nav { display: none; width: 100%; max-height: calc(100vh - 4rem); overflow-y: auto; }
.menu-toggle:checked ~ nav { display: flex; }
.menu-toggle:focus-visible ~ .menu-button { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 40%, transparent); }
main { padding: .75rem .6rem 2rem; }
h1 { font-size: 1.25rem; margin-bottom: .75rem; }
fieldset > :not(legend):not(input[type=hidden]), section.panel > :not(h2) { margin-left: .75rem; margin-right: .75rem; }
fieldset > .scroll { padding: 0 .75rem; }
.cols { columns: auto; }
}
.ranges { display: flex; gap: .25rem; margin: 0 0 1rem; }
.ranges a { padding: .25rem .8rem; border: 1px solid var(--line); border-radius: var(--radius); text-decoration: none; color: var(--fg); background: var(--surface); }