2 Commits
Author SHA1 Message Date
Cooper Ry LeesandClaude Opus 5 a73a2e0ab2 fix: don't send unused credentials to a Protect-only console
Found by running this against a real UNVR4 rather than only the fake one.

setDefaults fills an unset user with the placeholder "unifipoller". For a
Protect-only console that placeholder was reaching Login(), the console answered
403, and the controller died during initialisation -- the same symptom #1066 set
out to fix, one layer further in. A Protect API key and no local account is the
config an operator actually writes for a UNVR, so this was the common case, not
an edge one.

Nothing on such a console uses a session unless Protect logs are wanted: the
Integration API authenticates with the key alone. So withhold the credentials
entirely in that case, and say so in the startup summary rather than naming a
username that is never sent.

Also pins the go.mod bump to unifi v6.0.1, which is the release that carries
NewProtectClient (unpoller/unifi#240).

Verified end to end against a UNVR4 (UniFi OS 5.1.31, Protect 7.2.105): the
controller comes up, logs "Auth: Protect API key only (no session needed)",
makes no login request at all, and exports 37 unpoller_protect_* series across
9 cameras and 2 bridges with unpoller_controller_up = 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVreutpEATmBjm6PBw9RjQ
2026-08-31 13:08:34 +00:00
Cooper Ry LeesandClaude Opus 5 31cc5a0f31 feat: poll Protect-only consoles via disable_network (closes #1066)
A UNVR or UNVR Pro runs UniFi Protect with no Network application installed.
UnPoller could not poll one at all: NewUnifi ends with GetServerData(), a GET of
/proxy/network/status, which such a console answers with its UniFi OS SPA HTML.
The controller entry died during initialisation and re-failed every interval,
never even printing a config summary -- while the Protect Integration API on the
same host answered every endpoint with the same key.

Set disable_network = true on that controller. It defaults to false, so nothing
about an existing config changes.

The Protect collectors were already complete and already not site-scoped; three
things stood between them and a Protect-only console:

  - getUnifi now calls unifi.NewProtectClient, which skips the Network probe and
    validates the Protect Integration API instead (unpoller/unifi#240).

  - pollController aborted on getFilteredSites long before reaching
    collectProtect, and collectControllerEvents did the same before
    collectProtectLogs. The Network pass is extracted into pollNetwork and
    skipped wholesale; the event collector list reduces to collectProtectLogs,
    the only site-independent one.

  - Metrics counted a poll successful only if it produced devices or clients. A
    Protect-only console produces neither, so a filtered scrape of one -- the
    Prometheus per-target path -- fell through to the dynamic-controller branch
    and reported ErrDynamicLookupsDisabled despite a successful collection.
    ProtectDevices now counts too.

Two smaller things worth calling out for reviewers:

  - extractDevices dereferenced metrics.Devices unguarded. That was already a
    latent panic; skipping the Network pass makes it reachable, so it is fixed
    here rather than left for the first person to hit it.

  - RawMetrics answers the raw-path kind for these consoles and rejects the
    site-scoped kinds with ErrNetworkDisabled. Returning an empty result would
    read as "this console has no devices" rather than "wrong question".

warnProtectOnly logs an error, without failing the controller, for the two
configurations that can never collect anything: disable_network with neither
Protect save flag, and save_protect_devices with no key to authenticate with.
Silently collecting nothing is the failure mode hardest to spot in a log.

pkg/inputunifi had no tests before this. input_test.go follows inputunas'
input_test.go: an httptest fake UNVR serving the console's SPA HTML for
everything but the Protect paths and the login, covering initialisation,
metrics, events, the filtered scrape, RawMetrics, both warnings, config binding
across toml/json/yaml/env, and that the shipped examples leave Network enabled.
TestProtectOnlyControllerFailsWithoutFlag pins the original bug against that
same console, so the flag is demonstrably what makes the difference.

Requires github.com/unpoller/unifi/v6 with NewProtectClient (unpoller/unifi#240).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVreutpEATmBjm6PBw9RjQ
2026-08-31 13:08:27 +00:00