unifioptimizer 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. unifioptimizer-0.1.0/LICENSE +21 -0
  2. unifioptimizer-0.1.0/MANIFEST.in +18 -0
  3. unifioptimizer-0.1.0/PKG-INFO +483 -0
  4. unifioptimizer-0.1.0/README.md +440 -0
  5. unifioptimizer-0.1.0/netadmin/__init__.py +9 -0
  6. unifioptimizer-0.1.0/netadmin/_webui/assets/index-CGEuMFCp.js +16 -0
  7. unifioptimizer-0.1.0/netadmin/_webui/assets/index-DIoqNyN6.css +1 -0
  8. unifioptimizer-0.1.0/netadmin/_webui/assets/inter-cyrillic-ext-opsz-normal-CfYZpDVp.woff2 +0 -0
  9. unifioptimizer-0.1.0/netadmin/_webui/assets/inter-cyrillic-opsz-normal-BFQXbQ-f.woff2 +0 -0
  10. unifioptimizer-0.1.0/netadmin/_webui/assets/inter-greek-ext-opsz-normal-BmPQvyvo.woff2 +0 -0
  11. unifioptimizer-0.1.0/netadmin/_webui/assets/inter-greek-opsz-normal-BM0NlUE_.woff2 +0 -0
  12. unifioptimizer-0.1.0/netadmin/_webui/assets/inter-latin-ext-opsz-normal-DpK-iCPk.woff2 +0 -0
  13. unifioptimizer-0.1.0/netadmin/_webui/assets/inter-latin-opsz-normal-BwkfbSeq.woff2 +0 -0
  14. unifioptimizer-0.1.0/netadmin/_webui/assets/inter-vietnamese-opsz-normal-Ci9T9ieq.woff2 +0 -0
  15. unifioptimizer-0.1.0/netadmin/_webui/favicon.svg +18 -0
  16. unifioptimizer-0.1.0/netadmin/_webui/index.html +34 -0
  17. unifioptimizer-0.1.0/netadmin/_webui/vite.svg +13 -0
  18. unifioptimizer-0.1.0/netadmin/analytics/__init__.py +30 -0
  19. unifioptimizer-0.1.0/netadmin/analytics/offenders.py +240 -0
  20. unifioptimizer-0.1.0/netadmin/cli.py +813 -0
  21. unifioptimizer-0.1.0/netadmin/config.py +528 -0
  22. unifioptimizer-0.1.0/netadmin/correlate/__init__.py +59 -0
  23. unifioptimizer-0.1.0/netadmin/correlate/engine.py +544 -0
  24. unifioptimizer-0.1.0/netadmin/correlate/models.py +157 -0
  25. unifioptimizer-0.1.0/netadmin/correlate/rules.py +433 -0
  26. unifioptimizer-0.1.0/netadmin/correlate/store_repository.py +144 -0
  27. unifioptimizer-0.1.0/netadmin/correlate/topology.py +184 -0
  28. unifioptimizer-0.1.0/netadmin/demo/__init__.py +26 -0
  29. unifioptimizer-0.1.0/netadmin/demo/seed.py +1684 -0
  30. unifioptimizer-0.1.0/netadmin/detect/__init__.py +1 -0
  31. unifioptimizer-0.1.0/netadmin/detect/baseline.py +463 -0
  32. unifioptimizer-0.1.0/netadmin/detect/catalog.py +586 -0
  33. unifioptimizer-0.1.0/netadmin/detect/context.py +274 -0
  34. unifioptimizer-0.1.0/netadmin/detect/detectors/__init__.py +1 -0
  35. unifioptimizer-0.1.0/netadmin/detect/detectors/client.py +572 -0
  36. unifioptimizer-0.1.0/netadmin/detect/detectors/infra.py +299 -0
  37. unifioptimizer-0.1.0/netadmin/detect/detectors/net.py +420 -0
  38. unifioptimizer-0.1.0/netadmin/detect/detectors/wan.py +922 -0
  39. unifioptimizer-0.1.0/netadmin/detect/detectors/wifi.py +1722 -0
  40. unifioptimizer-0.1.0/netadmin/detect/detectors/wired.py +880 -0
  41. unifioptimizer-0.1.0/netadmin/detect/engine.py +549 -0
  42. unifioptimizer-0.1.0/netadmin/domain/__init__.py +1 -0
  43. unifioptimizer-0.1.0/netadmin/domain/entities.py +87 -0
  44. unifioptimizer-0.1.0/netadmin/domain/types.py +67 -0
  45. unifioptimizer-0.1.0/netadmin/fixes/__init__.py +77 -0
  46. unifioptimizer-0.1.0/netadmin/fixes/applier.py +448 -0
  47. unifioptimizer-0.1.0/netadmin/fixes/models.py +291 -0
  48. unifioptimizer-0.1.0/netadmin/fixes/planner.py +475 -0
  49. unifioptimizer-0.1.0/netadmin/fixes/reader.py +102 -0
  50. unifioptimizer-0.1.0/netadmin/fixes/service.py +432 -0
  51. unifioptimizer-0.1.0/netadmin/fixes/verifier.py +92 -0
  52. unifioptimizer-0.1.0/netadmin/fixes/writer.py +142 -0
  53. unifioptimizer-0.1.0/netadmin/ingest/__init__.py +1 -0
  54. unifioptimizer-0.1.0/netadmin/ingest/backfill.py +430 -0
  55. unifioptimizer-0.1.0/netadmin/ingest/collector.py +582 -0
  56. unifioptimizer-0.1.0/netadmin/ingest/events.py +426 -0
  57. unifioptimizer-0.1.0/netadmin/ingest/factory.py +596 -0
  58. unifioptimizer-0.1.0/netadmin/ingest/mapping.py +501 -0
  59. unifioptimizer-0.1.0/netadmin/ingest/probes.py +453 -0
  60. unifioptimizer-0.1.0/netadmin/ingest/unifi/__init__.py +38 -0
  61. unifioptimizer-0.1.0/netadmin/ingest/unifi/auth.py +367 -0
  62. unifioptimizer-0.1.0/netadmin/ingest/unifi/client.py +330 -0
  63. unifioptimizer-0.1.0/netadmin/ingest/unifi/detect.py +608 -0
  64. unifioptimizer-0.1.0/netadmin/ingest/unifi/endpoints.py +297 -0
  65. unifioptimizer-0.1.0/netadmin/ingest/unifi/models.py +326 -0
  66. unifioptimizer-0.1.0/netadmin/ingest/unifi/ws.py +156 -0
  67. unifioptimizer-0.1.0/netadmin/integrations/__init__.py +1 -0
  68. unifioptimizer-0.1.0/netadmin/integrations/home_assistant.py +866 -0
  69. unifioptimizer-0.1.0/netadmin/issues/__init__.py +40 -0
  70. unifioptimizer-0.1.0/netadmin/issues/engine.py +618 -0
  71. unifioptimizer-0.1.0/netadmin/issues/inhibition.py +108 -0
  72. unifioptimizer-0.1.0/netadmin/issues/models.py +208 -0
  73. unifioptimizer-0.1.0/netadmin/issues/store_repository.py +165 -0
  74. unifioptimizer-0.1.0/netadmin/llm/__init__.py +45 -0
  75. unifioptimizer-0.1.0/netadmin/llm/anthropic.py +156 -0
  76. unifioptimizer-0.1.0/netadmin/llm/copilot.py +105 -0
  77. unifioptimizer-0.1.0/netadmin/llm/dossier.py +608 -0
  78. unifioptimizer-0.1.0/netadmin/llm/manual.py +72 -0
  79. unifioptimizer-0.1.0/netadmin/llm/provider.py +194 -0
  80. unifioptimizer-0.1.0/netadmin/llm/service.py +239 -0
  81. unifioptimizer-0.1.0/netadmin/logging.py +100 -0
  82. unifioptimizer-0.1.0/netadmin/report/__init__.py +14 -0
  83. unifioptimizer-0.1.0/netadmin/report/assembler.py +1280 -0
  84. unifioptimizer-0.1.0/netadmin/report/charts.py +209 -0
  85. unifioptimizer-0.1.0/netadmin/report/models.py +408 -0
  86. unifioptimizer-0.1.0/netadmin/report/playbook.py +97 -0
  87. unifioptimizer-0.1.0/netadmin/report/severity.py +136 -0
  88. unifioptimizer-0.1.0/netadmin/server/__init__.py +1 -0
  89. unifioptimizer-0.1.0/netadmin/server/auth.py +436 -0
  90. unifioptimizer-0.1.0/netadmin/server/main.py +460 -0
  91. unifioptimizer-0.1.0/netadmin/server/routers/__init__.py +1 -0
  92. unifioptimizer-0.1.0/netadmin/server/routers/changes.py +56 -0
  93. unifioptimizer-0.1.0/netadmin/server/routers/events.py +69 -0
  94. unifioptimizer-0.1.0/netadmin/server/routers/fixes.py +331 -0
  95. unifioptimizer-0.1.0/netadmin/server/routers/incidents.py +189 -0
  96. unifioptimizer-0.1.0/netadmin/server/routers/inventory.py +274 -0
  97. unifioptimizer-0.1.0/netadmin/server/routers/issues.py +271 -0
  98. unifioptimizer-0.1.0/netadmin/server/routers/metrics.py +164 -0
  99. unifioptimizer-0.1.0/netadmin/server/routers/ondemand.py +163 -0
  100. unifioptimizer-0.1.0/netadmin/server/routers/report.py +49 -0
  101. unifioptimizer-0.1.0/netadmin/server/routers/setup.py +459 -0
  102. unifioptimizer-0.1.0/netadmin/server/routers/sle.py +175 -0
  103. unifioptimizer-0.1.0/netadmin/server/routers/system.py +83 -0
  104. unifioptimizer-0.1.0/netadmin/server/runtime.py +404 -0
  105. unifioptimizer-0.1.0/netadmin/server/serialize.py +77 -0
  106. unifioptimizer-0.1.0/netadmin/server/services/__init__.py +1 -0
  107. unifioptimizer-0.1.0/netadmin/server/services/discovery.py +304 -0
  108. unifioptimizer-0.1.0/netadmin/server/ws.py +302 -0
  109. unifioptimizer-0.1.0/netadmin/sle/__init__.py +31 -0
  110. unifioptimizer-0.1.0/netadmin/sle/classifiers.py +453 -0
  111. unifioptimizer-0.1.0/netadmin/sle/minutes.py +796 -0
  112. unifioptimizer-0.1.0/netadmin/sle/scores.py +190 -0
  113. unifioptimizer-0.1.0/netadmin/store/__init__.py +42 -0
  114. unifioptimizer-0.1.0/netadmin/store/db.py +151 -0
  115. unifioptimizer-0.1.0/netadmin/store/metrics.py +105 -0
  116. unifioptimizer-0.1.0/netadmin/store/migrations/0001_init.sql +126 -0
  117. unifioptimizer-0.1.0/netadmin/store/migrations/0002_issues_resolved_index.sql +7 -0
  118. unifioptimizer-0.1.0/netadmin/store/migrations/0003_raw_tier_indexes.sql +13 -0
  119. unifioptimizer-0.1.0/netadmin/store/migrations/0004_incidents.sql +36 -0
  120. unifioptimizer-0.1.0/netadmin/store/migrations/__init__.py +1 -0
  121. unifioptimizer-0.1.0/netadmin/store/repository.py +1887 -0
  122. unifioptimizer-0.1.0/netadmin/visit/__init__.py +21 -0
  123. unifioptimizer-0.1.0/netadmin/visit/report.py +499 -0
  124. unifioptimizer-0.1.0/netadmin/visit/runner.py +672 -0
  125. unifioptimizer-0.1.0/pyproject.toml +131 -0
  126. unifioptimizer-0.1.0/requirements.txt +19 -0
  127. unifioptimizer-0.1.0/setup.cfg +4 -0
  128. unifioptimizer-0.1.0/unifioptimizer.egg-info/PKG-INFO +483 -0
  129. unifioptimizer-0.1.0/unifioptimizer.egg-info/SOURCES.txt +131 -0
  130. unifioptimizer-0.1.0/unifioptimizer.egg-info/dependency_links.txt +1 -0
  131. unifioptimizer-0.1.0/unifioptimizer.egg-info/entry_points.txt +2 -0
  132. unifioptimizer-0.1.0/unifioptimizer.egg-info/requires.txt +16 -0
  133. unifioptimizer-0.1.0/unifioptimizer.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gary Neitzke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,18 @@
