Add signed updates from a bundle or a URL

router-release signs a release as a binary cache; router-update verifies
it against the router's update keys, migrates settings.json, and applies
it with commit-confirm. Includes the web UI page and a VM test.

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-29 13:34:21 +01:00
co-authored by Claude Sonnet 5.5
parent 2a111d012b
commit b9c63273e4
21 changed files with 1500 additions and 21 deletions
+63
View File
@@ -0,0 +1,63 @@
{{define "content"}}
{{$d := .Data}}
<section>
<h2>Installed</h2>
<dl>
<dt>Version</dt><dd>{{or $d.Installed.Version "unknown"}}{{if $d.Installed.Rev}} <span class="muted">{{$d.Installed.Rev}}</span>{{end}}</dd>
<dt>Settings schema</dt><dd>{{$d.Installed.Schema}}</dd>
</dl>
</section>
{{if $d.Status.Running}}
<section class="busy"><h2>Updating…</h2><p>Verifying, importing and building the new version. This page refreshes on its own;
the web UI restarts when the new version is switched on.</p></section>
{{else if $d.Pending}}
<section class="pending"><h2>Waiting for confirmation</h2>
<p>A new configuration is active and not confirmed yet. Confirm it on the <a href="/">Overview</a>, or the router
goes back to the previous version.</p></section>
{{end}}
<section>
<h2>Install a bundle</h2>
<p>A <code>.bundle</code> file made with <code>nix run .#release</code>. It is installed only if it is signed with
this router's update key. The settings are kept (and migrated if the new version needs it); the new version
must be confirmed afterwards, or the router goes back.</p>
<form method="post" action="/update/upload?csrf={{.CSRF}}" enctype="multipart/form-data">
<p><label><input type="checkbox" name="force"> also install a version that is not newer</label></p>
<p><input type="file" name="bundle" required> <button>Upload and install</button></p>
</form>
</section>
<section>
<h2>Update server</h2>
<form method="post" action="/update/url" class="actions">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<label>Address <input name="url" value="{{$d.URL}}" size="40" placeholder="https://updates.example.com/router"></label>
<button class="secondary">Save</button>
</form>
{{if $d.URL}}
<form method="post" action="/update/check" class="actions">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<button class="secondary">Check for updates</button>
</form>
{{end}}
{{if $d.OfferErr}}<p class="error">Could not check: {{$d.OfferErr}}</p>{{end}}
{{with $d.Offer}}
<p>{{if .Newer}}Available: <strong>{{.Version}}</strong>{{if .Rev}} <span class="muted">{{.Rev}}</span>{{end}}.{{else}}Nothing newer: the server has {{.Version}}.{{end}}</p>
<form method="post" action="/update/install">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
{{if not .Newer}}<p><label><input type="checkbox" name="force"> install it anyway</label></p>{{end}}
<button>{{if .Newer}}Install {{.Version}}{{else}}Install{{end}}</button>
</form>
{{end}}
<p class="hint">The server only has to serve static files: the <code>latest</code> file and <code>cache/</code> that
<code>nix run .#release</code> writes. The data is signed, so plain HTTP works too.</p>
</section>
{{if $d.Status.Log}}
<section>
<h2>Last update{{if $d.Status.Result}} — {{if eq $d.Status.Result "success"}}succeeded{{else}}failed ({{$d.Status.Result}}){{end}}{{end}}</h2>
<pre class="log">{{$d.Status.Log}}</pre>
</section>
{{end}}
{{end}}