Add dynamic DNS and show the running kernel on the overview

router.ddns keeps provider records on the uplink's addresses with ddclient,
run by a timer and by a watcher that starts it when an address changes. The
Dynamic DNS page edits it; the secret is a private file on the router. The
overview shows the running kernel and says when the current system boots a
different one. PLAN.md records why the A/B image approach was not pursued.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
2026-10-30 13:35:57 +01:00
co-authored by Claude Sonnet 5.5
parent c198ffbe37
commit a36cebc827
19 changed files with 732 additions and 8 deletions
@@ -33,6 +33,7 @@
<h2>System</h2>
<dl>
<dt>Uptime</dt><dd>{{age $sys.Uptime}}</dd>
{{if $sys.Kernel}}<dt>Kernel</dt><dd>{{$sys.Kernel}}{{if $sys.KernelNext}} <span class="bad">reboot to run {{$sys.KernelNext}}</span>{{end}}</dd>{{end}}
<dt>Load</dt><dd>{{$sys.Load}}</dd>
<dt>CPU</dt><dd>{{value $sys.CPU "%"}}</dd>
{{if $sys.MemTotal}}<dt>Memory</dt><dd>{{bytes $sys.MemUsed}} of {{bytes $sys.MemTotal}}</dd>{{end}}
+50
View File
@@ -0,0 +1,50 @@
{{define "content"}}
{{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
right away when the address changes (a new PPPoE session, a new lease). Uses
<a href="https://ddclient.net/protocols.html">ddclient</a>; one provider account.</p>
<form method="post" action="/ddns">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<fieldset>
<legend>Provider</legend>
<label class="inline"><input type="checkbox" name="enabled" {{if $f.Enabled}}checked{{end}}> Keep these names up to date</label>
<div>
<label>Provider (protocol) <input name="protocol" value="{{$f.Protocol}}" size="16" list="protocols" spellcheck="false"></label>
<datalist id="protocols"><option value="cloudflare"><option value="duckdns"><option value="dyndns2"><option value="namecheap"><option value="porkbun"><option value="hetzner"><option value="godaddy"><option value="digitalocean"></datalist>
<label>Server <input name="server" value="{{$f.Server}}" size="26" spellcheck="false" placeholder="only for dyndns2"></label>
<label>Zone <input name="zone" value="{{$f.Zone}}" size="22" spellcheck="false" placeholder="example.org (Cloudflare)"></label>
</div>
{{range $k := list "protocol" "server" "zone"}}{{with index $e $k}}<p class="field-error">{{$k}}: {{.}}</p>{{end}}{{end}}
<div>
<label>Login <input name="username" value="{{$f.Username}}" size="18" spellcheck="false" autocomplete="off"></label>
<label>Password or API token <input type="password" name="secret" size="30" autocomplete="new-password"
placeholder="{{if $f.SecretSet}}stored; leave empty to keep{{else}}required{{end}}"></label>
</div>
{{with index $e "secret"}}<p class="field-error">{{.}}</p>{{end}}
<p class="hint">Login is <code>token</code> for a Cloudflare API token (permission Zone → DNS → Edit). The secret is kept on the router
in a private file, not in the settings.</p>
</fieldset>
<fieldset>
<legend>Names</legend>
<label>Host names, one per line <textarea name="domains" rows="6" cols="40" spellcheck="false">{{$f.Domains}}</textarea></label>
{{with index $e "domains"}}<p class="field-error">{{.}}</p>{{end}}
<div>
<label class="inline"><input type="checkbox" name="ipv4" {{if $f.IPv4}}checked{{end}}> IPv4 (A records)</label>
<label class="inline"><input type="checkbox" name="ipv6" {{if $f.IPv6}}checked{{end}}> IPv6 (AAAA records)</label>
<label class="inline"><input type="checkbox" name="ssl" {{if $f.SSL}}checked{{end}}> HTTPS to the provider</label>
<label>Check every <input name="interval" value="{{$f.Interval}}" size="6"></label>
</div>
{{range $k := list "ipv4" "interval"}}{{with index $e $k}}<p class="field-error">{{$k}}: {{.}}</p>{{end}}{{end}}
</fieldset>
<button>Save</button>
</form>
<section>
<h2>Last updates</h2>
{{if .Status.Log}}<p class="muted">ddclient: {{or .Status.State "unknown"}}{{if .Status.Last}}, last run {{.Status.Last}}{{end}}</p>
<pre class="log">{{.Status.Log}}</pre>
{{else}}<p class="muted">Nothing yet. Once applied, the log of ddclient shows up here.</p>{{end}}
</section>
{{end}}
{{end}}
+1
View File
@@ -18,6 +18,7 @@
<a href="/dns">DNS</a>
<a href="/wireguard">WireGuard</a>
<a href="/firewall">Firewall</a>
<a href="/ddns">Dynamic DNS</a>
<a href="/update">Update</a>
<a href="/advanced">Advanced</a>
<form method="post" action="/logout"><input type="hidden" name="csrf" value="{{.CSRF}}"><button class="link">Log out</button></form>