Repository navigation
Release Notes 0.11.0
Released: 2026-10-12
This release covers 82 commits since v0.10.0, touching 315 files (+26,689 / -6,481). The main additions are an offline demo fleet, MFA sessions that renew themselves through totp_command, and version-matched agent guidance served by the binary. Commands now look entities up by name, IP, or any MAC spelling. Refresh keeps data through partial failures, list ordering is deterministic, the TUI uses less memory, and the release binary is smaller.
🌟 Highlights
Offline demo fleet
unifly tui --demo-fleet runs the real TUI against "Neon Harbor Studios", a fictional network that needs no controller, credentials, or network access. The fleet has a UDM Pro Max gateway, three switches, nine access points, about 140 clients on six VLANs and four SSIDs, a zone-based firewall, NAT, ACLs, and a week of events. A once-a-second simulation keeps WAN traffic, device telemetry, client roams, and security events moving. Running with the same --demo-seed produces the same fleet and the same animation. A curated set of read-only CLI commands also answers from the fleet, including devices ports and devices ports-export. All writes are refused. Library users get the same fleet from Controller::demo behind the new demo feature of unifly-api.
MFA sessions that renew themselves
The new totp_command profile setting names a command, given as an argv array with no shell, that prints the current TOTP code, for example ["op", "item", "get", "UniFi", "--otp"]. unifly runs it for every login that needs a code, with no stdin, a 10 s limit, and trimmed stdout. A long-running TUI on an MFA account therefore keeps its Session-backed features after the first cookie expiry. In unifly-api, this works through a new TotpProvider closure on ControllerConfig::totp_provider.
Session expiry no longer kills the TUI
SessionClient keeps login credentials (enable_reauth) and logs in again once on SessionExpired. Reads are retried transparently. State-changing requests are not replayed. Failed re-logins are rate-limited to one per 30 s. The WebSocket reads a fresh cookie on every reconnect.
Agent guidance from the binary
unifly skill serves concepts, workflows, payload examples, and a command inventory generated from Clap, all matched to the installed version and available offline. unifly skill --install writes the discovery skill to ~/.agents/skills/unifly/SKILL.md. Use --claude to target Claude Code, or --dir for any other skills directory. install.sh and install.ps1 install the skill automatically. To skip it, set UNIFLY_NO_SKILL=1 or pass -NoSkill.
Lookups by name, IP, or any MAC spelling
Device commands (get, stats, restart, locate, upgrade, provision, remove, port-cycle, ports, ports-export, port-set) accept the ID, the device name (case-insensitive), the IP, or the MAC in colon, dash, dotted (aabb.ccdd.eeff), or bare form. get commands for networks, WiFi, firewall policies, zones, groups, NAT, ACL, traffic lists, DNS policies, and hotspot vouchers resolve display names. A name shared by two entities is an error that lists both (exit 2). No match exits with code 4.
Settings output no longer leaks credentials
settings export and JSON/YAML output from settings list and settings get now leave out every x_-prefixed credential field at any depth. Use the new --with-secrets flag to include them. settings set carries over credentials that an update omits, so an edited export can be applied back safely. --allow-secret-removal overrides this check.
✨ CLI and Agent Workflow
- One shared resolver (
cli/commands/util/lookup.rs) handles every lookup, so ambiguity and not-found errors read the same everywhere. clients getaccepts a name, hostname, IP, or MAC.clients roamsandclients wifinow report a name shared by two clients instead of silently picking the first.devices adoptstays MAC-only, and it now rejects malformed MACs. The client actionsblock,unblock,kick,forget,set-ip, andremove-ipread MACs strictly, so an IPv6 address is never treated as a MAC.unifly skill conceptslists the JSON keys for devices, clients, events, alarms, site health, WANs, and stats rows. A test checks each listed key at its record path.- Skill sources moved from
skills/unifly/references/andskills/unifly/examples/intocrates/unifly/skill/, and the examples are now checked byskill_examples_test.rs. --demo-fleetand--demo-seedare global flags (UNIFI_DEMO_FLEET,UNIFI_DEMO_SEED). Plain--demonow falls back to the demo fleet when no controller is configured.- The Homebrew caveats and the AUR install message (
aur/unifly-bin.install) point users atunifly skill --install.
🖥️ TUI
- Wide-terminal columns: from 120 columns, the Devices table adds Clients and Firmware, and the Clients table adds Network and Uplink (for example "sw-core port 14"). WiFi AP Health shows every radio in 2.4, 5, and 6 GHz columns. From 96 columns, Firewall policies show their source and destination zones.
- Content-sized columns: a new column planner (
tui/table/columns.rs) sizes columns to fit their contents, and every table stays readable at 80x24. - Ordering and selection: list screens sort with the same functions as the CLI, and a refresh or re-sort keeps the cursor on the same entity (
tui/table/selection.rs). - Topology: devices are now laid out under their reported uplinks, with port numbers on wired links, dashed wireless uplinks, and wrapping for wide sibling sets.
- Pixel charts: on kitty-graphics terminals, charts no longer flash, blank rows, or squash the upload lane. The lanes split 2:1 with their own scales, and unused chart images are deleted.
- Fixes: the NAT tab on the Firewall screen was always empty. Other fixes cover the Stats time axis (milliseconds vs seconds), event backlog order, Events heatmap alignment, network edits saving to the wrong network, and the browser opener writing onto the TUI.
- Settings screen: saving no longer erases
totp_env,totp_command,ca_cert, ortimeout. Keyring-stored credentials are no longer written toconfig.tomlin plaintext.
🐛 Data Integrity and Refresh
- When an optional fetch fails (firewall policies, zones, ACL, DNS, vouchers, NAT, firewall groups, events), the last known collection is kept instead of being wiped.
- Networks whose detail fetch fails keep their last record. A NAT response that is HTTP 200 but not a rule array is now treated as an error.
- Clients and devices keep their session-merged fields (hostname, uplink, ports, radios) through a failed session fetch (
controller/refresh/carry.rs). - Every store-backed list is sorted before filtering and paging, so
--offset/--limitpages no longer overlap or skip records.firewall policies listshows policies in evaluation order. Client::network_nameties session and hybrid clients to their network, and the field now appears in JSON and YAML output.
⚡️ Performance and Size
- The binary builds its own tokio runtime: one worker for
tui, two for CLI commands. This fixes glibc arena fragmentation that took TUI memory from about 100 MB to over 250 MB overnight. polling_interval_secsis now honoured: when no WebSocket is available, per-device stats are polled between full refreshes.- Session models carry typed fields instead of a flattened map. WebSocket
device:syncframes parse into a typedDeviceSyncand are coalesced into one snapshot per second. - Release builds use fat LTO, and reqwest uses
rustls-no-providerwithringonly. Seventeen unused dependencies were dropped. The stripped x86_64 binary shrinks from 22.0 MB to 18.2 MB. - A new
release-smallcargo profile (opt-level = "s") builds a 13.0 MB binary for low-power hosts, at about 5% more TUI CPU.
👷 Build, CI, and Docs
- The toolchain moves from Rust 1.97 to 1.99, and the new clippy lints are cleared.
- The release workflow runs once per tag,
scripts/changelog-cut.shcuts the changelog, and release outputs and checksum download errors are handled correctly (#38). Workflow actions moved to current major versions. - A key-gated Integration API end-to-end test lane was added.
- New flat "Circuit Dragonfly" brand identity, with assets in
docs/static/brand/and a compact README masthead. The docs site's mobile navigation drawer now works. - The README leads with the agent workflow. The gallery was reshot on the demo fleet.
🔥 Breaking Changes
unifly-apisession models:SessionDevice,SessionClientEntry, andSessionUserEntryno longer have a flattenedextramap. UseSessionClient::get_device_rawwhen you need the full record.WebSocketHandle::newnow takes aCookieProviderclosure instead of a fixed cookie string.ControllerConfighas a newtotp_providerfield. Struct literals needtotp_provider: Noneor..Default::default().- Settings output: scripts that read
x_credential fields fromsettings export,settings list, orsettings getmust now pass--with-secrets.
Upgrade Notes
- Add
--with-secretsto any script that depends on credential fields in settings output. - MFA users running the TUI for long periods should set
totp_commandin their profile so sessions renew automatically. - Run
unifly skill --installto update the agent skill. Older binaries cannot serve the new discovery skill. - Library users should update
ControllerConfigliterals andWebSocketHandle::newcall sites, and enable thedemofeature if they needController::demo. - On low-power hosts, consider building with
cargo build --profile release-small -p unifly.