Skip to content

Release v0.11.0

Latest

Choose a tag to compare

@github-actions github-actions released this 12 Oct 20:13
· 3 commits to main since this release

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 get accepts a name, hostname, IP, or MAC. clients roams and clients wifi now report a name shared by two clients instead of silently picking the first.
  • devices adopt stays MAC-only, and it now rejects malformed MACs. The client actions block, unblock, kick, forget, set-ip, and remove-ip read MACs strictly, so an IPv6 address is never treated as a MAC.
  • unifly skill concepts lists 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/ and skills/unifly/examples/ into crates/unifly/skill/, and the examples are now checked by skill_examples_test.rs.
  • --demo-fleet and --demo-seed are global flags (UNIFI_DEMO_FLEET, UNIFI_DEMO_SEED). Plain --demo now 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 at unifly 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, or timeout. Keyring-stored credentials are no longer written to config.toml in 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/--limit pages no longer overlap or skip records. firewall policies list shows policies in evaluation order.
  • Client::network_name ties 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_secs is 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:sync frames parse into a typed DeviceSync and are coalesced into one snapshot per second.
  • Release builds use fat LTO, and reqwest uses rustls-no-provider with ring only. Seventeen unused dependencies were dropped. The stripped x86_64 binary shrinks from 22.0 MB to 18.2 MB.
  • A new release-small cargo 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.sh cuts 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-api session models: SessionDevice, SessionClientEntry, and SessionUserEntry no longer have a flattened extra map. Use SessionClient::get_device_raw when you need the full record.
  • WebSocketHandle::new now takes a CookieProvider closure instead of a fixed cookie string.
  • ControllerConfig has a new totp_provider field. Struct literals need totp_provider: None or ..Default::default().
  • Settings output: scripts that read x_ credential fields from settings export, settings list, or settings get must now pass --with-secrets.

Upgrade Notes

  • Add --with-secrets to any script that depends on credential fields in settings output.
  • MFA users running the TUI for long periods should set totp_command in their profile so sessions renew automatically.
  • Run unifly skill --install to update the agent skill. Older binaries cannot serve the new discovery skill.
  • Library users should update ControllerConfig literals and WebSocketHandle::new call sites, and enable the demo feature if they need Controller::demo.
  • On low-power hosts, consider building with cargo build --profile release-small -p unifly.

Contributors

  • Stefanie Jane
  • jasonwitty (TUI memory and session renewal, #37)
  • AuStdX (release workflow fixes, #38)