channels.nixos.org answers <branch>/git-revision with a redirect, which
router-upgrade check did not follow: on a real router it saw an empty
answer. The test channel is now an HTTP server that redirects the same way.
The page labelled the running release "stable", but nixos-unstable already
calls itself the next release (26.11 does not exist as a branch yet). It
offers none, unstable and the stable releases listed in the code (26.05).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
A page under System for router-upgrade: the channel to follow, Check,
Preview (the package versions that change, the download size, what would be
built here, a reboot note for a new kernel), Download now and Upgrade. The
steps run in router-upgrade.service, which the page asks through a request
file; the last run's output is kept in upgrade/last.log because a finished
one-shot unit has no journal to look up.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
router.update.channel names the nixpkgs branch to follow (unstable or a
release like 26.05). router-upgrade check compares its newest tested
revision with the running one; preview locks a copy of the configuration
flake to it and lists the packages that change version and the size of the
download without fetching anything; download fetches it; apply takes the
lock and activates with commit-confirm, the lock going back on a rollback
or a failed build. router-update keeps an upgraded nixpkgs pinned.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
systemd-networkd resolves an endpoint name once, when it creates the
device. A peer behind a dynamic DNS name that changes its address was
never found again until the next restart.
A timer (router.wireguardEndpointRefresh, default 1min, null for off)
runs wg set for the peers with a name whose last handshake is older than
135 seconds, as wireguard-tools' reresolve-dns does. Working tunnels are
left alone.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
Every tool now returns a Go struct (apitypes.go) instead of a map, and
typeschema.go derives the OpenAPI response schemas from those types by
reflection (json, doc and enum tags), so the document cannot drift from
what is sent. Text results are documented as text/plain, settings in
answers use the typed settings schema, and edits answer with EditResponse.
TestResponsesMatchTheirSchemas checks real responses against the
generated schemas.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
Replace the hand-built reference page with Redoc rendering the generated
OpenAPI document. The bundle is fetched by package.nix (pinned hash) and
served from the UI's own address; only that page gets a style nonce and
worker-src blob: in its CSP.
The spec now explains authentication, saved-not-applied, ETags (If-Match,
409) and errors, documents the ETag header, gives each operation a curl
sample, and no longer names MCP tools. Area GETs no longer emit
"parameters": null, which Redoc could not render.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
/api/v1/settings/router/firewall, .../router/lans/<name>, .../system, down to
a single option: GET reads, PUT replaces, PATCH merges, DELETE removes, with
the settings' ETag/If-Match and the same limits as the whole-document edit
(editSettings is now shared). Values are checked against a JSON Schema made
from the option documentation (schema.go reads the types, which nixosOptionsDoc
describes in words); a wrong type, a value out of range or an unknown key is
refused with its place. The OpenAPI document has the area paths with their
schemas; the API reference page lists every area's options.
Tests: unit tests for the type reader, the validator and the paths; the real
option file and a router's own settings validate (ROUTER_OPTIONS_JSON,
LIVE_SETTINGS_JSON); a subtest in apply.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
/apireference lists the REST operations (from the same route list and tool
schemas as the OpenAPI document) with access, parameters and a curl example
for this router. Linked from the API access page.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
The rule meant for the settings box on the Advanced page matched every
textarea with a rows attribute (DNS providers, DDNS, certificate, WireGuard
import). It now only matches that box.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
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
Web UI: dark/light themes (appliance layout, sidebar, switch checkboxes),
theme preference on the Advanced page, Changes page with the pending diff.
Interface names: router.interfaces.<name>.mac, lans.<l>.deviceName and
pppoe.vlanName; a rename is staged for the next reboot by router-apply.
Remote access: an MCP server (POST /mcp) and an OpenAPI REST API
(/api/v1, /api/openapi.json) with the same tokens, scopes, limits and audit
log; tools to read status, settings, leases, logs (get_logs), and to edit
and check the saved settings (never applied). Tokens on the API access page.
Firewall: a reserved `local` zone (the router itself) as the destination of
rules, so the web UI or SSH can be allowed from the WAN.
PLAN.md lists what is still left.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
Phosphor green on black, square corners, outlined buttons, uppercase
headings. Checkboxes in front of their label become switches (small
ticks inside tables) instead of stacking above the text. Fieldsets no
longer grow wider than the page; wide tables scroll inside their panel.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
- router.dnsProviders: named accounts at DNS providers, used by dynamic
DNS and certificates (ddclient and lego provider lists in
pkgs/router-webui/providers.json). Schema 2 migrates router.ddns.
- router.acme: a Let's Encrypt certificate for the web UI via
security.acme (DNS-01, resolver 1.1.1.1), served and reloaded by the
web UI; Certificate page.
- Web UI: password change (web UI and root), reboot button, HTTP to HTTPS
redirect (router.webui.httpPort), grouped menu.
- Latest kernel (linuxPackages_latest); 7.3 and 7.3.0 are the same kernel
for the reboot hint.
- SPECS.md: wanted later (REST API, GraphQL, MCP).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
router.firewall.nat replaces the dnat/toPort/hairpin fields of the filter
rules. Filter rules gain natOnly, and may use wan with log and rate limit
in the web UI. The Firewall page edits the NAT table and can add the
matching allow rule. Settings schema 1 (migrations/1.jq) converts old
forwards.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
The hand-written port of antigua's router role predates the installer and
the web UI; the router is configured through settings.json now. Its flake
outputs (the home configuration, installer-home, the home check) go too.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
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
Rules from wan are edited in their own table: protocol, ports, zone, host,
host port, allowed sources, port forward (NAT), hairpin, log and rate
limit, with the module's checks done up front.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
The firewall page now manages zones (assign each LAN or tunnel, add and
remove zones) and rules with zone, address and port on both sides plus the
protocol. router.firewall.zones declares zones without members and rules
take sourcePorts. The temperature chart draws every thermal zone and hwmon
sensor; the overview defaults to 15 minutes, 1h stays an option.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
Allow rules between zones (LANs and tunnels) are edited on their own page:
from, to, protocol, ports, destination. Port forwards and rules with more
options are listed and kept as they are.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
A client with a hostname is published with its reverse record, a client
claiming a configured name does not take it over, and a name outside the
domain is cut to its first label.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
router-webui leases-dns reads Kea's leases and publishes <name>.<domain>
with reverse records in the running Unbound (router.dns.dynamicLeases).
Names the configuration publishes are never taken over; a client's name
is cut to its first label and kept inside the local domain; of two
clients the newer lease wins. The DNS page switches it on and shows what
is published and what was skipped; router-setup enables it with a domain.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
Web UI: a WireGuard page (tunnels, peers, keys made on the router, client
configs with a QR code, import of wg-quick configs, live status), a DNS page
(records, wildcards, forward zones), interface dropdowns with VLAN devices,
IPv6 options and a local domain. router-setup asks about VLANs, IPv6 and
the domain. The router publishes its own name in DNS and hands the local
domain to DHCP clients; wg and jq are on the router. Includes a script
that takes over antigua's tunnels.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
The DHCP page edits the reservations per LAN (checked like the module's
assertions) and offers active clients for reservation. The web UI checks
that need no installed system moved from apply.nix into webui.nix (one
node, under a minute); apply.nix shares one apply for the pool change and
a reservation.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
The web UI lists physical and VLAN interfaces and takes a VLAN uplink as
a device name (enp2s0.1001); it has IPv6 checkboxes for the WAN and each
LAN. router-setup asks whether to create VLANs before WAN and LAN are
picked, and whether to enable IPv6. --config-only writes the
configuration without installing, which the setup test uses for a PPPoE
uplink on a VLAN.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
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