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
+9 -6
View File
@@ -1,5 +1,5 @@
{{define "content"}}
<section>
<section class="panel">
<h2>Settings</h2>
<p>The complete settings: the <code>router</code> options of nixos-router as JSON, plus <code>system</code>.
They are checked when applied; nothing changes until then.</p>
@@ -10,7 +10,9 @@
</form>
</section>
<section>
<div class="cols">
<section class="panel">
<h2>Appearance</h2>
<form method="post" action="/prefs" class="narrow">
<input type="hidden" name="csrf" value="{{.CSRF}}">
@@ -26,7 +28,7 @@
</form>
</section>
<section>
<section class="panel">
<h2>Dashboard</h2>
<form method="post" action="/prefs" class="narrow">
<input type="hidden" name="csrf" value="{{.CSRF}}">
@@ -37,7 +39,7 @@
</form>
</section>
<section>
<section class="panel">
<h2>Password</h2>
<p>One password for this web UI and for root on the console and over SSH.</p>
<form method="post" action="/password" class="narrow">
@@ -49,17 +51,18 @@
</form>
</section>
<section>
<section class="panel">
<h2>Reboot</h2>
<p>Restarts the router; routing stops for a minute or two. A new kernel from an update only runs after this.</p>
<form method="post" action="/reboot" data-confirm="Reboot the router now?"><input type="hidden" name="csrf" value="{{.CSRF}}"><button>Reboot</button></form>
</section>
<section>
<section class="panel">
<h2>Backup</h2>
<p>Everything on <code>/var/lib/router</code>: settings, PPPoE password, WireGuard keys, passwords, the web UI's
certificate and the dashboard history. Keep the file safe. To restore, put it on a USB stick and run
<code>router-setup</code> from the installer: it offers to install from it.</p>
<form method="post" action="/backup"><input type="hidden" name="csrf" value="{{.CSRF}}"><button>Download backup</button></form>
</section>
</div>
{{end}}
@@ -1,4 +1,5 @@
{{define "content"}}
<div class="cols">
{{with .Data}}
{{$e := .Errors}}{{$f := .Form}}
<section>
@@ -42,4 +43,5 @@
{{else}}<p class="muted">Nothing yet. After you save and apply, the router orders the certificate (a minute or two); renewals follow 30 days before it expires and need no restart.</p>{{end}}
</section>
{{end}}
</div>
{{end}}
+1
View File
@@ -3,6 +3,7 @@
{{if .Diff}}
<p>The saved settings differ from the confirmed (running) ones: <code>-</code> lines go away, <code>+</code> lines are new.
Nothing is applied until you press Apply.</p>
{{with .TokenEdit}}<p class="error">{{.}}</p>{{end}}
<pre class="log">{{.Diff}}</pre>
<div class="actions">
<form method="post" action="/apply"><input type="hidden" name="csrf" value="{{$.CSRF}}"><button>Apply</button></form>
+2
View File
@@ -1,4 +1,5 @@
{{define "content"}}
<div class="cols">
{{with .Data}}
{{$e := .Errors}}{{$f := .Form}}
<p class="hint">Keeps DNS records at your provider pointing at the router's internet addresses: it checks regularly and
@@ -41,4 +42,5 @@
{{else}}<p class="muted">Nothing yet. Once applied, the log of ddclient shows up here.</p>{{end}}
</section>
{{end}}
</div>
{{end}}
+2
View File
@@ -11,6 +11,8 @@
<header>
<strong>router</strong>
{{if .LoggedIn}}
<input type="checkbox" id="menu" class="menu-toggle" aria-label="Menu">
<label for="menu" class="menu-button">Menu</label>
<nav>
<a href="/"{{if eq .Title "Overview"}} aria-current="page"{{end}}>Overview</a>
<div class="group"><span>Network</span>
+2 -2
View File
@@ -63,7 +63,7 @@
<legend>Uplink (WAN)</legend>
<label>Interface
<select name="wan.interface">{{$wan := .Form.WAN.Interface}}
{{range choices $.Data.Devices $wan}}<option value="{{.}}" {{if eq . $wan}}selected{{end}}>{{.}}</option>{{end}}
{{range choices $.Data.Devices $wan}}<option value="{{.}}" {{if eq . $wan}}selected{{end}}>{{devLabel $.Data.Devices .}}</option>{{end}}
</select>
</label>
{{with index $e "wan.interface"}}<span class="field-error">{{.}}</span>{{end}}
@@ -95,7 +95,7 @@
<td><input name="lan.{{$i}}.name" value="{{$l.Name}}" size="8" placeholder="new"></td>
<td><select name="lan.{{$i}}.interface">{{$cur := $l.Interface}}
{{if not $cur}}<option value="" selected></option>{{end}}
{{range choices $.Data.Devices $cur}}<option value="{{.}}" {{if eq . $cur}}selected{{end}}>{{.}}</option>{{end}}
{{range choices $.Data.Devices $cur}}<option value="{{.}}" {{if eq . $cur}}selected{{end}}>{{devLabel $.Data.Devices .}}</option>{{end}}
</select></td>
<td><input name="lan.{{$i}}.address" value="{{$l.Address}}" size="13"></td>
<td><input name="lan.{{$i}}.prefix" value="{{$l.Prefix}}" size="2" inputmode="numeric" placeholder="24"></td>
@@ -1,4 +1,5 @@
{{define "content"}}
<div class="cols">
{{$s := .Data.Status}}
{{if $s.Applying}}
<section class="busy"><h2>Applying…</h2><p>Building the new configuration. This page refreshes on its own.</p></section>
@@ -14,6 +15,7 @@
{{else if $s.Unapplied}}
<section class="pending">
<p>The settings have changes that are not part of the confirmed system. <a href="/changes">Review the changes</a>.</p>
{{with $s.TokenEdit}}<p><strong>{{.}}</strong></p>{{end}}
<div class="actions">
<form method="post" action="/apply"><input type="hidden" name="csrf" value="{{.CSRF}}"><button>Apply</button></form>
<form method="post" action="/discard"><input type="hidden" name="csrf" value="{{.CSRF}}"><button class="secondary">Discard changes</button></form>
@@ -44,4 +46,5 @@
</section>
{{end}}
<script src="/static/dashboard.js" defer></script>
</div>
{{end}}
+11 -1
View File
@@ -23,8 +23,9 @@
{{if .Tokens}}
<div class="scroll">
<table>
<tr><th>Name</th><th>Access</th><th>Created</th><th>Last used</th><th></th></tr>
<tr><th>Name</th><th>Access</th><th>Created</th><th>Expires</th><th>Last used</th><th></th></tr>
{{range .Tokens}}<tr><td>{{.Name}}</td><td>{{.Scope}}</td><td>{{.Created.Format "2006-01-02"}}</td>
<td>{{if .Expires.IsZero}}<span class="hint">never</span>{{else if .Expired}}<span class="bad">expired {{.Expires.Format "2006-01-02"}}</span>{{else}}{{.Expires.Format "2006-01-02"}}{{end}}</td>
<td>{{if .LastUsed.IsZero}}<span class="hint">never</span>{{else}}{{.LastUsed.Format "2006-01-02 15:04"}}{{end}}</td>
<td><form method="post" action="/tokens/revoke" data-confirm="Revoke this token?"><input type="hidden" name="csrf" value="{{$.CSRF}}"><input type="hidden" name="id" value="{{.ID}}"><button class="link">revoke</button></form></td></tr>{{end}}
</table>
@@ -43,6 +44,15 @@
<option value="change">change: also edit the settings</option>
</select>
</label>
<label>Valid for
<select name="valid">
<option value="1d">1 day</option>
<option value="7d">7 days</option>
<option value="30d" selected>30 days</option>
<option value="90d">90 days</option>
<option value="never">no limit</option>
</select>
</label>
<button>Create token</button>
</fieldset>
</form>
+2
View File
@@ -1,4 +1,5 @@
{{define "content"}}
<div class="cols">
{{$d := .Data}}
<section>
<h2>Installed</h2>
@@ -60,4 +61,5 @@
<pre class="log">{{$d.Status.Log}}</pre>
</section>
{{end}}
</div>
{{end}}
@@ -132,7 +132,6 @@
</section>
<section>
<h2>New tunnel</h2>
{{template "tunnelform" (tform .New $.CSRF true)}}
</section>
{{end}}