Strategy: observe first, then manage (D-004). Each work package (WP) is sized for one focused session; a merged WP (D-054, D-055) names its split points (Split:), parts that each fit one, and every split point is a checkpoint with the clear-or-keep test (AGENTS.md directive 2). The next milestones have detailed acceptance criteria; later ones are refined just in time, and the first task of every milestone is to refine its WPs.
Legend: [x] done · [ ] to do · (S/M/L/XL) size, where XL is two L-sized parts · → dependencies
Goal: a real (if empty) product ships. Every push to main builds, tests and releases binaries plus a scratch image for 4 architectures.
- M0.1 Plan & directives (M): README, AGENTS/CLAUDE, docs, license, GitHub repo.
- M0.2 Rust workspace + server skeleton (M) → M0.1
- Cargo workspace with
crates/server(packageuniwrt, binuniwrt),rust-toolchain.tomlpinned to stable, workspace lints, and the release profile from BUILD.md. uniwrt serve(axum):GET /api/v1/healthreturns{status, version, commit, built_at}. Serves the embedded UI with precompressed assets and an SPA fallback. Whenweb/distis absent, a placeholder page is embedded instead, so the backend builds without Node.uniwrt healthcheck(exit code 0/1, for Docker) anduniwrt version. Config through flags or env:UNIWRT_HTTP_LISTEN(default0.0.0.0:8080) andUNIWRT_DATA_DIR(default./data). Tracing, and graceful shutdown on SIGTERM/SIGINT.- Tests: the health handler, asset serving (content-encoding negotiation, cache headers) and the SPA fallback.
- Cargo workspace with
- M0.3 Web skeleton (M) → M0.1
web/: bun, Vite, Solid 1.9, strict TypeScript, Tailwind v4 (tokens.csswith dark and light themes), Biome, Vitest with Testing Library.- Placeholder app shell (wordmark, sidebar skeleton, a health badge fed by
/api/v1/health) and a Vite proxy to the backend. - At least one component test. The build writes hashed assets to
web/dist/assets/(BUILD.md, Embedded UI) and uses no inline scripts or styles (the CSP in security.md). - The health badge's type is generated from the Rust DTO (
crates/server/src/http/health.rs) with ts-rs (D-017); M0.4 wraps the export injust gen.
- M0.4 Build tooling & image (M) → M0.2, M0.3
- justfile (dev, check, test, fmt, lint, build, cross, image, map, plus a demo stub) and
scripts/gen-map.sh→docs/MAP.md. - Cross builds for the 4 musl targets with cargo-zigbuild; locally they run inside the cargo-zigbuild container image.
FROM scratchDockerfile per BUILD.md: prebuilt binary chosen by TARGETARCH/TARGETVARIANT, UID 65532,/data, HEALTHCHECK.- Local verification: the image runs and
/api/v1/healthanswers. Record binary sizes in STATUS.
- justfile (dev, check, test, fmt, lint, build, cross, image, map, plus a demo stub) and
- M0.5 CI/CD (M) → M0.4
.github/workflows/ci.ymlper BUILD.md: version → web → rust → build ×4 (with qemu smoke tests) → image (GHCR, SBOM and provenance, cosign) → release (tag, assets, notes) onmain. PRs only build and test. CI fails ifdocs/MAP.mdis stale.scripts/next-version.shwith a test script, cargo-deny config, Dependabot (cargo, npm, actions), and CI/release badges in the README.- Acceptance: a push to
mainproduces release v0.1.0 with 4 tarballs and SHA256SUMS plus a 4-platform image, anddocker run --platform linux/arm/v7 ghcr.io/robertzas/uniwrt:latest versionworks.
Goal: adopt the owner's existing nodes read-only and watch them live in a polished app shell.
- M1.1 core, model (facts), store (M) → M0
core: IDs, MacAddr, network types, error conventions, aClocktrait.model::facts: NodeFacts, Capabilities, RadioFacts, StationFacts and InterfaceFacts (serde, with fixtures).store: SQLite in WAL mode, embedded migrations, a batching writer actor and repository traits. Tests use a temporary DB.
- M1.2 SSH transport (L) → M1.1
nodecrate: russh client; a controller ed25519 key (generated and kept in a vault stub); TOFU host-key pinning; aNodeLinktrait (call/exec/stream/upload/events); a connection actor with keepalive and jittered backoff; jump hosts throughdirect-tcpip.- Tests against an in-process russh server, plus dropbear in an OpenWrt rootfs container (CI job).
- Also done here (D-032): the agent envelope in
proto.rs(requests, answers, events, ticks, hello, the protocol range) and the RPC multiplexer, whichNodeLink::callneeds.
- M1.3 node-agent v0: core, bootstrap, system facts (L) → M1.2
- A ucode agent with two loop modes (D-034): hello and caps,
ping,collect(sets board, system, capabilities, interfaces, devices),ubus,exec(argv, timeout, bounded output),read_file. The ops are typed inproto/ops.rsand mirrored inproto.uc/ops.uc; the agent exits when its stdin closes. - Upload-by-hash bootstrap into
/tmp/uniwrt/agent; parsing intomodel::facts, tested with fixtures captured from rootfs containers and with the canonical gateway. - Agent tests in OpenWrt rootfs containers booted with procd (24.10 and 25.12; the CI job
agent).
- A ucode agent with two loop modes (D-034): hello and caps,
- M1.3b node-agent: wireless, clients and mesh sets (L) → M1.3 (split from M1.3; the streams moved to M1.3c)
- Collect sets phys, radios, stations, leases, neighbors (with the bridge FDB) and batman, with their model types and parsers: nl80211 through ucode-mod-nl80211, netifd and hostapd over ubus, rtnetlink with a /proc fallback, dnsmasq and odhcpd, and batctl's JSON queries (D-035).
- The hwsim lab (
just test-wifi): virtual radios and batman-adv in OpenWrt 24.10 and 25.12 containers. Its sanitized captures are the wireless fixtures until the owner's nodes are captured.
- M1.3c node-agent: streams (M) → M1.3b
- Under the event loop (D-036):
subscribe/unsubscribe(the topics hostapd, netifd and dhcp over ubus, a subscriber per object, re-subscribing as objects appear),log_follow(logd polled by id by default, orlogread -fstreamed) andset_intervalticks. The synchronous mode answers themunsupported, and the engine polls instead. node::eventsparses the pushes into ticks,model::events (stations, devices, interfaces, leases) andmodel::logentries, tested against a lab capture (fixtures/events/).- Tested in the agent containers (ticks, both log modes, netifd) and the hwsim lab (a kicked client, its lease, a radio restart).
- Under the event loop (D-036):
- M1.4 engine (M) → M1.3c
- A NodeActor per node, the live state store, the event bus, subscription-driven adaptive intervals, snapshot persistence, and node status (online/degraded/offline) with reasons (D-037).
- Pushed events refresh the sets they touch; roams are inferred across nodes; logs are followed on demand.
servewatches the adopted nodes over SSH links. Tested over a scripted link on tokio's paused clock, and against the agent containers in both loop modes.
- M1.5 Simulator + demo mode (M) → M1.4
simcrate implementingNodeLink: a gateway, 3 APs and about 30 clients, with roaming, signal drift, events and logs. Deterministic seed.uniwrt serve --demo.- Done (D-038): a seeded world (path loss and fading, 802.11r roams, clients that leave and come back, bursty traffic, lease renewals, a wired AP and a multi-hop batman mesh, AP reboots) rendered in the agent's raw shapes, so the real parsers run;
SimLinkanswers the event-loop agent's ops and pushes events, ticks and both log modes.serve --demo [--demo-seed]runs the engine onFleetLinkover a fresh database in the data directory, deleted at exit;just demo. Tested through the node crate's parsers and with the engine on tokio's paused clock.
- M1.6 Vault and TLS (M) → M1.4 (split from M1.6, owner's choice: auth moved to M1.6b, REST and WS to M1.6c)
- The real vault: XChaCha20-Poly1305 under the master key (
master.key,UNIWRT_MASTER_KEY_FILEorUNIWRT_MASTER_KEY), resealing the stub'salg = 'plain'rows (D-032). HTTPS on 8443 with a certificate the user can download and trust. - Done (D-039): a name-constrained local CA and the leaf it signs (rcgen), both in the vault; rustls with ring, HTTP/2 and a hot-swapped leaf renewed by a daily check;
GET /api/v1/tlsand/api/v1/tls/ca.crt; plain HTTP redirects except the health check and--trusted-proxies;--tls-names. Tested with webpki (chains, name constraints, validity), a TLS handshake, and the binary in CI's smoke test.
- The real vault: XChaCha20-Poly1305 under the master key (
- M1.6b api crate, auth and sessions (M) → M1.6
- The
apicrate (the routes, problem+json errors and security headers move there fromserver/src/http), first-run admin setup, login and sessions (Argon2id), CSRF defenses (SameSite, Origin, a custom header), login rate limiting and lockout, and the audit of logins (features/security.md).ConnectInfo<Peer>gives the client's address; behind a trusted proxy,X-Forwarded-ForandX-Forwarded-Proto. - Done (D-040): crate
uniwrt-api(Api,router,ApiError,Client,CurrentUser, theCertificatetrait for/v1/tls); setup within 10 minutes of a start without users (owner's choice);/v1/auth,/setup,/login,/logout,/me; migration 0003 (users,sessions,audit) and their repos; the listeners setClient(forwarded headers only from trusted proxies). API tokens are left for later. Tested through the router over a temporary store and with the binary.
- The
- M1.6c REST and the WS hub (M) → M1.6b
- Node and fleet routes over
Engine<FleetLink>, a WS hub with topics and deltas (at most 10 per second per topic), and ts-rs codegen (just gen) for the DTOs; tested againstserve --demo. - Done (D-041):
uniwrt_engine::Fleet(the owner's choice) andEngine::interests;GET v1/nodes,GET v1/nodes/{id};/v1/wswith the topicsfleet,node:<id>(holds focus) andlogs?node=<id>&min=…(follows the log), snapshots then merged deltas every 100 ms, resync after lag, closing on logout, silence, stalls and shutdown, over HTTP/1.1 and HTTP/2 (extended CONNECT); thetsfeature ofcoreandmodel, so the DTOs carry the model's types. Tested with unit tests of the merging, over loopback sockets against the simulated fleet (HTTP/1.1 and HTTP/2), and withserve --demoover HTTPS.
- Node and fleet routes over
- M1.7 Adopt existing node (observe mode) (M) → M1.6c (split, owner's choice: discovery and batch adoption moved to M1.7b, the wizard to M1.8b)
- API and job: IP + root password → confirm the host-key fingerprint → install the key → inventory and config backup → live view.
- Done (D-042): crate
uniwrt-jobs(Jobs, the adopt job,Dial/Remote/SshDial), jobs kept as history (no resume); migration 0004 (jobs,node_inventory,node_backupssealed in the DB,audit.detail);POST v1/adopt/probe,POST v1/adopt,GET v1/jobs[/{id}], the WS topicjobs; adopted nodes are watched at once. Tested over a scripted dial and a temporary store, and by adopting OpenWrt 24.10 and 25.12 containers, checking that onlyauthorized_keyschanges.
- M1.7b Discovery and batch adoption (S) → M1.7
- Discovered nodes from the gateway's DHCP leases (and batman originators), adopted in one go with one password.
- Acceptance (with M1.8b): the owner's 4 nodes are adopted, with zero config changes other than
authorized_keys. - Done (D-044):
GET v1/discovered, built from the adopted nodes' leases, LAN neighbors (owner's choice) and batman meshes, with SSH banner and host-key probes (uniwrt_node::banner,Dial::banner; 4 at a time, held 10 minutes);POST v1/adopt/batch(one password, a job per node); at most 4 adoptions at once. Tested over the gateway fixture with a scripted dial, through the router (batches, demo mode), and the banner against dropbear (just test-dropbear).
- M1.8 App shell & design system v1 (L) → M1.6c (split: sign-in, the API and WS clients, the Devices list and the live Device overview moved to M1.8c)
- Decision gates: is Solid 2.0 GA, and is the ecosystem ready? Settle the UI direction (O-5) by showing the owner 3 rendered mockups.
- Tokens and primitives (Button, Card, Badge, virtualized Table, Tabs, Dialog, Drawer, Tooltip, Toast, Stat, Sparkline), layout, sidebar, top bar, ⌘K palette, theme toggle.
- Done (D-045, D-046): Solid 2 RC with router 2, solid-query 6 and a patched Kobalte 2 (owner's choice after a spike); three skins (Graphite pro, Midnight glass, UniFi-familiar) × light and dark, chosen in the sidebar's Appearance menu or ⌘K and applied before the first paint;
tokens.test.tschecks WCAG contrast for every skin and theme; primitives inweb/src/ui/(Icon overlucidedata, a Table virtualized above 200 rows with@tanstack/virtual-core); the shell with an icon rail (UniFi-familiar), a mobile drawer, the ⌘K palette (pages, skins, themes) and a page error boundary; the dev-only gallery at/dev/ui?skin=…&theme=…. Tested with Vitest (67 tests, the Kobalte patch included) and by screenshots of the gallery in each skin.
- M1.8c Sign-in and live devices (M) → M1.8
- First-run setup and login screens over
/v1/auth(sessions, CSRF header), with the shell shown only when signed in;lib/apifor the node routes andlib/ws(thefleetandnode:<id>topics, deltas applied once per animation frame). - A Devices list (the Table, live status, ⌘K finds devices by name, MAC or IP) and a live Device overview (stats, radios, clients) against
serve --demo; a Playwright smoke test in CI against demo mode. - Done (D-047):
app/App.tsxgates the shell on/v1/auth(setup, setup closed, login with 401/429 explained; a 401 or a 4401 close returns to sign-in), an account menu with sign-out;lib/ws(LiveSocket: one socket, ref-counted topics, jittered reconnect, a flush per frame or per second when hidden),lib/live(the fleet store merged in place,createNodeViewwith patches and CPU/memory history),lib/session,lib/format,ui/TextField;features/auth,features/devices(list, overview with stats, radios, details and clients named from the gateway's leases). Tested with Vitest (94 tests) and Playwright againstserve --demoin the runner's Chrome (just e2e, CI jobe2e, which the image waits for).
- First-run setup and login screens over
- M1.8b Adoption wizard (M) → M1.8c, M1.7b
- Devices → Adopt: address and password → the fingerprint to confirm → the job's steps live (WS
jobs) → the node's live view; Discovered nodes with batch adoption; failures explained by their code. A simulated adoption in demo mode. - Done (D-048): the demo leaves its last access point unadopted (owner's choice) and adopts it through the real job and discovery over
DemoDial, the simulated node answering adoption's commands; the wizard is a dialog over Devices (owner's choice,?adopt=1&host=…, ⌘K), with failures explained and acted on by code; the Discovered card under the list (selection, batch dialog with a progress bar per device);lib/jobsfor thejobstopic,ui/Checkbox. Tested with Vitest (105 tests), the demo's adoption in the server's tests, and Playwright adopting the demo's access point through the wizard. The owner's acceptance (4 nodes adopted, onlyauthorized_keyschanged) waits for their hardware.
- Devices → Adopt: address and password → the fingerprint to confirm → the job's steps live (WS
- M1.9 Dev accelerators (S) → M1.6b
just new-feature <area>(scaffolds the files in the feature recipe) andjust arch-check(enforces the layering rules from ARCHITECTURE.md), both wired intojust checkand CI.- Done (D-050):
scripts/arch-check.sh(crates per the table in ARCHITECTURE.md, read from the doc; pure L0 trees and sources; web import directions; injust checkand CI, tested on fixture trees) andscripts/new-feature.sh(model, API, web and, with--telemetry, the collector and parser, wired in and compiling; tested end to end in CI byscripts/new-feature.test.sh). Dialog and Drawer now return focus to their opener.
- M2.1 Topology engine (M): the pure
topocrate builds the graph from facts; API and WS topic; fixture-based tests. - M2.2 Topology view (L): Canvas renderer, d3-force in a worker, tree/RF/clients views, live flows, pin/drag, details panel, 24 h replay slider.
- M2.3 Clients (M): inventory, OUI vendor, device icons, history and roams, experience score v1, rename; a Signal tab from an hour of samples in memory. Kick and block moved to M5.1 (D-053, D-054).
- M2.4 Metrics & dashboard (L, D-056, D-057, D-058): rollups, retention, LTTB downsampling, uPlot charts, dashboard v1 with a topology mini-map (the canvas renderer moves to
web/src/viz/topology), and the Metrics explorer (/insights/metrics: any entity's series over a range). Split: a storage & API ✓ | b charts on device and client pages (the client's Traffic tab; uPlot replacesLineChart.tsx) and the Metrics explorer ✓ | c dashboard and mini-map ✓. - M2.5 Logs, events & alerts (XL; merged M2.5 and M2.6, D-055): syslog receiver (UDP/TCP), logread fallback, FTS5 search, live-tail UI, pattern annotations (
log-patterns.toml); the WSlogstopic's snapshot (empty since M1.6c) then comes from storage; event timeline, alert rules, notifiers (ntfy, webhook and email first). Split (D-059): a1 log ingest, storage, annotations and API ✓ | a2 the logs UI (/insights/logs: live tail, filters, search, histogram, saved searches, context; D-060) ✓ | b1 events end to end (derivation, storage, API and WS topic,/insights/events, the dashboard's card, logs around an event; D-061) ✓ | b2 alert rules over metrics and events, notifiers (in-app, ntfy, webhook, email over SMTP) and their settings (D-061); split (D-062): b2a alert rules and in-app alerts end to end, itself split: b2a1 the backend (rules, evaluator, storage, engine, API and WS topicalerts) ✓ | b2a2 the UI (/insights/alertswith Active, History, Rules and Maintenance, the top bar's bell, the rule editor, maintenance windows and snooze, active alerts on the dashboard) ✓ | b2b notifiers end to end (ntfy, webhooks with templates, email; Settings › Notifications; a test send; D-063), split: b2b1 the backend (thenotifycrate, storage and the vault, dispatch with reminders and digests, the API and the test send) ✓ | b2b2 Settings › Notifications (the list, an editor per kind, the filter, the template editor, the test button, the link address) ✓. - M2.6 RF insight (M, D-064): channel surveys, scheduled scans (every 4 h while idle), the inter-node RSSI matrix, DFS and congestion events, the spectrum view (
/insights/rf) and the dashboard's RF card.
- M3.1 Desired-state model v1, revisions, vault (L; D-065). Split: a | b.
- M3.1a Backend:
SiteConfigv1 (every section; M3.3's fields), validation, structural diff, schema migrations with fixtures; therevisionstable with secrets sealed per value in the vault;v1/siteandv1/revisions(restore, audited reveal); the defaults as revision 1. - M3.1b History page: a read-only Settings › Revisions page: the list (author, time, message, changes), a revision's structural diff from its parent or any other, newest first with paging.
- M3.1a Backend:
- M3.2
ucicrate (M; D-066): AST, parser, serializer, semantic diff; proptest round-trips; fixtures from real configs. - M3.3
renderv1 & system test harness (XL; merged M3.3 and M3.7, D-055): system, network (DSA and swconfig), dhcp, firewall and wireless basics; capability-based; golden tests; OpenWrt x86-64 images (24.10, 25.12) under QEMU or as rootfs containers in CI, validating rendered configs (uci,fw4 check, netifd, wifi on mac80211_hwsim). Split: render | harness.- M3.3a Render (D-067): the board's layout in the facts (board.json), the
rendercrate (system, network on DSA, swconfig and plain NICs with one VLAN-awarebr-lan, the WAN, dhcp, firewall zones from the purpose matrix, wireless, the mesh basics), values carried over from the node, raw sections, golden tests; the embedded time-zone table. - M3.3b Harness (D-068): rendered configs accepted by real OpenWrt 24.10 and 25.12 (
uci,fw4 check, netifd up, Wi-Fi and the mesh on mac80211_hwsim), in CI; settles the⚠️ assumptions in docs/openwrt/REFERENCE.md (Stock packages, board.json, Render's assumptions).
- M3.3a Render (D-067): the board's layout in the facts (board.json), the
- M3.4 Transactions, jobs & review UI (XL; merged M3.4 and M3.6, D-055; D-069): agent transaction and boot guard, controller verify/confirm, the apply job (persisted as history; jobs don't resume, D-042), multi-node ordering with a fleet-wide two-phase commit; pending-changes tray, per-node diffs, impact summary, live progress, history and rollback. Split: a1 | a2 | b.
- M3.4a1 Node transactions & management (D-069): the transaction tool and the boot guard (
node-agent/txn/: stage, check, apply with a dead-man timer under procd, confirm, extend, rollback, rollback at boot),uniwrt_node::txn, the manage and unmanage jobs,PUT v1/nodes/{id}/management, the simulated tool; container tests (confirm, rollback on request, at the deadline and at boot, refusals). - M3.4a2 Apply job: render → fresh fetch → semantic diff per node → plan (leaves → gateway) → stage and check everywhere → apply everywhere with timers armed (
extendwhile the others apply) → verify each over a new connection (role health checks: WAN and DNS on the gateway, the controller reaches every node, mesh neighbors, radios) → confirm all, or let all roll back; the applied revision per node; the API to plan and start an apply; the simulated transactions; an end-to-end apply onscripts/test-system.sh's topology. - M3.4b Review & apply UI: pending-changes tray, per-node diffs (secrets masked), impact summary, live progress, history and rollback; management on/off on the device page.
- M3.4a1 Node transactions & management (D-069): the transaction tool and the boot guard (
- M3.5 Brownfield import & drift (L): lossless import, raw overrides, per-node managed-package toggles, drift detection; the review UI shows raw overrides and drift.
- M4.1 WAN, SQM & failover (L; merged M4.1 and M4.5, D-054): ISP preset format and seed set; WAN types including VLAN, 6rd and DHCPv6-PD; wizard; live connectivity test; cake with speed-test autotuning; optional mwan3 failover whose health checks reuse the connectivity test. Split: presets, WAN & wizard | SQM & failover.
- M4.2 Networks & firewall (L; merged M4.2 and M4.3, D-054): carving the 10.0.0.0/16 space (O-1), networks/VLANs, DHCP pools and reservations, local DNS; a zone per network, the inter-network matrix seeded by purpose, port forwards, UPnP. Split: IPAM & networks | firewall.
- M4.3 Factory gateway bootstrap (M): first-run wizard with make-before-break renumbering.
- M5.1 WLANs & client actions (M; took in M3.8, D-054): security modes, VLAN binding, bands, node scoping, guest isolation, schedules; kick (hostapd
del_clientwith a ban time) and block/unblock (MAC filter) on managed nodes, enabling the client page's disabled buttons (from M2.3, D-053). Split: WLANs | client actions. - M5.2 Roaming & steering (L; merged M5.2 and M5.5, D-054): 802.11r (PSK and SAE key distribution), controller-pushed 11k neighbor reports, 11v, management of the full wpad package; usteer profiles (O-2) and a steering/roaming timeline. Split: roaming | steering.
- M5.3 Mesh backhaul (L): batman-adv over 802.11s and wired uplinks (with an MTU probe), BLA/DAT, VLANs over the mesh, mesh health.
- M5.4 Adoption paths (L): ZTP via ASU with the pending-adoption inbox, the adoption port, the one-liner, the apk/opkg abstraction, dumb-AP conversion.
- M6.1 RF planner & apply (XL; merged M6.1 and M6.2, D-055):
rfcrate with constraints, scoring, search and explanations; simulation tests; rolling and synchronized cutovers with rollback; a nightly optimizer with hysteresis. Split: planner | apply & scheduler. - M6.2 Floor plans & heatmap (L): floor-plan editor and a calibrated path-loss heatmap.
- M7.1 Firmware & rolling upgrades (XL; merged M7.1 and M7.2, D-055): firmware catalog & ASU, the controller update notice (from M7.4, D-054); leaf → gateway ordering, health gates, soak periods, canary, pause/resume, pipeline UI. Split: catalog & ASU | rolling upgrades.
- M7.2 Backups, restore & replace (L; merged M7.3 and M7.4, D-054): node and controller backups, site export/import; replace, factory reset & re-adopt, decommission. Split: backups & restore | replace & decommission.
- M8.1 Network services & packages (L; merged M8.1 and M8.3, D-054): DNS suite, DDNS, NTP, the other services, and the package manager UI. Split: DNS, DDNS & NTP | other services & packages.
- M8.2 WireGuard VPN (M)
- M8.3 Diagnostics & troubleshooting (XL; merged M8.3 and M8.4, D-055): diagnostics toolkit, web terminal, UCI explorer; connection doctor, WAN quality monitor, support bundle. Split: toolkit, terminal & UCI explorer | doctor, WAN monitor & support bundle.
- M9.1 UX polish, accessibility, mobile, onboarding (L)
- M9.2 Performance budgets enforced in CI; profiling (M)
- M9.3 Security review, fuzzing, threat model (M)
- M9.4 User guide, screenshots from demo mode, 1.0 release (M)
AI troubleshooting assistant (O-7) · PPSK (O-8) · Home Assistant/MQTT presence (O-9) · Prometheus exporter · multi-site · captive portal (openNDS) · controller package for gateways (O-6) · DNS query analytics · parental-controls UI · PWA push notifications · secondary gateways and multi-WAN at APs.