1
+ # Source-distribution contents. The wheel's package data is declared in
2
+ # pyproject.toml ([tool.setuptools.package-data]); this file makes `python -m
3
+ # build` (which builds the wheel FROM the sdist) also carry the prebuilt web UI,
4
+ # so the bundled dashboard survives the sdist -> wheel round-trip.
5
+ #
6
+ # netadmin/_webui/ is a build artifact (produced by `python tools/build_web.py`,
7
+ # not tracked in git), so it is grafted here explicitly rather than via VCS.
8
+ graft netadmin/_webui
9
+
10
+ # Numbered SQL migrations applied at store startup — runtime data, must ship.
11
+ recursive-include netadmin/store/migrations *.sql
12
+
13
+ include README.md
14
+ include LICENSE
15
+ include requirements.txt
16
+
17
+ # Keep bytecode / caches out of the distribution.
18
+ global-exclude __pycache__ *.py[cod] *.so .DS_Store
@@ -0,0 +1,483 @@
1
+ Metadata-Version: 2.4
2
+ Name: unifioptimizer
3
+ Version: 0.1.0
4
+ Summary: A network admin that remembers: a stateful UniFi monitor with a local time-series store, confounder-aware detectors, and an issue-lifecycle engine.
5
+ Author-email: Gary Neitzke <gneitzke123@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/gneitzke/UnifiOptimizer
8
+ Project-URL: Repository, https://github.com/gneitzke/UnifiOptimizer
9
+ Project-URL: Issues, https://github.com/gneitzke/UnifiOptimizer/issues
10
+ Project-URL: Documentation, https://github.com/gneitzke/UnifiOptimizer/blob/main/docs/ARCHITECTURE.md
11
+ Keywords: unifi,ubiquiti,network-monitoring,wifi,self-hosted,sqlite,home-assistant,observability
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Framework :: FastAPI
16
+ Classifier: Intended Audience :: System Administrators
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: System :: Networking :: Monitoring
23
+ Classifier: Topic :: System :: Systems Administration
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: httpx>=0.27
28
+ Requires-Dist: pydantic>=2
29
+ Requires-Dist: pydantic-settings>=2
30
+ Requires-Dist: APScheduler>=3.10
31
+ Requires-Dist: websockets>=12
32
+ Requires-Dist: pyyaml>=6
33
+ Requires-Dist: rich>=13
34
+ Requires-Dist: fastapi>=0.104
35
+ Requires-Dist: uvicorn[standard]>=0.24
36
+ Requires-Dist: dnspython>=2
37
+ Requires-Dist: aiomqtt>=2
38
+ Provides-Extra: test
39
+ Requires-Dist: pytest>=8; extra == "test"
40
+ Requires-Dist: pytest-asyncio>=0.23; extra == "test"
41
+ Requires-Dist: respx>=0.21; extra == "test"
42
+ Dynamic: license-file
43
+
44
+ # UnifiOptimizer
45
+
46
+ **A network admin that remembers.** UnifiOptimizer watches a UniFi network the way a
47
+ good technician would: it keeps a running history, notices when something starts
48
+ misbehaving, waits to rule out the obvious false alarms, tells you what it thinks
49
+ is wrong and why, and then checks whether the fix actually held.
50
+
51
+ This is a ground-up rebuild of UnifiOptimizer. The original tool ran a stateless
52
+ snapshot on demand and printed a report. UnifiOptimizer keeps state. That one change is
53
+ the whole point: a controller throws away its fine-grained stats after about a
54
+ day, so anything you do not collect daily is simply gone, and no snapshot tool can
55
+ tell you that a port started erroring on Tuesday or that a mesh link has been
56
+ sliding for a week. UnifiOptimizer can, because it was watching.
57
+
58
+ The rebuild lives in the `netadmin/` Python package and is the whole project now;
59
+ the original stateless `optimizer.py` CLI was removed at cutover.
60
+
61
+ Architecture and design decisions are documented in full at
62
+ [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md), and there is a plain-language,
63
+ hand-drawn walkthrough at [`docs/HOW_IT_WORKS.md`](docs/HOW_IT_WORKS.md).
64
+
65
+ ![UnifiOptimizer dashboard: an 87/100 health score, a collector-health strip, six service-level cards with 24-hour trend sparklines, active incidents grouped by severity, and an Export report action.](docs/img/screenshots/dashboard-dark.png)
66
+
67
+ ## Quick start
68
+
69
+ ```bash
70
+ pip install unifioptimizer
71
+
72
+ # see it working on a fictional, PII-free demo network (no controller needed)
73
+ netadmin demo-seed --out data/netadmin-demo.db --now $(( $(date +%s) / 300 * 300 ))
74
+ NETADMIN_DB_PATH=data/netadmin-demo.db netadmin daemon # then open http://localhost:8765
75
+
76
+ # or point it at your own controller: put credentials in data/secrets.env (below)
77
+ netadmin daemon
78
+ ```
79
+
80
+ The wheel bundles the compiled dashboard, so `pip install` alone gives you a
81
+ working UI with no Node.js. Running from a source checkout instead? See
82
+ [Install](#install) — it needs one build step.
83
+
84
+ ---
85
+
86
+ ## What it does
87
+
88
+ UnifiOptimizer is one Python process that runs a collector, a detection engine, an
89
+ issue-lifecycle tracker, a health model, and a small web/API server on the same
90
+ event loop.
91
+
92
+ - **Collects history into a local store.** Every 60 seconds it pulls device,
93
+ client, and health stats over the controller's REST API, listens to the event
94
+ WebSocket in real time, and runs its own DNS and ICMP probes for the timing the
95
+ controller does not report. Everything lands in one SQLite file (`data/netadmin.db`,
96
+ WAL mode). On a small home network that file sits around 20 MB after a day with
97
+ a few hundred thousand samples. Counters are stored as per-interval rates, gaps
98
+ are recorded rather than papered over with zeros, and old data rolls up hourly
99
+ then daily so year-over-year comparisons stay possible.
100
+
101
+ - **Detects problems, with the false-alarm checks written down.** Detectors are
102
+ deterministic rules, not a black box: static thresholds plus rolling
103
+ quantile bands off each series' own baseline. A cable detector that fires on a
104
+ gigabit port stuck at 100 Mbps first confirms the port is genuinely
105
+ gigabit-capable and the attached device is not a known 100 Mbps class, and it
106
+ records those checks alongside the finding. That audit trail is what separates
107
+ an admin from an alarm generator. The catalog covers wired faults (bad cable,
108
+ duplex, port flapping, PoE budget, STP loops, SFP degradation), WiFi
109
+ (sticky clients, ping-pong roamers, channel plan, DFS, airtime saturation,
110
+ mesh backhaul), clients (flaky disconnects with reason-code weighting, DHCP
111
+ failures), and WAN (ISP degradation, bufferbloat, DNS slowness, WAN flapping).
112
+
113
+ - **Tracks each issue's whole life.** A finding does not become a fresh alert
114
+ every poll. It gets a fingerprint, and one open issue exists per fingerprint.
115
+ An issue moves `pending -> active -> resolving -> resolved`, carries a
116
+ "still occurring, day 5" clock, reopens the same row if it refires within a day
117
+ instead of spawning a duplicate, and gets suppressed when a bigger fault
118
+ explains it (a downed switch mutes its own ports' issues). Every state change
119
+ is logged, so nothing about an issue is untraceable.
120
+
121
+ - **Scores health honestly (Mist-style SLE).** Each five-minute bucket, each
122
+ active client contributes minutes judged pass or fail per service-level
123
+ expectation (coverage, roaming, capacity, connect, WAN, infrastructure). Every
124
+ failed minute is pinned to exactly one cause on one device. The headline health
125
+ number and its explanation are the same query, so "88.6% overall, coverage
126
+ 84%, 138 failed client-minutes, top offender the Living Room AP" is one click from
127
+ the score. An idle client with bad signal contributes zero failed minutes,
128
+ which is what keeps the number impact-weighted instead of theatrical.
129
+
130
+ - **Proposes fixes you approve.** When a fix maps cleanly to a controller config
131
+ change (channel plan, transmit power step-down, removing a misapplied min-RSSI,
132
+ cycling a PoE port), UnifiOptimizer can render the exact API payload, show you a
133
+ full before-state, and apply it only when you click. It then watches for a
134
+ verification window to confirm the issue actually cleared. Nothing applies on
135
+ its own. See [Safety model](#safety-model).
136
+
137
+ - **Explains, when you want a second opinion.** For any issue, UnifiOptimizer compiles
138
+ a markdown dossier: the issue trail, the evidence windows as compact tables,
139
+ related issues on the same segment, the confounders already ruled out, and the
140
+ relevant playbook entry. You can hand that to any model yourself (the default,
141
+ no API key needed), pipe it through GitHub Copilot CLI, or wire an Anthropic
142
+ key. The investigator explains and correlates; it never applies anything.
143
+
144
+ - **Alerts through Home Assistant.** Optional and off by default. Over MQTT
145
+ discovery it publishes a health sensor, per-severity issue counts, and a
146
+ binary sensor per active P1/P2 that clears on resolve, so HA automations can
147
+ notify you on a new critical issue.
148
+
149
+ ---
150
+
151
+ ## How it works
152
+
153
+ The one idea behind UnifiOptimizer is memory. A controller throws away its
154
+ fine-grained stats after about a day, so UnifiOptimizer keeps its own history and reads
155
+ everything else (detection, health, issue tracking) from that. The full
156
+ walkthrough is in [`docs/HOW_IT_WORKS.md`](docs/HOW_IT_WORKS.md); the short of it:
157
+
158
+ **It watches, remembers, then tells you.**
159
+
160
+ ![How UnifiOptimizer watches: the UniFi controller feeds a 60-second collector and DNS/ICMP probes; everything lands in one SQLite store that rolls up hourly then daily; detectors and the health model read from it and feed the issue engine, which reaches you over web and Home Assistant. Fixes loop back to the controller only on your approval.](docs/img/how-netadmin-watches.png)
161
+
162
+ **One issue per fingerprint, not a new alert every poll.**
163
+
164
+ ![The life of an issue: many repeated findings collapse into one fingerprint that moves through pending, active, resolving, and resolved. A refire within a day reopens the same row, a fire during resolving snaps back to active, and a bigger fault mutes the smaller ones it explains.](docs/img/life-of-an-issue.png)
165
+
166
+ **Health you can argue with.**
167
+
168
+ ![Health as user-minutes: each active client-minute is judged pass or fail, each failed minute is pinned to one cause on one device, and an idle client with bad signal contributes zero failed minutes. The score and its explanation are the same query.](docs/img/health-sle.png)
169
+
170
+ ---
171
+
172
+ ## The interface
173
+
174
+ The web UI ships with the daemon and renders in light and dark. Every screenshot
175
+ here is from the built-in `netadmin demo-seed` network: fictional devices,
176
+ fabricated MACs, documentation-range IPs, so nothing below is a real network.
177
+
178
+ The issues view is the triage surface: every open finding with its severity, the
179
+ detector that raised it, the affected device, and how long it has been going.
180
+
181
+ ![The issues list: fourteen findings ranked by severity, each with its state (active or resolving), the detector that raised it (port flapping, weak mesh backhaul, DNS slow, rogue AP, sticky client), the affected entity, and its duration.](docs/img/screenshots/issues-light.png)
182
+
183
+ Open one and it carries its whole lifecycle: the evidence, the false alarms ruled
184
+ out, and a proposed fix you approve before anything touches the controller.
185
+
186
+ ![An issue detail page showing a resolved channel-plan issue with its evidence, the confounders that were ruled out, the lifecycle trail, and a fix that was proposed, applied, and verified.](docs/img/screenshots/issue-fix-light.png)
187
+
188
+ When you need to hand someone the whole picture, **Export report** renders a
189
+ print-ready network assessment: executive summary, topology, per-service health,
190
+ RF and client analysis, and a walkthrough of every finding. It states its own data
191
+ window and poll coverage up front, and every number traces to a stored query.
192
+ There is no sample or projected data.
193
+
194
+ ![The exported assessment's executive summary: an overall health score, findings counted by severity, and the highest-impact findings in plain language with the client-hours each one cost.](docs/img/screenshots/report-exec-light.png)
195
+
196
+ First run points UnifiOptimizer at your console. Type the address, or let it scan
197
+ the network for you, and the API key is written to the daemon, never shown in the
198
+ browser or sent anywhere else.
199
+
200
+ ![The UnifiOptimizer first-run screen in dark mode: a "Connect your network" heading, a controller-address field with a "Scan my network" option, a "Detect" button, and a note that the API key is written only to the daemon.](docs/img/screenshots/setup-dark.png)
201
+
202
+ ---
203
+
204
+ ## Two ways to run it
205
+
206
+ Both modes share every layer. The on-demand mode is literally the daemon's
207
+ startup path without the scheduler.
208
+
209
+ ### Daemon (always-on)
210
+
211
+ The permanent home. It backfills whatever the controller still retains, then
212
+ polls, detects, tracks, and scores continuously.
213
+
214
+ ```bash
215
+ netadmin daemon # binds 127.0.0.1:8765 by default
216
+ netadmin status # hit a running daemon's /api/health
217
+ netadmin status --json # ...and print the raw health payload
218
+ ```
219
+
220
+ Healthy status shows `status: ok`, collector jobs green with resetting poll
221
+ ages, `websocket.state: running`, and `backfill: done`.
222
+
223
+ ### Tech visit (on demand)
224
+
225
+ One pass over the history the controller still holds, then exit. This is the
226
+ "walk in, look around, leave a report" mode for a network you are not running
227
+ the daemon against.
228
+
229
+ ```bash
230
+ netadmin visit --lookback-days 3 # backfill + detect + report over a 3-day window
231
+ ```
232
+
233
+ The daemon is the recommended mode. A visit can only analyze what the controller
234
+ still retains (roughly a day of five-minute stats), which is exactly the gap the
235
+ daemon exists to close.
236
+
237
+ ---
238
+
239
+ ## Install
240
+
241
+ **Requirements**
242
+
243
+ - **Python 3.11+**
244
+ - A **UniFi controller** (CloudKey Gen2/Gen2+, UDM/UDM-Pro, or self-hosted
245
+ Network application) reachable on your LAN
246
+ - An **admin** account on the controller, or an **API key** (UniFi OS consoles
247
+ on Network 9.x). Read-only controller accounts do not expose the stats and
248
+ events UnifiOptimizer needs.
249
+ - **Node.js 18+** only to build the web UI from a source checkout; the published
250
+ wheel ships it prebuilt, so `pip install unifioptimizer` needs no Node.js
251
+
252
+ **Get a credential.** On a modern UniFi OS console, create a revocable API key
253
+ (Settings -> Control Plane -> Integrations); on an older or self-hosted
254
+ controller, use a dedicated local admin account instead. Ubiquiti moves that
255
+ screen between firmware versions, so rather than guess, run
256
+
257
+ ```bash
258
+ netadmin detect --host YOUR-CONTROLLER
259
+ ```
260
+
261
+ and it reads your console and prints the exact path to click for your device.
262
+ The full per-console walkthrough, the version requirements, and what to put in
263
+ `data/secrets.env` are in [`docs/CONTROLLER_SETUP.md`](docs/CONTROLLER_SETUP.md).
264
+
265
+ **Install the package**
266
+
267
+ The published wheel ships the compiled dashboard inside it, so end users need no
268
+ Node.js:
269
+
270
+ ```bash
271
+ pip install unifioptimizer
272
+ ```
273
+
274
+ From a source checkout, build the dashboard once so the daemon can serve it:
275
+
276
+ ```bash
277
+ git clone https://github.com/gneitzke/UnifiOptimizer.git
278
+ cd UnifiOptimizer
279
+ pip install -e . # console script + runtime deps
280
+ python tools/build_web.py # compile + bundle the dashboard (needs Node 18+)
281
+ ```
282
+
283
+ `./install.sh` runs both steps (and creates a venv) in one command. Skipping the
284
+ build leaves the API and daemon fully working; only the web dashboard waits until
285
+ you build it. Either way you get the runtime deps: httpx, pydantic, APScheduler,
286
+ websockets, FastAPI/uvicorn, dnspython, aiomqtt.
287
+
288
+ ---
289
+
290
+ ## Configuration
291
+
292
+ Two files under `data/`, both read at runtime, neither ever committed:
293
+
294
+ - **`data/secrets.env`** (chmod 600, gitignored) holds credentials:
295
+
296
+ ```ini
297
+ UNIFI_HOST=https://192.168.1.1
298
+ UNIFI_API_KEY=your-api-key # preferred; or the pair below
299
+ # UNIFI_USERNAME=audit
300
+ # UNIFI_PASSWORD=...
301
+ UNIFI_SITE=default
302
+ ```
303
+
304
+ - **`data/config.yaml`**, under a `netadmin:` block, holds structural config:
305
+ the SQLite path, the API bind (`server_host`/`server_port`, default
306
+ `127.0.0.1:8765`), pinned CORS origins, collector cadences, retention tiers,
307
+ probe targets, and the Home Assistant block. Every key is optional and falls
308
+ back to the defaults in `netadmin/config.py`. Two settings are worth checking
309
+ on first run:
310
+
311
+ ```yaml
312
+ netadmin:
313
+ probe:
314
+ gateway_ip: 192.168.1.1 # DNS/RTT probe target; auto-discovered if unset
315
+ anchor: 1.1.1.1 # public resolver, the "is it me or my ISP" comparison
316
+ wan_plan_down_mbps: null # set your plan rate to enable WAN saturation
317
+ wan_plan_up_mbps: null # and bufferbloat detection (null = those detectors abstain)
318
+ ```
319
+
320
+ Environment variables override YAML: `DB_PATH`, `LOG_LEVEL`, `SITE_ID`,
321
+ `SERVER_HOST`, `SERVER_PORT` map directly to the field names (no prefix).
322
+
323
+ ---
324
+
325
+ ## Safety model
326
+
327
+ **Read-only by default.** The current build talks to the controller with GETs
328
+ and a small set of documented read-query POSTs. There is no automatic mutation
329
+ anywhere in the collector or the daemon.
330
+
331
+ When the fix engine is enabled, it holds to hard rules:
332
+
333
+ - **The daemon never applies on its own.** Every apply is a deliberate human
334
+ action, a CLI flag or a UI button. There is no scheduler job or callback that
335
+ applies a fix.
336
+ - **Dry-run is the default.** A dry-run renders the exact API payload without
337
+ sending it. Applying requires an explicit confirm flag.
338
+ - **Configuration changes are revertible.** An apply captures the full
339
+ before-state, records the change to the store, and reverts in one click. (A PoE
340
+ power-cycle is momentary and has no state to restore; it is marked as such.)
341
+ - **Blast radius is capped.** No more than a configured number of devices change
342
+ per apply, and mesh-uplink APs' min-RSSI is never touched except to remove a
343
+ misapplied one.
344
+
345
+ From the CLI a fix is a dry-run by default and applies only with an explicit
346
+ confirm:
347
+
348
+ ```bash
349
+ netadmin fix 42 # render the exact payload; send nothing
350
+ netadmin fix 42 --apply --confirm # apply it, after capturing the before-state
351
+ netadmin fix --revert 7 # restore change 7 from its saved before-state
352
+ ```
353
+
354
+ In the web UI the same plan appears under Proposed fix on the issue page: an
355
+ Apply button behind a confirmation modal, and Revert on any change already
356
+ applied. The confirm token binds each apply to the exact plan you reviewed, so a
357
+ device that changed since you looked is refused rather than applied blind.
358
+
359
+ The daemon's HTTP API has no authentication yet, so it binds loopback only. Do
360
+ not publish port 8765 to an untrusted network without your own authenticating
361
+ proxy in front of it; reach a remote daemon over an SSH tunnel. Full policy in
362
+ [`SECURITY.md`](SECURITY.md).
363
+
364
+ ---
365
+
366
+ ## What it can and cannot do
367
+
368
+ Honesty about limits is part of the design.
369
+
370
+ - **Physical faults it flags but cannot fix.** A bad cable, a mesh AP with a
371
+ −81 dBm backhaul, a coverage hole with no better AP for the affected clients:
372
+ UnifiOptimizer identifies these with evidence and tells you where to look, but the
373
+ fix is your hands on hardware. The fix engine only changes controller config.
374
+
375
+ - **WAN detection needs your plan rate, and Starlink is noisy.** Saturation and
376
+ bufferbloat detectors compare throughput against your configured plan rate;
377
+ leave `wan_plan_*_mbps` null and they abstain rather than guess. Starlink and
378
+ other variable-rate or CGNAT links make "plan rate" fuzzy and the WAN latency
379
+ baseline drift, so those detectors lean on trend over absolute thresholds and
380
+ will be less confident on such links than on a fixed-rate connection.
381
+
382
+ - **Controller version changes what is available.** On Network 9.x the `stat/event`
383
+ history endpoint was removed; events now come only from the live WebSocket,
384
+ which means no historical event backlog before the daemon first started
385
+ watching. Unofficial v2 endpoints are probed at startup and used only when
386
+ present. When evidence is thin, detectors return UNKNOWN instead of guessing.
387
+
388
+ - **Some things are out of scope, stated plainly.** No late-collision detection
389
+ (no counter exposed), no ARP-conflict visibility, no confident non-WiFi
390
+ interferer identification or hidden-node detection, no client-side downlink
391
+ RSSI. UnifiOptimizer infers "unexplained airtime utilization" but will not name the
392
+ microwave.
393
+
394
+ - **PoE port power-cycle is not revertible.** Cycling power to a port is a
395
+ momentary action with no before-state to restore, so that one fix template is
396
+ marked non-revertible. Every configuration change (channel, power, min-RSSI)
397
+ captures its full before-state and reverts in one click.
398
+
399
+ ---
400
+
401
+ ## Deployment
402
+
403
+ The daemon image, `Dockerfile.netadmin`, is a single arch-neutral Python image
404
+ that builds unchanged for both `linux/arm64` and `linux/amd64`. Every dependency
405
+ ships prebuilt wheels for both, and nothing shells out to an arch-specific binary.
406
+ There are two documented build paths, each producing the same image:
407
+
408
+ - **amd64 / x86 NAS or server, with `docker buildx`.**
409
+
410
+ ```bash
411
+ ./deploy/build-multiarch.sh # builds arm64 + amd64, inspects, verifies
412
+ ```
413
+
414
+ The script builds both platforms to a local OCI tarball, prints the manifest
415
+ list to prove both are present, and pushes nothing. Set `SMOKE=1` to also load
416
+ the host-native arch and run an import test. Publish to a registry only when you
417
+ have one and want to; the script prints the exact `--push` command.
418
+
419
+ - **arm64 / Apple Silicon, with Apple `container`.** On macOS, Apple's `container`
420
+ CLI builds and runs the same image from a git-archive tarball with no secrets in
421
+ it. Bind the daemon to loopback, hand off the SQLite path, and keep it up with a
422
+ LaunchAgent; the API is unauthenticated, so never expose it on the LAN.
423
+
424
+ Whichever way you build it, the container holds no secrets. Credentials and the
425
+ SQLite store live on a bind-mounted `data/` volume, and `.dockerignore` keeps
426
+ `secrets.env` and the database out of every image layer.
427
+
428
+ ---
429
+
430
+ ## Coming from the old tool
431
+
432
+ The original `optimizer.py` analyze/optimize CLI has been removed. Its
433
+ one-shot analysis is now the daemon's continuous job, its HTML report became the
434
+ web UI and the on-demand tech visit, and its change-application became the
435
+ approval-gated fix engine. There is nothing to migrate: point UnifiOptimizer at your
436
+ controller and it starts building the history the old tool never kept.
437
+
438
+ ---
439
+
440
+ ## Testing
441
+
442
+ ```bash
443
+ python -m pytest tests/netadmin -q # the rebuild's suite
444
+ pip install -e ".[test]" # pytest, pytest-asyncio, respx
445
+ ```
446
+
447
+ The pure-logic layers (issue engine, baselines, SLE, detectors) have exhaustive
448
+ unit tests including confounder cases, controller payloads are replayed from
449
+ sanitized recorded fixtures, and an end-to-end test drives a synthetic bad week
450
+ (a cable degrades, a client flaps, firmware regresses) through ingest, detection,
451
+ and the issue lifecycle. No test ever touches a live controller; every mutating
452
+ path is exercised against mocks and dry-run rendering only.
453
+
454
+ ---
455
+
456
+ ## Documentation
457
+
458
+ - [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md): the full design. Data model,
459
+ detector catalog, issue engine, SLE model, fix engine, the whole spine.
460
+ - [`docs/HOW_IT_WORKS.md`](docs/HOW_IT_WORKS.md): a plain-language, hand-drawn
461
+ walkthrough of the whole thing.
462
+ - [`docs/DESIGN_FOUNDATION.md`](docs/DESIGN_FOUNDATION.md): the web UI design
463
+ contract.
464
+ - [`docs/CONTROLLER_SETUP.md`](docs/CONTROLLER_SETUP.md): per-console API-key setup
465
+ (run `netadmin detect` and it tells you where to click for your device).
466
+ - [`docs/BACKUP.md`](docs/BACKUP.md): backing up and restoring the database.
467
+ - [`SECURITY.md`](SECURITY.md): credential handling and the safety model.
468
+ - [`docs/DEVICE_DATABASE.md`](docs/DEVICE_DATABASE.md): the device-capability
469
+ database.
470
+
471
+ ---
472
+
473
+ ## License
474
+
475
+ This project is open source, MIT License, and available for personal and
476
+ commercial use.
477
+
478
+ ---
479
+
480
+ ## Acknowledgments
481
+
482
+ Built for the UniFi community, to help people run their networks instead of just
483
+ photographing them.