Released: 2026-04-10
This release adds Site Manager cloud fleet management, a full WiFi TUI dashboard, demo mode for PII-safe screen recordings, and a comprehensive end-to-end test suite running against a real controller. The command count grows from 27 to 28, and the documentation site migrates from VitePress to Zola.
🌟 Highlights
☁️ Site Manager Cloud Fleet Commands
New cloud command group (hosts, sites, devices, isp, sdwan, switch) provides fleet-wide visibility through the Ubiquiti Site Manager API at api.ui.com. A new SiteManagerClient in unifly-api handles cursor-based pagination and cloud rate-limit responses with Retry-After parsing. The cloud switch <site> subcommand fuzzy-matches site names and persists the new target in the active cloud profile.
📡 WiFi TUI Dashboard
A dedicated WiFi screen (key 9) with four sub-tabs surfaces RF health and client experience data directly in the TUI. The Overview tab shows AP health scores, client counts, and a toggleable channel map. Clients lists wireless clients with signal strength and expandable WiFi experience detail. Neighbors displays rogue/nearby APs with band filtering and sortable columns. Roaming renders per-client roam event timelines showing AP transitions and signal changes.
🎭 Demo Mode for PII Sanitization
New Sanitizer in crates/unifly/src/sanitizer.rs intercepts all entity payloads in the TUI data bridge before rendering, replacing personal names with cute deterministic aliases (Starling, Moonbeam, Cosmo...), WAN IPs with RFC 5737 documentation addresses (198.51.100.x), SSIDs with fun replacements ("The Promised LAN"), and optionally redacting MACs and ISP names. Activated via --demo, UNIFI_DEMO=1, or the [demo] config section.
✅ End-to-End Test Suite
A UniFi controller runs in Docker simulation mode during CI, and the CLI executes against it to verify session auth, device listing, output formatting, health subsystems, observability endpoints, destructive-command guards, and error paths. Over 30 e2e tests cover devices, stats, events, WiFi, VPN, NAT, settings, and client resolution errors.
🔧 Interactive Cloud Setup Wizard
unifly config cloud-setup validates a Site Manager API key, discovers accessible consoles and sites, and writes a ready-to-use cloud profile. The wizard prompts for API key entry (or detects UNIFI_API_KEY), lets users pick a console and site, and offers keyring, env var, or plaintext storage.
⚡ Lightweight Connect for One-Shot Commands
Controller::connect_lightweight() authenticates and sets up the session but defers the full data snapshot load. System commands (info, sysinfo, backup) now use this path, avoiding unnecessary network round-trips.
☁️ Cloud & Site Manager
- New
auth_mode = "cloud"end-to-end across CLI profiles, TUI onboarding, and settings screens — cloud profiles default the controller URL tohttps://api.ui.com, force system TLS defaults, disable WebSocket and session caching, and use longer polling intervals (60s refresh, 30s poll) IntegrationClientdetects cloud platform connections and surfacesConsoleOffline/ConsoleAccessDeniederrors with actionable guidance- Cloud connector routing uses
/v1/connector/consoles/{host_id}prefix for Integration API tunneling --host-idglobal flag andUNIFI_HOST_IDenv var for CLI-level host targetingcloud ispandcloud sdwansubcommands for ISP metrics and SD-WAN configuration visibility
🖥️ TUI Improvements
- Device detail view now populates Ports, Radios, and Clients tabs with real data parsed from both Integration and Session API responses — ports show max speed, connector type, and PoE standard; radios show band, channel, width, standard, channel utilization, and TX retry rates
- Firmware upgrade action (
Ukey) with confirmation dialog in device detail - Screen key handlers now take priority over global handlers so device-specific bindings work correctly
- Navigation range extended to keys
1–9for the new WiFi screen - Notification TTL is now severity-aware — warnings and errors persist longer than informational toasts
- Sponsor button replaces the old Donate/PayPal link with GitHub Sponsors URL
🔧 CLI & Config
--themepromoted from TUI-only to a global flag shared by CLI and TUI — resolution order:--theme→UNIFLY_THEMEenv →config defaults.theme→silkcircuit-neonnat policies updatemerges only changed fields into the existing rule via the Session v2 API, eliminating the delete-and-recreate workaround- Non-interactive stdin detection — destructive commands (
device remove,client forget,backup delete) now return aNonInteractiveRequiresYeserror in CI/pipe contexts instead of blocking forever, guiding callers to pass--yes config setnormalizesauth_mode = "legacy"to"session"on write for backward compatibility- Error help text across
AuthFailed,NoCredentials,Unsupported,ProfileNotFound, andNoConfignow references bothconfig init(local) andconfig cloud-setup(Site Manager)
🐛 Bug Fixes
- Platform detection rewritten — both
/api/loginand/api/auth/loginare probed before deciding, fixing misclassification of recent standalone controllers that return 401 from the UniFi OS endpoint - NAT policy creation —
filter_typecorrected to use onlyNONE/ADDRESS_AND_PORT(controller rejectsADDRESS/PORT); interface fields now resolve Integration UUIDs to Session_idstrings;rule_indexauto-increments from existing rules instead of hardcoding 0 - API-key 401 classification —
SessionAuthenum distinguishes cookie vs API-key session clients; a rejected API key now returnsInvalidApiKeyinstead of a misleading "session expired" message - macOS config path — uses
~/.config/unifly/(XDG) instead of~/Library/Application Support/unifly/with automatic migration on first run - Tracing to stderr — CLI tracing subscriber output redirected from stdout to stderr so
warn-level messages no longer corrupt JSON/YAML pipe output - Wi-Fi observability parsing — roam events extract from nested
parameters.DEVICE_FROM.nameinstead of flat fields; WiFi experience useswlan_band; channels redesigned from per-radio rows to country-level regulatory data;format_radio_bandhandles bothstat/staandwifimanband code families - VPN IPsec SA 404 — controllers with no VPN tunnels return 404 for
stat/ipsec-sa;vpn statusnow catches this and shows an empty list - Client uplink MAC — resolution uses
ap_macfor wireless andsw_macfor wired clients regardless of wireless info presence - Session events — API-key session clients treat 404 on
stat/eventas optional-missing;events watchgated on cookie-backed session auth
♻️ Refactoring
convert.rs(2,189 lines) decomposed intoconvert/module directory with 13 per-entity submodules — no behavioral changes- Full
legacy→sessionrename across Rust modules, config keys, TUI labels, CLI error strings, and documentation —LegacyClient→SessionClient,Error::LegacyApi→Error::SessionApi,ensure_legacy_access→ensure_session_access, etc. ensure_session_accesspromoted from a private function inevents.rsto the sharedutil/access.rsmodule- CLI dispatch split into
dispatch()anddispatch_extended()to stay under clippy's cognitive-complexity threshold
📝 Documentation
- Documentation site migrated from VitePress to Zola — eliminates the Node.js build dependency; new SilkCircuit theme with dark/light modes, glassmorphism nav, modular SCSS token system, Elasticlunr search, lazy Mermaid rendering with theme-aware re-renders, and responsive three-column layout
llms.txtandllms-full.txtgenerated from Zola content at build time viadocs/scripts/gen-llms-txt.sh- Prettier configuration (
.prettierrc,.prettierignore) integrated intojustfilefor consistent non-Rust file formatting - GitHub issue templates (bug report, feature request), PR template,
CODE_OF_CONDUCT.md, andSECURITY.mdadded - AGENTS.md and SKILL.md updated with 28-command inventory, cloud auth mode, WiFi screen, and corrected auth guidance
🔧 CI/CD
- End-to-end workflow triggers on push-to-main and pull requests (was manual/weekly-only)
- E2e pipeline overlaps Docker controller startup with Rust compilation for faster runs
- AUR package publishing automated in the release pipeline via
KSXGitHub/github-actions-deploy-aur - ClawHub skill publish job runs after GitHub Release creation
- Version-files automation patches
.claude-plugin/plugin.json,.cursor-plugin/plugin.json, andskills/unifly/SKILL.mdon release
💥 Breaking Changes
auth_mode = "legacy"renamed to"session"— Existing config files withauth_mode = "legacy"are accepted as a backward-compatible alias and normalized to"session"on nextconfig setwrite. No manual migration required, but new configs should use"session".- macOS config path changed — Config and cache now resolve via XDG (
~/.config/unifly/,~/.cache/unifly/) instead of~/Library/Application Support/unifly/. Automatic migration runs on first launch; if it fails, a message instructs you to move files manually.
📋 Upgrade Notes
- If you have
auth_mode = "legacy"in your config, it will continue working but will be rewritten to"session"on next config write - macOS users: config files are auto-migrated from
~/Library/Application Support/unifly/to~/.config/unifly/on first run - Scripts using destructive commands without
--yesin non-interactive contexts (CI, pipes) will now fail with exit code 1 and a message pointing to the-yflag - Cloud profiles can be created with
unifly config cloud-setup— existing local profiles are unaffected - The
--themeflag is now global; if you were passing it totuispecifically, it still works but can now be used with any command
💜 Contributors
A huge thank you to the community contributor who helped shape this release:
- @TickTockBent (Wes) landed #10, the
nat policies updatesubcommand — replacing the old delete-and-recreate dance with a proper merge against the Session v2 API. A much nicer story for anyone scripting NAT changes. Thank you! 💜
Want to help shape the next release? The issue tracker is open, and PRs are always welcome.